Chrome拡張機能でリーディングリスト

はじめに

普段ブラウザの自動操作を行う時はPython+Seleniumを使用していましたが、 chromeの拡張機能はユーザーとインタラクティブな分、柔軟性を有する部分があるように感じたので、 試しに拡張機能を作りました。イメージ的にseleniumはプログラムと連携してページの操作のみを目的としたものですが、 拡張機能はページ操作の他にページやブラウザの機能を拡張することができると思うので興味がありました。 また独自のページをUIのように作成することもできるようなのでゲームのようなものも作成できそうです。 今回作ったのは後で読むようにページのURLを保持しておくリーディングリストです。 リーディングリストはchromeの機能として既に存在しますが、 新たに自分で作成しリストをcsvファイルで出力できるような機能も付けました。

当初はリストはメモリに保持し、ブラウザを閉じるとリストは消去されるような一時リストのような仕様にしたかったのですが、 chrome側のservice workerの仕様でそれができなかったので仕方なくstorageに保存という形にしました。

機能

以下の機能を有するリーディングリストを作成しました。

  • 現在フォーカスしているタブのページ"Add"ボタンを押すとタイトルとURLをリストに追加する
  • チェックボックスにチェックが入っている項目を削除する
  • 全ての項目にチェックを入れる
  • 全てのリストの内容をcsvファイルの出力する

構成

基本的なファイル構成はマニフェストファイル、拡張機能のボタンを押したときにポップアップするhtmlファイル(popup.html)、popup.htmlに付属するscriptファイル(popup.js)です。 popup.jsはpopup.htmlの操作や現在開いているブラウザ自体とのやり取りを行います。 その他にbackgroundで動作させるbackground.jsやアクティブなページの操作を行うcontent.jsなどを追加することができます。

    manifest.json
popup.html
popup.js
    

コード

Chrome version 88 よりManifest V3が使用可能になります。V2は将来的に廃止になるようです。 変更の内容としては以下になります。

https://developer.chrome.com/docs/extensions/mv3/intro/mv3-overview/

  • backgroundページがservice workerに置き換わります
  • ネットワークリクエストの編集がdeclarativeNetRequest APIにとって代わります(webRequestが使えない)
  • リモートにホストされたコードは使えなくなります
  • 将来的にPromiseをすべてのmethodでサポートされます

manifest.jsonは以下になります。 今回はservice workerは使いません。 tabとstorageに対してアクセスできるようにpermissionを与えています。

    {
    "manifest_version": 3,
    "name": "tmp ReadingList",
    "version": "1.0",
    
    "action": {
        "default_popup": "popup.html"
    },
    "permissions": [
        "tabs",
        "storage"
    ]
}
    

popup.htmlは以下になります。 多少cssで装飾していますがシンプルなものにしています。

    <!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        body {
            background: #110f18;
            font-size: small;
            font-family: sans-serif;
        }
        button {
            margin: 5px;
            width: 80px;
            background: #d3d3d3;
            border: 3px solid orange;
            border-radius: 50vh;
        }
        #list {
            width: 500px;
            height: 200px;
            overflow: scroll;
            white-space: nowrap;
        }
        a:link {
            color: #279dcc;
        }
        a:visited {
            color: #16628d;
        }
    </style>
</head>
<body>
    <button id="add_btn">Add</button>
    <button id="remove_btn">Remove</button>
    <button id="checkAll_btn">CheckAll</button>
    <button id="save_btn">Save</button>

    <script src="popup.js"></script>
    <div id="list">
    </div>
</body>
</html>
    

popup.jsは以下になります。 ボタンクリックのイベントリスナーとリストを挿入するときの要素の作成、csvファイル出力のコードになります。 コードは長くなりますのでリスト追加とcsvファイル保存の部分のみになります。 addボタンをクリックすることでフォーカスしたtabのタイトルとURLをchrome.tabs.queryで取得します。 その後storageから保存したリストを取得、追加した後、popup.htmlの更新を行います。 それぞれのリスト要素はcreateInfo関数を実装し作成しています。 ファイルの保存にはshowSaveFilePicker APIを使用します。

全実装はこちらから

    document.getElementById("add_btn").addEventListener("click", async () => {
    await chrome.tabs.query({active: true, lastFocusedWindow: true}, tabs => {
        let titles = [];

        for (let i = 0; i < tabs.length; i++) {
            titles.push({ title: tabs[i].title, url: tabs[i].url });
        }
        chrome.storage.sync.get({titles: []}, function (value) {
            if (value.titles) {
                titles = titles.concat(value.titles);
            }
            titles = Array.from(
                new Map(titles.map((item) => [item.url, item])).values()
            );
            chrome.storage.sync.set({'titles': titles}, function () {
            });

            let list_elem = document.getElementById("list");
            while (list_elem.firstChild) {
                list_elem.removeChild(list_elem.firstChild);
            }
            for (let [id, item] of titles.entries()) {
                list_elem.appendChild(createInfo(item.title, item.url, id));
            }
        });
    });
});
    
document.getElementById("save_btn").addEventListener("click", async () => {
    let elems = document.querySelectorAll('a');
    let data = "";
    for (let [i, elem] of elems.entries()) {
        data += i.toString() + ",\"" + elem.textContent + "\",\"" + elem.getAttribute("href") + "\"\n";
    }
    try {
        const handle = await getNewFileHandle();
        await writeFile(handle, data);
    } catch (e) {
        console.log(e);
    }
});

async function getNewFileHandle() {
    const options = {
        suggestedName: 'Untitled.csv',
        types: [
            {
                description: 'Text Files',
                accept: {
                    'text/plain': ['.csv'],
                },
            },
        ],
    };
    const handle = await window.showSaveFilePicker(options);
    return handle;
}

async function writeFile(fileHandle, contents) {
    const writable = await fileHandle.createWritable();
    await writable.write(contents);
    await writable.close();
}
    

嵌った点

javascriptはあまり慣れておらず、言語の仕様等に嵌ってしまった部分があるのでメモとして残します。

  • service worker

    Manifest V3ではbackgroundにservice workerを使用しますが、イベントが一定時間起こらないとbackground.jsが停止しメモリを開放します。 当初はbackground.jsでリストデータを保持しようと考えていましたが、このせいで実際にやってみると出来ないということがわかりました。 しばらく放置するとリストデータは消えます。よってstorageを使用する仕様に変更必要がありました。

  • オブジェクトの配列でスプレッド構文を使用

    リストを追加するときにオブジェクトを配列に追加するときにスプレッド構文を使用としましたが、うまくいきませんでした。 代わりに素直にひとつづつpushしましたが、スプレッド構文の場合は参照であることがいけなかったのでしょうか?

  • オブジェクト配列のunique

    当初は配列をsetに変換してさらに配列に変換することでunique処理を使用としましたが、オブジェクトはkeyとitemが同じでも別物として扱われるようです。 よってurlをkeyとして連想配列からunique処理をしました。

まとめ

今回はchrome拡張機能を初めて作成したものでシンプルなものでしたが、 WASMやReact等と組み合わせることも可能なのでうまく使えばいろいろ便利なものや面白いものが作れそうです。

Reference

  1. https://developer.chrome.com/docs/extensions/
Next Post Previous Post
No Comment
Add Comment
comment url