V2 マニフェストの Chrome 拡張機能は今年で終わりみたい
そういえば V3 ってどうなってるんだっけと思って調べてみたら もう V2 が終わろうとしてた
https://developer.chrome.com/docs/extensions/mv3/mv2-sunset/

すでに今年の 1 月に private 以外の V2 拡張機能の受け入れを停止していて 6 月には private も受け入れ停止
ストア公開しない拡張機能だと 2022 年中は変わりなし
2023 年 1 月に Enterprise policy というのを設定してない限り V2 拡張機能は動作しなくなる
2023 年 6 月に完全に動作しなくなる

今年で終わりみたい
V3 はそのままじゃ使えない変更が大きくてあまり移行する気が起きない
過去にブログで公開してた拡張機能があったけど もう自分でも入れてないし 誰も使ってないと思うし更新はしないかな

ドキュメント見てたら結構前の話みたいだけど難読化してるとストアで受け付けてもらえないというのがあった
セキュリティ対策は必要だと思うけど V3 はそのせいで結構制限多いって聞く
完全に V2 同等のことができるのかな

心配だった webRequest API は一応あるみたいだけど強制インストールが必要になるとか
リクエストをブロックしたりリクエスト先を書き換えたりレスポンスを書き換えたりアクセスした URL のログを保存して分析したりと結構便利なんだけど 悪用するにしても便利すぎる機能だしね
Amazon で見てる本の図書館蔵書情報を調べるツール
ネットで見かけた便利ツール
Amazon で本を見てたら図書館にあることを教えてくれるみたい
https://sonohon.com/

図書館なんて小さい頃以来言ったこと無いなーとか思ってたけど 考えてみたらどうやってそんな情報とって来てるんだろう?
ちょっと気になったのでソースコードを覗いてみました
API 呼び出ししてるみたいで外部サービスを使ってるみたいです
拡張機能自体は API 呼び出しの結果を表示するだけの小さめのシンプルなものみたい

そもそも Chrome ストアの説明の最後の注意書き見たら
「APIの仕様上で1000リクエスト/時という制限があるため、利用時間帯によっては検索が実行されない時があります。」って書かれてました

それを見て 今度は 各自が API キー取らずツール作成者のキーを使い回すの?と疑問が
それだとユーザ数増えたらすぐに 1000 とか超えそうだけど

ソースを見たところ 3 つの API キーが埋め込まれてました
オプションに 1 つ(設定用?)
バックグラウンド処理に 2 つ(ここがメインの情報取得用みたい)
この 2 つはランダムでどっちかを使うようです

API 制限あるとやりたくなるやつですが 1 アプリがいくつも使っていいのかなと思ってやりづらいやつです
どうせ複数使うなら中途半端に 3 つじゃなくて 20 近くは作っておけばいいのに 2 つだけなのはあんまりユーザ以内想定なのかな
content script と page script で通信する
window に dispatchEvent したイベントは両方で受け取れる
これを使ってもう一方のスクリプトにメッセージを送る

const connectPageScript = (name, script, onmessage) => {
const key_prefix = `p-cs-messaging [${name}] `
const scr = document.createElement("script")
scr.innerHTML = `
window.addEventListener("${key_prefix}to-page", eve => {
window.oncsmessage && window.oncsmessage(eve.detail)
})
window.send = msg => window.dispatchEvent(new CustomEvent("${key_prefix}to-cscript", { detail: msg }))
${script}
`
document.head.append(scr)
window.addEventListener(key_prefix + "to-cscript", eve => onmessage(eve.detail))
const send = msg => window.dispatchEvent(new CustomEvent(key_prefix + "to-page", { detail: msg }))
return send
}

使用例: content script で実行

const script = `
window.xyz = 100
window.oncsmessage = msg => {
console.log(1, msg)
setTimeout(() => send({ frompage: true, xyz: window.xyz }), 1000)
}
`

const onmessage = msg => {
console.log(2, msg)
}
const send = connectPageScript("foo", script, onmessage)
window.xyz = 200
send({ fromcs: true, xyz: window.xyz })

// 1 {fromcs: true, xyz: 200}
// (1 秒後)
// 2 {frompage: true, xyz: 100}
Remote Fetch
拡張機能でバックグラウンド側で fetch するためのもの
以前どこかで作ったのを ESModules 対応で新しく作り直した

バックグラウンド側で行うので CORS 無視かつ https から http の fetch ができる
ページ内に JavaScript を埋め込む content_script で使うのが主な用途

fetch の API と完全な互換性はなし

export const remoteFetch = (url, type, option) => {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ url, type, option }, res => {
if (res) {
if (res.error) {
reject(new Error(res.error))
return
}
if (res.result) {
if (type === "buffer") {
resolve(Uint8Array.from(res.result).buffer)
} else {
resolve(res.result)
}
return
}
} else {
reject("Something went wrong.")
}
})
})
}

