PRエビデンス作成時に便利なChrome拡張機能をFlutterで実装してみた
こんにちは、スペースマーケットでモバイルエンジニアをしている村田です。
今回、普段開発で触っているFlutterでChromeの拡張機能を作ってみたので、その内容を本記事でご紹介します!
作成物
タイトルの通り、PR作成時に便利な以下の2つの機能を備えたツールを作成しました
- Markdown形式の画像記法

をHTMLの<img>
タグ に変換 - 最大4x3のMarkdownテーブル生成
背景
PRのエビデンスを作成する際に愛用していた、ImgConverterが利用不可になってしまい、作業効率が落ちてしまいました。代替サービスも見当たらなかったため、それならもう自分で作ってしまおうと思い、今回拡張機能を実装しました。
恐らくManifest V3未対応で利用不可になったのかな?
開発
プロジェクト作成
Webのみに対応したプロジェクト構成にしたいため、プラットフォームへwebを指定してcreateコマンドを実行
$ fvm flutter create --platforms web pr_evidence_chrome_extension
.
├── README.md
├── analysis_options.yaml
├── lib
│ └── main.dart
├── pr_evidence_chrome_extension.iml
├── 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
ImgTagConverterコンポーネント
Markdownテーブル生成についても記載すると長くなるので、<img>
タグ変換に関するコンポーネントに焦点を当てて紹介します!
<img>
タグ変換JavaScriptメソッド
Dartから呼び出すJavaScriptメソッドを実装します。
ユーザーが選択した画像幅を受け取り、変換した<img>
タグへ受け取ったwidthを指定します。
タブ取得
chrome.tabs APIを利用し、現在アクティブなタブ(=開いているPR画面)を取得
let queryOptions = { active: true, currentWindow: true };
let [tab] = await chrome.tabs.query(queryOptions);
DOM操作のためのJavaScript実行
chrome.scripting APIを利用し、取得したタブに対してJavaScriptの関数を実行
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (width) => {
// 実行内容
},
args: [width]
});
<img>
タグ変換DOM操作
executeScript
で実行するDOM操作は以下になります
- PR本文を書く要素
<textarea>
を取得 - Markdown形式の画像をHTMLの
<img>
タグに変換 - すでに
<img>
タグになっているものも、指定した幅に揃えて更新
const textarea = document.querySelector('textarea[name="pull_request[body]"]');
textarea.value = textarea.value.replace(/!\[.*?\]\((.*?)\)/g, `<img src="$1" width=${width}>`);
textarea.value = textarea.value.replace(/<img src="(.*?)" width=\d+>/g, `<img src="$1" width=${width}>`);
コード全体
まとめると、Dartから呼び出すJavaScriptメソッドは以下になります
// github_pr_evidence.js
async function convertImgTag(width) {
let queryOptions = { active: true, currentWindow: true };
let [tab] = await chrome.tabs.query(queryOptions);
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (width) => {
const textarea = document.querySelector('textarea[name="pull_request[body]"]');
textarea.value = textarea.value.replace(/!\[.*?\]\((.*?)\)/g, `<img src="$1" width=${width}>`);
textarea.value = textarea.value.replace(/<img src="(.*?)" width=\d+>/g, `<img src="$1" width=${width}>`);
}
,args: [width]
});
}
Dart -> JavaScript 連携
DartからJavaScriptのメソッドを呼び出すため、公式パッケージのdart:js_interopを利用します。
以前はjsライブラリが主流だったようですが、dart:js_interop
の改善によりDart3.3以降こちらの利用が推奨されているようです。
JavaScript連携定義
@JSは、DartからJavaScriptと連携するためのアノテーションです。
@JS('JavaScriptメソッド名')
のように、JavaScript側で定義した名前を指定できます。
先ほど実装した convertImgTag(width)
と連携するための定義は以下になります。
('convertImgTag')
external JSPromise _convertImgTag(JSNumber width);
実行メソッド
宣言されたJS連携メソッドをDartから呼び出すためのメソッドを定義します。
受け取ったint型のwidthをJSNumber型に変換します。
その後、JavaScriptのPromiseをDartのFutureに変換するためにtoDartを使い、_convertImgTagを実行します。
Future<void> convertImgTag(int width) async {
await _convertImgTag(width.toJS).toDart;
}
コード全体
まとめると、JavaScriptメソッドと連携し、それを実行するためのDartコードは以下になります
// github_pr_evidence.dart
import 'dart:js_interop';
('convertImgTag')
external JSPromise _convertImgTag(JSNumber width);
Future<void> convertImgTag(int width) async {
await _convertImgTag(width.toJS).toDart;
}
UIコンポーネント
github_pr_evidence.dart
をインポートし、width変更ボタン押下時にconvertImgTagをコール
import 'package:flutter/material.dart';
import 'package:pr_evidence_chrome_extension/github_pr_evidence.dart';
class ImgTagConverterView extends StatelessWidget {
const ImgTagConverterView({super.key});
Widget build(BuildContext context) {
const widthList = [150, 200, 250, 300, 350, 400];
return Padding(
padding: const EdgeInsets.all(8),
child: Column(
spacing: 6,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
'Img Tag Converter',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFFfb4b28),
),
),
GridView.count(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 2,
children: widthList.map((width) {
return TextButton(
onPressed: () async {
// よろしくお願いしまああああすッ!!!
await convertImgTag(width);
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(Color(0xFFfb4b28)),
),
child: Text(
'${width}px',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
);
}).toList(),
),
],
),
);
}
}
webフォルダ内設定
web/manifest.json
拡張機能API使用するため、permissions
へ以下設定します
{
"version": "1.0.0",
"name": "PR EVIDENCE",
"action": {
"default_popup": "index.html"
},
"icons": {
"16": "icons/Icon.png",
"32": "icons/Icon.png",
"48": "icons/Icon.png",
"128": "icons/Icon.png"
},
"permissions": ["scripting", "activeTab"],
"manifest_version": 3
}
web/index.html
実装したJavaScriptファイルを埋め込み、表示したいサイズをstyleタグとして記述します
<!DOCTYPE html>
<html style="height: 390px; width: 250px">
<head>
<meta charset="UTF-8">
<title>PR EVIDENCE</title>
</head>
<body>
<script src="github_pr_evidence.js" type="application/javascript"></script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
まとめ
JavaScriptとの連携が型安全かつ楽に実装できるようになり、簡単なWebアプリケーションであればFlutterでサクッと実装できるなと感じました!自分が自由にカスタマイズできるChrome拡張機能を持てたことは、今後の開発効率を爆上げできる予感がしています。
それぞれひとつのChrome拡張機能
それぞれが選んだstyle
最後に
スペースマーケットでは一緒に働く仲間を絶賛募集中です!
詳しくは以下をご確認の上ご応募ください。

スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion