您可能正在使用瀏覽器擴展程序。一些擴展程序非常流行且實用,例如廣告攔截器、密碼管理器和PDF 查看器。這些擴展程序(或“附加組件”)的功能並非僅限於此——您可以用它們做更多的事情!本文將向您介紹如何創建一個擴展程序。最終,我們將使其在多個瀏覽器中運行。
我們將製作一個名為“Reddit 轉錄器”的擴展程序,它將通過將特定評論移動到評論部分的頂部並為屏幕閱讀器添加aria- 屬性來提高Reddit 的可訪問性。我們還將通過添加選項來為評論添加邊框和背景以獲得更好的文本對比度,從而使我們的擴展程序更進一步。
整個想法是讓您很好地了解如何開發瀏覽器擴展程序。我們將首先為基於Chromium 的瀏覽器(例如Google Chrome、Microsoft Edge、Brave 等)創建擴展程序。在未來的文章中,我們將移植該擴展程序以使其與Firefox 兼容,以及最近在其MacOS 和iOS 版本的瀏覽器中都添加了對Web Extensions 支持的Safari。
GitHub 代碼庫準備好了嗎?讓我們一步一步來。
首先,我們需要一個項目的工作空間。我們真正需要的只是創建一個文件夾並為其命名(我將其命名為transcribers-of-reddit)。然後,在其中創建一個名為src 的文件夾用於我們的源代碼。
入口點是一個包含有關擴展程序的常規信息(即擴展程序名稱、描述等)並定義要執行的權限或腳本的文件。
我們的入口點可以是位於我們剛剛創建的src 文件夾中的manifest.json 文件。在其中,讓我們添加以下三個屬性:
<code>{ "manifest_version": 3, "name": "Reddit 转录器", "version": "1.0" }</code>
manifest_version 類似於npm 或Node 中的版本。它定義了哪些API 可用(或不可用)。我們將使用最新的版本3(也稱為mv3)進行前沿工作。
第二個屬性是name,它指定我們的擴展程序名稱。此名稱是我們的擴展程序出現在所有地方時顯示的名稱,例如Chrome 網上應用店和Chrome 瀏覽器中的chrome://extensions 頁面。
然後是version。它使用版本號標記擴展程序。請記住,此屬性(與manifest_version 相反)是一個字符串,只能包含數字和點(例如1.3.5)。
實際上,我們可以添加更多內容來幫助添加擴展程序的上下文。例如,我們可以提供一個描述來解釋擴展程序的功能。最好提供這些信息,因為它可以讓用戶更好地了解他們在使用擴展程序時會遇到什麼。
在本例中,我們不僅添加了描述,還提供了Chrome 網上應用店在其擴展程序頁面上指向的圖標和網址。
<code>{ "description": "使Reddit 对残疾用户更易于访问。", "icons": { "16": "images/logo/16.png", "48": "images/logo/48.png", "128": "images/logo/128.png" }, "homepage_url": "https://www.php.cn/link/530ebdeb0491c0459e00298fcdb3a2bd/extensions/tor/" }</code>
擴展程序的一大優勢是它們的API 允許您直接與瀏覽器交互。但是我們必須明確地授予擴展程序這些權限,這也包含在manifest.json 文件中。
<code>{ "manifest_version": 3, "name": "Reddit 转录器", "version": "1.0", "description": "使Reddit 对残疾用户更易于访问。", "icons": { "16": "images/logo/16.png", "48": "images/logo/48.png", "128": "images/logo/128.png" }, "homepage_url": "https://www.php.cn/link/530ebdeb0491c0459e00298fcdb3a2bd/extensions/tor/", "permissions": [ "storage", "webNavigation" ] }</code>
我們剛剛授予此擴展程序什麼權限?首先是storage。我們希望此擴展程序能夠保存用戶的設置,因此我們需要訪問瀏覽器的Web 存儲來保存它們。例如,如果用戶希望評論顯示紅色邊框,那麼我們將保存該設置以備下次使用,而不是讓他們再次設置。
我們還授予擴展程序查看用戶如何導航到當前屏幕的權限。 Reddit 是一個單頁應用程序(SPA),這意味著它不會觸發頁面刷新。我們需要“捕獲”此交互,因為只有在我們單擊帖子時,Reddit 才會加載帖子的評論。因此,這就是我們利用webNavigation 的原因。
我們稍後將介紹在頁面上執行代碼,因為它需要在manifest.json 中添加一個全新的條目。
/解釋根據允許的權限,瀏覽器可能會向用戶顯示警告以接受權限。但是,只有某些權限是這樣,Chrome 對這些權限有很好的概述。
瀏覽器擴展程序具有內置的國際化(i18n) API。它允許您管理多種語言的翻譯(完整列表)。要使用API,我們必須在manifest.json 文件中定義我們的翻譯和默認語言:
<code>"default_locale": "en"</code>
這將英語設置為語言。如果瀏覽器的設置為任何不受支持的其他語言,則擴展程序將回退到默認語言環境(在此示例中為en)。
我們的翻譯在_locales 目錄中定義。讓我們在其中為每種要支持的語言創建一個文件夾。每個子目錄都有自己的messages.json 文件。
<code>src └─ _locales └─ en └─ messages.json └─ fr └─ messages.json</code>
翻譯文件包含多個部分:
這是一個將所有這些整合在一起的示例:
<code>{ "userGreeting": { // 翻译键(“id”) "message": "Good $daytime$, $user$!" // 翻译"description": "用户问候", // 翻译人员的可选描述"placeholders": { // 可选占位符"daytime": { // 如消息中所引用"content": "$1", "example": "morning" // 我们内容的示例值}, "user": { "content": "$1", "example": "Lars" } } } }</code>
使用佔位符有點困難。首先,我們需要在消息中定義佔位符。佔位符需要用$ 字符括起來。之後,我們必須將佔位符添加到“佔位符列表”。這有點不直觀,但Chrome 希望知道應該為我們的佔位符插入什麼值。我們(顯然)希望在這裡使用動態值,因此我們使用特殊的content 值$1 來引用我們插入的值。
example 屬性是可選的。它可用於向翻譯人員提示佔位符可能是什麼值(但實際上並未顯示)。
我們需要為我們的擴展程序定義以下翻譯。將它們複製並粘貼到messages.json 文件中。隨意添加更多語言(例如,如果您說德語,請在_locales 中添加一個de 文件夾,依此類推)。
<code>{ "name": { "message": "Reddit 转录器" }, "description": { "message": "子reddits 的辅助图像描述。" }, "popupManageSettings": { "message": "管理设置" }, "optionsPageTitle": { "message": "设置" }, "sectionGeneral": { "message": "常规设置" }, "settingBorder": { "message": "显示评论边框" }, "settingBackground": { "message": "显示评论背景" } }</code>
您可能想知道為什麼我們在沒有i18n 權限的情況下註冊了權限,對吧? Chrome 在這方面有點奇怪,因為您不需要註冊每個權限。有些(例如chrome.i18n)不需要在manifest 中註冊。其他權限需要一個條目,但在安裝擴展程序時不會顯示給用戶。其他一些權限是“混合”的(例如chrome.runtime),這意味著它們的一些功能可以在不聲明權限的情況下使用——但同一API 的其他功能需要在manifest 中註冊一個條目。您需要查看文檔以全面了解這些差異。
最終用戶首先看到的是Chrome 網上應用店中的條目或擴展程序概述頁面。我們需要調整manifest 文件以確保所有內容都已翻譯。
<code>{ // 更新这些条目"name": "__MSG_name__", "description": "__MSG_description__" }</code>
應用此語法將使用messages.json 文件中的相應翻譯(例如,_MSG name使用name 翻譯)。
在HTML 文件中應用翻譯需要一些JavaScript。
<code>chrome.i18n.getMessage('name');</code>
該代碼返回我們定義的翻譯(即Reddit 轉錄器)。佔位符也可以類似的方式完成。
<code>chrome.i18n.getMessage('userGreeting', { daytime: 'morning', user: 'Lars' });</code>
以這種方式將翻譯應用於所有元素會很麻煩。但是我們可以編寫一個小的腳本,根據data- 屬性執行翻譯。因此,讓我們在src 目錄中創建一個新的js 文件夾,然後在其中添加一個新的util.js 文件。
<code>src └─ js └─ util.js</code>
這可以完成任務:
<code>const i18n = document.querySelectorAll("[data-intl]"); i18n.forEach(msg => { msg.innerHTML = chrome.i18n.getMessage(msg.dataset.intl); }); chrome.i18n.getAcceptLanguages(languages => { document.documentElement.lang = languages[0]; });</code>
將此腳本添加到HTML 頁面後,我們可以向元素添加data-intl 屬性來設置其內容。文檔語言也將根據用戶語言設置。
<code> </code>
<code>管理设置</code>
在我們深入實際編程之前,我們需要創建兩個頁面:
以下是我們需要創建頁面所需的文件夾和文件的概述:
<code>src ├─ css | └─ paintBucket.css ├─ popup | ├─ popup.html | ├─ popup.css | └─ popup.js └─ options ├─ options.html ├─ options.css └─ options.js</code>
.css 文件包含純CSS,僅此而已。我不會詳細介紹,因為我知道你們大多數讀者已經完全了解CSS 的工作原理。您可以從該項目的GitHub 代碼庫中復制和粘貼樣式。
請注意,彈出窗口不是選項卡,其大小取決於其中的內容。如果您想使用固定大小的彈出窗口,可以在html 元素上設置width 和height 屬性。
這是一個HTML 骨架,它鏈接CSS 和JavaScript 文件,並在
中添加標題和按鈕。 ```
<code><meta charset="UTF-8"> <meta content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" name="viewport"> <meta content="ie=edge" http-equiv="X-UA-Compatible"></code> <title data-intl="name"></title><link href="../css/paintBucket.css" rel="stylesheet"><link href="popup.css" rel="stylesheet"><h1></h1>
<code>h1 包含扩展程序名称和版本;按钮用于打开选项页面。标题将不会填充翻译(因为它缺少data-intl 属性),并且按钮还没有任何点击处理程序,因此我们需要填充popup.js 文件:</code>
const title = document.getElementById('title'); const settingsBtn = document.querySelector('button'); const manifest = chrome.runtime.getManifest();
title.textContent = ${manifest.name} (${manifest.version})
;
settingsBtn.addEventListener('click', () => { chrome.runtime.openOptionsPage(); });
<code>此脚本首先查找manifest 文件。Chrome 提供了包含getManifest 方法的runtime API(此特定方法不需要runtime 权限)。它将我们的manifest.json 返回为JSON 对象。在我们使用扩展程序名称和版本填充标题后,我们可以向设置按钮添加事件侦听器。如果用户与之交互,我们将使用chrome.runtime.openOptionsPage() 打开选项页面(同样不需要权限条目)。弹出窗口页面现在已完成,但扩展程序尚不知道它的存在。我们必须通过将以下属性附加到manifest.json 文件来注册弹出窗口。</code>
"action": { "default_popup": "popup/popup.html", "default_icon": { "16": "images/logo/16.png", "48": "images/logo/48.png", "128": "images/logo/128.png" } },
<code>#### 创建选项页面创建此页面的过程与我们刚刚完成的过程非常相似。首先,我们填充options.html 文件。以下是一些我们可以使用的标记:</code>
<circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
<code><div></div></code> <p>由<a href="https://www.php.cn/link/530ebdeb0491c0459e00298fcdb3a2bd" target="_blank">lars.koelker.dev</a>提供的Reddit 轉錄器擴展程序。</p>
Reddit 是Reddit, Inc. 的註冊商標。此擴展程序未以任何方式獲得Reddit, Inc. 的認可或關聯。
<code>目前还没有实际的选项(只有它们的包装器)。我们需要编写选项页面的脚本。首先,我们在options.js 中定义变量以访问我们的包装器和默认设置。“冻结”我们的默认设置可以防止我们以后意外修改它们。</code>
const defaultSettings = Object.freeze({ border: false, background: false, }); const generalSection = document.getElementById('generalOptionsWrapper');
<code>接下来,我们需要加载保存的设置。我们可以为此使用(先前注册的)存储API。具体来说,我们需要定义是要在本地存储数据(chrome.storage.local) 还是通过登录的所有设备同步设置(chrome.storage.sync)。让我们在这个项目中使用本地存储。需要使用get 方法检索值。它接受两个参数: 1. 我们要加载的条目2. 包含值的回调我们的条目可以是字符串(例如,下面的settings)或条目数组(如果我们想要加载多个条目,则很有用)。回调函数中的参数包含我们先前在{ settings: ... } 中定义的所有条目的对象:</code>
chrome.storage.local.get('settings', ({ settings }) => { const options = settings ?? defaultSettings; // 如果未定義設置,則回退到默認設置if (!settings) { chrome.storage.local.set({ settings: defaultSettings, }); }
// 創建和顯示選項const generalOptions = Object.keys(options).filter(x => !x.startsWith('advanced'));
generalOptions.forEach(option => createOption(option, options, generalSection)); });
<code>为了呈现选项,我们还需要创建一个createOption() 函数。</code>
function createOption(setting, settingsObject, wrapper) { const settingWrapper = document.createElement("div"); settingWrapper.classList.add("setting-item"); settingWrapper.innerHTML = <div><label for="${setting}">${chrome.i18n.getMessage(</label></div>
setting${setting}`)}
`;
const toggleSwitch = settingWrapper.querySelector("label.is-switch"); const input = settingWrapper.querySelector("input");
input.onchange = () => { toggleSwitch.setAttribute('aria-checked', input.checked); updateSetting(setting, input.checked); };
toggleSwitch.onkeydown = e => { if(e.key === " " || e.key === "Enter") { e.preventDefault(); toggleSwitch.click(); } }
wrapper.appendChild(settingWrapper); }
<code>在我们的开关(又名单选按钮)的onchange 事件侦听器中,我们调用函数updateSetting。此方法将把单选按钮的更新值写入存储中。为此,我们将使用set 函数。它有两个参数:我们要覆盖的条目和(可选)回调(在本例中我们不使用)。由于我们的settings 条目不是布尔值或字符串,而是一个包含不同设置的对象,因此我们使用扩展运算符(...) 并仅覆盖settings 对象中的实际键(setting)。</code>
function updateSetting(key, value) { chrome.storage.local.get('settings', ({ settings }) => { chrome.storage.local.set({ settings: { ...settings,
<code> } })</code>
}); }
<code>同样,我们需要通过将以下条目附加到manifest.json 来“通知”扩展程序我们的选项页面:</code>
"options_ui": { "open_in_tab": true, "page": "options/options.html" },
<code>根据您的用例,您还可以通过将open_in_tab 设置为false 来强制选项对话框作为弹出窗口打开。 ### 安装开发扩展程序现在我们已经成功设置了manifest 文件并将弹出窗口和选项页面都添加到了组合中,我们可以安装扩展程序来检查我们的页面是否正常工作。导航到chrome://extensions 并启用“开发者模式”。将出现三个按钮。单击标记为“加载解压”的按钮,然后选择扩展程序的src 文件夹以加载它。扩展程序现在应该已成功安装,并且我们的“Reddit 转录器”图块应该在页面上。我们现在已经可以与扩展程序交互了。单击地址栏旁边的拼图块(?) 图标,然后单击新添加的“Reddit 转录器”扩展程序。您现在应该会看到一个小的弹出窗口,其中包含一个按钮,用于打开选项页面。不错吧?在我的设备上它可能看起来有点不同,因为我在这些屏幕截图中启用了深色模式。如果您启用“显示评论背景”和“显示评论边框”设置,然后重新加载页面,则状态将保留,因为我们将其保存在浏览器的本地存储中。 ### 添加内容脚本好的,我们现在已经可以触发弹出窗口并与扩展程序设置交互,但是扩展程序本身还没有做任何特别有用的事情。为了让它发挥作用,我们将添加一个内容脚本。在js 目录中添加一个名为comment.js 的文件,并确保在manifest.json 文件中定义它:</code>
"content_scripts": [ { "matches": [ " ://www.reddit.com/ " ], "js": [ "js/comment.js" ] } ],
<code>content_scripts 由两部分组成: - matches:此数组保存URL,这些URL 告诉浏览器我们希望内容脚本在何处运行。作为Reddit 的扩展程序,我们希望它在与://www.redit.com/* 匹配的任何页面上运行,其中星号是通配符,用于匹配顶级域之后的任何内容。 - js:此数组包含实际的内容脚本。内容脚本无法与其他(普通)JavaScript 交互。这意味着如果网站的脚本定义了变量或函数,我们就无法访问它。例如:</code>
// script_on_website.js const username = 'Lars';
// content_script.js console.log(username); // 錯誤:username 未定義
<code>现在让我们开始编写内容脚本。首先,我们在comment.js 中添加一些常量。这些常量包含稍后将使用的RegEx 表达式和选择器。CommentUtils 用于确定帖子是否包含“tor 评论”,或者是否存在评论包装器。</code>
const messageTypes = Object.freeze({ COMMENT_PAGE: 'comment_page', SUBREDDIT_PAGE: 'subreddit_page', MAIN_PAGE: 'main_page', OTHER_PAGE: 'other_page', });
const Selectors = Object.freeze({ commentWrapper: 'div[style ="--commentswrapper-gradient-color"] > div, div[style ="max-height: unset"] > div', torComment: 'div[data-tor-comment]', postContent: 'div[data-test-]' });
const UrlRegex = Object.freeze({ commentPage: /\/r\/. \/comments\/. /, subredditPage: /\/r\/.*\// });
const CommentUtils = Object.freeze({ isTorComment: (comment) => comment.querySelector('[data-test-]') ? comment.querySelector('[data-test-]').textContent.includes('ma human volunteer content transcriber for Reddit') : false, torCommentsExist: () => !!document.querySelector(Selectors.torComment), commentWrapperExists: () => !!document.querySelector('[data-reddit-comment-wrapper="true"]') });
<code>接下来,我们检查用户是否直接打开评论页面(“帖子”),然后执行RegEx 检查并更新directPage 变量。当用户直接打开URL(例如,通过将其键入地址栏或单击另一个页面上的<a> 元素(例如Twitter))时,就会发生这种情况。</a></code>
let directPage = false; if (UrlRegex.commentPage.test(window.location.href)) { directPage = true; moveComments(); }
<code>除了直接打开页面外,用户通常还会与SPA 交互。为了捕获这种情况,我们可以通过使用runtime API 向comment.js 文件添加消息侦听器。</code>
chrome.runtime.onMessage.addListener(msg => { if (msg.type === messageTypes.COMMENT_PAGE) { waitForComment(moveComments); } });
<code>我们现在只需要这些函数。让我们创建一个moveComments() 函数。它将特殊的“tor 评论”移动到评论部分的开头。它还会根据设置中是否启用了边框,有条件地将背景颜色和边框应用于评论。为此,我们调用存储API 并加载settings 条目:</code>
function moveComments() { if (CommentUtils.commentWrapperExists()) { return; }
const wrapper = document.querySelector(Selectors.commentWrapper); let comments = wrapper.querySelectorAll( ${Selectors.commentWrapper} > div
); const postContent = document.querySelector(Selectors.postContent);
wrapper.dataset.redditCommentWrapper = 'true'; wrapper.style.flexDirection = 'column'; wrapper.style.display = 'flex';
if (directPage) { comments = document.querySelectorAll("[data-reddit-comment-wrapper='true'] > div"); }
chrome.storage.local.get('settings', ({ settings }) => { // HIGHLIGHT 18 comments.forEach(comment => { if (CommentUtils.isTorComment(comment)) { comment.dataset.torComment = 'true'; if (settings.background) { comment.style.backgroundColor = 'var(--newCommunityTheme-buttonAlpha05)'; } if (settings.border) { comment.style.outline = '2px solid red'; } comment.style.order = "-1"; applyWaiAria(postContent, comment); } }); }) }
<code>applyWaiAria() 函数在moveComments() 函数中调用——它添加aria- 属性。另一个函数创建唯一标识符以与aria- 属性一起使用。</code>
function applyWaiAria(postContent, comment) { const postMedia = postContent.querySelector('img[class*="ImageBox-image"], video'); const commentId = uuidv4();
if (!postMedia) { return; }
comment.setAttribute('id', commentId); postMedia.setAttribute('aria-describedby', commentId); }
function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }
<code>以下函数等待评论加载,如果找到评论包装器,则调用回调参数。</code>
function waitForComment(callback) { const config = { childList: true, subtree: true }; const observer = new MutationObserver(mutations => { for (const mutation of mutations) { if (document.querySelector(Selectors.commentWrapper)) { callback(); observer.disconnect(); clearTimeout(timeout); break; } } });
observer.observe(document.documentElement, config); const timeout = startObservingTimeout(observer, 10); }
function startObservingTimeout(observer, seconds) { return setTimeout(() => { observer.disconnect(); }, 1000 * seconds); }
<code>### 添加服务工作者还记得我们在内容脚本中添加了消息侦听器吗?此侦听器当前未接收消息。我们需要自己将其发送到内容脚本。为此,我们需要注册一个服务工作者。我们必须通过将以下代码附加到manifest.json 来注册服务工作者:</code>
"background": { "service_worker": "sw.js" }
<code>不要忘记在src 目录中创建sw.js 文件(服务工作者始终需要在扩展程序的根目录src 中创建)。现在,让我们为消息和页面类型创建一些常量:</code>
const messageTypes = Object.freeze({ COMMENT_PAGE: 'comment_page', SUBREDDIT_PAGE: 'subreddit_page', MAIN_PAGE: 'main_page', OTHER_PAGE: 'other_page', });
const UrlRegex = Object.freeze({ commentPage: /\/r\/. \/comments\/. /, subredditPage: /\/r\/.*\// });
const Utils = Object.freeze({ getPageType: (url) => { if (new URL(url).pathname === '/') { return messageTypes.MAIN_PAGE; } else if (UrlRegex.commentPage.test(url)) { return messageTypes.COMMENT_PAGE; } else if (UrlRegex.subredditPage.test(url)) { return messageTypes.SUBREDDIT_PAGE; }
<code>return messageTypes.OTHER_PAGE;</code>
} });
<code>我们可以添加服务工作者的实际内容。我们使用历史状态上的事件侦听器(onHistoryStateUpdated) 来执行此操作,该侦听器检测何时使用History API 更新页面(通常在SPA 中用于在没有页面刷新情况下导航)。在此侦听器中,我们查询活动选项卡并提取其tabId。然后,我们将包含页面类型和URL 的消息发送到我们的内容脚本。</code>
chrome.webNavigation.onHistoryStateUpdated.addListener(async ({ url }) => { const [{ id: tabId }] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.tabs.sendMessage(tabId, { type: Utils.getPageType(url), url }); });
<code>### 全部完成!我们完成了!导航到Chrome 的扩展程序管理页面(chrome://extensions),然后点击解压扩展程序上的重新加载图标。如果您打开包含“Reddit 转录器”评论和图像转录的Reddit 帖子(例如此帖子),只要我们在扩展程序设置中启用了它,它就会被移动到评论部分的开头并突出显示。 ### 结论这是否像您想象的那么难?在我深入研究之前,它肯定比我想象的要简单得多。在设置manifest.json 并创建任何我们需要的文件和资产后,我们真正做的只是像往常一样编写HTML、CSS 和JavaScript。如果您在途中遇到任何问题,Chrome API 文档是一个很好的资源,可以帮助您重回正轨。再次声明,这是包含我们在本文中介绍的所有代码的GitHub 代码库。阅读它,使用它,并让我知道您的想法!</code>
以上是如何創建瀏覽器擴展程序的詳細內容。更多資訊請關注PHP中文網其他相關文章!