Chrome拡張機能「Amazing Searcher」を作った時の備忘録

10 min read読了の目安(約9800字

概要

Googleの検索結果に期間や言語の絞り込みボタン、検索ワードに関連したリンクを表示するChrome拡張機能「Amazing Searcher」を作りました。この記事はその際の備忘録です。

DEMO

↓公開先

https://chrome.google.com/webstore/detail/amazing-searcher/poheekmlppakdboaalpmhfpbmnefeokj

↓紹介編

https://zenn.dev/eetann/articles/2021-05-30-introduction-amazing-searcher

Manifest V3

以前作ったChrome拡張機能「Mr.Sagasu (紹介記事)」や「Mr.Jimaku (紹介記事)」は Manifest V3 の登場前だったため、Manifest V2で作成しましたが、今回作成した Amazing Searcher はV3です。

V3での変更点については、Manifest V3 への移行ガイドManifest V3 チェックリスト を参考にしました。

自動リロード対応

background pagesが Service worker になった影響で、rubenspgcavalcante/webpack-extension-reloaderxpl/crx-hotreloadといった、ファイル保存時に自動でビルドやリロードを行うプラグインが使えなくなりました(本当に便利だった…)。
そこで、自作プラグインを作りました。Service Worker そのものの機能を考えずにとりあえずWebSocketで作ったため、折りたたみの中に簡単に書きます。
次に拡張機能を作るときには大胆に変更するかもしれません。

自動リロードについて

機能は以下です。

  • 拡張機能のcontent_scriptsoptions_pageが開かれていれば、拡張機能のスクリプト変更時に拡張機能を再読み込み
  • content_scriptsが開かれていれば、拡張機能再読み込み時に関連タブを自動リロード
  • options_pageが開かれていれば、拡張機能再読み込み時にオプションページを自動で開く

options_pageは拡張機能再読み込み時に自動で閉じてしまうため、リロードではなく再び「開く」ことにしました。

extension-reloader.jsがプラグインとして読み込むファイルです。
content_scripts, options_page, background それぞれのjsファイルに、WebSocketで通信するコードをビルド時に差し込みます。

画像を読み込みたいとき

拡張機能のフォルダに含まれている画像ファイルなどにアクセスする場合、web_accessible_resourceschrome.extension.getURLを使う必要があります。
V2までは "web_accessible_resources": ["imgs/*.png"] のように書きました。V3からはアクセス範囲を限定するために、以下のようにresourcesmatchesをセットで書くようになりました。

"web_accessible_resources": [{
  "resources": [ "hoge.png", "foo.png" ],
  "matches": [ "https://example.com/*" ]
}],

パスの指定は"imgs/hoge.svg"ではなく、chrome.extension.getURL("imgs/hoge.svg")のように書きます。これを指定しないと、Chrome拡張機能のスクリプトが差し込まれる側のURLからリソースを探そうとしてしまいます。

最初はSVGを画像として読み込んでいたのでこの方法を使っていましたが、途中からはSVGをVueのコンポーネントとして読み込むことにしたため、現在のAmazing Searcherでこの方法は使っていません。
参考:Manifest - Web Accessible Resources - Chrome Developers

Webpackの設定

webpackの設定は、 共通のwebpack.common.js, 本番用webpack.prod.js, 開発用webpack.dev.js の3つに分けました。
この構成は、Production | webpackに書かれているものです。

webpack.common.js

splitChunksの設定

backgroundで指定するファイルは1つだけのため、他のエントリポイントと共通化をしないようchunksを設定しました。

optimization: {
  splitChunks: {
    name: 'chunk',
    chunks(chunk) {
      return chunk.name !== 'background';
    },
  }
},

参考:webpack.js.org/split-chunks-plugin.md at master · webpack/webpack.js.org

webpack.dev.js

前述のManifest V3自動リロード対応で書いた自作プラグインを読み込んでいます。
また、Webpackのビルドの表示を抑えるため、以下のようにstatsを設定しました。

stats: {
  colors: true,
  hash: false,
  version: false,
  timings: false,
  assets: false,
  chunks: false,
  modules: false,
  reasons: false,
  children: false,
  source: false,
  publicPath: false
},

statsに文字列を指定してプリセットを使用する方法もあります。

参考

webpack.prod.js

minifyの設定

こちらのブログによると、

Ordinary minification, on the other hand, typically speeds up code execution as it reduces code size, and is much more straightforward to review. Thus, minification will still be allowed, including the following techniques:
* Removal of whitespace, newlines, code comments, and block delimiters
* Shortening of variable and function names
* Collapsing the number of JavaScript files

と書かれており、以下がOKでそれ以外がだめなようです。

  • 「ホワイトスペース、改行、コメント、ブロックを区切る文字」の削除
  • 変数と関数の名前を短くすること
  • ファイルの数を減らすこと

デフォルトでも問題ないですが、デフォルトがアップデート等で変わったら困るため、一応こちらのブログのようにterserを使ってascii_onlyfalseに設定しました。

VueのCLIを使うときでもデフォルトで問題ないですが、自分で設定したい場合はVue側で既に設定している内容

const TerserPlugin = require('terser-webpack-plugin')
    const terserOptions = require('./terserOptions')
    webpackConfig.optimization
      .minimizer('terser')
        .use(TerserPlugin, [terserOptions(options)])
  })

