Flutterエンジニアの井戸田です。 今回はFlutterでGoogleChromeの拡張機能を作ることができることを知ったので、その方法を紹介します。
はじめに
この記事はMobility Technologies Advent Calendar 2022の9日目です。
プロジェクトの作成
まずはFlutterでGoogleChrome拡張機能を作るため、Javascript系で動作するFlutter Webのプロジェクトを生成します。
$ flutter create --platforms web chrome_extension_sample
上記のコマンド実行した結果、下記のプロジェクトが生成されます。
. ├── README.md ├── analysis_options.yaml ├── chrome_extension_sample.iml ├── lib │ └── main.dart ├── pubspec.lock ├── pubspec.yaml ├── test │ └── widget_test.dart └── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── index.html └── manifest.json
Flutter Webとしてプロジェクトを生成したので、Webアプリとして実行してみます。
$ flutter run -d chrome
manifest.jsonの編集
/web
フォルダを見てみると manifest.json
がありますが、こちらはPWA機能のために用意されています。(PWAとはProgressive Web Appsの略で、ウェブアプリをネイティブアプリのような体験を提供するための技術です。)
拡張機能でも manifest.json
が必要なので、書き換えていきます。
Before
{ "name": "chrome_extension_sample", "short_name": "chrome_extension_sample", "start_url": ".", "display": "standalone", "background_color": "#0175C2", "theme_color": "#0175C2", "description": "A new Flutter project.", "orientation": "portrait-primary", "prefer_related_applications": false, "icons": [ { "src": "icons/Icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "icons/Icon-512.png", "sizes": "512x512", "type": "image/png" }, { "src": "icons/Icon-maskable-192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "icons/Icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] }
After
{ "manifest_version": 3, "version": "1.0.0", "name": "chrome extension sample", "description": "A Flutter chrome extension", "content_security_policy": { "extension_pages": "script-src 'self' ; object-src 'self'" }, "action": { "default_popup": "index.html", "default_icon": { "16": "icons/Icon-192.png", "32": "icons/Icon-192.png", "48": "icons/Icon-192.png", "128": "icons/Icon-192.png" } }, "icons": { "16": "icons/Icon-192.png", "32": "icons/Icon-192.png", "48": "icons/Icon-192.png", "128": "icons/Icon-192.png" } }
- manifest_version
manifest.json
のバージョンを指定。最新は3
です
- version
- name
- description
- content_security_policy
- Content Security Policyを指定する(後述)
- action
- icons
- ウェブストアやfaviconなどに使われます
- 上の指定ではdefault_iconと同様にFlutterプロジェクト作成時から用意されている192pxのアイコンを使用していますが、16x16px, 32x32px, 48x48px, 128x128pxのアイコンを用意する必要があります。
詳しくは公式ドキュメントを参照してください。
Content Security Policy(CSP)
CSPはXSSやデータインジェクション攻撃などを検知し、ユーザーを保護するためのブラウザのセキュリティ層です。
manifest.jsonでは content_security_policy
を "extension_pages": "script-src 'self' ; object-src 'self'"
と指定しました。
その結果ブラウザはインラインスクリプトや別のオリジンからのスクリプトを実行しません。今回はindex.htmlにインラインスプリントが含まれているためCSPエラーが発生します。
エラーを解消するために web/index.html 内のscriptタグを全て削除し、bodyタグ内に <script src="main.dart.js" type="application/javascript"></script>
を埋め込みます。
main.dart.js
はビルド時に生成されるトランスパイルされたFlutterのコードです。
Before
<!DOCTYPE html> <html> <head> <base href="$FLUTTER_BASE_HREF"> <meta charset="UTF-8"> <meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta name="description" content="A new Flutter project."> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-title" content="chrome_extension_sample"> <link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="icon" type="image/png" href="favicon.png"/> <title>chrome_extension_sample</title> <link rel="manifest" href="manifest.json"> <script> var serviceWorkerVersion = null; </script> <script src="flutter.js" defer></script> </head> <body> <script> window.addEventListener('load', function(ev) { _flutter.loader.loadEntrypoint({ serviceWorker: { serviceWorkerVersion: serviceWorkerVersion, } }).then(function(engineInitializer) { return engineInitializer.initializeEngine(); }).then(function(appRunner) { return appRunner.runApp(); }); }); </script> </body> </html>
After
<!DOCTYPE html> <html> <head> <base href="$FLUTTER_BASE_HREF"> <meta charset="UTF-8"> <meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta name="description" content="A new Flutter project."> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-title" content="chrome_extension_sample"> <link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="icon" type="image/png" href="favicon.png"/> <title>chrome_extension_sample</title> <link rel="manifest" href="manifest.json"> </head> <body> <script src="main.dart.js" type="application/javascript"></script> </body> </html>
web/index.htmlの整理の続き
CSPのエラーは発生しなくなりました。続いて下記の2つを修正していきます。
- 不要なメタタグなどを削除
- htmlタグにサイズを指定
不要なメタタグなどを削除
Safari用のメタタグなど拡張機能を実装する上では不要なタグがあるので削除します。
Before
<!DOCTYPE html> <html> <head> <base href="$FLUTTER_BASE_HREF"> <meta charset="UTF-8"> <meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta name="description" content="A new Flutter project."> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-title" content="chrome_extension_sample"> <link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="icon" type="image/png" href="favicon.png"/> <title>chrome_extension_sample</title> <link rel="manifest" href="manifest.json"> </head> <body> <script src="main.dart.js" type="application/javascript"></script> </body> </html>
After
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>chrome_extension_sample</title> </head> <body> <script src="main.dart.js" type="application/javascript"></script> </body> </html>
htmlタグにサイズを指定
index.htmlは「manifest.jsonの編集」で記載した通り、アイコンをタップした時に表示されるHTMLです。 表示させたいサイズを指定します。
<html style="height: 500px; width: 300px">
ちなみにサイズを指定しないと下の画像の様に表示されます。
flutter buildコマンドを叩き必要なファイルを生成する
$ flutter build web
を叩くと、モバイルブラウザ場合HTML renderer、PCブラウザの場合CanvasKit rendererが使用されます。
しかしCanvasKitでは既知の問題があるため --web-renderer html
オプションをつけ、常にHTML rendererを使用するようにします。実際のissueがこちらです。
またCSP の制限を満たすために --csp
をつけます。
よって下記のコマンドを実行します。
$ flutter build web --web-renderer html --csp
拡張機能をデバッグする
1. Google ChromeでURL入力欄に chrome://extensions
を入力
2. 右上のデベロッパーモードをONにする
3. 「パッケージ化されていない拡張機能を読み込む」をクリック
4. chrome_extension_sample/build/web/
を選択する
上記を行えば下の画像が表示されると思います。
ちなみに一度追加するとリンクされるので、再度 $ flutter build
をした時は、右下のリフレッシュアイコンをクリックすれば読み込まれます。
右上の拡張機能のアイコンをクリックすると、追加した拡張機能をクリックすると表示されると思います。
FlutterでChrome ExtensionのAPIを使う
最後にJavascriptで提供しているChrome ExtensionのAPIをFlutterで使う方法を解説します。 公式ドキュメントはこちらです。
今回は現在開いているタブのURLを表示していきます。
web側の実装
タブのURLを取得するためには Tabs APIを使用します。
1. manifest.jsonにpermissionを追加
manifest.jsonにpermissions
を追加し、そこに tabs
を追加。
{ "manifest_version": 3, "version": "1.0.0", "name": "chrome extension sample", "description": "A Flutter chrome extension", "content_security_policy": { "extension_pages": "script-src 'self' ; object-src 'self'" }, "action": { "default_popup": "index.html", "default_icon": { "16": "icons/Icon-192.png", "32": "icons/Icon-192.png", "48": "icons/Icon-192.png", "128": "icons/Icon-192.png" } }, "icons": { "16": "icons/Icon-192.png", "32": "icons/Icon-192.png", "48": "icons/Icon-192.png", "128": "icons/Icon-192.png" }, // 追加 "permissions": [ "tabs" ] }
2. 現在開いているタブのURLを取得するコードを追加
/web
ディレクトリに chrome_api.js
を追加します。
async function getUrl() { let queryOptions = { active: true, currentWindow: true }; let [tab] = await chrome.tabs.query(queryOptions); return tab.url; }
3. index.htmlでchrome_api.jsを読み込む
index.html
で chrome_api.js
を読み込みます。
<!DOCTYPE html> <html style="height: 500px; width: 300px"> <head> <meta charset="UTF-8"> <title>chrome_extension_sample</title> <link rel="manifest" href="manifest.json"> </head> <body> <!-- 追加 --> <script src="chrome_api.js" type="application/javascript"></script> <script src="main.dart.js" type="application/javascript"></script> </body> </html>
これでweb側の実装は終わりです!
Flutter側の実装
js
をインストール
Flutter側からJavaScript側のコードを呼び出すために js
というライブラリを使います。
dependencies: js: ^0.6.5
chrome_api.dartを作成
/lib
にchrome_api.dartを作成し、下記のように実装します。
// JavaScriptのファイル名を指定 @JS() library chrome_api; import 'package:js/js.dart'; import 'package:js/js_util.dart'; // JavaScriptのメソッドを指定 @JS('getUrl') external Object _getUrl(); Future<String> getUrl() async { // `promiseToFuture` メソッドを使い、JavaScriptのPromiseをDartのFutureに変換 return promiseToFuture<String>(_getUrl()); }
MyHomePageにURLを表示する
今回はFutureBuilderを使って、URLを表示させます。
import 'package:chrome_extension_sample/chrome_api.dart'; class _MyHomePageState extends State<MyHomePage> { // ... @override Widget build(BuildContext context) { return Scaffold( // ... body: Center( child: FutureBuilder( initialData: 'initial data', future: getUrl(), builder: (context, snapshot) { if (snapshot.hasData) { return Text(snapshot.data!); } else { return const Text('no data'); } }, ), ), ); } }
実行
まずはflutter buildします。
$ flutter build web --web-renderer html --csp
次に chrome://extensions
に表示されているリフレッシュアイコンをクリック。
MyHomePageに現在開いているURLが表示されることを確認しました。
※ もし「no data」と表示されてしまう場合はGoogle Chromeを再起動してみてください
最後に
Chrome拡張の申請に関しては、色々な記事があるので省略させていただきますが、ChromeWebStoreというサイトで申請します。会員登録で初回だけ5$がかかります。また拡張機能の審査には最大30日間かかると書かれており、私が申請した時には審査が通るまで約1週間弱かかりました。
Flutterでも作るれることが分かり試してみましたが、意外と簡単に作ることができると知りました。皆さんも拡張機能を作りたいと思ったら、Flutterで作ってみるのはいかがでしょうか。
ここまで読んでいただきありがとうございました!