今回初めてFireFoxのアドオンを作ってみました。熱が冷め止まぬうちに手続き等を記録しておきたいと思います。内容はあくまで忘備録程度のモノで、間違っている可能性が高いです。
まずはMozillaが公開している拡張機能のチュートリアルをなぞることから始めました。最初のチュートリアルの中身はとてもシンプルなもので、ページを開いたときに裏でjavascriptを走らせるものでした。シンプルとは言っても、これを使えばページに内容を追加/削除したりできるので、機能面では十分なことができます。
その後に次のチュートリアルをやってみました。こちらはツールバーのポップアップ等を組み込んだ、実用的なチュートリアルでした。正直こちらから始めても良かったかもしれません。
manifest.jsonです。
manifest.json
content/
content.js
popup/
popup.html
popup.js
option/
option.html
option.js
background/
background.js
icons/
icon.png
フォルダやファイルの名前に多少の際はあれど、おおよそこのような構造になっています。content.jsは開いているwebサイト中で実行されます。そのためcontent.jsではページ内の要素を取得できます。
popup.htmlはツールバーに表示されるアイコンをクリックしたときに表示されるポップアップメニューの中身です。popup.jsではポップアップメニューの状態にアクセスできます。
option.htmlはアドオンの設定に表示されるオプションメニューの中身です。option.jsではオプションメニューの状態にアクセスできます。
background.jsは少し特殊で、ブラウザを立ち上げている間常に実行され続けます。
sendMessage()やonMessage.addListener()などの関数を用いて互いに通信できます。そのため、「popup.jsでポップアップメニューのボタンクリックを感知し、content.jsに送信して、content.js内でwebサイトの中身を書き換える」といった使い方ができます。直接getBackgroundPage()などで取得することもできるようです。
content.jsの読み込ませ方manifest.json内で指定する、contentScript APIを使う、tabs.executeScript()を使う
"permissions": ["download"]をmanifest.jsonに追加して使う。downloads APIはcontent.js内では使えない。background.jsでは使える。
"permissions": ["notifications"]が必要。
function createNotification(message) {
browser.notifications.create({
type: "basic",
title: "title",
message: message,
});
}
"permissions": ["storage"]が必要。データの追加と読み込みは非同期処理になる。
セット:
browser.storage.local.set({
test_text: "hoge",
});
ゲット:
let result;
browser.storage.local
.get()
.then((restoredSettings) = > {
result = restoredSettings.test_text;
console.log(result)
})
.catch((e) => {
console.error("Failed : " + e.message);
});
ドキュメント読め。
その関数の処理が終わったらthenに進む。
function checkAndDownload(url){
fetch(url).headers.get("Content-Type").then((response) => console.log(response));
}
"permissions": ["notifications"]が必要。
browser.menus.create({
id: "abcdef",
title: "メニューに出る文字",
contexts: ["all"],
});
browser.menus.onClicked.addListener((info, tab) => {
func();
})
送信先でonMessage.addListener()が設定されていないかもしれない。content.jsを本当に実行できているか確認。
なんか警告が出るので、代わりにdocument.createElement()してappendChildを使う
"permissions": ["<all_urls>", "activeTab"]を追加。
並列処理ではない(重要)が、forとかで一つずつやるより遥かに速い。
async function func() {
let array = new Array();
const result = await Promise.all(
array.map(async (item, index, all) => {
// なんか同期を待つ処理
const fetch_result = await fetch(item.url).catch(() => new Response());
let new_item = func2(fetch_result)
return new_item
}))
}
"/"から始めればルートから、そうでなければ相対パス。
リンク先が画像かどうかの判定を、「GET送ってレスポンスのヘッダーを見る」と実装していた。後でHEADメソッドとかいうヘッダーだけ持ってくるものがあることを知った。というかGETとPOSTしかないと思っていた。アホ。
新しいバージョンのアップロード手続き中にキャンセルすると、そのバージョンが無効化されてしかも有効化できなくなるっぽい。
文字列を正規表現扱いするには、RegExp()を使う。
let re = new RegExp("ab+c", "g");
というかそもそも何で用意されてないんですかね?
事前にAbortController()を作っておき、fetchの前にsetTimeoutで一定時間後にAbortController.abort()が呼ばれるようにしておく。そしてfetchの引数でsignal:AbortController.signalを指定すれば、一定時間後にfetchがキャンセルされる。うまく行った場合のために、fetch後にsetTimeoutは消しておく。
const controller = new AbortController();
const timeout = window.setTimeout(() => {
controller.abort();
console.log(`Connection to ${url} timed out.`)
}, 10000);
const fetch_result = await fetch(url, {
method: "HEAD",
signal: controller.signal,
}).catch(() => new Response());
window.clearTimeout(timeout);
フレーム内のHTMLドキュメントはcontentDocumentで取得できるが、同一起源ポリシーにより外部ドメインサイトへのアクセスはできない。そのようなときcontentDocumentが例外を起こすのかnullを返すのかまちまちで分からん。ネットでは例外と書いてあったが、実際に動かすとnullだったりする。取り合えず外部サイトへのアクセスが予想されるときはtry/catchとnullチェック。
let iframe_elements = document.getElementsByTagName("iframe");
for (let j = 0; j < iframe_elements.length; j++) {
try {
var iframe_document = iframe_elements[j].contentDocument;
} catch {
continue;
}
// iframe_documentを使った処理
}
manifest.json内のcontent_scriptsなどで全てのURLを指定したいときに"*://*"などとしても動かない。"<all_urls>"を使う。
web-ext runで開発中に動かなくなったら、他のタブで例外とかで停止していないか確認。デバッガーとかをすべて閉じれば動くかも。
こちらのドキュメントが非常に分かりやすい。Rangeは始点と終点を持つ、ノードの集まり。Selectionは複数のRange(とはいうもののFirefox以外は一つしか持たないらしい)を持ち、getRangeAt(i)でアクセスできる。具体的にRangeに触るには、その後にcloneContents()をしてHTMLフラグメントとして取り出す。下は選択範囲のimgタグを全て抜き取る例。
let selection = window.getSelection();
let ranges = new Array();
for (let i = 0; i < selection.rangeCount; i++) {
ranges.push(selection.getRangeAt(i).cloneContents());
}
//基本的にrangeは要素一つ。Firefoxだけ複数のrangeをサポートしている
for (let index = 0; index < ranges.length; index++) {
const fragment = ranges[index];
let elements_from_fragment = fragment.querySelectorAll("img");
for (let j = 0; j < elements_from_fragment.length; j++) {
const element = elements_from_fragment[j];
//あれやこれや
}
}
onloadとかではなく、スクリプト実行時に読み込み中なら即returnみたいな使い方をしたいときに。tab.statusが使える。ただし現在のタブを取得するtabs.getCurrent()は罠で、backgroundでは使えない。browser.tabs.query({ active: true, currentWindow: true })などで取得する。
function func(tab) {
if (tab.status === "complete") {
//処理
} else {
setTimeout(() => {
func(tab);
}, 100);
}
}
content scriptに送るときはtabs.sendMessageじゃないとダメらしい。