をいじるので、以下のようになると思います。

vue.config.js
config.optimization.minimizer('terser').tap(args => {
  const { terserOptions } = args[0];
  terserOptions.output.ascii_only = false;
  return args
  }

参考

Zipにする

erikdesjardins/zip-webpack-pluginを使い、ビルド時にZipファイルを生成するようにしました。

const ZipPlugin = require('zip-webpack-plugin');
// ...
  plugins: [
    new ZipPlugin({
      filename: path.basename(__dirname) + ".zip",
      pathPrefix: path.basename(__dirname)
    }),
  ]
//...

content scripts

Tailwind CSS適用時にVueで差し込まれる側の要素の表示が崩れる問題

Amazing Searcher では、Googleの検索結果(https://www.google.com/search?*)に当てはまるページへ content scripts で作ったものを差し込んでいます。そこで、差し込まれる側(Googleの検索結果)に差し込む側(拡張機能)のCSSが混じらないように、以下のようにprefixを付けることにしました。

postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
    'postcss-prefix-selector': {prefix: '#amzSchRoot'}
  },
}

#amzSchRootは差し込む側のルートのidです。

Tailwind CSSで訪問済みリンクの色を変える

visitedはTailwind CSSにもありますが、デフォルトではオフになっているため設定が必要です。

tailwind.config.js
module.exports = {
  variants: {
    extend: {
      textColor: ['visited'],
    }
  },
}

設定すれば、以下のように使えます(結局 Amazing Searcher では使いませんでした)。

<a  class="text-blue-600 visited:text-purple-600">hoge</a>

参考:https://tailwindcss.com/docs/hover-focus-and-other-states#visited

createAppの段階でデータを渡したい

第2引数に指定してあげれば良いです。

main.js
createApp(App, {nowURL: "hoge", paramTbm: "fuga"}).mount('#amzSchRoot');

参考:propsData | Vue.js

options

chrome.storage.local.setに配列を保存&取り出し

chrome.storage.local.set({ key: array });で配列を保存しても、chrome.storage.local.get(key、(hoge)=>{fuga});するときには配列ではなくObjectとして取り出されてしまいます。
例

保存する値はJSON化できる必要があるため、JSON.stringify(array)JSON.parse(data)を使う必要があります。

取り出す時の例
chrome.storage.local.get("terms", (result) => {
  if (typeof result.terms !== "undefined") {
    termRef.value.push(...JSON.parse(result.terms));
  }
});

Vueでの値の受け渡しが悪いのかと思ってめちゃくちゃ時間食った部分でした。

参考

Vueでformの検証を使いたい

formだけではなく、イベント修飾子を使います。preventへ渡すonSubmitでリアクティブなデータを処理することにしました。

<form @submit.prevent="onSubmit">
  <!-- なにかinputタグ書く -->
  <button type="submit">Add</button>
</form>

参考:イベントハンドリング | Vue.js

VueでNumberをpropsで渡したい

Javascriptの式であると伝えなければ文字列だと思われてしまうので、動的にv-bindとして渡す必要があります。

公式ドキュメントより引用
<blog-post :likes="42"></blog-post>

参考:プロパティ | Vue.js

最後に

スクラップのまとめなんですが、もう少し文脈を入れて書くべきだったなぁと思いました。逆に、文脈を添えて書いたものはマークダウンをコピペして整えるだけなので楽でした。