CRXJS Vite Pluginを使って爆速でChrome拡張機能を作るチュートリアル(V3対応)

CRXJS Vite Pluginを使ってChrome拡張機能を作る方法を紹介します。初心者向けの入門記事です。
2022.07.07

どうも。 えーたん(@eetann092)です。

先日社内LTに登壇しました。 タイトルは「Chrome拡張機能の開発がやめられねぇ」です。

今回はその発表内容を一部抜粋し、CRXJS Vite Pluginを使ったChrome拡張機能の作り方をチュートリアル形式で紹介します。

完成形を見る

このチュートリアルでは、「ユーザーが入力した文字列と一致するハッシュタグ」がブックマーク名にある時に、まとめてタブとして開く拡張機能を作ります。

たとえば、以下のようなブックマークがあるとします。 ブックマーク名として、ハッシュタグ#morning, #zennなどが含まれています。

ここで、ブラウザの右上にある拡張機能のアイコンをクリックすると、入力欄が開きます。 ユーザーが入力欄にmorningと入力してEnterを押します。

すると、ハッシュタグ#morningをブックマーク名に含む3つのブックマークがタブとして開かれます。

これと同じ機能を実装します。

インストール

今回は、ViteReactTypeScriptを入れて作ります。 CRXJS Vite pluginと、Chrome拡張機能固有の型を追加する @types/chromeも入れます。

ゼロから拡張機能の開発をすると、コードを書き換えるたびに管理画面の更新ボタンを押す必要があります。 しかし、CRXJS Vite pluginを使えば自動で再読み込みされるため、更新ボタンを手動で押す手間が省けます。

また、manifest.jsonという拡張機能固有のファイルをTypeScriptを使って書くこともできます。

実際にファイルを作成していきます。まず、Viteのプロジェクトを作成します。

npm init vite@latest

プロジェクト名はopen-bookmarksにしました。 途中、React(react)とTypeScript(react-ts)を選択します。

次に、作成されたディレクトリに移動してパッケージをインストールします。

cd open-bookmarks
npm install
npm i @crxjs/vite-plugin -D
npm i @types/chrome

vite.config.tsの更新

最初にvite.config.tsを書き換えます。

以下が書き換え後のvite.config.tsの全体です。

vite.config.ts

import { crx, defineManifest } from "@crxjs/vite-plugin";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

const manifest = defineManifest({
  manifest_version: 3,
  name: "Open Bookmarks",
  version: "1.0.0",
  permissions: ["bookmarks"],
  action: {
    default_popup: "index.html",
  },
});

export default defineConfig({
  plugins: [react(), crx({ manifest })],
});

CRXJS Vite Pluginのimport、manifestの定義、プラグインの読み込みを加えました。

manifestについて詳しく見ていきます。

CRXJS Vite PluginのdefineManifestを使うことでmanifest.jsonをTypeScriptで書いています。 manifest.jsonは拡張機能固有のファイルです。拡張機能の名前やバージョン、パーミッションなどを書きます。

vite.config.ts

const manifest = defineManifest({
  manifest_version: 3,
  name: "Open Bookmarks",
  version: "1.0.0",
  permissions: ["bookmarks"],
  action: {
    default_popup: "index.html",
  },
});

今回のpermissionsには、ブックマークを取得するためのbookmarksを指定しました。 これらを指定することで、chrome.bookmarks.hogeのようなChrome拡張機能のAPIが使えます。

↓2022-09-21追記
公開当初はpermissionsにtabsを追加していましたが、chrome.tabs.createであればこのpermissionは指定しなくても動くようです(参考:chrome.tabs - Chrome Developers)。

manifest_versionには3を指定します。このmanifest自体のバージョンによって拡張機能の書き方が違います。 以前のバージョンである2の情報が多いため、記事やコードを参考にする際はmanifest_version3であるか注意しましょう。

actionでは、拡張機能のアイコンをクリックしたらindex.htmlをポップアップ表示するように指定しました。