const remoteFetchListener = (req, sender, sendResponse) => {
if (!req.url) {
sendError("missing url")
return false
}
try {
new URL(req.url)
} catch {
sendError("Invalid URL: " + req.url)
return false
}

fetch(req.url, req.option)
.then(async res => {
if (!res.ok) {
throw new Error("Rensponse is not ok: " + res.status)
}
let result = null
if (req.type === "json") {
result = await res.json()
} else if (req.type === "buffer") {
const ab = await res.arrayBuffer()
result = Array.from(new Uint8Array(ab))
} else {
result = await res.text()
}
sendResponse({ result })
})
.catch(err => {
sendError("Fetch error: " + err.message)
})
return true

function sendError(message) {
sendResponse({ error: message })
}
}

export const listenRemoteFetch = () => {
chrome.runtime.onMessage.addListener(remoteFetchListener)
}

export const unlistenRemoteFetch = () => {
chrome.runtime.onMessage.removeListener(remoteFetchListener)
}

content_script 側では remoteFetch をインポートして使う
2 つ目の引数に text, json, buffer のどれかを指定する

import { remoteFetch } from "./remoteFetch.js"

remoteFetch(url, "text").then(console.log)

バックグラウンド側では listenRemoteFetch を実行しておく

import { listenRemoteFetch } from "./remoteFetch.js"

listenRemoteFetch()

作ったはいいけど このまま使う機会は少なそう
1 ファイルにまとめてるけど 使う側で実行する関数も別で共通処理もないから ロードする量減らすならファイル分けたほうが良いかもだし
拡張機能で Modules のロードは不便なところもあるから直接スクリプトに埋め込んだり バンドルしたほうが良さそう
Chrome Extension の background scripts で module を使いたい
Chrome Extension の background scripts は通常こういう風に指定します

  "background": {
"scripts": ["bg.js"]
}

しかし これだと自動で作られた HTML に通常の JavaScript として実行されます
module ではないので import や export は syntax error です
今のところ module としてロードする機能はなさそうなので 自分で HTML ページを作って module としてロードする必要があります

  "background": {
"page": "bg.html"
}

<!doctype html>
<meta charset="utf-8"/>
<script src="bg.js" type="module"></script>
ユーザスクリプトと webcomponent
ページに <ex-elem></ex-elem> みたいなタグを入れておいて
ブラウザ拡張機能でカスタム要素を define しておけば 拡張機能入れてる人だけ特別な機能が使えるとかあっておもしろそう

自分のページだけなら 直接拡張機能でやりたいことすればいいけど 拡張機能は汎用的なもので個人サイトに依存させないで ページ作る側が拡張機能使ってる人向けの特別な機能を用意するとか

ただ拡張機能権限の機能使うと悪用される可能性もあるし 拡張機能ならではのことは難しいかな

Chrome 拡張機能の manifest.json を簡単に見る
オプションページを開いて URL を manifest.json にすれば普通に見れる

例えば pocket だと
chrome-extension://niloccemoadcdkdjlinkgdfekeahmflj/options.html
なので
chrome-extension://niloccemoadcdkdjlinkgdfekeahmflj/manifest.json

普通の人には何の役にも立たないけど 拡張機能作る人には書き方ググらなくてもさっとサンプルを見れるので手間が省けて便利
Chrome Extension の webRequest でキャンセルする
onBeforeRequest, onBeforeSendHeaders, onHeadersReceived, onAuthRequired のイベントはキャンセル可能
3 つ目の引数に blocking を指定してれば 返すオブジェクトでリクエストを改変できる

{cancel: true} を返せばリクエストを防げる
POST をブロックする例
chrome.webRequest.onBeforeRequest.addListener(details => {
if(details.method === "POST"){
return {cancel: true}
}
}, {urls: ["<all_urls>"]}, ["blocking"])

これだと キャンセルされたエラー画面が表示される

完全にキャンセルして何もおきないようにするなら 「javascript:void 0」 にリダイレクトさせる

chrome.webRequest.onBeforeRequest.addListener(details => {
if(details.method === "POST"){
return {redirectUrl: "javascript:void 0"}
}
}, {urls: ["<all_urls>"]}, ["blocking"])

拡張機能の設定を参照する content script
Extension の content script の埋め込みしたいときに background ページでページの遷移やタブの更新を監視して executeScript で埋め込むのって面倒だから manifest.json の "content_scripts" だけで済ませたい

{
"content_scripts": [
{
"matches": ["<all_urls>"],
"css": ["style.css"],
"js": ["script.js"]
}
],
}

だけど manifest レベルで宣言してるから ユーザが on/off 切り替えたり 設定に応じたフラグを追加したりできない

これまで諦めてたけど content_script 内でメッセージングや共通のストレージ読み取ればできそう

永続するデータなら chrome のストレージに入れておけば content script から読み取れる
chrome.storage.local.get({enabled: true, mode: "default"}, ({enabled, mode}) => {
if(!enabled) return
switch(mode){
// some cases
}
})

localStorage だと拡張機能じゃなくてスクリプトが埋め込まれたページのストレージを参照するので使えない
localStorage を使いたい(非同期がイヤ)とか バックグランドページのメモリ上のみのデータを使いたいなら messaging

// content script
chrome.runtime.sendMessage("require-cscript-data", ({enabled, mode}) => {
if(!enabled) return
switch(mode){
// some cases
}
})

// background
let enabled = true
let mode = "default"
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if(message === "require-cscript-data"){
sendResponse({enabled, mode})
}
})

CSS の場合は content_scripts で埋め込まれた JavaScript で style タグを作る