CRXJSを使ってReact(Vite+Docker)でChrome拡張を開発してみる
初めに
ReactでChromeの拡張機能を開発する際、CRXJというライブラリが使いやすかったので実際にこのライブラリを使った開発の流れを紹介したいと思います。
CRXJSはviteを使って拡張機能を高速に開発できるライブラリです。
manifest.jsonにファイル名を書いておけば、ビルドディレクトリが編集中のファイルを直接参照するため、各ファイルの更新が即座に反映されます。
実際の開発の流れを見てみましょう。
GitHubリポジトリ
ディレクトリ構成
.
├── Dockerfile
├── docker-compose.yml
├── index.html
├── manifest.config.ts
├── manifest.json
├── options.html
├── package.json
├── public
│ └── vite.svg
├── src
│ ├── assets
│ │ ├── favicon.svg
│ │ └── logo.svg
│ ├── background.ts
│ ├── components
│ │ └── Button.tsx
│ ├── content_scripts
│ │ └── content_script.tsx
│ ├── options.tsx
│ ├── popup.tsx
│ └── vite-env.d.ts
├── tsconfig.json
└── vite.config.ts
React + CRXJS + Viteでプロジェクトを作成
CRXJSのドキュメントを参照しながら進めます。
このドキュメントの手順を、自分の環境に合うように修正しつつ進めていきます。
Create a project|CRXJS Vite Plugin
プロジェクト作成
npm init vite@latest
$ npm init vite@latest
Need to install the following packages:
create-vite@4.3.1
Ok to proceed? (y) y
✔ Project name: … vite-project
✔ Select a framework: › React
✔ Select a variant: › TypeScript
CRXJSをインストール
npm i @crxjs/vite-plugin@beta -D
SVGRのインストール (必要な人だけ)
React componentsでsvgを使いたい場合はインストール
vite-plugin-svgr.
npm i vite-plugin-svgr
Change vite-env.d.ts
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />
Vite configの修正
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { crx, ManifestV3Export } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
import svgr from "vite-plugin-svgr";
export default defineConfig({
plugins: [
svgr(),
react(),
crx({ manifest: manifest as unknown as ManifestV3Export }),
],
});
manifest.json
ファイルをルートディレクトリに作成します。
{
"name": "Extension App",
"description": "",
"version": "0.0.1",
"manifest_version": 3,
"action": {
"default_popup": "index.html",
"default_title": "Open Extension App"
}
}
tsconfigをまとめる(必要な人だけ)
構成を簡潔にしたかったので、tsconfig.node.json
をtsconfig.json
に統合しました。
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src", "vite.config.ts", "*.json"]
}
プロジェクトを動かしてみる
npm run dev
Manage Extensions
ページを開きます。
chrome://extensions/
右上にあるdveloper mode
スイッチをオンにします。
左上にあるLoad unpacked
ボタンをクリックして、dist
ディレクトリを選択。
Build
プロジェクトを実際にアップロードする時は、buildする必要があります。
npm run build
Dockerfileを作成
開発環境をすぐに構築する為にDockerを構築します。
Dockerfile
FROM node:18.15.0-alpine3.16
WORKDIR /usr/src/app
COPY package*.json ./
RUN yarn install
COPY . .
docker-compose.yml
version: '3'
services:
extension:
container_name: extension
hostname: extension
restart: always
tty: true
build:
context: .
dockerfile: Dockerfile
ports:
- 5173:5173
volumes:
- .:/usr/src/app
command: yarn dev --host
networks:
- default
platform: linux/amd64
networks:
default:
私はM1のMacBookを使っているので、docker-compose.yml
ファイルに platform: linux/amd64
の記述を追加して、Dockerの設定の Use Rosetta for x86/amd64 emulation of Apple Silicon
をONにしました。
Run Docker
docker compose up -d --build
Popupnの修正
ファイル名と機能が一致せず、ちょっとわかりにくいので、少し修正します。
App.tx
を削除して、main.tx
のファイル名をpopup.tx
に変更しました。
次に、popup.tsx
を修正します。
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import logo from "./assets/logo.svg";
function Popup() {
const [count, setCount] = useState(0);
return (
<div className="App" style={{ height: 300, width: 300 }}>
<header className="App-header">
<img
src={chrome.runtime.getURL(logo)}
className="App-logo"
alt="logo"
/>
<p>Hello Vite + React!</p>
<p>
<button type="button" onClick={() => setCount((count) => count + 1)}>
count is: {count}
</button>
</p>
<p>
Edit <code>App.tsx</code> and save to test HMR updates.
</p>
<p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
{" | "}
<a
className="App-link"
href="https://vitejs.dev/guide/features.html"
target="_blank"
rel="noopener noreferrer"
>
Vite Docs
</a>
</p>
</header>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<Popup />
</React.StrictMode>
);
また、index.html
ファイルのscriptタグのsrc
属性を変更したファイル名に修正します。
<script type="module" src="/src/popup.tsx"></script>
Content Scriptsの作成
manifest.json
にcontent_scripts
の項目を追加。
{
"name": "Extension App",
"description": "",
"version": "0.0.1",
"manifest_version": 3,
"action": {
"default_popup": "index.html",
"default_title": "Open Extension App"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content_scripts/content_script.tsx"]
}
]
}
content_scripts
ディレクトリをsrc
ディレクトリの中に作成して、サンプルのcontent_scriptコンポーネントを作ります。
src/content_scripts/content_script.tsx
.
import React from "react";
import ReactDOM from "react-dom/client";
import Button from "../components/Button";
function ContentScript() {
return (
<div className="App">
<header className="App-header">
<h1>ContentScript</h1>
<Button>button</Button>
</header>
</div>
);
}
const index = document.createElement("div");
index.id = "content-script";
document.body.appendChild(index);
ReactDOM.createRoot(index).render(
<React.StrictMode>
<ContentScript />
</React.StrictMode>
);
同時にcomponent
ディレクトリとその中にButton.tsx
ファイルを作成しました。
import React from "react";
const Button = (props: any) => <button {...props} />;
export default Button;
Backgroundの作成
manifest.json
にbackground
の項目を定義します。
注: クロームAPIの特定の機能を利用したい場合は、permissions
に機能に対応した特定のパーミッションを追加する必要がある(今回はbackgroundを作成するためbackgroundを追加してます)。以下参考。
Chrome Extensions Declare permissions
{
"name": "Extension App",
"description": "",
"version": "0.0.1",
"manifest_version": 3,
"action": {
"default_popup": "index.html",
"default_title": "Open Extension App"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content_scripts/content_script.tsx"]
}
],
"background": {
"service_worker": "src/background.ts",
"type": "module"
},
"permissions": [
"background",
"contextMenus",
"bookmarks",
"tabs",
"storage",
"history"
]
}
background.ts
ファイルをsrc
ディレクトリ配下に作成。
以下のbackground.ts
のサンプルコードには、タブを変更を検知する機能と、ブックマークを取得する機能を加えました。
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
console.log(`Change URL: ${tab.url}`);
});
chrome.bookmarks.getRecent(10, (results) => {
console.log(`bookmarks:`, results);
});
console.log(`this is background service worker`);
export {};
Optionsの作成
オプションページを作成していきます。
オプションページとは、ブラウザのツールバーの拡張機能アイコンを右クリックし、「オプション」を選択することでアクセスできるページです。
manifest.jsonに
options_page`セクションを追加します。
{
"name": "Extension App",
"description": "",
"version": "0.0.1",
"manifest_version": 3,
"action": {
"default_popup": "index.html",
"default_title": "Open Extension App"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/content_scripts/content_script.tsx"]
}
],
"background": {
"service_worker": "src/background.ts",
"type": "module"
},
"options_page": "options.html",
"permissions": [
"background",
"contextMenus",
"bookmarks",
"tabs",
"storage",
"history"
]
}
options.html
を作成します。
中身はindex.htmlとほぼ同じですが、
scriptタグの
src`属性を変更する必要があります。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Extension App</title>
</head>
<body>
<script type="module" src="/src/options.tsx"></script>
</body>
</html>
script
タグの参照先のoptions.tsx
ファイルをsrc
ディレクトリに作成します。
import React from "react";
import ReactDOM from "react-dom/client";
import Button from "./components/Button";
function Options() {
console.log(`this is options page`);
return (
<div className="App">
<header className="App-header">
<h1>Title</h1>
<Button>button</Button>
</header>
</div>
);
}
const index = document.createElement("div");
index.id = "options";
document.body.appendChild(index);
ReactDOM.createRoot(index).render(
<React.StrictMode>
<Options />
</React.StrictMode>
);
最後に
拡張機能の開発は通常のwebアプリと違ってクセがあるので開発手法に悩みますが、今回はCRXJSを使うことでReactによる拡張機能の開発体験を大幅に向上させることができました。
Developerツールのパネルや他の機能の作成にも取り組もうと思っています。他機能に関しては随時記事を更新する予定です。
お読み頂きありがとうございました。
参考
- Introduction | CRXJS Vite Plugin
- Create a project | CRXJS Vite Plugin
- vite-plugin-svgr
- Chrome Extensions Declare permissions
Discussion