浏览器扩展速成课程
本书后面的章节将详细探讨浏览器扩展的各个方面。然而,本章将引导您以最快的方式上手浏览器扩展的开发。速成课程将涵盖如何从头开始构建一个简单的浏览器扩展。我们将不使用任何第三方库或依赖项,只使用纯HTML、CSS和JavaScript。
本章面向从未构建过扩展的开发者;但是,即使您已经对扩展开发有所了解,本章也可能对您有所帮助,因为它涉及了一些您可能尚未尝试过的领域。
注意:为了简化起见,速成课程将只关注为Google Chrome开发manifest v3浏览器扩展。创建manifest v2浏览器扩展或支持其他浏览器需要执行一些在本速成课程中未涵盖的步骤。
创建清单文件
如前一章所述,扩展的清单文件主要定义了以下内容:
- 扩展被允许执行哪些操作
- 其文件位于何处
首先,创建一个名为extension-crash-course
的新空目录。在本速成课程中创建的所有文件都将放在这个目录内:
extension-crash-course/
└─ manifest.json
在这个目录中,我们来创建最简单的manifest.json
文件。文件结构如下所示:
{
"name": "Extension Crash Course",
"description": "Browser extension created from scratch",
"version": "1.0",
"manifest_version": 3
}
这些字段定义了正式的浏览器扩展名称字符串“Extension Crash Course”,以及描述字符串“Browser extension created from scratch”。这些字符串将在扩展安装时在浏览器内部以及扩展发布到Chrome网上应用店时显示。此外,此文件中还定义了语义版本“1.0”,表示扩展包的版本。它还定义了清单版本字符串“3”,指示浏览器应如何解析manifest.json
文件。
最小可行扩展
除了清单文件之外,浏览器扩展的所有元素都是可选的。因此,这个包含少量样板代码的manifest.json
文件是最小可行的浏览器扩展。该扩展没有任何功能或用户界面,并且几乎不占用浏览器的开销。它基本上是浏览器扩展中的“无操作(NOOP)”指令的等价物。然而,Google Chrome会很高兴地将这个单文件作为新的浏览器扩展进行安装,就像安装任何其他扩展一样。
安装您的扩展
安装和测试扩展的最快方法是通过启用扩展开发者模式将其加载到Google Chrome中。可以在浏览器的“Chrome扩展程序”页面上启用此模式。到达此页面的方法有两种:
更多工具 > 扩展程序
在浏览器地址栏输入:chrome://extensions
Google Chrome 的默认行为是不允许从本地文件系统加载扩展。为了启用此行为,在 Chrome 的“扩展程序”页面上,您需要启用开发者模式切换按钮,如图 3-2 和 3-3 所示。
启用开发者模式后,您可以通过点击“加载已解压的扩展程序”并选择包含 manifest.json 的 extension-crash-course 目录来加载扩展(如图 3-4)。
选择目录后,您的扩展将在浏览器中安装,Chrome 的“扩展程序”页面将通过为新的扩展添加一个卡片来反映这一点(如图 3-5)。
注意:为了方便访问,您应该固定扩展工具栏图标,以便其始终显示。点击拼图碎片形状的扩展图标,然后点击固定按钮。
重新加载您的扩展
当对扩展代码进行修改时,了解这些更改何时会显示出来可能会比较困难。扩展的不同部分会在不同的时间重新加载。此外,多个标签页和窗口意味着您可能会同时运行扩展的多个版本!
扩展的部分内容会在几个不同的时间点重新加载:
扩展重新加载会获取 manifest.json 的新副本,更新后台服务工作线程,并关闭任何打开的过时弹出窗口和选项页面。当 manifest.json 发生变化时,这是必需的。
扩展页面重新加载是使用扩展协议 chrome-extension:// 的任何页面的页面刷新。这是为了反映弹出窗口和选项页面中 HTML、JS、CSS 和图像的变化。
网页重新加载是注入了内容脚本的任何网页的页面刷新。这是为了反映注入的内容脚本的变化。
开发工具重新加载是关闭并打开浏览器的开发者工具界面。这是为了反映开发工具页面的变化。
有三种方法可以强制扩展重新加载:
卸载并重新安装扩展
在 Chrome 的“扩展程序”页面上,点击相应卡片中的重新加载图标 ↺
使用 chrome.runtime.reload() 或 chrome.management.setEnabled() 以编程方式重新加载扩展
注意:关于以编程方式重新加载扩展的更多内容,将在“扩展开发与部署”章节中介绍。
在使用 WebExtensions API 发送消息时,您偶尔会看到类似于以下的错误:
Uncaught (in promise) Error: Could not establish connection. Receiving end does not exist.
浏览器知道可以同时运行不同版本的扩展,因此它们明确禁止在不同版本的扩展部分之间发送消息。看到此错误通常表示您的扩展的某个部分需要重新加载。
注意:在本快速入门课程中,重新加载扩展和扩展页面就足以反映您所做的任何更改。
添加背景脚本
接下来,让我们添加一个简单的背景脚本并验证它是否正在运行。在与 manifest.json 相同的目录中,创建一个 background.js 文件:
`extension-crash-course/`
├─ `manifest.json`
└─ `background.js`
文件内容如下所示:
background.js
console.log('Hello from the background script!');
目前 manifest 文件还不知道 background.js 文件的存在,因此我们需要更新 manifest 文件,以便它将其作为后台服务工作线程加载此脚本文件:
manifest.json
{
"name": "Extension Crash Course",
"description": "Browser extension created from scratch",
"version": "1.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
}
}
在重新加载扩展后,您会注意到 Chrome 的“扩展程序”页面上的卡片显示了一个指向服务工作线程的链接(如图 3-6):
此链接将打开后台服务工作线程的开发者工具控制台,您将在其中看到 console.log 的输出内容(见图 3-7)。
现在背景脚本已经正常工作,我们来配置它以记录通过 WebExtensions API 接收到的事件消息:
background.js
console.log('Hello from the background script!');
chrome.runtime.onMessage.addListener((msg) => {
console.log(msg.text);
});
请注意,这个监听器在浏览器中的其他地方发送消息之前不会打印任何内容。
您可能会注意到,在 Chrome 扩展页面上显示的卡片在一段时间后可能会显示服务工作线程为不活动状态。为了释放未使用的系统资源,Google Chrome 会判断服务工作线程处于空闲状态并自动卸载它。当再次需要时(例如,当发送扩展消息时),工作线程会重新加载。
注意:卸载和重新加载服务工作线程是一个重要概念,将在“背景脚本”章节中进一步讨论。
添加弹窗页面
接下来,让我们为扩展添加第一个用户界面。在与 manifest.json
相同的目录中创建一个新的 popup
目录。在新目录内,创建三个新文件:popup.html
、popup.css
和 popup.js
:
`extension-crash-course/`
├─ `manifest.json`
├─ `background.js`
└─ `popup/`
├─ `popup.html`
├─ `popup.css`
└─ `popup.js`
文件内容如下所示:
popup/popup.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link href="popup.css" rel="stylesheet" />
</head>
<body>
<h1>这是弹窗页面!</h1>
<script src="popup.js"></script>
</body>
</html>
popup/popup.css
body {
width: 400px;
margin: 2rem;
}
popup/popup.js
console.log('Hello from the popup!');
现在,我们需要更新 manifest.json
文件,以便在点击工具栏图标时打开弹窗页面。以下是更新后的 manifest.json
文件内容:
{
"name": "Extension Crash Course",
"description": "Browser extension created from scratch",
"version": "1.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup/popup.html"
}
}
在重新加载扩展后,点击工具栏上的扩展图标即可打开弹窗页面(见图 3-8)。此时,您应该会看到弹窗页面显示了“这是弹窗页面!”的标题,并且开发者工具的控制台中会显示来自 popup.js
的日志:“Hello from the popup!”。
注意:由于您尚未为扩展程序定义图标,Google Chrome 会根据扩展程序的名称自动生成一个图标。如所示,默认图标是扩展程序名称的首字母(在此例中为“e”),显示在灰色背景上。
接下来,让我们从弹窗页面向背景脚本发送消息。更新以下文件:
popup/popup.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link href="popup.css" rel="stylesheet" />
</head>
<body>
<h1>这是弹窗页面!</h1>
<button id="btn">发送弹窗消息</button>
<script src="popup.js"></script>
</body>
</html>
popup/popup.js
console.log('Hello from the popup!');
document.querySelector("#btn").addEventListener('click', () => {
chrome.runtime.sendMessage({ text: "Popup" });
});
// 注意:以下监听器通常不应放在 popup.js 中,而应放在 background.js 中以接收来自弹窗的消息。
// 此处仅为了演示目的而保留,实际应用中请移除或移至 background.js。
chrome.runtime.onMessage.addListener((msg) => {
document.body.innerHTML += `<div>${msg.text}</div>`;
});
这段新代码在弹窗页面中添加了一个按钮,用于向扩展程序的其他部分发送消息。它还添加了一个用于接收传入消息的监听器。重新加载扩展程序,点击弹窗中的新按钮,并检查背景脚本以查看记录的消息(图3-9)。
注意:尽管我们在弹窗页面中为消息设置了监听器,但它并没有处理传入的消息。尽管扩展程序的消息基础架构像广播一样工作,但发送消息的来源并不会同时接收到它。
添加选项页面
接下来,让我们添加一个与弹窗页面结构类似的选项页面:
extension-crash-course/
├─ manifest.json
├─ background.js
├─ popup/
│ ├─ popup.html
│ ├─ popup.css
│ └─ popup.js
└─ options/
├─ options.html
├─ options.css
└─ options.js
文件内容展示如下:
options/options.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="options.css" rel="stylesheet" />
</head>
<body>
<h1>这是选项页面!</h1>
<button id="btn">发送选项消息</button>
<script src="options.js"></script>
</body>
</html>
options/options.css
body {
margin: 2rem;
}
options/options.js
console.log('来自选项页面的问候!');
document.querySelector("#btn").addEventListener('click', () => {
chrome.runtime.sendMessage({ text: "Options" });
});
// 注意:通常,此消息监听器应放置在背景脚本(如 background.js)中,
// 以便接收来自扩展程序中其他组件(如弹窗页面或内容脚本)的消息。
// 在此示例中,为了演示目的,我们将其保留在 options.js 中。
chrome.runtime.onMessage.addListener((msg) => {
document.body.innerHTML += `<div>${msg.text}</div>`;
});
清单文件需要配置为使用此HTML作为选项页面: manifest.json
{
"name": "扩展速成课程",
"description": "从零开始创建的浏览器扩展",
"version": "1.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup/popup.html"
},
"options_page": "options/options.html"
}
重新加载扩展,并通过右键点击工具栏图标并选择“选项”来打开扩展选项页面(图3-10)。
为了测试扩展组件之间的消息传递系统,请打开弹窗页面并点击其按钮。选项页面上的消息处理程序将会把消息添加到页面中(图3-11)。
添加内容脚本
接下来,我们将创建一些内容脚本,为选项页面和弹窗页面添加类似的行为:
扩展速成课程目录结构:
extension-crash-course/
├─ manifest.json
├─ background.js
├─ popup/
│ ├─ popup.html
│ ├─ popup.css
│ └─ popup.js
├─ options/
│ ├─ options.html
│ ├─ options.css
│ └─ options.js
└─ content-scripts/
├─ content-script.css
└─ content-script.js
内容脚本将在页面中添加一个带有样式的自定义容器:
content-scripts/content-script.css
#container {
position: absolute;
background-color: gray;
color: white;
padding: 2rem;
top: 0;
left: 0;
}
content-scripts/content-script.js
console.log('Hello from content script!');
document.body.innerHTML += `
<div id="container">
<h1>This is the content script!</h1>
<button id="btn">发送内容脚本消息</button>
</div>
`;
document.querySelector("#btn").addEventListener('click', () => {
chrome.runtime.sendMessage({ text: "内容脚本消息" });
});
chrome.runtime.onMessage.addListener((msg) => {
document.querySelector('#container').innerHTML += `<div>${msg.text}</div>`;
});
配置清单文件,以便将这些内容脚本注入到所有有效的网页中:
manifest.json
{
"name": "扩展速成课程",
"description": "从零开始创建的浏览器扩展",
"version": "1.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup/popup.html"
},
"options_page": "options/options.html",
"content_scripts": [
{
"matches": ["<all_urls>"],
"css": ["content-scripts/content-script.css"],
"js": ["content-scripts/content-script.js"]
}
]
}
打开一个真实的网页,如 https://blank.org, 以查看内容脚本的渲染(图3-12):
在这个状态下,你将能够点击内容脚本的按钮,并让消息在扩展的视图(如选项页面)中显示出来。
处理多个标签页
在当前状态下试验过扩展之后,你会发现内容脚本能够成功地向扩展的其他部分发送消息,但内容脚本却接收不到消息。
这是因为内容脚本本质上与浏览器的标签页模型绑定在一起。可能有许多标签页都在运行相同的内容脚本,因此必须考虑应该将消息发送给哪个内容脚本实例。为了向内容脚本发送消息,WebExtensions API允许你单独指定目标标签页。将弹窗脚本更新为以下内容:
popup/popup.js
console.log('Hello from the popup!');
document.querySelector("#btn").addEventListener('click', () => {
chrome.runtime.sendMessage({ text: "Popup" }); // 这行代码实际上在当前上下文中是多余的,因为后面有更具体的发送消息到标签页的代码
chrome.tabs.query({
active: true,
currentWindow: true
}, (tabs) => {
chrome.tabs.sendMessage(
tabs[0]?.id, // 使用可选链操作符确保tabs[0]存在时访问其id属性
{ text: "Popup message to content script" } // 更改消息内容以明确这是发送给内容脚本的
);
});
});
chrome.runtime.onMessage.addListener((msg) => {
document.body.innerHTML += `<div>${msg.text}</div>`; // 这段代码监听来自运行时(可能是背景脚本或其他地方)的消息,并在弹窗页面中显示
});
重新加载扩展后,你现在应该能够看到,从弹窗页面的按钮发送的消息将显示在内容脚本视图中,但仅限于当前活动的标签页。注意,我修改了发送给内容脚本的消息文本,以更清楚地表明这是专门发送给内容脚本的消息。同时,chrome.runtime.sendMessage
在弹窗脚本中的调用现在是多余的,因为我们已经使用chrome.tabs.sendMessage
更具体地指定了消息的目标。然而,保留这行代码也不会导致错误,只是它发送的消息没有特定的接收者(除非有背景脚本或其他监听全局消息的组件)。
小贴士:在继续之前,请先试验一下当前状态下的扩展。打开多个标签页、多个浏览器窗口、在不同窗口中打开多个弹窗,并从不同位置发送消息,观察它们在哪里显示或不显示。结果可能会让你惊讶。这将是一个关于扩展中广播消息模型如何工作的富有教育意义的实践。
添加开发者工具面板
接下来,让我们在Chrome开发者工具中添加一个面板。创建以下文件:
extension-crash-course/
├─ manifest.json
├─ background.js
├─ popup/
│ ├─ popup.html
│ ├─ popup.css
│ └─ popup.js
├─ options/
│ ├─ options.html
│ ├─ options.css
│ └─ options.js
├─ content-scripts/
│ ├─ content-script.css
│ └─ content-script.js
└─ devtools/
├─ devtools.html
├─ devtools.js
├─ devtools_panel.html
└─ devtools_panel.js
添加开发者工具接口与其他扩展接口略有不同。扩展提供了一个顶层的无头页面,该页面使用WebExtensions API在脚本中插入自定义的开发者工具接口。文件结构应如下所示:
devtools/devtools.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script src="devtools.js"></script>
</body>
</html>
devtools/devtools.js
chrome.devtools.panels.create(
"开发者工具面板",
"",
"/devtools/devtools_panel.html"
);
接下来,配置开发者工具面板以使用开发者工具API记录页面上所有传出网络活动的URL:
devtools/devtools_panel.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>这是开发者工具面板!</h1>
<script src="devtools_panel.js"></script>
</body>
</html>
devtools/devtools_panel.js
console.log("来自开发者工具面板的问候!");
chrome.devtools.network.onRequestFinished.addListener(
(request) => {
document.body.innerHTML +=
`<div>${request.request.url}</div>`;
}
);
manifest.json
{
"name": "扩展速成课程",
"description": "从零开始创建的浏览器扩展",
"version": "1.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup/popup.html"
},
"options_page": "options/options.html",
"devtools_page": "devtools/devtools.html",
"content_scripts": [
{
"matches": ["<all_urls>"],
"css": ["content-scripts/content-script.css"],
"js": ["content-scripts/content-script.js"]
}
]
}
您需要重新加载扩展,并关闭再重新打开所有已打开的开发者工具窗口,才能看到所做的更改。访问任何网页都会记录其网络流量(如图3-13所示)。
总结
在本章中,您被引导着创建了一个简单的Chrome扩展。这个速成课程的目的是提供一步一步的指导,教授如何创建一个包含了浏览器扩展所能使用的所有不同用户界面元素的扩展。完成这个速成课程后,您应该能够很好地理解如何从源代码生成各种用户界面,以及为了实现这些目的应该如何组织manifest文件。
您还应该对如何使用WebExtensions API有了非常基础的了解。这个速成课程只涉及了几个基本的方法,但它为您提供了浏览器扩展可以以各种方式操纵浏览器来实现其目的的一个初步概念。
下一章将探讨浏览器扩展的架构,包括文件的组织方式以及各个元素在浏览器内部的行为。