動かしてみよう

vite.config.tsを書き換えた時点で、拡張機能として動かすことができるため、試してみます。

まずは以下のコマンドを実行します。

npm run dev

次に、拡張機能の管理ページを開きます。 chrome://extensions/にアクセスするか、ブラウザ右上のメニューより「その他のツール>拡張機能」をクリックします。

拡張機能の管理ページで、デベロッパーモードをオンにします。

「パッケージ化されていないされていない拡張機能を読み込む」のボタンを押します。

先程作ったプロジェクトのdistディレクトリを選択します。

ブラウザの右上に表示される拡張機能のアイコンをクリックすると、index.htmlが表示されます。

これで拡張機能として動かせることは確認できました。

ブックマークの一覧を取得する

次は、拡張機能固有の処理を書いてみます。 まずはブックマークの一覧を取得します。

src/App.tsxを以下のように書きまえました。確認のため、取得したブックマーク名を表示しています。

src/App.tsx

import { useEffect, useState } from "react";

const App = (): JSX.Element => {
  const [allBookmarks, setAllBookMarks] = useState<
    chrome.bookmarks.BookmarkTreeNode[]
  >([]);

  useEffect(() => {
    chrome.bookmarks.search({}, (bookmarkItems) => {
      setAllBookMarks(bookmarkItems.filter((item) => "url" in item));
    });
  }, []);

  return (
    <div>
      {allBookmarks.map((bookmark) => {
        return <p key={bookmark.id}>{bookmark.title}</p>;
      })}
    </div>
  );
};

export default App;

1つずつ見ていきます。

src/App.tsx

const [allBookmarks, setAllBookMarks] = useState<
  chrome.bookmarks.BookmarkTreeNode[]
>([]);

まず、useStateですべてのブックマークをstateに入れる準備をします。

src/App.tsx

useEffect(() => {
  chrome.bookmarks.search({}, (bookmarkItems) => {
    setAllBookMarks(bookmarkItems.filter((item) => "url" in item));
  });
}, []);

useEffectでは、ブックマークを取得する処理を書きました。

chrome.bookmarks.searchを使い、第一引数に空のオブジェクトを指定することで、すべてのブックマーク情報を取得します。 ただし、取得したブックマークの情報bookmarkItemsにはブックマークフォルダの情報も含んでいます。そこで、ブックマークフォルダを除外するために、urlプロパティを含むもの(=フォルダ以外)だけにします。

最後に、取得したブックマークのタイトルを確認のために表示します。

src/App.tsx

<div>
  {allBookmarks.map((bookmark) => {
    return <p key={bookmark.id}>{bookmark.title}</p>;
  })}
</div>

ブラウザの右上に表示される拡張機能のアイコンをクリックすると、ブックマークのタイトルがずらりと並んでいます。

指定したタグを持つブックマークを開く

指定したタグを持つブックマークをタブとして開けるように、さらにsrc/App.tsxを書き換えます。

以下が書き換え後のsrc/App.tsxの全体です。

src/App.tsx

import { useEffect, useRef, useState } from "react";

const App = (): JSX.Element => {
  const [allBookmarks, setAllBookMarks] = useState<
    chrome.bookmarks.BookmarkTreeNode[]
  >([]);
  const query = useRef<HTMLInputElement>(null);

  useEffect(() => {
    chrome.bookmarks.search({}, (bookmarkItems) => {
      setAllBookMarks(bookmarkItems.filter((item) => "url" in item));
    });
  }, []);

  return (
    <div>
      <input
        autoFocus={true}
        type="text"
        ref={query}
        onKeyPress={(e) => {
          if (e.key === "Enter") {
            const regexp = new RegExp(`#${query.current?.value}(\\s|$)`);
            const bookmarks = allBookmarks.filter((item) =>
              regexp.test(item.title)
            );
            for (const bookmark of bookmarks) {
              chrome.tabs.create({ url: bookmark.url });
            }
          }
        }}
      />
    </div>
  );
};

