🧚

Angular v19 で始める、Chrome拡張機能作り

2024/12/05に公開

これは Angular Advent Calendar 2024 5日目の記事です。


普段みなさんはどのような拡張機能を使われていますか?
Chrome ウェブストア - 拡張機能

公開されている便利な拡張機能は様々ありますが、個人で楽しむ拡張機能を自分で作ることも可能です。
今回は、毎日開くWebサイト上にある画像を自分好みに置き換える、というとても小さな拡張機能を作ってみました。
Angularを使って作ってみたので、簡単に作り方を記述します。

拡張機能の Hello world

ng new するまで。

❯ mise install node@22
mise node@22.11.0 ✓ installed                                                                                                    
❯ mise use node@22
mise ~/opt/.mise.toml tools: node@22.11.0

❯ node -v
v22.11.0

❯ npm install -g @angular/cli

added 296 packages in 15s

52 packages are looking for funding
  run `npm fund` for details
Reshimming mise 22...

❯ ng version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 19.0.2
Node: 22.11.0
Package Manager: npm 10.9.0
OS: darwin arm64

Angular: 
... 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.1900.2 (cli-only)
@angular-devkit/core         19.0.2 (cli-only)
@angular-devkit/schematics   19.0.2 (cli-only)
@schematics/angular          19.0.2 (cli-only)

❯ ng new image-change-chrome-extension
✔ Which stylesheet format would you like to use? CSS             [ https://developer.mozilla.org/docs/Web/CSS                    
 ]
✔ Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? no
CREATE image-change-chrome-extension/README.md (1489 bytes)
CREATE image-change-chrome-extension/.editorconfig (314 bytes)
CREATE image-change-chrome-extension/.gitignore (587 bytes)
CREATE image-change-chrome-extension/angular.json (2682 bytes)
CREATE image-change-chrome-extension/package.json (1060 bytes)
CREATE image-change-chrome-extension/tsconfig.json (915 bytes)
CREATE image-change-chrome-extension/tsconfig.app.json (424 bytes)
CREATE image-change-chrome-extension/tsconfig.spec.json (434 bytes)
CREATE image-change-chrome-extension/.vscode/extensions.json (130 bytes)
CREATE image-change-chrome-extension/.vscode/launch.json (470 bytes)
CREATE image-change-chrome-extension/.vscode/tasks.json (938 bytes)
CREATE image-change-chrome-extension/src/main.ts (250 bytes)
CREATE image-change-chrome-extension/src/index.html (312 bytes)
CREATE image-change-chrome-extension/src/styles.css (80 bytes)
CREATE image-change-chrome-extension/src/app/app.component.css (0 bytes)
CREATE image-change-chrome-extension/src/app/app.component.html (19903 bytes)
CREATE image-change-chrome-extension/src/app/app.component.spec.ts (985 bytes)
CREATE image-change-chrome-extension/src/app/app.component.ts (305 bytes)
CREATE image-change-chrome-extension/src/app/app.config.ts (310 bytes)
CREATE image-change-chrome-extension/src/app/app.routes.ts (77 bytes)
CREATE image-change-chrome-extension/public/favicon.ico (15086 bytes)
✔ Packages installed successfully.
    Successfully initialized git.
