このブログははてなブログからの移行記事です。
この記事は
JavaScriptアドベントカレンダーの1日目の記事です。
JavaScriptというよりはChromeの話かもしれませんが、最近エクステンションを作って楽しかったので簡単に作り方を書いてみます。
Chromeエクステンションとは
github-label-creater
私は普段、GitHubで開発するときにIssueのラベルを自分好みに変えてから使うのですが、
毎回作り直すのが面倒でブックマークレットを作ろうとしました。
ただ、ブックマークレットだとソースコードの量が限られてしまうので、Chromeエクステンションにしようと思い立ち、github-label-createrなるエクステンションを作成しました。
こんな感じで使えます。
エクステンションの種類
一言でエクステンションと言っても、いくつか種類があります。
とてもざっくり言うと以下の3種類のタイプが存在します。
Browser Action
追加するとURLのバーの横にアイコンが追加され、それでいろいろ機能が使えるタイプ。
はてブのエクステンションなんかはこれですね。
Page actions
特定のページでのみ動くエクステンションです。
Override Pages
Chromeの内部ページを書き換えるタイプのエクステンションです。
作りたいもの
今回作りたいものの要件は以下の通り。
https://github.com/{username}/{reponame}/labels
でのみ動けばよい- ページ上のDOMを操作してラベルを作り替える
- アイコンをクリックして出てくるボタンを押すとラベルが作り替わってほしい
ということで、先ほどのPage ActionsとBrowser Actionの機能を組み合わせて作っていきます。
雑な実装イメージ図
構造的にはこんな感じで作っていきます
Content Scripts
まずは特定のページ上で読み込まれるContent Scriptsを作成します。
Chromeエクステンションではエクステンションの情報や読み込むスクリプトの情報をmanifest.json
というファイル名で作成します。
{ "manifest_version": 2, "name": "GitHub label creater", "version": "0.0.1", "description": "Create labels of GitHub.", "author": "sota1235" }
ここにcontent_scripts
という名前で情報を追加します。
今回はhttps://github.com/*/*/labels
に一致するページでdist/index.js
を読み込ませたいので以下のように書きます。
URL matchの記法はドキュメントで確認できます。
{ "manifest_version": 2, "name": "GitHub label creater", "version": "0.0.1", "description": "Create labels of GitHub.", "content_scripts": [ { "matches": [ "https://github.com/*/*/labels" ], "js": ["dist/index.js"] } ], "author": "sota1235" }
これでGitHubのLabelページでdist/index.js
が読み込まれるようになりました。
Browser Action
今回は「アイコンをクリックして出てくるボタンをクリックしたらWebページにアクションを起こす」といった形で実装するので、Browser Actionのスクリプトと設定を追加します。
Browser Actionではアイコンがクリックされたときに表示するHTMLを設定できます。
今回はpopup.html
を表示するようにします。
manifest.json
に以下の設定を追加しましょう。
{ "browser_action": { "default_icon": { }, "default_title": "GitHub label creater", "default_popup": "popup.html" } }
default_icon
のところにはアイコンに表示したい画像を入れることができますが、無い場合は空でも動きます(誰かアイコン作って…|ω・`))。
popup.html
上ではボタンがクリックしたときの処理をしたいので<script>
タグを使ってdist/popup.js
を読み込んでいます。
Browser ActionとContent Scriptsで通信する
さて、ここから少しChromeエクステンションならではの処理が出てきます。
仕様上、Browser Actionからページ上のDOMに直接アクセスすることはできません。
なのでBrowser Actionからイベントを発火し、Content Script側でそれを監視する形で実装を行います。
Browser Actionでのイベント発火
import domready from 'domready'; import $ from 'jquery'; domready(async () => { // 作成するラベルデータ一覧 const labels = await get('labels'); $('button.create-labels').on('click', () => { // chrome.tabs.query({active: true, currentWindow: true}, tabs => { chrome.tabs.sendMessage(tabs[0].id, { type: 'CLICK_POPUP', labels }); }); }); });
ただ、あらかじめmanifest.json
でchrome.tabs
APIの使用を許可する必要があります。
以下のように追記しましょう。
{ "permissions": [ "tabs", "https://github.com/*/*/labels" ] }
これで現在表示されてるタブのContent ScriptにイベントをPublishできます。
Content Scriptでのイベント監視
Content Script側でのイベント監視はこんな感じ。
import domready from 'domready'; import { createNewLabels, deleteLabels } from './label-creater'; domready(() => { // popup.jsからのイベント監視 chrome.runtime.onMessage.addListener(message => { // コールバックでpopup.jsからの値を受け取れる if (message.type !== 'CLICK_POPUP') { return; } // ここでページ上のDOMを書き換える if (window.confirm('All labels will be overwritten. Are you OK?')) { deleteLabels(); createNewLabels(message.labels); } }); });
これでアイコン上のボタンを押すとContent Scriptが実行される流れが作れました!
History APIの罠
ただ、このままだと特定の条件で動作することができません。
Content Scriptはそのページに対してHTTPリクエストが飛んだ際に読み込まれますが、History APIによるURL書き換えの際は読み込まれません。
GitHubではIssueページからLabelページに行くタイミング等ではHistory APIによる遷移を行っているため、Content Scriptが読み込まれません。
そこでBackground Scriptを使って少しハックします。
Chromeエクステンションではchrome.webNavigation
というAPIが用意されており、これに生えてるonHistoryStateUpdated.addListener
でURL履歴が変更された際のイベントをcatchできます。
なのでこれを利用し、History update時にContent Scriptを読み込むBackground Scriptを作成します。
まずmanifest.json
に以下を書き足します。
{ "background": { "scripts": ["background.js"] } }
webNavigatoin
APIの使用を許可するため、permissions
に値を足しましょう。
{ "permissions": [ "https://github.com/*/*/labels", "tabs", "webNavigation" ] }
そして以下のようなシンプルなBackground Scriptを作成します。
/** @var {boolean} Is the content script always executed. */ let isExecuted = false; chrome.webNavigation.onHistoryStateUpdated.addListener(() => { if (!isExecuted) { chrome.tabs.executeScript(null, { file: "dist/index.js"} ); } isExecuted = true; });
ページ遷移するたびにContent Script
を実行するとイベントが多重に登録されてしまうので、変数でイベント登録済みかどうか判定しています。
これでHTTPリクエストを伴わないページ遷移時にもContent Scriptが呼ばれるようになりました!めでたし!
開発の時に便利だったもの
Content Scriptはページで読み込まれるのでconsole.log
でデバッグできますが、Browser Actionでは同じようにデバッグができません。
そこで、chromeにエクステンションを読み込ませた際に発行されるextension IDを利用して直接、popup.htmlにアクセスすることができます。
下記画像のID部分を
chrome-extension://{エクステンションID}/popup.html
といった形式でアクセスします。
これでデバッグ作業ができます!
まとめ
独自APIとか仕様に振り回されそうな印象が強く、今まで避けてたエクステンション開発でしたが思ったより簡単だなというのが僕の感想です。
古い情報とかも多いので困ったら公式ドキュメントを読めばだいたい解決するのでみなさんもぜひ何か作ってみてください。
ブックマークレットと違ってnpmライブラリを使えたりするので幅は広いんじゃないかなと思います。
参考サイト
Google Chrome Extensionを作ってみた-その2(デバッグ)- | Developers.IO
Chromeのオリジナル拡張機能を開発しよう(ソースコードあり) | 株式会社LIG
javascript - Chrome extension Content Script not loaded until page is refreshed - Stack Overflow