💭

Dart (altJS) でChrome拡張機能を作成してみた

2024/04/21に公開

画面上に可愛いキャラクターを配置したくなることはありますよね?
もう既にそういった拡張機能を作成、公開されている方もいるようですが...

今回は勉強も兼ねてゼロから作成してみようと思います。

作業リポジトリ↓
https://github.com/char5742/summon_character

まずはじめに

作成時間は8h
使用言語はFlutterでおなじみのDart言語です。これは一応?altJSなので拡張機能もそのまま作れてしまうのです。
下記の公式チュートリアルを行ったところ、単一のjsとmanifestさえあれば拡張機能が作れることが判明したので、最近javascriptとの連携で話題のDart言語を採用しました。
https://developer.chrome.com/docs/extensions/get-started?hl=ja

拡張機能の概要

ディレクトリ構成
ディレクトリ構成
.
├── .dart_tool
├── build
├── lib
│   ├── character
│   │   ├── character_movement.dart
│   │   ├── chatater.dart
│   │   ├── background.dart
│   │   └── chrome_api.dart
│   └── content_script.dart
├── public
│   ├── images
│   └── global.css
├── .gitignore
├── analysis_options.yaml
├── CHANGELOG.md
├── manifest.json
├── pubspec.lock
├── pubspec.yaml
└── README.md

今回作成したものはとてもシンプル、chromeの画面上をキャラクターがランダムに動き回るだけです!

manifest.json
{
  ...
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["build/content_script.js"],
      "css": ["public/global.css"]
    }
  ],
  ...
  "background": {
    "service_worker": "build/background.js"
  }
}

content_scriptsで画面上にキャラクターを召喚、移動させ、
backgroundでアクティブタグの検知、キャラクターの行動ON,OFFを切り替えます。

content_scriptsだけでもよかったのですが、そのままではタブを切り替えるたびに中身が別のキャラクターとなり寂しいので。

以下具体的な実装です。

content_script.dart
void main() {
  final divElement = HTMLDivElement();
  final imgUrl = chrome.runtime.getURL("public/images/dog.png").toDart;
  final imageElement = HTMLImageElement();
  imageElement.src = imgUrl;
  divElement.append(imageElement);
  document.documentElement?.append(divElement);
  divElement.setAttribute("id", "summonCharacters");
  final character = Character.instance;
  character.applyPositionToStyle = (p0) {
    divElement.style.setProperty("--top", "${p0.top}vh");
    divElement.style.setProperty("--left", "${p0.left}vw");
  };
  character.startAction();

  chrome.runtime.onMessage
      .addListener((String method, JSObject sender, JSFunction sendResponse) {
    switch (method) {
      case "startCharacter();":
        character.startAction();
      case "stopCharacter();":
        character.stopAction();
    }
  }.toJS);
}

  1. content_script.dartでhtmlエレメントにキャラクター要素を追加
  2. キャラクターの行動を開始
  3. backgroundのserviceから送られてくるmessagをlistenする
background.dart
void main() {
  chrome.tabs.onActivated.addListener((ActiveInfo activeInfo) {
    chrome.tabs.query(
        {}.toJSBox,
        (JSArray<Tab> tabs) {
          for (final tab in tabs.toDart) {
            if (tab.id != activeInfo.tabId) {
              chrome.tabs
                  .sendMessage(tab.id, "stopCharacter();")
                  .toDart
                  .catchError((_) => null);
            } else {
              chrome.tabs
                  .sendMessage(tab.id, "startCharacter();")
                  .toDart
                  .catchError((_) => null);
            }
          }
        }.toJS as JSTabsQueryCallback);
  }.toJS as JSTabsJSOnactivatedCallback);
}
  1. アクティブなタブの変更を検知する
  2. 新しくアクティブなタブでは行動を開始し、非アクティブなタブでは行動をストップさせる

Characterクラスの方で、stop時に現在地点をstorage api経由で読み込んだり保存したりしています。

あとはそれぞれをjsでコンパイルすれば終わりです。

dart compile js lib/content_script.dart -o build/content_script.js
dart compile js lib/background.dart -o build/background.js

新しい学び

拡張機能の作成について

思った以上に簡単で驚きました。これならばちょっとしたUIの拡張やアクションの変更くらいならパッと実装できそうです。

js_interopについて

今回は新しいdart:js_interopを使用して、jsのchrome apiにdartの型を与えて実行しています。これは Dart 3.3 で書かれていますが、私は3.2以前の方を触れていないので違いがわかりません。

chrome_api.dart
import 'dart:js_interop';

()
external JSChrome get chrome;

()

class JSChrome {}

extension JSChromeJS on JSChrome {
  external JSRuntime runtime;
...
}

extension type JSRuntime._(JSObject _) {
  external JSString getURL(String path);
...
}
...

chrome apiと変数名を揃えるだけで、このようにdartで呼び出せるのです。

  final imgUrl = chrome.runtime.getURL("public/images/dog.png").toDart;
// dart:js
//final imageUrl = context['chrome']['runtime'].callMethod('getUrl', ['public/images/dog.png']);

もしjs側にオブジェクトを渡したいのならオブジェクトの型を追加するだけです。

extension type JSPosition._(JSObject _) implements JSObject {
  external factory JSPosition({
    num top,
    num left,
  });
  external num get top;
  external num get left;
}
final jsPosition = JSPosition(
    top: _movement.currentPosition.top,
    left: _movement.currentPosition.left);
chrome.storage.local.set(jsPosition);

感想

作業時間の大半をjs_interopに費やしているので寄り道しすぎた感もありますが、JSの資産を使わない開発においてはDartでも十分行えるというのがわかったのはとても良かったです。
本当は歩行差分のある立ち絵キャラクターを使用して歩かせようとしていたのですが、そこまでは間に合いませんでした。
https://booth.pm/ja/items/4539371

参考にしたサイト

https://dart.dev/interop/js-interop/usage
https://zenn.dev/koji_1009/scraps/b7ed67932982e6

Discussion