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
/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"
}
}
詳しくは公式ドキュメントを参照してください。
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>
CSPのエラーは発生しなくなりました。続いて下記の2つを修正していきます。
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>
index.htmlは「manifest.jsonの編集」で記載した通り、アイコンをタップした時に表示されるHTMLです。 表示させたいサイズを指定します。
<html style="height: 500px; width: 300px">
ちなみにサイズを指定しないと下の画像の様に表示されます。
$ 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 をした時は、右下のリフレッシュアイコンをクリックすれば読み込まれます。
右上の拡張機能のアイコンをクリックすると、追加した拡張機能をクリックすると表示されると思います。
最後にJavascriptで提供しているChrome ExtensionのAPIをFlutterで使う方法を解説します。 公式ドキュメントはこちらです。
今回は現在開いているタブのURLを表示していきます。
タブのURLを取得するためには Tabs APIを使用します。
Tabs APIドキュメントはこちらです。
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"
]
}
/web ディレクトリに chrome_api.js を追加します。
async function getUrl() {
let queryOptions = { active: true, currentWindow: true };
let [tab] = await chrome.tabs.query(queryOptions);
return tab.url;
}
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側からJavaScript側のコードを呼び出すために js というライブラリを使います。
dependencies:
js: ^0.6.5
/lib にchrome_api.dartを作成し、下記のように実装します。
// JavaScriptのファイル名を指定
()
library chrome_api;
import 'package:js/js.dart';
import 'package:js/js_util.dart';
// JavaScriptのメソッドを指定
('getUrl')
external Object _getUrl();
Future<String> getUrl() async {
// `promiseToFuture` メソッドを使い、JavaScriptのPromiseをDartのFutureに変換
return promiseToFuture<String>(_getUrl());
}
今回はFutureBuilderを使って、URLを表示させます。
import 'package:chrome_extension_sample/chrome_api.dart';
class _MyHomePageState extends State<MyHomePage> {
// ...
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で作ってみるのはいかがでしょうか。
ここまで読んでいただきありがとうございました!
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @mot_techtalk のフォローもよろしくお願いします!