ある特定のページ内にある、特定の URL のみ IE で開き直す Chrome拡張機能 がほしくなったので作りました。
動機
普段閲覧するほとんどのサイトは Chrome系ブラウザ で問題ないのですが、ある特定のリンク集ページの特定の URLリンク のみ表示崩れなどが発生してしまうため、そこだけは IE で開き直したい、というようなシチュエーションが発生しました。
開く度にこのトラップに引っかかって、手動で「ここはダメな URL だから IE で開き直して……」という作業は、地味にストレスを蓄積します。そこで、この「IE で開き直す」という虚無の作業を自動化したいと考えました。
コード
動作イメージ
設計
今回必要になる機能は以下。
- Chrome から IE で開き直す
- Chrome からコンピュータのローカルリソースにアクセス: ブラウザからブラウザ、というと一見簡単そうに見えますが、 IE も実体としては exeファイル のため、「 Chrome というブラウザのサンドボックスから、どうにかしてホスト(コンピュータ)のネイティブなリソース (exe の実行ファイル) にアクセスする」という課題をくりあする必要があります
- URL をパラメータとして渡す: IE がただ開くだけでは要求を達成できません。 Chrome でクリックしたリンクの URL を IE に渡し、その URL を IE のタブで開き直す動作が必要です
- Chrome のタブを閉じる: IE が開けば Chrome で開いたタブは不要なので、自動的に閉じる必要があります
- 設定画面
manifest.json
に「特定ページ内で動作」を決め打ちで指定したり、「特定リンクをクリック」の対象 URL をコード中に記述して決め打ちに指定したり、といったことは避けたかったので、個人的には初めて Chrome拡張機能 の設定画面を作る必要に迫られました
- PowerShell
- IE を起動するためには、ホスト側のプログラムが必要です。ただ起動する、URLをパラメータとして渡す、までであれば bat でもできなくはなさそうでしたが、不要になった Chrome のタブを閉じるためには Chrome から送信されてきた JSON形式 のメッセージにレスポンスを返さなければなりません。それには、 bat だけでは不十分だと感じたので Powershell で対応することにしました
ディレクトリ構造
/
├ app/ // Chrome拡張機能 側のコード
│ ├ icon/
│ ├ index.css
│ ├ index.html // Native messaging を実行するための Chromeタブ
│ ├ nmancie.js // index.html で動作する JavaScript
│ ├ options.css
│ ├ options.html // 設定画面
│ └ options.js
│
├ host/ // ネイティブ側のコード
│ ├ register-host.bat // レジストリに Chrome拡張機能 を登録する bat
│ ├ revenant.bat // nmancie.js から Native messaging で呼ばれるプログラム。渡されたメッセージを横流しして nmancie.ps1 をキックする
│ ├ revenant.json // register-host.bat で登録される manifest.json (アプリマニフェスト)
│ └ revenant.ps1 // Native messaging を Chrome拡張機能 側とやり取りしつつ IE をキックするホスト側プログラムのメインコード
│
├ background.js // Chrome拡張機能 の Content scripts
├ manifest.json
├ readme.md
└ transi.js // Chrome拡張機能 の Background scripts (イベントページ)
実装 (重要な部分のみ)
manifest.json と各プログラムの連携
"background": {
"scripts": [
"transi.js"
],
"persistent": false
},
"content_scripts": [
{
"matches": [
"*://*/*"
],
"js": [
"background.js"
]
}
],
"app": {
"launch": {
"local_path": "./app/index.html"
}
},
"options_page": "./app/options.html",
"permissions": [
"tabs",
"activeTab",
"storage",
"nativeMessaging"
],
今回は必要な機能が多いので、込み入ってます。
まず重要な点として、 Native messaging を始めとする chrome.runtime
の一部機能や chrome.tabs
といったプロパティ・メソッドは Content scripts (content_scripts
) からは操作できません。
そのため、「特定ページのみで動作する」 Content scripts から Background scripts に処理を引き継いであげる必要があります。その際にクリックしたリンクの URL を渡したかったので、 chrome.runtime.sendMessage
を使用しました。なお、 chrome.runtime.sendMessage
は上述の Native messaging とは異なり、 Content scripts からもアクセスできます。
そこで、 content_scripts
は全てのページで background.js
が動作する(設定で保存された URL でのみ処理が走るように最初に条件分岐させていますが)ようにしました。
一方、 background
で Background scripts として transi.js
を指定しています。
transi.js
は background.js
からクリックされた URL を受け取り、 chrome.tabs.create
で新しいタブを開きます。開くタブは app/index.html
、つまりこの Chrome拡張機能 の専用ページです。
専用ページをひらくために app
→ launch
→ local_path
で開くページを指定しています。この app/index.html
から nmancie.js
が読み込まれ、 chrome.runtime.sendNativeMessage
で Native messaging によりホスト側に処理が引き継がれます。
なお、ホスト側の処理で IE が開いた場合は Native messaging のレスポンスが返却されるので、それを受け取ったら app/index.html
のタブを閉じます。
また、設定画面のために options_page
の指定もしています。
以上から、 permissions
には tabs
, activeTab
, storage
, nativeMessaging
を指定しました。
background.js
次に Chrome拡張機能 側のコードを見て行きます。
const bokor = () => {
let powder = {
pageUrl: 'example.com',
actionUrl: [
'example.jp',
'example.co.jp',
'example.or.jp',
'example.ac.jp',
'example.go.jp',
'example.org'
]
};
// default options
let defaultPowder = powder;
defaultPowder.actionUrl = JSON.stringify(powder.actionUrl);
// get options
chrome.storage.local.get(
defaultPowder,
function (items) {
if (
chrome.runtime.error
) {
console.log(`Error: ${chrome.runtime.error}`);
}
else {
powder.pageUrl = items.pageUrl;
powder.actionUrl = JSON.parse(items.actionUrl);
// call action with options
zombie(powder);
}
}
);
};
まず設定読み込みを実施 ( options.js
もほぼ同じことをしています) し、成功したら処理の呼び出しをしています。設定読み込み失敗時は chrome.runtime.error
を出力しています。
const zombie = (powder) => {
// 略
chrome.runtime.sendMessage(
{
args: targetLink.href
},
function (response) {
// メッセージリッスンが終わった後に実行
const msg = `${response.return ? 'done' : 'failed'}: ${response.msg}`;
console.log(msg);
});
//略
};
transi.js
呼び出し部分。上述の通り chrome.runtime.sendMessage
でやり取りをしています。引数に args
でクリックしたリンクの URL を渡しています。
transi.js
// content script (background.js) からメッセージが渡されて実行
chrome.runtime.onMessage.addListener(
function(
request,
sender,
sendResponse
) {
try {
// 新規タブを開き、拡張機能ページを表示 (メッセージで渡された URL を GETパラメータにセット)
chrome.tabs.create(
{
url: `app/index.html?transi=${request.args}`
}
);
// レスポンス返却
sendResponse(
{
return: true,
msg: 'OK.'
}
);
} catch (error) {
// レスポンス返却
sendResponse(
{
return: false,
msg: error
}
);
}
}
);
chrome.runtime.onMessage.addListener
で chrome.runtime.sendMessage
によるメッセージが送信されたら、というイベントリスナを登録。
ここから chrome.tabs.create
で新しいタブで Chrome拡張機能 専用ページを開くようにしています。上述の通り、このスクリプトは Background scripts なので chrome.tabs.create
にアクセス可能です。
また、 sendResponse
で元の background script
にメッセージを返却しています。
app/nmancie.js
const revenant = () => {
// 略
chrome.runtime.sendNativeMessage(
'transi.nmancie.revenant',
{
'args': params.get('transi')
},
(responseFromNativeHost) => {
if (
chrome.runtime.lastError !== undefined
&& chrome.runtime.lastError !== null
) {
console.error(chrome.runtime.lastError.message);
msgBox.textContent = chrome.runtime.lastError.message;
}
else if (
responseFromNativeHost !== undefined
&& responseFromNativeHost !== null
&& responseFromNativeHost.message !== 'ok'
) {
console.log(`Error occured: ${responseFromNativeHost.message}`);
msgBox.textContent = `Error occured: ${responseFromNativeHost.message}`;
}
else {
console.log(`done: ${responseFromNativeHost.message}`);
msgBox.textContent = `done: ${responseFromNativeHost.message}`;
// IE を開くことに成功したら、自身のタブを閉じる
chrome.tabs.getCurrent(function (tab) {
chrome.tabs.remove(tab.id);
});
}
}
);
};
chrome.tabs.create
で開いた専用ページで、ホスト側とのやり取りを実装しています。
chrome.runtime.sendNativeMessage
がそれです。第一引数がレジストリに登録する名前空間になるので
- この第一引数
- アプリマニフェストの
name
- レジストリに登録するキー
の3つを一致させます。
今回は transi.nmancie.revenant
ですね。
第二引数はホスト側に送信するメッセージ。今回は URL を指定したいのでそれを args
として渡しています。オブジェクトでの指定ですが、実際にはバイト文字列として送信されます。
第三引数はコールバック関数で、引数はホスト側からのレスポンスメッセージです。なので、レスポンスメッセージの内容で条件分岐させます。エラーでないならば、chrome.tabs.getCurrent()
で自身の タブID を取得し、その タブID を指定して chrome.tabs.remove()
、つまりタブを閉じています。これで、「 IE を開いた後に不要となったタブを閉じる」の部分を実現しています。
host/register-host.bat
REG ADD "HKCU\Software\Google\Chrome\NativeMessagingHosts\transi.nmancie.revenant" /ve /t REG_SZ /d "%~dp0revenant.json" /f
まずはホストプログラムをレジストリに登録する bat 。上述の通り、キーに transi.nmancie.revenant
を指定しています。値はアプリマニフェストのファイルパスですが、 %~dp0
でディレクトリを指定すると自身の位置を返すので、これで「同じディレクトリにある revenant.json
」を指定することができます。
host/revenant.bat
@echo off
pushd %~dp0
powershell -ExecutionPolicy RemoteSigned -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -File ".\revenant.ps1"
ホスト側の Powershell をキックする bat 。 pushd %~dp0
で bat が受け取った引数をそのまま実行するファイルに横流しすることができます。今回は .\revenant.ps1
です。
host/revenant.ps1
# 略
try {
$reader = New-Object System.IO.BinaryReader([System.Console]::OpenStandardInput())
$len = $reader.ReadInt32()
$buf = $reader.ReadBytes($len)
$msg = [System.Text.Encoding]::UTF8.GetString($buf)
$obj = $msg | ConvertFrom-Json
$url = $obj.args # 引数をURLに指定
$shell = New-Object -ComObject Shell.Application
$ie = New-Object -ComObject InternetExplorer.Application # IE起動
$ie = $objShell.Windows() | ? {$_.Name -eq "Internet Explorer"} | Select-Object -First 1
$ie.Visible = $true
$ie.Navigate($url, 4)
# 略
}
# 略
最初の $reader
で Native messaging のメッセージを受け取ります。なお、このメッセージは
- JSON でシリアライズされ
- UTF-8 でエンコードされ
- メッセージ長を表す 32-bit の値がネイティブのバイト順で先頭に付加
されているので(特に最後)、メッセージから必要な値を取り出すデコード処理が必要です。その過程を経たものが $obj = $msg | ConvertFrom-Json
で $obj
に代入されます。上述のキー args
に URL が値としてセットされているため、 $obj.args
が開きたい URL になります。
それを IE のオブジェクトに引数で渡すことで、「指定された URL を IE で開く」ことを実現しています ( $ie.Navigate($url, 4)
)。
以上がメインの動作となります。
app/options.js
// Saves options to chrome.storage
const disperseSkeleton = () => {
const pageUrl = document.querySelector('#pageUrl').value;
const actionUrl = document.querySelector('#actionUrl').value;
chrome.storage.local.set(
{
pageUrl: pageUrl,
actionUrl: JSON.stringify(actionUrl.split('\n'))
},
function() {
const status = document.querySelector('#c-saveMsg');
status.textContent = '保存されました';
}
);
};
// stored in chrome.storage.
const constructSkeleton = () => {
const defaultData = {
pageUrl: 'example.com',
actionUrl: '["example.jp","example.co.jp","example.or.jp","example.ac.jp","example.go.jp","example.org"]'
};
chrome.storage.local.get(
defaultData,
function (items) {
if (
chrome.runtime.error
) {
console.log(`Error: ${chrome.runtime.error}`);
}
else {
document.querySelector('#pageUrl').value = items.pageUrl;
document.querySelector('#actionUrl').value = JSON.parse(items.actionUrl).join('\n');
}
}
);
};
const main = () => {
constructSkeleton();
const $btnSave = document.querySelector('#c-btnSave');
$btnSave.addEventListener('click', function () {
disperseSkeleton();
});
};
document.addEventListener('DOMContentLoaded', main());
最後にオプションページ(設定画面)。ここは主に2つの動作から構成されています。
- 設定読み込み: 保存された設定の読み込み。画面表示時に処理が走り、 Chrome のストレージに保存された設定内容を読み込みます
- 最初にデフォルト値をセットし、
chrome.storage.local.get()
で読み込みを実施します。読み込みに成功されれば、画面の入力フォームに値をセットします
- 最初にデフォルト値をセットし、
- 設定の保存: 「保存」ボタンクリック時に、オプションページに入力された内容を Chrome のストレージに保存します
- ボタン押下時に
chrome.storage.local.set()
で入力フォームにセットされた値を取得、 chrome のストレージに保存します
- ボタン押下時に
まとめ
Chrome拡張機能 も掘り下げると奥が深いということがよく分かりました。
特に冒頭で述べたように
chrome.runtime
の一部機能やchrome.tabs
といったプロパティ・メソッドは Content scripts (content_scripts
) からは操作できない- そもそも Content scripts と Background scripts の違い
- マニフェストの
permission
この辺りについて理解するまでかなり時間を要しました。
また、オプションページも今まで作ったことがなかったので、こちらについても未知の領域でしたが、何とか形にすることができました。
余談: 名前の由来
さて、最後は例によって名前の由来を。
このご時世もはや IE は滅びゆく定めだと思うのですが、そんな死に体の IE をわざわざ呼び起こすなんて、(いわゆるファンタジー世界のアンデッドモンスターを生み出す・操るような) ネクロマンシー(necromancy) 以外の何物でもない、と思います (元々 necromancy は necro(死) + mancy(占い) で口寄せ・降霊術を指していたらしいです)。
アンデッドといえばゾンビやスケルトンが有名どころだと思います。ちなみにレヴナントというのもありますが、これは旧フランス語の「戻る」「再び来る」を意味する revenir に由来する名前とのこと。
さて、先のネクロマンシーをフランス語で綴ると necromancIE 。 IE を呼び起こすにはふさわしい名前だろう、ということでこの綴りを採用しました。
それはそれとして、朽ちていく死体を彫刻に取り込んだ transi というものがあります。これもフランス語由来です。こちらは特にネクロマンシーとは関係なく、美術におけるヴァニタスの一環として生まれた表現技法だそうですが、今回はこれも取り込みました。
あとは先述の通りアンデッドモンスターの名前やそれに関する語句から関数などの名前を採っていきました。
参考
「Chrome拡張機能 からコンピュータのローカルリソース (プログラム) にアクセスする」というアイディア
- [Chrome拡張機能]ローカルのフォルダ/ファイルを開く機能を作ってみた – Qiita
- Google ChromeでトレイからCDを排出できるブラウザ拡張をつくりました(Native messaging版) – laiso
- 覚え書き: Open TortoiseSVN for Google Chrome の Native Messaging Host 対応
Native messaging
Native messaging を利用した Chrome拡張機能 のサンプル
- chrome-extensions-samples/mv2-archive/api/nativeMessaging at main ・ GoogleChrome/chrome-extensions-samples ・ GitHub
- GitHub – hmemcpy/ChromeRegJump: Chrome extension to open any selected Windows Registry path in Regedit using Sysinternals RegJump
- bat経由で PowerShell を動かすサンプル
Error when communicating with the native messaging host.
chrome.runtime.lastError
- chrome.runtime – Chrome Developers
- runtime.lastError – Mozilla | MDN
- chrome.runtimeのsendMessageとonMessageでハマった – C-3TO
Chrome拡張機能 でリンクの URL 一覧を取得する
manifest.json
permission
chrome.runtime
- chrome.runtime – Chrome Developers
- Content scriptss からはアクセスできない。 background (バックグラウンドページ or イベントページ) からならばアクセスできる
- Chrome拡張 備忘録 1から10まで – Qiita
- Content scripts からイベントページのスクリプトに処理を流す場合は message passing (
chrome.runtime.sendMessage
)を使用
- Content scripts からイベントページのスクリプトに処理を流す場合は message passing (
- Chrome拡張 備忘録 1から10まで – Qiita
- javascript – Chrome Native Messaging API chrome.runtime.connectNative is not a function – Stack Overflow
console.log("" + Object.getOwnPropertyNames(chrome.runtime));
で認識されているメソッド・プロパティの一覧を表示できる
chrome.tabs
- NewTab-Redirect/background.js at master ・ jimschubert/NewTab-Redirect ・ GitHub
- Content scriptss からはアクセスできない。 background (バックグラウンドページ or イベントページ) からならばアクセスできる
- Chrome拡張 備忘録 1から10まで – Qiita
- Content scripts からイベントページのスクリプトに処理を流す場合は message passing (
chrome.runtime.sendMessage
)を使用
- Content scripts からイベントページのスクリプトに処理を流す場合は message passing (
- Chrome拡張 備忘録 1から10まで – Qiita
タブを閉じる
- chrome.tabs – Chrome Developers
getCurrent()
: 現在開いているタブのオブジェクトを取得
- chrome.tabs – Chrome Developers
remove
: 閉じる
Chrome拡張機能 の設定画面
- 【Chrome拡張機能開発入門】設定画面などで情報をローカルに保存する、また読み込む|Masaru Suzuki|note
- Give users options – Chrome Developers
- Chrome拡張機能の開発入門(オプションページ編)
Chrome apps の移行期間
PowerShell から IE を操作
- PowerShellからIEを操作 – Qiita
- PowershellでInternetExplorerを操作する – Qiita
- WINDOWS7からWINDOWS10にアップデートした際にpowershellでIEのDOM操作ができなくなった話 – Qiita
ダークモード
- CSSだけでWebページをダークモード対応。ついにChromeにも対応した prefers-color-scheme が超便利|平田 / U-NEXT|note
- prefers-color-scheme – CSS: カスケーディングスタイルシート | MDN