この記事では、Web サイトをブロックするためのブラウザー拡張機能を構築する私のプロセスを段階的に説明し、私が遭遇した課題と私が思いついた解決策について説明します。これは包括的なガイドを意図したものではありません。私は何かの専門家であるとは主張しません。このプロジェクトを構築する背後にある私の思考プロセスを共有したいだけです。したがって、ここでのすべてを割り引いて考えてください。すべての行を説明するのではなく、プロジェクトのキーポイント、苦労、興味深いケース、プロジェクトの癖に焦点を当てます。ご自身でソース コードをさらに詳しく調べてみてください。
目次:
多くの人と同じように、私もさまざまなタスクに集中するのに苦労しています。特にインターネットが気を散らすものであることが多いためです。幸いなことに、私はプログラマーとして優れた問題作成スキルを身につけてきたので、より優れた既存のソリューションを探すのではなく、ユーザーがアクセスを制限したい Web サイトをブロックする独自のブラウザー拡張機能を作成することにしました。
まず、要件と主な機能の概要を説明します。拡張機能は次のことを行う必要があります:
まず、私が選んだメインスタックは次のとおりです。
拡張機能開発と通常の Web 開発との主な違いは、拡張機能がほとんどのイベント、コンテンツ スクリプト、およびそれらの間のメッセージングを処理する Service Worker に依存していることです。
クロスブラウザー機能をサポートするために、2 つのマニフェスト ファイルを作成しました。
manifest.chrome.json:
{ "manifest_version": 3, "action": { "default_title": "Click to show the form" }, "incognito": "split", "permissions": [ "activeTab", "declarativeNetRequestWithHostAccess", "scripting", "storage", "tabs" ], "host_permissions": ["*://*/"], // get access to all URLs "background": { "service_worker": "background.js" }, "content_scripts": [{ "matches": ["<all_urls>"] }], "web_accessible_resources": [ { "resources": ["blocked.html", "options.html", "about.html", "icons/*.svg"], "matches": ["<all_urls>"] } ], "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" }, }
manifest.firefox.json:
{ "manifest_version": 2, "browser_action": { "default_title": "Click to show the form" }, "permissions": [ "activeTab", "declarativeNetRequest", "declarativeNetRequestWithHostAccess", "scripting", "storage", "tabs", "*://*/" ], "background": { "scripts": [ "background.js" ], "persistent": false }, "content_scripts": [{ "matches": ["<all_urls>"], "js": [ "options.js", "blocked.js", "about.js" ] }], "web_accessible_resources": [ "blocked.html", "options.html", "icons/*.svg" ], "content_security_policy": "script-src 'self'; object-src 'self'", }
ここで興味深い点の 1 つは、Chrome には "incognito": "split" が必要であるということです。これは、Firefox がシークレット モードなしで正常に動作するのに対し、シークレット モードで適切に動作するように指定されたプロパティです。
拡張機能の基本的なファイル構造は次のとおりです:
dist/ node_modules/ src/ |-- background.tsc |-- content.ts static/ |-- manifest.chrome.json |-- manifest.firefox.json package.json tsconfig.json webpack.config.js
ここで、拡張機能がどのように機能するかについて説明しましょう。ユーザーは、ブロックしたい URL を送信するために、ある種のフォームをトリガーできる必要があります。ユーザーが URL にアクセスすると、拡張機能がリクエストをインターセプトし、リクエストをブロックするか許可するかをチェックします。また、ユーザーがブロックされたすべての URL のリストを表示し、リストから URL を追加、編集、無効化、または削除できる、ある種のオプション ページも必要です。
ユーザーが拡張機能アイコンをクリックするかキーボード ショートカットを入力すると、HTML と CSS が現在のページに挿入されてフォームが表示されます。ポップアップを呼び出すなど、フォームを表示するにはさまざまな方法がありますが、私の好みに合わせたカスタマイズ オプションは限られています。バックグラウンド スクリプトは次のようになります:
background.ts:
import browser, { DeclarativeNetRequest } from 'webextension-polyfill'; // on icon click const action = chrome.action ?? browser.browserAction; // Manifest v2 only has browserAction method action.onClicked.addListener(tab => { triggerPopup(tab as browser.Tabs.Tab); }); // on shortcut key press browser.commands.onCommand.addListener(command => { if (command === 'trigger_form') { browser.tabs.query({ active: true, currentWindow: true }) .then((tabs) => { const tab = tabs[0]; if (tab) { triggerPopup(tab); } }) .catch(error => console.error(error)); } }); function triggerPopup(tab: browser.Tabs.Tab) { if (tab.id) { const tabId = tab.id; browser.scripting.insertCSS(({ target: { tabId }, files: ['global.css', './popup.css'], })) .then(() => { browser.scripting.executeScript ? browser.scripting.executeScript({ target: { tabId }, files: ['./content.js'], // refer to the compiled JS files, not the original TS ones }) : browser.tabs.executeScript({ file: './content.js', }); }) .catch(error => console.error(error)); } }
ℹ Web ページのさまざまなスタイルがフォームにどのような影響を与えるかを予測するのは難しいため、HTML をすべてのページに挿入すると、予測できない結果が生じる可能性があります。より良い代替案は、スタイルの独自のスコープを作成する Shadow DOM を使用することのようです。潜在的な改善であることは間違いなく、今後取り組んでいきたいと思っています。
ブラウザの互換性のために webextension-polyfill を使用しました。これを使用することで、マニフェストのバージョンごとに個別の拡張機能を作成する必要がなくなりました。機能の詳細については、こちらをご覧ください。これを機能させるために、マニフェスト ファイル内の他のスクリプトの前に、browser-polyfill.js ファイルを含めました。
manifest.chrome.json:
{ "content_scripts": [{ "js": ["browser-polyfill.js"] }], }
manifest.firefox.json:
{ "background": { "scripts": [ "browser-polyfill.js", // other scripts ], }, "content_scripts": [{ "js": [ "browser-polyfill.js", // other scripts ] }], }
フォームを挿入するプロセスは簡単な DOM 操作ですが、1 つのテンプレート リテラルを要素に適用するのではなく、各要素を個別に作成する必要があることに注意してください。より冗長で退屈ですが、このメソッドは、ブラウザでコンパイルされたコードを実行しようとしたときに表示される安全でない HTML インジェクションの警告を回避します。
content.ts:
import browser from 'webextension-polyfill'; import { maxUrlLength, minUrlLength } from "./globals"; import { GetCurrentUrl, ResToSend } from "./types"; import { handleFormSubmission } from './helpers'; async function showPopup() { const body = document.body; const formExists = document.getElementById('extension-popup-form'); if (!formExists) { const msg: GetCurrentUrl = { action: 'getCurrentUrl' }; try { const res: ResToSend = await browser.runtime.sendMessage(msg); if (res.success && res.url) { const currUrl: string = res.url; const popupForm = document.createElement('form'); popupForm.classList.add('extension-popup-form'); popupForm.id = 'extension-popup-form'; /* Create every child element the same way as above */ body.appendChild(popupForm); popupForm.addEventListener('submit', (e) => { e.preventDefault(); handleFormSubmission(popupForm, handleSuccessfulSubmission); // we'll discuss form submission later }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { if (popupForm) { body.removeChild(popupForm); } } }); } } catch (error) { console.error(error); alert('Something went wrong. Please try again.'); } } } function handleSuccessfulSubmission() { hidePopup(); setTimeout(() => { window.location.reload(); }, 100); // need to wait a little bit in order to see the changes } function hidePopup() { const popup = document.getElementById('extension-popup-form'); popup && document.body.removeChild(popup); }
次に、フォームがブラウザに表示されることを確認します。必要なコンパイル手順を実行するために、Webpack を次のように構成しました:
webpack.config.ts:
{ "manifest_version": 3, "action": { "default_title": "Click to show the form" }, "incognito": "split", "permissions": [ "activeTab", "declarativeNetRequestWithHostAccess", "scripting", "storage", "tabs" ], "host_permissions": ["*://*/"], // get access to all URLs "background": { "service_worker": "background.js" }, "content_scripts": [{ "matches": ["<all_urls>"] }], "web_accessible_resources": [ { "resources": ["blocked.html", "options.html", "about.html", "icons/*.svg"], "matches": ["<all_urls>"] } ], "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" }, }
基本的に、実行したコマンドの環境変数からブラウザ名を取得して 2 つのマニフェスト ファイルから選択し、TypeScript コードを dist/ ディレクトリにコンパイルします。
ℹ 拡張機能の適切なテストを作成するつもりでしたが、Puppeteer がコンテンツ スクリプトのテストをサポートしていないため、ほとんどの機能をテストできないことがわかりました。コンテンツ スクリプトのテストに関する回避策についてご存知の場合は、ぜひコメント欄でお知らせください。
package.json のビルド コマンドは次のとおりです:
{ "manifest_version": 2, "browser_action": { "default_title": "Click to show the form" }, "permissions": [ "activeTab", "declarativeNetRequest", "declarativeNetRequestWithHostAccess", "scripting", "storage", "tabs", "*://*/" ], "background": { "scripts": [ "background.js" ], "persistent": false }, "content_scripts": [{ "matches": ["<all_urls>"], "js": [ "options.js", "blocked.js", "about.js" ] }], "web_accessible_resources": [ "blocked.html", "options.html", "icons/*.svg" ], "content_security_policy": "script-src 'self'; object-src 'self'", }
それで、たとえば、私が走るときはいつでも
dist/ node_modules/ src/ |-- background.tsc |-- content.ts static/ |-- manifest.chrome.json |-- manifest.firefox.json package.json tsconfig.json webpack.config.js
Chrome のファイルは dist/ ディレクトリにコンパイルされます。アクション アイコンをクリックするかショートカットを押して、任意のサイトでフォームをトリガーすると、フォームは次のようになります:
メイン フォームの準備ができたので、次のタスクはそれを送信することです。ブロック機能を実装するために、declarativeNetRequest API と動的ルールを活用しました。ルールは拡張機能のストレージに保存されます。動的ルールの操作は Service Worker ファイル内でのみ可能であるため、Service Worker とコンテンツ スクリプトの間でデータを交換するには、必要なデータを含むメッセージをそれらの間で送信します。この拡張機能にはかなりの種類の操作が必要なので、アクションごとにタイプを作成しました。操作タイプの例を次に示します:
types.ts:
import browser, { DeclarativeNetRequest } from 'webextension-polyfill'; // on icon click const action = chrome.action ?? browser.browserAction; // Manifest v2 only has browserAction method action.onClicked.addListener(tab => { triggerPopup(tab as browser.Tabs.Tab); }); // on shortcut key press browser.commands.onCommand.addListener(command => { if (command === 'trigger_form') { browser.tabs.query({ active: true, currentWindow: true }) .then((tabs) => { const tab = tabs[0]; if (tab) { triggerPopup(tab); } }) .catch(error => console.error(error)); } }); function triggerPopup(tab: browser.Tabs.Tab) { if (tab.id) { const tabId = tab.id; browser.scripting.insertCSS(({ target: { tabId }, files: ['global.css', './popup.css'], })) .then(() => { browser.scripting.executeScript ? browser.scripting.executeScript({ target: { tabId }, files: ['./content.js'], // refer to the compiled JS files, not the original TS ones }) : browser.tabs.executeScript({ file: './content.js', }); }) .catch(error => console.error(error)); } }
メイン フォームとオプション ページの両方から新しい URL を追加できるのは合理的であるため、送信は新しいファイル内の再利用可能な関数によって実行されました。
helpers.ts:
{ "content_scripts": [{ "js": ["browser-polyfill.js"] }], }
content.ts で handleFormSubmission() を呼び出しています。これは、提供された URL を検証し、それを Service Worker に送信してブラックリストに追加します。
ℹ 動的ルールには、考慮する必要がある最大サイズが設定されています。長すぎる URL 文字列を渡すと、動的ルールを保存しようとしたときに予期しない動作が発生します。私の場合、ルールの最大長は 75 文字の URL が適切であることがわかりました。
Service Worker が受信したメッセージを処理する方法は次のとおりです。
background.ts:
{ "background": { "scripts": [ "browser-polyfill.js", // other scripts ], }, "content_scripts": [{ "js": [ "browser-polyfill.js", // other scripts ] }], }
送信のために、新しいルール オブジェクトを作成し、それを含めるように動的ルールを更新します。単純な条件付き正規表現を使用すると、ドメイン全体をブロックするか、指定された URL のみをブロックするかを選択できます。
完了後、コンテンツスクリプトに応答メッセージを返します。このスニペットで最も興味深いのは、nanoid の使用です。試行錯誤の結果、動的ルールの量には制限があることがわかりました。古いブラウザでは 5000、新しいブラウザでは 30000 です。 5000 を超えるルールに ID を割り当てようとしたときに、バグによってこの問題が発生したことがわかりました。ID を 4999 未満に制限することはできなかったので、ID を 3 桁の数字に制限する必要がありました ( 0 ~ 999、つまり合計 1000 個の一意の ID)。これは、拡張機能のルールの総量を 5000 から 1000 に削減したことを意味します。これは、一方では非常に重要ですが、他方では、ユーザーがブロックする URL をそれほど多く持つ可能性は非常に低いため、私たちは、このあまり優雅ではない解決策で妥協することにしました。
これで、ユーザーは新しい URL をブラックリストに追加し、それらに割り当てるブロックの種類を選択できるようになりました。ブロックされたリソースにアクセスしようとすると、ブロック ページにリダイレクトされます:
ただし、対処する必要がある特殊なケースが 1 つあります。ユーザーが直接アクセスした場合、拡張機能は不要な URL をブロックします。ただし、Web サイトがクライアント側リダイレクトを備えた SPA である場合、拡張機能はそこで禁止されている URL をキャッチしません。このケースに対処するために、現在のタブをリッスンして URL が変更されたかどうかを確認するために、background.ts を更新しました。この問題が発生した場合、URL がブラックリストに含まれているかどうかを手動で確認し、含まれている場合はユーザーをリダイレクトします。
background.ts:
{ "manifest_version": 3, "action": { "default_title": "Click to show the form" }, "incognito": "split", "permissions": [ "activeTab", "declarativeNetRequestWithHostAccess", "scripting", "storage", "tabs" ], "host_permissions": ["*://*/"], // get access to all URLs "background": { "service_worker": "background.js" }, "content_scripts": [{ "matches": ["<all_urls>"] }], "web_accessible_resources": [ { "resources": ["blocked.html", "options.html", "about.html", "icons/*.svg"], "matches": ["<all_urls>"] } ], "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" }, }
getRules() は、declarativeNetRequest.getDynamicRules() メソッドを利用して、より読みやすい形式に変換するすべての動的ルールのリストを取得する関数です。
拡張機能は、直接および SPA 経由でアクセスされた URL を正しくブロックするようになりました。
オプション ページには、以下に示すようなシンプルなインターフェイスがあります:
これは、編集、削除、無効化、厳密モードの適用などの主要な機能が含まれるページです。配線方法は次のとおりです。
編集はおそらく最も複雑な作業でした。ユーザーは、文字列を変更するか、ブロック タイプを変更する (ドメイン全体または特定のドメインのみをブロックする) ことによって URL を編集できます。編集するときは、編集した URL の ID を配列に収集します。保存時に、変更を適用するために Service Worker に渡す更新された動的ルールを作成します。変更を保存するかリロードするたびに、動的ルールを再フェッチしてテーブルにレンダリングします。以下はその簡略版です:
options.ts:
{ "manifest_version": 3, "action": { "default_title": "Click to show the form" }, "incognito": "split", "permissions": [ "activeTab", "declarativeNetRequestWithHostAccess", "scripting", "storage", "tabs" ], "host_permissions": ["*://*/"], // get access to all URLs "background": { "service_worker": "background.js" }, "content_scripts": [{ "matches": ["<all_urls>"] }], "web_accessible_resources": [ { "resources": ["blocked.html", "options.html", "about.html", "icons/*.svg"], "matches": ["<all_urls>"] } ], "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" }, }
特定のルールをブロックするか許可するかを決定する方法は、その isActive プロパティを条件付きでチェックするだけです。ルールの更新とルールの取得 - これらはバックグラウンド リスナーに追加する 2 つの操作です:
background.ts:
{ "manifest_version": 2, "browser_action": { "default_title": "Click to show the form" }, "permissions": [ "activeTab", "declarativeNetRequest", "declarativeNetRequestWithHostAccess", "scripting", "storage", "tabs", "*://*/" ], "background": { "scripts": [ "background.js" ], "persistent": false }, "content_scripts": [{ "matches": ["<all_urls>"], "js": [ "options.js", "blocked.js", "about.js" ] }], "web_accessible_resources": [ "blocked.html", "options.html", "icons/*.svg" ], "content_security_policy": "script-src 'self'; object-src 'self'", }
編集された URL が既存のルールの複製になるという特殊なケースがあるため、更新機能を正しく理解するのは少し困難でした。それ以外は同じです。動的ルールを更新し、完了時に適切なメッセージを送信します。
URL の削除はおそらく最も簡単な作業です。この拡張機能には、特定のルールの削除とすべてのルールの削除の 2 種類の削除があります。
options.ts:
dist/ node_modules/ src/ |-- background.tsc |-- content.ts static/ |-- manifest.chrome.json |-- manifest.firefox.json package.json tsconfig.json webpack.config.js
そして、前と同じように、Service Worker リスナーにさらに 2 つのアクションを追加しました。
background.ts:
import browser, { DeclarativeNetRequest } from 'webextension-polyfill'; // on icon click const action = chrome.action ?? browser.browserAction; // Manifest v2 only has browserAction method action.onClicked.addListener(tab => { triggerPopup(tab as browser.Tabs.Tab); }); // on shortcut key press browser.commands.onCommand.addListener(command => { if (command === 'trigger_form') { browser.tabs.query({ active: true, currentWindow: true }) .then((tabs) => { const tab = tabs[0]; if (tab) { triggerPopup(tab); } }) .catch(error => console.error(error)); } }); function triggerPopup(tab: browser.Tabs.Tab) { if (tab.id) { const tabId = tab.id; browser.scripting.insertCSS(({ target: { tabId }, files: ['global.css', './popup.css'], })) .then(() => { browser.scripting.executeScript ? browser.scripting.executeScript({ target: { tabId }, files: ['./content.js'], // refer to the compiled JS files, not the original TS ones }) : browser.tabs.executeScript({ file: './content.js', }); }) .catch(error => console.error(error)); } }
おそらく、この拡張機能の主な機能は、ブラウジング習慣をより厳密に制御する必要があるユーザーに対して、無効化された (アクセスを許可された) ルールのブロックを自動的に強制する機能です。これは、厳密モードがオフになると、ユーザーによって無効にされた URL は、ユーザーが変更するまで無効のままになるという考えです。厳密モードをオンにすると、無効になったルールは 1 時間後に自動的に再び有効になります。このような機能を実装するために、拡張機能のローカル ストレージを使用して、無効になっている各ルールを表すオブジェクトの配列を保存しました。すべてのオブジェクトには、ルール ID、ブロック解除日、および URL 自体が含まれます。ユーザーが新しいリソースにアクセスするか、ブラックリストを更新するたびに、拡張機能はまずストレージに期限切れのルールがないかチェックし、それに応じてルールを更新します。
options.ts:
{ "content_scripts": [{ "js": ["browser-polyfill.js"] }], }
isStrictModeOn ブール値もストレージに保存されます。それが true の場合、すべてのルールをループし、新しく作成されたブロック解除時間を使用して無効になっているルールをストレージに追加します。次に、応答ごとに、無効なルールがないかストレージを確認し、期限切れのルールが存在する場合は削除して、更新します。
background.ts:
{ "background": { "scripts": [ "browser-polyfill.js", // other scripts ], }, "content_scripts": [{ "js": [ "browser-polyfill.js", // other scripts ] }], }
これでウェブサイトブロック拡張機能は完成です。ユーザーは、必要な URL を追加、編集、削除、無効化したり、部分的または全体のドメイン ブロックを適用したり、厳密モードを使用してブラウジングの規律を維持したりできます。
これが、私のサイトブロック拡張機能の基本的な概要です。これは私にとって初めての拡張機能であり、特に Web 開発の世界が時々平凡になり得ることを考えると、興味深い経験でした。改善と新機能の余地は間違いなくあります。ブラックリスト内の URL の検索バー、適切なテストの追加、厳密モードのカスタム期間、一度に複数の URL を送信する - これらは、いつかこのプロジェクトに追加したいと考えている、私の頭の中にあるもののほんの一部です。また、当初は拡張機能をクロスプラットフォームにすることを計画していましたが、私の携帯電話では実行できませんでした。
このウォークスルーを読んで面白かった場合、何か新しいことを学んだ場合、またはその他のフィードバックがある場合は、コメントをいただければ幸いです。読んでいただきありがとうございます。
ソースコード
ライブバージョン
以上がサイトブロッキングのクロスブラウザ拡張機能を構築するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。