Google Chrome拡張機能開発のチュートリアルを実施してみた(コンテンツスクリプト自動実行編)
チュートリアルの概要
前回はアイコンをクリックすると"Hello Extensions"と表示される簡単なGoogle Chromeの拡張機能を作成しました。
今回もGoogle公式のチュートリアルに従い、特定のWebページを読み終えるまでの時間を計算して表示する拡張機能を作成します。 チュートリアルの流れは以下の通りです。- Step 1. マニフェストファイルを作成
- Step 2. アイコンをダウンロード
- Step 3. コンテンツスクリプトファイルを作成
- Step 4. 動作を確認
チュートリアル完了後、Google Chromeの拡張機能やウェブストアに関する開発者向けドキュメントを開くと、以下のように読了までの時間が表示されます。
Step 1. マニフェストファイルを作成
まず、拡張機能のルートディレクトリreading-time
を作成し、移動します。
次に、reading-time
ディレクトリ直下に以下のようなマニフェストファイルmanifest.json
を作成します。
{
"manifest_version": 3,
"name": "Reading time",
"version": "1.0",
"description": "Add the reading time to Chrome Extension documentation articles",
"icons": {
"16": "images/icon-16.png",
"32": "images/icon-32.png",
"48": "images/icon-48.png",
"128": "images/icon-128.png"
},
"content_scripts": [
{
"js": ["scripts/content.js"],
"matches": [
"https://developer.chrome.com/docs/extensions/*",
"https://developer.chrome.com/docs/webstore/*"
]
}
]
}
manifest_version
name
version
description
の4フィールドの説明は前回の記事に記載しているので省略します。
icons
フィールドは拡張機能のアイコンのサイズと相対パスの辞書です。
画像は透過(透明な背景)をサポートしているPNG形式が推奨されています。
今回用意する16×16px、32×32px、48×48px、128×128pxのアイコンの用途は以下の通りです。
画像サイズ(px) | 用途 |
---|---|
16×16 | 拡張機能の管理ページや右クリックした時に表示されるメニューのファビコン用[1] |
32×32 | Windows用 |
48×48 | 拡張機能の管理ページ用 |
128×128 | インストール時やGoogle Chromeウェブストア用 |
content_scripts
フィールドではコンテンツスクリプト(ユーザが開いたWebページに挿入するJavaScriptまたはCSSファイル)の相対パスや挙動を定義します。
今回はコンテンツスクリプトをJavaScriptで作成するため、js
フィールドが存在しますが、CSSで作成する場合はcss
フィールドを用意します。
これらのフィールドの値はコンテンツスクリプトの相対パスの配列で、実行したい順序で記載します。
matches
フィールドではコンテンツスクリプトを実行するページを記載します。
今回はGoogle Chromeの拡張機能やウェブストアに関する開発者向けドキュメントを対象とします。
Step 2. アイコンをダウンロード
マニフェストファイルに記載した4つの異なるサイズのアイコンを用意します。
まず、アイコン用のディレクトリimages
をreading-time
ディレクトリ直下に作成して移動します。
次に、Google ChromeのGitHubリポジトリからアイコンをダウンロードし、マニフェストファイルの定義通りに命名します。
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/GoogleChrome/chrome-extensions-samples/main/functional-samples/tutorial.reading-time/images/icon-16.png" -OutFile "icon-16.png"
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/GoogleChrome/chrome-extensions-samples/main/functional-samples/tutorial.reading-time/images/icon-32.png" -OutFile "icon-32.png"
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/GoogleChrome/chrome-extensions-samples/main/functional-samples/tutorial.reading-time/images/icon-48.png" -OutFile "icon-48.png"
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/GoogleChrome/chrome-extensions-samples/main/functional-samples/tutorial.reading-time/images/icon-128.png" -OutFile "icon-128.png"
Step 3. コンテンツスクリプトファイルを作成
マニフェストファイルに記載したコンテンツスクリプトを作成します。
まず、コンテンツスクリプト用のディレクトリscripts
をreading-time
ディレクトリ直下に作成して移動します。
次に、以下のようなコンテンツスクリプトcontent.js
を作成します。
function renderReadingTime(article) {
if (!article) {
return;
}
const text = article.textContent;
const wordMatchRegExp = /[^\s]+/g;
const words = text.matchAll(wordMatchRegExp);
const wordCount = [...words].length;
const readingTime = Math.round(wordCount / 200);
const badge = document.createElement("p");
badge.classList.add("color-secondary-text", "type--caption");
badge.textContent = `⏱️ ${readingTime} min read`;
const heading = article.querySelector("h1");
const date = article.querySelector("time")?.parentNode;
(date ?? heading).insertAdjacentElement("afterend", badge);
}
renderReadingTime(document.querySelector("article"));
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof Element && node.tagName === 'ARTICLE') {
renderReadingTime(node);
}
}
}
});
observer.observe(document.querySelector('devsite-content'), {
childList: true
});
コードの解説
function renderReadingTime(article) {
関数renderReadingTime
を定義します。
引数のarticle
はHTMLElement
オブジェクト(HTMLタグを表すオブジェクト)で、ここではarticle
タグを想定しています。
if (!article) {
return;
}
もし、article
タグが無いなら何もしないで終えます。
const text = article.textContent;
article
タグに囲まれたテキスト部分を変数text
に格納します。
const wordMatchRegExp = /[^\s]+/g;
-
/
で囲まれた部分は正規表現のパターンです。 -
\s
は空白文字(改行なども含む)です。 -
^
は否定です。[2] -
[
と]
は間に挟まれた部分のどれか1文字という意味です。 -
+
は直前のパターンを1回以上繰り返すという意味です。 -
g
は合致する部分を全て探索するフラグです。
つまり、/[^\s]+/g
は空白文字以外が1回以上続く部分(要するに単語)を全て探索する正規表現です。
この正規表現を変数wordMatchRegExp
に格納します。
const words = text.matchAll(wordMatchRegExp);
article
タグのテキスト部分の単語のイテレータを変数words
に格納します。
const wordCount = [...words].length;
イテレータwords
を配列に変換し、その長さ(要するに単語数)を変数wordCount
に格納します。
const readingTime = Math.round(wordCount / 200);
200単語を1分で読むという想定で、article
タグの単語数を200で割って四捨五入した値を変数readingTime
に格納します。
const badge = document.createElement("p");
p
タグを作成して変数badge
に格納します。
badge.classList.add("color-secondary-text", "type--caption");
p
タグにクラスcolor-secondary-text
とtype--caption
を追加します。
-
color-secondary-text
はテキストの色をセカンダリカラーにするクラスです。 -
type--caption
はテキストをキャプション用にするクラスです。
どちらのクラスも今回の拡張機能の対象としているWebサイトで使われています。
badge.textContent = `⏱️ ${readingTime} min read`;
p
タグのテキストとして「⏱️ <articleタグの単語数を200で割って四捨五入した値> min read」を指定します。
const heading = article.querySelector("h1");
article
タグ内のh1
タグを変数heading
に格納します。
h1
タグが無ければheading
はnullです。
const date = article.querySelector("time")?.parentNode;
article
タグ内のtime
タグの1つ外側のタグを変数date
に格納します。
time
タグが無ければdate
はundefinedです。
(date ?? heading).insertAdjacentElement("afterend", badge);
}
date
がundefinedでなければ(要はtime
タグがあれば)time
タグの1つ外側のタグの終了直後にp
タグを挿入します。
もし、date
がundefinedならば、h1
タグの終了直後にp
タグを挿入します。
ここで、関数renderReadingTime
の定義は終了です。
renderReadingTime(document.querySelector("article"));
Webページのarticle
タグを引数として関数renderReadingTime
を実行します。
つまり、Webページのtime
タグの1つ外側のタグの終了直後またはh1
タグの終了直後に「⏱️ <articleタグの単語数を200で割って四捨五入した値> min read」を挿入します。
上記のコードだけでも読了時間は上手く表示されそうです。
しかし、今回の拡張機能の対象としているWebページはシングルページアプリケーションであり、左メニューをクリックして記事を切り替えてもページ全体は切り替わりません。
そのため、上記のコードのままでは新しく表示した記事においてコンテンツスクリプトが実行されず、読了時間が表示されません。[3]
そこで、HTMLドキュメントの構造の変化を監視する仕組みを導入します。
const observer = new MutationObserver((mutations) => {
MutationObserver
コンストラクタは、HTMLドキュメントの構造の変化をトリガーとして指定したコールバック関数を実行するオブザーバを作成して返します。
ここでのコールバック関数は(mutations) => {
からrenderReadingTime(node);}}}}
まで(アロー関数)です。
for (const mutation of mutations) {
コールバック関数の引数mutations
はMutationRecord
オブジェクトの配列で、ここではその要素を変数mutation
に順に取り出します。
MutationRecord
オブジェクトはHTMLドキュメントの構造の変化の情報を持っています。
for (const node of mutation.addedNodes) {
mutation.addedNodes
にはHTMLドキュメントに新しく追加された部分が入っており、ここではその要素を変数node
に順に取り出します。
if (node instanceof Element && node.tagName === 'ARTICLE') {
HTMLドキュメントに新しく追加された部分がHTML要素かつタグ名がARTICLE
であれば、後続の処理を実施します。
renderReadingTime(node);
}
}
}
});
article
タグが追加されたということは読了時間を再計算・再表示する必要があるので、renderReadingTime
関数を呼び出して実行します。
ここでコールバック関数の定義は終了です。
observer.observe(document.querySelector('devsite-content'), {
observe
メソッドを使用し、HTMLドキュメントの構造の変更の監視を開始します。
監視対象はdevsite-content
タグ配下です。
childList: true
});
devsite-content
の子要素の追加・削除のみを監視します。
Step 4. 動作を確認
拡張機能が完成したので、Google Chromeに読み込みます。
なお、拡張機能の読み込み方法は前回の記事に記載しているので省略します。
作成した拡張機能が有効になっている状態で、今回のチュートリアルのページ[4]を開くと、コンテンツスクリプトが実行されて読了までの時間が表示されます。
今回のチュートリアルはこれにて終了です。
-
icons
フィールドの説明ページの日本語版に、16×16pxのアイコンがkubectl(!?)の拡張機能のページのファビコンとして使用されるかのような記載がありますが、英語版だとkubectlという単語は出てきません。謎機械翻訳です。 ↩︎ -
今回は
[]
内の先頭に^
があるため否定となりますが、そうでなければ行頭を意味します。 ↩︎ -
正確に言うと、左メニューから記事を切り替えると読了時間を表示していた部分ごと切り替わるので、読了時間が表示されなくなります。 ↩︎
-
今回のチュートリアルのWebページのURLは
https://developer.chrome.com/docs/extensions/
から始まっており、マニフェストファイルのmatches
フィールドで定義したコンテンツスクリプトの実行場所に合致します。 ↩︎
Discussion