はじめに
こんにちは、レコメンドチームで機械学習エンジニアをしている24新卒の菊谷です。
最近、業務の補助ツールとしてChrome拡張機能を作りましたので、経緯や作り方を紹介したいと思います。
背景
レコメンドチームではレコメンドIDと呼ばれる識別子を管理しています。レコメンドIDはレコメンドが表示される場所やレコメンドの結果の保存先DBのパーテーションキー、レコメンドAPIのパラメータなどに紐付く大変重要な文字列です。
重要な識別子ではあるのですが、少し困った点があります。
それは、ランダムな英数字を組み合わせて発行されるために、識別子がどのレコメンドに対応しているかの類推ができない点です。例えば「r1aih9rg」のような値でこのままでは類推できないため、必要に応じて対応関係が記載されているファイルを確認する必要があります。
この確認作業が特に面倒になるのがレビュー時です。 このレコメンドIDは設定用のファイルなどに頻出するのですが、IDが問題ないかについてはレビュー画面から離れて別途対応ファイルを参照しなくてはなりません。レビュー時にチェックすべき箇所は他にもありますし、面倒さも相まって、正しいかの確認はしないという判断になってしまう可能性もありえます。
以上の背景のもと、確認の面倒さを少しでも減らせるようなChrome拡張機能を作成することにしました。
作成する拡張機能について
面倒と思う部分はレビュー画面以外のページに移って作業しなければならない点でした。このことから、作成するChrome拡張機能ではレビュー画面内で識別子に対応する情報を表示することで、レビュー画面外に移らずに確認できることを目指します。
識別子に対応する情報の表示方法はいろいろ考えられますが、今回は識別子にカーソルを当てると補足説明を表示できるツールチップを使うことにしました。このツールチップによって、以下のように補足説明を表示できます。
それでは実際に拡張機能を作っていきます!
拡張機能の作成
事前準備
拡張機能を作るうえで必ず用意しなければならないのが設定ファイルのmanifest.jsonです。manifest.jsonは拡張機能の名前や説明、バージョン情報、使用するスクリプトのパスなどのメタデータを記載します。
{ "manifest_version": 3, "name": "ID-Viewer", "description": "識別子にカーソルを当てると補足説明を表示させる拡張機能", "version": "1.0", "content_scripts": [ { "js": ["scripts/content.js"], "css": ["styles.css"], "matches": [ "<all_urls>" ] } ] }
ここではCSSとJavaScriptを使うので、content_scripts
に使用するファイルパスを与えています。また、matches
にはスクリプトを適用するWebページのURLパターンを指定します。今回は開発フェーズなので<all_urls>
を与えており、これはすべてのページに適用することを意味します。
使用するstyles.cssについてはツールチップのクラス(tooltip
クラスとtootip-text
クラス)を記載します。以下に素敵な見た目のツールチップのCSSがあるので、そのまま使用させていただきました。
最後にscripts/content.jsを追加しておきます。こちらについては後ほど実装するので空のファイルです。
フォルダ構成としては以下となっています。
id-viewer ├── manifest.json ├── scripts │ └── content.js └── styles.css
拡張機能のパッケージ化
まずは、Google Chromeの拡張機能ページ(chrome://extensions)を開きます。次に画面右上のデベロッパーモードを有効化します。
続いて「パッケージ化されていない拡張機能を読み込む」というボタンを押して、id-viewerフォルダを選択します。
すると、すべての拡張機能の下に読み込んだパッケージが反映されます。
ファイルを変更した際は、追加したパッケージの再読み込みを押すことで変更内容の反映ができます。
スクリプトの実装
ここでは、scripts/content.jsを編集していきます。
やりたいことはWebページ内にある識別子を見つけてツールチップを埋め込むことです。全体の流れとしては次の通りです。
- ページ全体からテキストノードのみをフィルタリングする。
- テキストノードからテキストを取り出し、正規表現でテキスト内に識別子を含むテキストノードのみを取得する。
- 識別子を含むテキストノードに対して、識別子の文字列はツールチップの要素ノードに、それ以外はテキストノードとして元のテキストノードと置き換える。
以降では、簡単な例として、ページのBody要素が以下で構成されている場合を考えます。識別子は「hogefuga_id」としましょう。
<body> <!-- 要素ノード --> 拡張機能の例 <!-- テキストノード --> hogefuga_id <!-- テキストノード --> </body>
Webページは文書の内容をノードによるツリー構造によって表現し、<body>
などは要素ノード、テキスト部分はテキストノードと呼ばれます。そのため、「hogefuga_id」はテキストノードに含まれるテキストに該当します。
ここで行いたいことは、識別子を持つテキストノードをツールチップのノードに置き換えることです。「hogefuga_id」に対応する説明を「hogefuga_idの説明」にした場合、「hogefuga_id」のテキストノードをstyles.cssで定義されているツールチップのクラスで装飾すると次のようになります。
<span class="tooltip"> <span class="tooltip-text">hogefuga_idの説明</span> hogefuga_id </span>
ここでのtooltip
とtooltip-text
クラスは要素ノードをツールチップとして機能させるためのスタイルを適用しています。tooltip
はツールチップの表示位置の相対化とカーソルを当てた時にポインタが変化し、tooltip-text
は見た目の定義とカーソルを当てるまではテキストが非表示になる設定がされています。これにより、識別子にマウスカーソルを合わせると説明文が表示されるようになります。
識別子に対して上記の変換ができると、先ほどのGIFのようなツールチップによる補足説明機能が実現できます。テキストからツールチップの要素ノードを作成する関数については次のように書けます。
function createTooltipElement(text, tooltipText) { const tooltipElement = document.createElement("span"); tooltipElement.className = "tooltip"; tooltipElement.textContent = text; const tooltipTextElement = document.createElement("span"); tooltipTextElement.className = "tooltip-text"; tooltipTextElement.textContent = tooltipText; tooltipElement.appendChild(tooltipTextElement); return tooltipElement; }
使い方としては、引数text
には識別子の文字列、引数tooltipText
に補足説明の文字列を渡します。
この関数を使うには識別子を見つける必要があるため、識別子を含むテキストノードを集めるための処理を作ります。ページ内のテキストノードについてはdocument.createTreeWalkerの引数にNodeFilter.SHOW_TEXT
を与え、nextNode()
メソッドを繰り返すことで取得できます。
さらに現在のノードであるtextNode
が持つテキストはtextNode.textContent
で取得可能です。このテキストに対して正規表現で識別子見つけて、textNodes
の要素として追加していきます。コードにすると以下の通りです。
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); const textNodes = []; const regex = new RegExp(`(${Object.keys(tooltipTextMap).join("|")})`, "g"); while (walker.nextNode()) { const textNode = walker.currentNode; const text = textNode.textContent; if (regex.test(text)) { textNodes.push(walker.currentNode); } }
ここでのtooltipTextMap
はキーに識別子、値に補足説明を持つMapオブジェクトです。テキストノードのテキストがtooltipTextMap
の持つキー(識別子)と一致した場合は、そのテキストノードをtextNodes
に追加します。
続いて、取得したテキストノードをツールチップの要素ノードからなる新しいノードに置き換えていきます。テキストノードには識別子以外の文字列も含まれているため、正規表現を使って識別子とそれ以外の文字列に分割します。分割した文字列は識別子ならツールチップに変換し、それ以外ならテキストノードを作成します。そして、createDocumentFragmentで作成した新しい仮の親ノードに対してそれぞれを子ノードとして追加します。その後、元のテキストノードを置き換えるという一連の操作をtextNodes
の数だけ繰り返します。
textNodes.forEach(textNode => { const text = textNode.textContent; const parentNode = textNode.parentNode; const fragment = document.createDocumentFragment(); text.split(regex).forEach(part => { if (tooltipTextMap[part]) { const tooltipElement = createTooltipElement(part, tooltipTextMap[part]); fragment.appendChild(tooltipElement); } else { fragment.appendChild(document.createTextNode(part)); } }); parentNode.replaceChild(fragment, textNode); });
2つコードはreplaceTextNodesToTooltips
関数としてまとめました。ページが読み込み終わったタイミングでtooltipTextMap
を定義し、replaceTextNodesToTooltips
関数を呼ぶようにします。
window.onload = function() { const tooltipTextMap = { "hogefuga_id": "hogefuga_idの説明" }; replaceTextNodesToTooltips(tooltipTextMap); }
以上のコードをscripts/content.jsとして保存します。最終的なscripts/content.jsは次の通りです。
function createTooltipElement(text, tooltipText) { const tooltipElement = document.createElement("span"); tooltipElement.className = "tooltip"; tooltipElement.textContent = text; const tooltipTextElement = document.createElement("span"); tooltipTextElement.className = "tooltip-text"; tooltipTextElement.textContent = tooltipText; tooltipElement.appendChild(tooltipTextElement); return tooltipElement; } function replaceTextNodesToTooltips(tooltipTextMap) { const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); const textNodes = []; const regex = new RegExp(`(${Object.keys(tooltipTextMap).join("|")})`, "g"); while (walker.nextNode()) { const textNode = walker.currentNode; const text = textNode.textContent; if (regex.test(text)) { textNodes.push(walker.currentNode); } } textNodes.forEach(textNode => { const text = textNode.textContent; const parentNode = textNode.parentNode; const fragment = document.createDocumentFragment(); text.split(regex).forEach(part => { console.log(part); if (tooltipTextMap[part]) { const tooltipElement = createTooltipElement(part, tooltipTextMap[part]); fragment.appendChild(tooltipElement); } else { fragment.appendChild(document.createTextNode(part)); } }); parentNode.replaceChild(fragment, textNode); }); } window.onload = function() { const tooltipTextMap = { "hogefuga_id": "hogefuga_idの説明" }; replaceTextNodesToTooltips(tooltipTextMap); }
保存後は拡張機能ページの再読み込みを押してパッケージに反映をすれば完成です。
動作確認
以下をsample.htmlとして保存し、Chrome上で開きます。
<html lang="ja"> <head> <meta charset=”utf-8″> <title>拡張機能のサンプル</title> <link rel="stylesheet" href="styles.css" type="text/css"> </head> <body> 拡張機能の例 hogefuga_id </body> </html>
「hogefuga_id」の部分にカーソルを当てた際に補足説明が表示されれば成功です。
おわりに
本記事では、判読困難な識別子をレビュー時に確認する作業が面倒であるという問題を解決するために、Chrome拡張機能を作成する方法を紹介しました。運用はこれからですが、この拡張機能を活用することで作業効率やレビューの正確性が高まると考えています。
この開発を通して、Chrome拡張機能の作成自体は想定よりも簡単で、日々の作業を効率化するツールは自作可能なことを実感しました。今後も業務の効率を上げるための取り組みを進めていきたいと思っています。
もしわかりにくい識別子でお困りでしたら、ぜひ皆さんの開発環境でも導入してみてください!
最後に、DMMグループでは一緒に働いてくれる仲間を募集しています!ご興味のある方は、ぜひ下記の募集ページをご確認ください!