export default App;

1つずつ見ていきます。

src/App.tsx

const query = useRef<HTMLInputElement>(null);

まず、useRefで入力欄の値を取得できるようにしました。

src/App.tsx

<input
  autoFocus={true}
  type="text"
  ref={query}
  onKeyPress={(e) => {
    if (e.key === "Enter") {
      const regexp = new RegExp(`#${query.current?.value}(\\s|$)`);
      const bookmarks = allBookmarks.filter((item) =>
        regexp.test(item.title)
      );
      for (const bookmark of bookmarks) {
        chrome.tabs.create({ url: bookmark.url });
      }
    }
  }}
/>

inputタグでは、Enterが押されたときにブックマークを開く処理をしています。

src/App.tsx

const regexp = new RegExp(`#${query.current?.value}(\\s|$)`);
const bookmarks = allBookmarks.filter((item) =>
  regexp.test(item.title)
);

ブックマーク名(ここではitem.title)にユーザーの入力queryを含むものだけに絞り込みます。

src/App.tsx

for (const bookmark of bookmarks) {
  chrome.tabs.create({ url: bookmark.url });
}

最後に、絞り込んだブックマークのURLをchrome.tabs.createを使ってタブとして開きます。

ブラウザの右上に表示される拡張機能のアイコンをクリックすると、入力欄が表示されます。 Enterを押した時に「入力した文字列と一致するハッシュタグ」がブックマーク名に存在すると、タブとして開きます。

ショートカットキーを追加する

いちいち拡張機能のアイコンをクリックするのは時間がかかります。そこで、ショートカットキーで入力欄が表示されるようにします 。 ショートカットキーを追加するには、manifestを書き換えます。

以下が書き換え後のvite.config.tsの全体です。

vite.config.ts

import { crx, defineManifest } from "@crxjs/vite-plugin";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

const manifest = defineManifest({
  manifest_version: 3,
  name: "Open Bookmarks",
  version: "1.0.0",
  permissions: ["bookmarks"],
  action: {
    default_popup: "index.html",
  },
  commands: {
    _execute_action: {
      suggested_key: {
        default: "Ctrl+Shift+Y",
      },
    },
  },
});

export default defineConfig({
  plugins: [react(), crx({ manifest })],
});

ショートカットキーはmanifestのcommandsに登録します。

本来はchrome.commands.onCommand.addListenerを使って実行されたコマンドの判定が必要です。しかし、今回のようにmanifestのactionの処理をショートカットキーに登録する場合は、予約された名前_execute_actionに書くだけでよいです。

_execute_actionsuggested_keyにOSごとにショートカットキーを登録できます。今回は共通のdefaultにしました。

上記の例ではCtrl+Shift+Yと書いていますが、Macの場合はCtrlCommandに変換されて登録されます。逆に、macOSのcontrolキーを指定する場合は、defaultではなくmacCtrlではなくMacCtrlと書いて指定します。

使えるキーや組み合わせなど、詳しくは公式ドキュメントを御覧ください。

実際に今回登録したキーを押すと、入力欄が開きます。また、拡張機能管理ページの左上のメニューより「キーボードショートカット」を開くと、登録したショートカットキーが確認できます。chrome://extensions/shortcutsからもアクセスできます。

npm run devの実行を止めると、localhostとの接続が切れるためエラーになります(管理画面から確認できます)。 これ以上ソースコードを編集しない場合は、ここで一度ビルドし、管理画面の更新ボタンを押して再読み込みしましょう。

npm run build

以上でチュートリアルは終了です。今回はポップアップ表示だけでしたが、拡張機能には既存のページに処理を加えるContent Scriptbackground、オプションページなどさまざまな機能があります。

以下のリンク集を参考に、ぜひオリジナルの拡張機能を作ってみてください。

リンク集

サンプルコードのGitHubのリンクは以下です。

以下、Chrome拡張機能開発で役に立ちそうなリンク集です。