はじめに

とあるページ向けに作っていた拡張機能が上手く動作しなくなったので確認すると、React.js を利用した SPA に変更されていた。

リロードするとちゃんと動作するのだが、クリックなどでページ遷移すると拡張機能が動作しないので、対応方法について調べた。

続編: Chrome 拡張機能で background scripts から content scripts にメッセージを送信する

TL;DR

目次

  1. はじめに
  2. TL;DR
  3. 環境・条件
  4. 詳細
    1. chrome.tabs.onUpdated.addListener について
      1. chrome.tabs.executeScript について
    2. SPA で試す
  5. まとめ
  6. その他・メモ
  7. 参考文献

環境・条件

  • Chrome バージョン: 78.0.3904.70(Official Build) (64 ビット)

詳細

ChromeExtensionをSPAに対応したお話 - 生涯未熟 がとても参考になった。

chrome.tabs.onUpdated.addListenerchrome.tabs.executeScript を使えば良いとのこと。

どちらも使ったことがないので、使い方も含めて調べてまとめた。

リポジトリ: 17number/chrome-extension-with-spa-example

chrome.tabs.onUpdated.addListener について

まずは chrome.tabs.onUpdated.addListener の確認、名前の通りタブの更新時に発火する。実際に使ってみる。

manifest.jsonpermissionstabs を指定していないと、コールバック関数でタブの URL が取得できないので注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"manifest_version": 2,
"name": "Hello from background",
"version": "0.0.1",

"background": {
"persistent": true,
"scripts": [
"background.js"
]
},

"permissions": [
"tabs"
]
}

background.jschangeInfo で具体的なイベントの情報が、tab で各種情報が取得できる。

具体的に何が取れるのかをログ表示してみるだけのプログラム。Google 検索ページにアクセスすると、updated: https://google.com/xxx を表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
console.log({
tabId: tabId,
changeInfo: changeInfo,
tab: tab,
tabUrl: tab.url,
locationHref: location.href,
this: this
});
if (changeInfo.status === "complete" && tab.url.indexOf("https://www.google.com/") > -1) {
console.log(`updated: ${tab.url}`);
}
});

changeInfo.status === "complete" でタブ更新が完了したかどうかを判別できる。

拡張機能が動作する URL を限定したい場合は location.href ではなく tab.url を使う。以下ログを見れば分かるが、location.href には拡張機能の URL が格納されている。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
changeInfo:
status: "complete"
locationHref: "chrome-extension://dgndcmcpjeoekipnbacjcgdjgnecdnnp/_generated_background_page.html"
tab:
active: true
audible: false
autoDiscardable: true
discarded: false
favIconUrl: "https://www.google.com/favicon.ico"
height: 977
highlighted: true
id: 883
incognito: false
index: 10
mutedInfo:
muted: false
pinned: false
selected: true
status: "complete"
title: "test - Google 検索"
url: "https://www.google.com/search?q=test&oq=test&..."
width: 1187
windowId: 209
tabId: 883
tabUrl: "https://www.google.com/search?q=test&oq=test&..."

ちなみに、↑のログはバックグラウンドページ側に表示されるので注意。(Google を表示しているタブ側ではない)

chrome.tabs.executeScript について

chrome.tabs.executeScript の確認。

manifest.jsonpermission に対象の URL (今回は "https://www.google.com/search*") を追加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 {
"manifest_version": 2,
"name": "Hello from background",
"version": "0.0.1",

"background": {
"persistent": true,
"scripts": [
"background.js"
]
},

"permissions": [
- "tabs"
+ "tabs",
+ "https://www.google.com/search*"
]
}

background.js。Google 検索(https://www.google.com/search)ページの読み込みが完了したら、executeScript を実行。hello.js を注入する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
if (changeInfo.status === "complete" && tab.url.indexOf("https://www.google.com/search") > -1) {
console.log(`updated: ${tab.url}`);
chrome.tabs.executeScript(
tabId,
{
file: "hello.js",
},
function(result) {
console.log(`executed: ${result}`);
}
);
}
});

hello.jsconsole.loghello と表示するだけ。

1
console.log("hello");

これで Google 検索をすると、検索が完了するたびに Google のタブ側のコンソールに hello が表示される。(今度はバックグラウンドページ側ではない)

コンソールだと分かりづらいので、ボタンをページ中に挿入してみる。hello.js を以下に変更。

1
document.body.firstElementChild.insertAdjacentHTML("afterbegin", "<button>hello</button>");

これで拡張機能を再読込して動かすとこうなる。

「タブ更新を検知 → バックグラウンドから JS を注入 → 注入された JS でページを拡張」という一連の流れを確認できた。

SPA で試す

【Vue.js, React, Angular】事例から見るSPAフレームワーク - Qiita によると、Airbnb が React.js 使ってるとのことなので、Airbnb で動作するように変更する。

manifest.json。通常の枠組み(Content Scripts)だと上手くいかないよね、も合わせて確認するために content_scripts の記述も追加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"manifest_version": 2,
"name": "Hello from background",
"version": "0.0.1",

"background": {
"persistent": true,
"scripts": [
"background.js"
]
},

"content_scripts": [
{
"matches": [
"https://www.airbnb.jp/**/*"
],
"js": [
"content.js"
]
}
],

"permissions": [
"tabs",
"activeTab",
"https://www.airbnb.jp/"
]
}

background.js は変更なし。

hello.js。SPA なので(追加位置によっては)既存の追加した DOM が削除されない。なので、残存している場合には削除するようにしている。乱数は SPA でのページ遷移でも拡張機能がちゃんと動作できているかの識別用。

1
2
3
4
5
6
if (document.getElementById('from-background'))
document.getElementById('from-background').remove();
document.body.firstElementChild.insertAdjacentHTML(
'afterEnd',
`<button id="from-background">hello from background: ${Math.random()}</button>`
);

content.js。処理内容は hello.js と同じ。

1
2
3
4
5
6
7
8
9
(() => {
'use strict';
if (document.getElementById('from-content'))
document.getElementById('from-content').remove();
document.body.firstElementChild.insertAdjacentHTML(
'afterEnd',
`<button id="from-content">hello from content: ${Math.random()}</button>`
);
})();

この拡張機能を読み込んで Airbnb でページ遷移すると、hello from content の方は初期表示時こそボタン追加されるものの、乱数が変更されない(つまり、その後拡張機能の JS が動作していない)ことが分かる。

一方で hello from background の方は、ページ遷移ごとに乱数値が書き換わっていて、SPA でもページ遷移を検出して動作できていることが分かる。

まとめ

その他・メモ

参考にした記事(ChromeExtensionをSPAに対応したお話 - 生涯未熟)も結構前なので、もっと良いやり方があるかもしれない。

参考文献

関連記事