くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Vite/VueでChrome拡張機能を作ってみる | 4. 環境構築/開発編

前回の続き。Vite/CRXJS/Vueで作るときの備忘録(*´ω`*)

www.memory-lovers.blog

使ったサンプルはこちらで公開中(*´ω`*)

github.com

環境構築

プロジェクトの作成

# 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.tsaction.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を利用するように、
permissionsbackgroundmanifest.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",
+   },
  });

拡張機能の画面から「ビューを検証」の横にある
「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_scriptsmanifest.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.scriptingAPIで差し込む。

// 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
permissionshost_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>"],
  });

permissionschrome.scriptingAPIを使う権限を追加し、
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}`,
  // ...
}));

以上!! これで基本的な使い方はわかった気がする(*´ω`*)

github.com