前回の続き。Vite/CRXJS/Vueで作るときの備忘録(*´ω`*)
使ったサンプルはこちらで公開中(*´ω`*)
環境構築
プロジェクトの作成
# viteでプロジェクトを作成 $ pnpm create vite chrome-extension-sample --template vue-ts $ cd chrome-extension-sample # .npmrcを設定 $ echo "auto-install-peers=true" > .npmrc # @crxjs/vite-pluginの追加。vite3はbeta版 $ pnpm add @crxjs/vite-plugin@beta -D
manifest.jsonの設定
manifest.jsonが必要だけど、CRXJSでは.tsにも対応してる。
補完や環境変数で切り替えができるのでJSONよりも便利。
// manifest.config.ts import { defineManifest } from "@crxjs/vite-plugin"; export const manifest = defineManifest({ manifest_version: 3, name: "chrome-extension-sample", description: "chrome-extension-sample", version: "0.0.1", action: { default_popup: "index.html", }, });
viteでビルドするときにmanifest.config.ts
が含まれるように、
tsconfig.node.json
を変更しておく。
# tsconfig.node.json { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "manifest.config.ts"] }
最後に、vite.config.ts
へCRXJSプラグイン関連の設定を追加する。
// vite.config.ts + import { crx } from "@crxjs/vite-plugin"; import vue from "@vitejs/plugin-vue"; import { defineConfig } from "vite"; + import { manifest } from "./manifest.config"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [vue()], + plugins: [vue(), crx({ manifest })], });
Chrome Extesions APIの型定義を追加
型定義が提供されているので、追加しておく。
$ pnpm add -D chrome-types
実際の拡張機能側で使うので、tsconfig.json
に追加
# tsconfig.json { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "resolveJsonModule": true, - "isolatedModules": true, + "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true, - "noEmit": true + "noEmit": true, + "types": ["chrome-types"] }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] }
Tailwind CSSのインストール
必要なパッケージをインストール。
$ pnpm add -D postcss tailwindcss autoprefixer
各種設定ファイルを追加。
// postcss.config.cjs module.exports = { plugins: { "postcss-import": {}, "tailwindcss/nesting": {}, tailwindcss: {}, autoprefixer: {}, }, };
// tailwind.config.cjs /** @type {import('tailwindcss').Config} */ module.exports = { prefix: "twcrx-", content: ["./pages/*.html", "./src/**/*.{vue,ts}"], };
/* src/assets/tailwind.css */ @tailwind base; @tailwind components; @tailwind utilities;
最後にtailwind.css
を読み込むように、
src/main.ts
を変更。
// src/main.ts
import { createApp } from "vue";
+ import "./assets/tailwind.css";
import "./style.css";
import App from "./App.vue";
createApp(App).mount("#app");
使い方
popupの追加
デフォルトではindex.html
が追加されているので、それを参考に。
構成としては、以下のような関連。
src/App.vue
で画面を構成src/main.ts
で、src/App.vue
をマウントindex.html
で、src/main.ts
を呼び出しmanifest.config.ts
のaction.default_popup
で、index.html
を指定
ポップアップ画面の要素を確認したい場合は、
拡張機能ボタンを右クリックして、「ポップアップを検証」を選択すると、
DevToolsが開くので、そこで確認ができる。
backgroundの追加
backgroundの処理を追加するときはこんな感じ。
まずは、実際の処理を追加。
// src/background.ts // タブがアクティブになったとき chrome.tabs.onActivated.addListener(async ({ tabId, windowId }) => { const tab = await chrome.tabs.get(tabId); console.table({ onActivated: { tabId: tabId, windowId: windowId, "tab.url": tab?.url, "tab.active": tab?.active, "tab.status": tab?.status, "location.href": location.href, }, }); }); // タブが更新されたとき chrome.tabs.onUpdated.addListener((tabId, info, tab) => { console.table({ onUpdated: { tabId: tabId, windowId: tab?.windowId, "tab.url": tab?.url, "tab.active": tab?.active, "tab.status": tab?.status, "location.href": location.href, }, }); });
次にsrc/background.ts
を利用するように、
permissions
とbackground
をmanifest.config.ts
に追加。
// manifest.config.ts import { defineManifest } from "@crxjs/vite-plugin"; export const manifest = defineManifest({ manifest_version: 3, name: "chrome-extension-sample", description: "chrome-extension-sample", version: "0.0.1", + permissions: ["tabs"], action: { default_popup: "index.html", }, + background: { + service_worker: "src/background", + type: "module", + }, });
- Background | CRXJS Vite Plugin
- Chrome Extensions: Manage events with service workers - Chrome Developers
拡張機能の画面から「ビューを検証」の横にある
「Service Worker」をクリックすると、DevToolsでコンソールを確認できる。
static contentsの追加
画面を開いたら、開いている画面になにか処理を埋め込みたい場合はこれ。
まずは、追加したいcontent scriptsを用意。
// src/scripts/addOutline.ts // <a>に赤色のアウトラインを追加 document.body.querySelectorAll("a").forEach((elm) => { elm.style.outline = "dotted 1px red"; }); // <img>に青色のアウトラインを追加 document.body.querySelectorAll("img").forEach((elm) => { elm.style.outline = "dotted 1px blue"; });
次にsrc/scripts/addOutline.ts
を利用するように、
content_scripts
をmanifest.config.ts
に追加。
// manifest.config.ts import { defineManifest } from "@crxjs/vite-plugin"; export const manifest = defineManifest({ // ... permissions: ["tabs"], // ... background: { // ... }, + content_scripts: [ + { + matches: ["<all_urls>"], + js: ["src/scripts/addOutline"], + }, + ], });
dynamic contentsの追加
任意のタイミングで開いている画面になにか処理を埋め込みたい場合はこっち。
SPAなどの場合、ページの変更をタブで検知できないので、
ページが更新されたときに処理を入れたりできる。
background.ts
から、chrome.scripting
APIで差し込む。
// background2.ts import addOutline from "./scripts/addOutline?script"; // タブが更新されたときにcontent scriptsを実行する chrome.tabs.onUpdated.addListener((tabId, info, tab) => { // chromeの画面などの場合は、content scriptsを実行できないので、スキップ if (!tab.url || tab.url.startsWith("chrome://")) return; // 読み込みが完了していない場合は、スキップ if (info.status != "complete") return; chrome.scripting.executeScript({ target: { tabId }, files: [addOutline], }); });
そのままだと型の警告が出るので、vite-env.d.ts
を修正。
/// <reference types="vite/client" /> + declare module "*?script" { + const script: any; + export default script; + }
最後に、動的に差し込めるようにmanifest.config.ts
の
permissions
とhost_permissions
を変更する。
// manifest.config.ts import { defineManifest } from "@crxjs/vite-plugin"; export const manifest = defineManifest({ // ... - permissions: ["tabs"], + permissions: ["tabs", "scripting"], // ... background: { - service_worker: "src/background", + service_worker: "src/background2", type: "module", }, - content_scripts: [ - { - matches: ["<all_urls>"], - js: ["src/scripts/addOutline"], - }, - ], + host_permissions: ["<all_urls>"], });
permissions
でchrome.scripting
APIを使う権限を追加し、
host_permissions
で差し込めるURLを指定する感じ。
小ネタ
iconを設定する
public/
配下に配置して、manifest.config.ts
で設定すればOK
// manifest.config.ts import { defineManifest } from "@crxjs/vite-plugin"; export const manifest = defineManifest({ manifest_version: 3, // ... icons: { "16": "icons/icon_16.png", "32": "icons/icon_32.png", "48": "icons/icon_48.png", "128": "icons/icon_128.png", }, // ... });
必要なアイコンのサイズは以下の通り。形式はPNGがよいっぽい。
Icon Size | Icon Use |
---|---|
16x16 | 拡張機能で表示されるアイコン |
32x32 | Windowsで必要なサイズ |
48x48 | 拡張機能の管理画面で表示されるサイズ |
128x128 | Chrome Web Storeのインストール画面で表示されるサイズ |
画像などのアセットを読み込む
差し込んだcontent scriptsで画像などを扱う場合は、ひと手間必要。
拡張機能のURL配下に配置されるので、
chrome.runtime.getURL()
を使う必要がある。
import logo from './logo.png' const url = chrome.runtime.getURL(logo) const iconUrl = chrome.runtime.getURL("/icons/icon.png")
また、拡張機能内のファイルにアクセスするには、
web_accessible_resources
の設定が必要だけど、
import
を使わない場合は、CRXJSで自動設定されないので、
手動でmanifest.config.ts
に指定が必要。
// manifest.config.ts import { defineManifest } from "@crxjs/vite-plugin"; export const manifest = defineManifest({ // ... + web_accessible_resources: [ + { + matches: ["<all_urls>"], + resources: ["icons/*"], + use_dynamic_url: false, + }, + ], });
content scriptsで.vueを差し込む
content scriptsを使って開いている画面に.vue
を追加したい場合は、
マウント先のdiv
を作って配置する必要がある。
ただの四角を表示する.vue
。
<!-- src/components/DummyView.vue --> <template> <div class="twcrx-w-20 twcrx-h-20 twcrx-bg-red-400"></div> </template>
content scriptsはこんな感じ。
// src/scripts/addDummyView.ts import { createApp } from "vue"; import DummyView from "../components/DummyView.vue"; import "../assets/tailwind.css"; const ROOT_ELEMENT_ID = "crx-root"; // マウント先のdiv要素をbody直下に配置 const root = document.createElement("div"); root.id = ROOT_ELEMENT_ID; root.className = "twcrx-fixed twcrx-top-0 twcrx-left-0 twcrx-z-10"; document.body.append(root); // 追加したdiv要素にマウント const app = createApp(DummyView).mount(root);
.vue
が型定義で警告が出るので、
vue-env.d.ts
に設定を追加しておく。
/// <reference types="vite/client" /> declare module "*.vue" { import type { DefineComponent } from "vue"; const component: DefineComponent<{}, {}, any>; export default component; } declare module "*?script" { const script: any; export default script; }
開発用と本番用でviteの設定を切り替える
vite.config.ts
では、mode
を受け取ることができる。
それに応じて、設定を変更すると便利。
開発時はデバッグしやすくするためにminifyしないようにしたり、
本番時はconsole.logが表示されないようにしたりできる。
import { crx } from "@crxjs/vite-plugin"; import vue from "@vitejs/plugin-vue"; import { defineConfig } from "vite"; import { manifest } from "./manifest.config"; // https://ja.vitejs.dev/guide/env-and-mode.html#modes // mode = "development" | "production" export default ({ mode }) => { const isProd = mode === "production"; return defineConfig({ plugins: [vue(), crx({ manifest })], build: { // 開発時はminifyしない minify: isProd, }, esbuild: { // 本番時はconsoleを削除する drop: isProd ? ["console"] : undefined, }, }); };
manifestでpackage.jsonの値を利用する
名前やバージョンなどはpackage.jsonから使いたいときはこんな感じ。
// package.json { "name": "chrome-extension-sample", "description": "sample for chrome extention", "version": "0.0.0", // ... }
// manifest.config.ts import { defineManifest } from "@crxjs/vite-plugin"; import pkg from "./package.json"; export const manifest = defineManifest({ manifest_version: 3, name: pkg.name, description: pkg.description, version: pkg.version, // ... });
viteでJSONをimportするので、
tsconfig.node.json
側に設定を追加する。
// tsconfig.node.json { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true }, - "include": ["vite.config.ts", "manifest.config.ts"] + "include": ["vite.config.ts", "manifest.config.ts", "package.json"] }
開発用と本番用でmanifestの内容を切り替える
viteの設定と同様に、manifestもmode
を使って切り替えることができる。
開発版では名前の前に「【DEV】」をつける場合はこんな感じ。
// manifest.config.ts import { defineManifest } from "@crxjs/vite-plugin"; import pkg from "./package.json"; export const manifest = defineManifest(async (env) => ({ manifest_version: 3, name: env.mode == "production" ? pkg.name : `【DEV】 ${pkg.name}`, // ... }));
以上!! これで基本的な使い方はわかった気がする(*´ω`*)