機能が上手く動作しないケースと例
まず、前提として、
- 複数ページを遷移するような機能
- ページを遷移した後に、ページ内のDOM要素に対して何らかの処理を施す機能
これらの処理を、chrome拡張のコンテンツスクリプトで実現したい場合、コンテンツスクリプトの埋め込み先のページがSPA(Single Page Application)の場合、直感的に実装できますが、MPA(Multi Page Application)の場合、ページの再読み込みの度にコンテンツスクリプトも再読み込みされてしまうので、コンテンツスクリプトで、以下のような実装をしていると、上手く動かない、ということがありました。
content.js
chrome.runtime.onMessage.addListener(async function (request) {
if (request.action === "pageTransition") {
await new Promise((resolve) => setTimeout(resolve, 3000)); // 3秒待
document.querySelector("#linkb").click(); // ここは動く
// -------------------------------------------------------
// 上記の処理で、ページ読み込みが入ると、以下の処理から動作しない
// ------------------- ↓↓↓↓↓ ------------------------
await new Promise((resolve) => setTimeout(resolve, 3000)); // 3秒待つ
document.querySelector("#linkc").click();
await new Promise((resolve) => setTimeout(resolve, 3000)); // 3秒待つ
}
});
MPAでも上手く動くようにするには
ページの読み込みがされるとコンテンツスクリプトがリセットされてしまうので、遷移前(ページ読み込み前)に、コンテンツスクリプトから拡張機能service workerに実行状況を渡して、遷移後に、読み込まれたコンテンツスクリプトに実行状況を返して、処理を再開する、というやり方が選択肢としてはあるかと思います。
力業ではありますが、上記のような手法を取りました。実装は下記です。
content.js
// 実行する関数と遅延時間を指定してメッセージを送信する関数
const executeWithDelay = (functionName, delayInSeconds, ...args) => {
chrome.runtime.sendMessage({
action: "startAlarm",
functionName: functionName,
delayInSeconds: delayInSeconds,
args: args,
});
};
const transitionPage = () => {
const linkSelectors = ["#linkb", "#linkc"];
for (let i = 0; i < linkSelectors.length; i++) {
executeWithDelay("clickElement", 2 * (i + 1), linkSelectors[i]);
}
};
const clickElement = (selector) => {
const element = document.querySelector(selector);
if (element) {
const clickEvent = new MouseEvent("click", {
view: window,
bubbles: true,
cancelable: true,
});
element.dispatchEvent(clickEvent);
} else {
throw Error("要素が見つけられずクリックできませんでした。セレクター:" + selector);
}
};
const functionMap = {
transitionPage: transitionPage,
clickElement: clickElement,
};
chrome.runtime.onMessage.addListener(function (request) {
switch (request.action) {
case "pageTransition":
transitionPage();
break;
case "executeFunction":
functionMap[request.functionName](...request.args);
break;
}
});
service-worker.js
// アラームが発火した時の処理
chrome.alarms.onAlarm.addListener(function (alarm) {
const { functionName, args } = JSON.parse(alarm.name);
// コンテンツスクリプトに指定された関数を実行するようにメッセージを送信
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, { action: "executeFunction", functionName: functionName, args: args });
});
});
// コンテンツスクリプトからのメッセージを受信
chrome.runtime.onMessage.addListener(function (request) {
if (request.action === "startAlarm") {
// 指定された秒数後にアラームを設定
/* chrome.alarms.createメソッドは、第一引数にアラームの名前を文字列として受け取る。しかし、
このメソッドでは、関数名だけでなく、その関数に渡す引数も一緒に保持する必要がある。
オブジェクトをそのまま渡すことはできないため、JSON.stringifyを使ってオブジェクトを文字列に変換している */
chrome.alarms.create(JSON.stringify({ functionName: request.functionName, args: request.args }), { delayInMinutes: request.delayInSeconds / 60 });
}
});
chrome APIのalarmsを用いて、拡張機能サービスワーカーにコンテンツスクリプトの関数とDOM Selectorを渡しておいて、時間差でそれらを実行・利用することで、ページ遷移を実現しています。
最後に
他にも、chrome.storage APIを用いて、同様のことが実現できるかと思います。この辺りは現在進行形で模索中なので、もっと綺麗でスマートな方法があれば追記します。ここまで読んでいただき、ありがとうございました。
コメント