拡張機能やブックマークレットが増えすぎる
Chrome拡張機能やブックマークレットをよく作ります。
サイトの文字を大きくしたり、見たい部分だけ残して他を非表示にしたり、ショートカットを追加したり、動画に部分リピート機能を付けたりしています。
お陰でPCでのWeb閲覧はたいへん快適になりました。
ブックマークレットや拡張機能はPC版ブラウザを使う大きなメリットだと思います。
ただ、ブックマークレットや拡張機能が増えてくると管理が面倒になってきます。
そこで、任意のサイト上で指定したJavascriptを自動実行する拡張機能を作りました。
URLとJavascriptファイルのペアを設定
サイトとJavascriptコードのペアは以下のように拡張機能内のオブジェクトで指定します。
{'URL:''filename.js'}
例:
const urlMappings = {
"togetter.com": "togetter.js",
"youtube.com/watch":"youtubeAbRepeat.js",
"www.amazon.co.jp/s?k=":"amazon.js"
// 他のURLとスクリプトのマッピングを追加...
};
あとは、background.jsがURLを監視して一致したら対応するJavascriptコードを対象ページに注入してくれます。
拡張機能アイコンの表示と機能
拡張機能アイコンは状態表示とオンオフスイッチを兼ねています。クリックするとON/OFFを切り替えられます。
赤で"ON":拡張機能はオンになっていますが、URLが一致しないのでJavascriptコード注入されていません。
緑で"ON":拡張機能はオンになっており、URLが一致したのでJavascriptコードが注入されました。
赤で"OFF":拡張機能はオフです。URLが一致してもJavascriptコードは注入されません。いったん「緑で"ON"」になった後でOFFにしても注入されたJavascriptは消えません。無効にするためにはページをリロードする必要があります。
表示やON/OFFに関しては、複数タブで同時に使うことは想定していないので、複数タブを開くと表示がおかしい時があります。
コード
コードは以下のようなものです。
URLの判定と対応するJavascriptの注入はbackground.js
が行っています。
単純な部分一致なので、abc.com
を設定した場合には、xxxabc.com
も注入対象になってしまいます。
manifest.json
{
"manifest_version": 3,
"name": "URL-based Script Injector",
"version": "1.0",
"permissions": [
"activeTab",
"storage",
"tabs",
"scripting"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_icon": {
"16": "icons/enabledIcon.png"
}
}
}
background.js
// background.js
console.log("background.js loaded");
const urlMappings = {
"youtube.com/watch":"youtubeAbRepeat.js"
// 他のURLとスクリプトのマッピングを追加...
};
let isEnabled = true; // デフォルトは有効
chrome.action.onClicked.addListener((tab) => {
isEnabled = !isEnabled; // 有効/無効を切り替え
if (isEnabled) {
chrome.action.setIcon({ path: 'icons/enabledIcon.png' });
chrome.storage.sync.set({ isEnabled: true });
} else {
chrome.action.setIcon({ path: 'icons/disabledIcon.png' });
chrome.storage.sync.set({ isEnabled: false });
}
});
// Webページに対するスクリプトの注入をチェックするロジック
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {//タブの更新で発火
console.log("chrome.tabs.onUpdated");
if (changeInfo.status === 'complete') {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs[0];
console.log(JSON.stringify(tab));
if (tab.url && !tab.url.startsWith('chrome://')) {
console.log("check passed");
chrome.storage.sync.get('isEnabled', (data) => {//chromeのストレージをチェック
if (data.isEnabled) {
console.log("switch is Enabled");
// 有効の場合、urlMappingに沿ってスクリプトを注入する
for (let [urlPart, scriptFile] of Object.entries(urlMappings)) {
console.log(tab.url, urlPart, scriptFile)
if (tab.url.includes(urlPart)) {
console.log("matched");
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: [scriptFile]
});
console.log("injected");
chrome.action.setIcon({ path: 'icons/injectedIcon.png' });
break; // URLにマッチした場合はループを抜ける
}
}
}
});
}
});
}
});
// 拡張機能起動時に前回の状態を取得
chrome.runtime.onStartup.addListener(() => {
console.log("on Startup")
chrome.storage.sync.get('isEnabled', (data) => {
isEnabled = data.isEnabled;
});
if (data.isEnabled) {
chrome.action.setIcon({ path: 'icons/enabledIcon.png' });
} else {
chrome.action.setIcon({ path: 'icons/disabledIcon.png' });
}
});
注入するコードの例として、Youtubeの部分リピートを可能にするコードを挙げておきます。以下の記事で作ったブックマークレットを拡張機能用にしたものです。
Youtubeの動画再生ページで再生中にAボタンを押すとリピート開始時間、Bボタンを押すとリピート終了時間を設定できます。Nを押すとリピートのON/OFFができます。
youtubeAbRepeat.js
(function () {
const videoElement = document.querySelector("video");
let repeatTime_A = 0;
let repeatTime_B = videoElement.duration;
let enable_loop = false;
console.log(repeatTime_A, repeatTime_B, enable_loop);
console.log("A-B repeat bookmarklet is on");
document.addEventListener("keydown", function (e) {
if (e.target.tagName.toLowerCase() === 'input') {
return; // input要素での入力中は何もしない
}
const now = videoElement.currentTime;
console.log(e.key);
if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey || e.isComposing) return;
switch (e.code) {
case 'KeyA':
repeatTime_A = now;
if (now > repeatTime_B) repeatTime_B = videoElement.duration;
enable_loop = true;
console.log("repeat", repeatTime_A, repeatTime_B);
break;
case 'KeyB':
repeatTime_B = now < 1 ? 1 : now;
if (now <= repeatTime_A) repeatTime_A = 0;
enable_loop = true;
console.log("repeat", repeatTime_A, repeatTime_B);
break;
case 'KeyN':
enable_loop = !enable_loop;
console.log("repeat", enable_loop);
break;
}
});
videoElement.addEventListener("seeking", (e) => {
const now = videoElement.currentTime;
if (now > repeatTime_B || now < repeatTime_A) {
enable_loop = false;
console.log("repeat is off");
}
// else {
// enable_loop = true;
// console.log("repeat is on");
// }
});
videoElement.addEventListener("timeupdate", (e) => {
const now = videoElement.currentTime;
if (enable_loop) {
if (now > repeatTime_B) {
videoElement.currentTime = repeatTime_A;
} else if (now < repeatTime_A) {
videoElement.currentTime = repeatTime_A;
}
}
});
})();
アイコンファイルも含めたプロジェクト全体をgithubに置いておきます。
https://github.com/ryjkmr/WebScriptInjector
よく使うコード
不要な要素を非表示にする
CSSセレクタで指定した要素をすべて非表示にします。
CSSセレクタはChromeのデベロッパーツールのElementsで消したい要素を右クリックしてCopy Selectorとしたり、条件をChatGPTに伝えて書いてもらうと楽です。
function hideElementsBySelector(selector) {
var elements = document.querySelectorAll(selector);
for (var i = 0; i < elements.length; i++) {
elements[i].style.display = 'none';
}
}
// 使用例:
// hideElementsBySelector('.your-css-class');
動的なページに対応する
動的に更新されるページは、window.onloadが発火した以降も更新されるため、上記のコードだけでは追加でロードされた要素が消えません。
以下のようにMutationObserverを使ってDOMを監視します。監視するDOMの属性や範囲などを変更する必要がある場合は、configオブジェクトの内容を適宜調整する必要があります。
function hideElementsBySelector(selector) {
var elements = document.querySelectorAll(selector);
for (var i = 0; i < elements.length; i++) {
elements[i].style.display = 'none';
}
}
// MutationObserverを初期化します
const observer = new MutationObserver(function(mutations) {
// DOMの変更が発生したときにhideElementsBySelector関数を起動
hideElementsBySelector('.your-css-class');
});
const config = {
childList: true,
attributes: true,
characterData: true,
subtree: true
};
// document.bodyを監視対象として指定
observer.observe(document.body, config);