diff --git a/angular.json b/angular.json
index 79879b6..c570663 100644
--- a/angular.json
+++ b/angular.json
@@ -24,7 +24,8 @@
               {
                 "glob": "**/*",
                 "input": "public"
-              }
+              },
+              "src/manifest.json"
             ],
             "styles": [
               "src/styles.css"
  • chrome://extensions/ にアクセスして、Load unpacked をクリック

開発中の拡張機能を読み込む
開発中の拡張機能を読み込む

  • dist/browser を選択
  • すると、拡張機能がロードされる

拡張機能が読み込まれた様子
拡張機能が読み込まれた様子

  • 拡張機能の一覧からPinして、

拡張機能をツールバーに表示させる
拡張機能をツールバーに表示させる

  • 拡張機能のアイコンをクリックすると、Angularの初期ページが表示される

拡張機能クリック時の様子
拡張機能クリック時の様子

99割できた。

ちなみに、build ではなく、watch しながらファイルを編集した場合、都度の拡張機能ロードは必要ありません。(便利!)
ただし、manifest.json 等を編集した場合は、再ロードが必要になります。

拡張機能の見た目を変更する

拡張機能をクリックした時に表示される領域を編集しましょう。
ここは通常のAngularアプリの作成と同じです。

src/app/app.component.html を編集します。
画像を選択させたいので、input タグを追加します。
あとでファイル選択時の処理を追加するので、onChangeイベントも設定しておきます。

<div class="container">
  <div class="item">
    <input type="file" accept="image/png, image/jpeg" (change)="onFileSelected($event)" />
  </div>
</div>

適宜 css も追加します。

HTMLを編集して拡張機能をクリックした様子
HTMLを編集して拡張機能をクリックした様子

拡張機能側から、Chrome の ActiveTab を取得する

拡張機能上で画像を選択させ、特定サイトの画像を上書きするため、拡張機能側でアクティブなタブを取得します。
そのために、chrome のAPIを使います。
Angular上で使用できるようにするために、npm install @types/chrome しておき、
compilerOptionschrome を追加します。

diff --git a/tsconfig.app.json b/tsconfig.app.json
index 3775b37..c4f4ede 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -4,7 +4,7 @@
   "extends": "./tsconfig.json",
   "compilerOptions": {
     "outDir": "./out-tsc/app",
-    "types": []
+    "types": ["chrome"]
   },
   "files": [
     "src/main.ts"

これで、以下のAPIが使えるようになりました。
https://developer.chrome.com/docs/extensions/reference/api/tabs?hl=ja

アクティブタブ内のDOMを変化させる

例として、https://www.google.com/ の画像を変化させてみましょう。

変化前のgoogle.com
変化前のgoogle.com

chrome.scripting.executeScript を使ってActive Tabに対してスクリプトを実行します。
ざっくりとしたイメージはこんな感じ。

▼ 拡張機能側

src/app/app.component.ts
import { Component } from '@angular/core';
import { setImage } from '../../set-image';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'image-change-chrome-extension';

  onFileSelected(event: Event): void {
    const input = event.target as HTMLInputElement;
    if (input.files && input.files[0]) {
      const reader = new FileReader();
      reader.onload = (e) => {
        const base64Image = e.target?.result as string | ArrayBuffer;
        if (typeof base64Image === 'string') {
          this.executeScriptOnTargetPage(base64Image);
        }
      };
      // 選択されたファイルをData URLとして読み込む
      reader.readAsDataURL(input.files[0]);
    }
  }

  executeScriptOnTargetPage(base64Image: string): void {
    chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
      // 対象のタブ内で動作させるスクリプト
      chrome.scripting.executeScript({
        target: { tabId: tabs[0].id! },
        func: setImage,
        args: [base64Image]
      });
    });
  }
}

▼ アクティブなタブで実行されるスクリプト

set-image.ts
export const setImage = (base64Image: string): void => {
  const element = document.querySelector('.lnXdpd');
  if (element) {
    const newImg = document.createElement('img');
    newImg.className = 'lnXdpd';
    newImg.src = base64Image;

    element.replaceWith(newImg);
  }
}

画像が差し替わりました。

google.comの画像が差し替わった様子
google.comの画像が差し替わった様子

実際には

拡張機能上で選択した画像ファイルは、拡張機能を閉じると消えてしまいます。
永続的に保持しておくためには、クラウド上に保存したり工夫が必要です。
特に懸念事項がなければ LocalStorage も使えるかもしれません。

また、画像を都度選択するのは面倒です。特定のページを開いたときに、保存された画像を自動で適用させたいです。

特定ページを開いたときに実行させるスクリプトを組み込みたい、そんな時は以下が使えます。

コンテンツ スクリプト  |  Chrome Extensions  |  Chrome for Developers

  • 特定サイトで動作するスクリプトを設置
    • 例)src/content_script.ts
      • 対象のページでDOMを書き換えます
  • @angular-builders/custom-webpack 等を使って、ビルド時に上記ファイルも含めるようにする
  • src/manifest.json コンテンツスクリプトの定義を記述
json
diff --git a/src/manifest.json b/src/manifest.json
index 09a34e1..6446634 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -9,5 +9,11 @@
   ],
   "action": {
     "default_popup": "index.html"
-  }
+  },
+  "content_scripts": [
+    {
+      "matches": ["https://google.com/"],
+      "js": ["contentScript.js"]
  • google.com にアクセス
  • スクリプトが動作し、DOMにアクセスし画像が書き換わる

このあたりは設定など複雑になるのでここでは割愛します。


拡張機能を作ってみようと思ったときの一助になれば幸いです。
良き拡張機能ライフを!

明日は、@nishitakuさんです。

参考

Discussion