이 기사에서는 웹사이트 차단을 위한 브라우저 확장 프로그램을 구축하는 단계별 과정을 설명하고 제가 겪었던 문제와 제가 생각해낸 해결책에 대해 설명하겠습니다. 이것은 철저한 가이드가 아닙니다. 나는 어떤 분야에서도 전문가라고 주장하지 않습니다. 저는 이 프로젝트를 구축하는 데 있어 제 사고 과정을 공유하고 싶습니다. 그러니 여기에 있는 모든 것을 소금 한 알로 가져가세요. 나는 모든 내용을 다루지는 않고 대신 프로젝트의 핵심 사항, 어려움, 흥미로운 사례 및 프로젝트의 특이점에 중점을 둘 것입니다. 소스 코드를 직접 살펴보실 수 있습니다.
목차:
많은 사람들과 마찬가지로 저도 다양한 작업에 집중하는 데 어려움을 겪습니다. 특히 인터넷은 어디에나 존재하는 방해 요소이기 때문입니다. 운 좋게도 저는 프로그래머로서 훌륭한 문제 생성 기술을 개발했기 때문에 더 나은 기존 솔루션을 찾는 대신 사용자가 액세스를 제한하려는 웹 사이트를 차단하는 자체 브라우저 확장 프로그램을 만들기로 결정했습니다.
먼저 요구사항과 주요 기능을 간략하게 설명하겠습니다. 확장 기능은 다음을 충족해야 합니다.
먼저 제가 선택한 메인 스택은 다음과 같습니다.
일반 웹 개발과 확장 프로그램 개발의 주요 차이점은 확장 프로그램이 대부분의 이벤트, 콘텐츠 스크립트 및 이들 간의 메시징을 처리하는 서비스 워커에 의존한다는 것입니다.
크로스 브라우저 기능을 지원하기 위해 두 개의 매니페스트 파일을 만들었습니다.
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'", }
여기서 흥미로운 점 중 하나는 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가 삽입되어 양식이 나타납니다. 팝업을 호출하는 등 양식을 표시하는 방법은 다양하지만 내 취향에 맞는 사용자 정의 옵션은 제한되어 있습니다. 배경 스크립트는 다음과 같습니다.
배경.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)); } }
ℹ 모든 페이지에 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 조작이지만, 요소에 하나의 템플릿 리터럴을 적용하는 것이 아니라 각 요소를 개별적으로 생성해야 한다는 점에 유의하세요. 더 장황하고 지루하지만 이 방법을 사용하면 브라우저에서 컴파일된 코드를 실행하려고 할 때 표시되는 안전하지 않은 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와 동적 규칙을 활용했습니다. 규칙은 확장 프로그램의 저장소에 저장됩니다. 동적 규칙 조작은 서비스 워커 파일에서만 가능하므로 서비스 워커와 콘텐츠 스크립트 간에 데이터를 교환하기 위해 필요한 데이터와 함께 메시지를 보내도록 하겠습니다. 이 확장에는 필요한 작업 유형이 꽤 많기 때문에 모든 작업에 대한 유형을 만들었습니다. 작업 유형의 예는 다음과 같습니다.
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"] }], }
제공된 URL의 유효성을 검사한 다음 이를 블랙리스트에 추가하기 위해 서비스 워커에 보내는 content.ts의 handlerFormSubmission()을 호출합니다.
ℹ 동적 규칙에는 고려해야 할 최대 크기가 설정되어 있습니다. 너무 긴 URL 문자열을 전달하면 이에 대한 동적 규칙을 저장하려고 할 때 예기치 않은 동작이 발생합니다. 내 경우에는 75자 길이의 URL이 규칙의 최대 길이에 적합한 것으로 나타났습니다.
서비스 워커가 수신된 메시지를 처리하는 방법은 다음과 같습니다.
배경.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을 추가하고 해당 URL에 할당하려는 차단 유형을 선택할 수 있습니다. 차단된 리소스에 액세스하려고 하면 차단 페이지로 리디렉션됩니다:
그러나 해결해야 할 한 가지 극단적인 경우가 있습니다. 확장 프로그램은 사용자가 직접 액세스하는 경우 원치 않는 URL을 차단합니다. 그러나 웹사이트가 클라이언트 측 리디렉션이 포함된 SPA인 경우 확장 프로그램은 그곳에서 금지된 URL을 포착하지 않습니다. 이 경우를 처리하기 위해 현재 탭을 수신하고 URL이 변경되었는지 확인하도록 background.ts를 업데이트했습니다. 그럴 때는 URL이 블랙리스트에 있는지 수동으로 확인하고, 있으면 사용자를 리디렉션합니다.
배경.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를 배열로 수집합니다. 저장 시 변경 사항을 적용하기 위해 서비스 워커에 전달하는 업데이트된 동적 규칙을 생성합니다. 모든 변경 사항을 저장하거나 다시 로드한 후 동적 규칙을 다시 가져와 테이블에 렌더링합니다. 다음은 단순화된 버전입니다.
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개 더 있습니다.
배경.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을 삭제하는 것이 아마도 가장 쉬운 작업이었을 것입니다. 이 확장 프로그램에는 특정 규칙 삭제와 모든 규칙 삭제라는 두 가지 유형의 삭제가 있습니다.
options.ts:
dist/ node_modules/ src/ |-- background.tsc |-- content.ts static/ |-- manifest.chrome.json |-- manifest.firefox.json package.json tsconfig.json webpack.config.js
그리고 이전과 마찬가지로 서비스 워커 리스너에 작업 2개를 더 추가했습니다.
배경.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 부울도 저장소에 저장됩니다. 그것이 사실이라면 모든 규칙을 반복하고 새로 생성된 차단 해제 시간과 함께 비활성화된 규칙을 스토리지에 추가합니다. 그런 다음 응답할 때마다 저장소에 비활성화된 규칙이 있는지 확인하고 만료된 규칙이 있으면 제거하고 업데이트합니다.
배경.ts:
{ "background": { "scripts": [ "browser-polyfill.js", // other scripts ], }, "content_scripts": [{ "js": [ "browser-polyfill.js", // other scripts ] }], }
이렇게 하면 웹사이트 차단 확장이 완료됩니다. 사용자는 원하는 URL을 추가, 편집, 삭제 및 비활성화하고, 부분 또는 전체 도메인 차단을 적용하고, 엄격 모드를 사용하여 탐색 시 더 많은 규율을 유지할 수 있습니다.
이것이 내 사이트 차단 확장 프로그램의 기본 개요입니다. 그것은 나의 첫 번째 확장이었고 특히 웹 개발의 세계가 때때로 평범해질 수 있다는 점을 고려하면 흥미로운 경험이었습니다. 확실히 개선의 여지와 새로운 기능이 있습니다. 블랙리스트에 있는 URL에 대한 검색 창, 적절한 테스트 추가, 엄격 모드에 대한 사용자 정의 기간, 한 번에 여러 URL 제출 등은 언젠가 이 프로젝트에 추가하고 싶은 마음속에 있는 몇 가지 사항입니다. 저도 처음에는 확장 프로그램을 크로스 플랫폼으로 만들려고 계획했지만 휴대폰에서 실행할 수 없었습니다.
이 연습을 재미있게 읽었거나, 새로운 내용을 배웠거나, 다른 피드백이 있으면 의견을 보내주시면 감사하겠습니다. 읽어주셔서 감사합니다.
소스코드
라이브 버전
위 내용은 사이트 차단 크로스 브라우저 확장 프로그램 구축의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!