こんにちは、エンジニアの yuma です。
突然ですが、皆さんは Redash を使っていますか?弊社では本番データを参照する際に Redash を使っており、エンジニアはもちろん、エンジニアでない方もデータ分析に活用しています。
Redash では豊富なビジュアライゼーションが提供されていますが、データ分析には有用な一方でエンジニア間でデータを共有するには少々不便でした。
そこで、Redash の出力結果を Markdown のテーブルでコピーできるようにする Chrome 拡張機能を作成しました。拡張機能自体を公開することはできませんが、 Chrome 拡張機能の開発や実装時のポイントを共有します。
なお、スクリーンショットを含む本記事で掲載しているデータは、全てデモ用のダミーデータです。
モチベーション
弊社では、原則エンジニアを含めて本番データの参照には Redash を利用しています。本番データの変更は特定のエンジニアのみが実行権限を持ち、どのような変更であっても GitLab 上で変更内容を起票、適切な承認フローを通した上で変更を依頼します。
その際、正しい変更が行われたかどうかを確認するために、変更前のデータと変更後の想定データを記入し、レビューを受ける必要があります。エンジニアは Redash でクエリを実行して変更前のデータを取得しますが、 Redash の出力結果はコピペがしづらく、GitLab へのデータの貼り付けが手間になっていました。
ワークアラウンド的な方法もあるようですが、頻度の高い作業であるため、せっかくならワンボタンでコピペできるようにしたい!と思い、Chrome 拡張機能を作成することにしました。
構成
簡易的な図ですが、以下のような構成で開発しました。
表示しているページに任意のスクリプトを挿入できる Content scripts でコピーボタンの挿入やボタン押下時のテーブル取得を行い、バックグラウンドで実行される Service worker にメッセージを送信します。
Content scripts では、 Redash のテーブル表示を CSV ライクな文字列配列に変換し、 Service worker へメッセージを送ります。 Markdown 形式への変換は Service worker で実行し、 Content scripts は返却された Markdown 文字列をクリップボードにコピーする、という流れです。
より詳しくみていきましょう。
Content scripts
Content scripts は、表示しているページに任意のスクリプトを挿入できる機能です。挿入されたページの DOM にアクセスできるため、ページ上の要素を取得・操作が可能です。
今回は、 Redash のクエリ実行画面(/queries
)にコピー用のボタンを挿入します。コピー用のボタンは React コンポーネントとして実装し、ReactDOM で root.render()
することで Redash の画面上に描画しています。以下の「Copy as Markdown」ボタンが、 Chrome 拡張機能で描画されたコンポーネントです。
ただし、 Redash は SPA で構築されており、ページ遷移時にリロードが発生しないため Content scripts が再実行されません。また遷移後のクエリ実行画面でも、ボタン部分を含め頻繁に表示が変化するため、単純にボタンを挿入するコードだけでは DOM の再構築によってボタンが消えてしまいます。
このような DOM の変更を監視・コンポーネントを再描画するために、 MutationObserver
を利用しました。
MutationObserver
MutationObserver
は、 DOM の変更を監視することができる WebAPI です。特定の DOM の変更をその子孫も含めて監視することができ、その変更をコールバック関数に通知します。
変更の内容は MutationRecord
というクラスインスタンスで通知され、この辺りに少々癖がある印象でしたが、フレームワークなしでも DOM の変更を監視して扱えるのは非常に便利でした。
今回は、クエリ実行画面の下部バー DOM が追加されたタイミングで、コピー用ボタンのコンポーネントを描画しました。クエリ実行画面の遷移や、クエリを実行した際の再描画でもボタンが表示されるようになりました。
const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { // MutationRecord.addedNodes で、追加されたノードを取得できる mutation.addedNodes.forEach((node) => { if (node.hoge() === "fuga")) { // ... 中略 ... // div を挿入して React コンポーネントを描画 const rootDom = document.createElement("div"); bottomController.insertBefore( rootDom, bottomController.lastElementChild ); const root = createRoot(rootDom); root.render(createElement(CopyAsMarkdown)); } }); } });
Service workers
Service workers は、ページ内で動作するスクリプトとは別にバックグラウンドで実行されるスクリプトです。
各ページから発火されるイベントのハンドラを定義することで様々なイベントを監視できるほか、ページに挿入した Content scripts からのメッセージを受けることもできます。今回はテーブルのデータをペイロードとして受け取り、 Markdown 形式に変換するメッセージハンドラを実装しました。
chrome.runtime.onMessage.addListener((message, _, sendResponse) => { if (message.type === "RRC_MARKDOWN_COPY") { const csvAsArray = (message as Messages["CopyMarkdown"]).data; sendResponse(markdownTable(csvAsArray)); } return; });
メッセージの送信も以下のようにシンプルな API で実現できます。
const execCopyMessage = (data: Csv, callback: (response: string) => void) => { const message = { type: "RRC_MARKDOWN_COPY", data, }; return chrome.runtime.sendMessage(message, callback); };
manifest.json
manifest.json は Chrome 拡張機能本体の設定ファイルです。配布時に重要になるような、拡張機能の名称や説明・アイコンなどはあまり気にせず、実装に必要な部分だけ設定しました。
ポイントとなる部分だけを抜粋して解説します。
content_scripts
/ background
実行したい Content scripts , Service workers のスクリプトを指定します。
Content scripts には、実行したいページの URL を正規表現や独自のパターン表現で設定できます。
"content_scripts": [ { "js": [ "content.js" ], "matches": [ "https://<Redash サービスのホスト>/queries/*" ] } ], "background": { "service_worker": "worker.js", },
permissions
Chrome 拡張機能向け API や一部のスクリプトのためには、ユーザーからの権限許可が必要です。 permissions
に必要な権限を記述することで、 Chrome 拡張機能からユーザーに権限の許可を求めることができます。
今回はクリップボードへの書き込み権限が必要なため、 clipboardWrite
を必須の権限に追加しました。
"permissions": [ "clipboardWrite" ]
補足: CRXJS Vite Plugin について
弊社では TypeScript を開発言語に採用しています。Chrome 拡張機能の開発でも TypeScript を使うことにしました。
「Chrome 拡張 TypeScript」などで検索すると、 CRXJS という Vite プラグインがヒットすると思いますが、今回はこちらの採用は見送りました。*1
CRXJS が対応しているのは Vite 2、ベータバージョンでも Vite 3 までのため、最新のバージョンとは大きく離れてしまっています。今回の開発内容で得られる恩恵は manifest.json
の型付け程度で重要度も低いことから、CRXJS を使わなくても特段問題にはなりませんでした。
まとめ
今回は、 Chrome 拡張機能を開発する中でのポイントをいくつかご紹介しました。
ストアでの公開・配布を考慮すると、安定度やメンテナンス性などをさらに考える必要がありそうですが、個人用や社内ツールとして簡単に使う上ではそこまでハードルは高くないように思います。皆さんも、身近なツールでかゆいところに手が届かないようなときには、ぜひ自分用の Chrome 拡張機能を作ってみてください!
最後に
みんなのマーケットでは、くらしのマーケットのサービス開発を一緒に盛り上げてくれるエンジニアを募集しています!
詳しくは、こちらをご覧ください。
*1:正確にはプロトタイプの段階では CRXJS を採用していましたが、 React コンポーネント化に際して Vite のバージョンが問題となってしまったため、CRXJS を採用しない方針に変更しました。