前言
本文介绍开发chrome扩展很重要的几种操作,如:操作网页dom、发送请求、渲染弹层、不同沙盒环境的通信方式、扩展与网页的通信方式、遇到iframe时的操作等。最终会提供一个简单的案例,其中涵盖了上述操作。
还有一些本人相关文章,有兴趣也可以看一下,如:判断是否安装了某个Chrome插件、background.js中log打印未出现在控制台、Edge扩展程序上架流程、Chrome扩展程序上架流程。
本文都是在该页面下测试扩展
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
localStorage.setItem("tag", "123456") // 标识是否需要执行插件
</script>
</head>
<body>
<input id="test-chrome-extensions" type="text">
</body>
</html>
- 需要一个标识用于确认是否需要在该页面下运行扩展,本文以本地存储举例,也可能是下面几种方式,需要根据实际场景判断使用。
- 根据域名判断,可以通过扩展的 manifest.json 中 content_scripts 的 matches 字段来定义允许执行的页面(下文会提及)。
- 根据url路径参数
- 根据页面中的特殊标签
- 根据本地存储
- …
- 需要一个input输入框用于测试扩展对网页dom元素的直接操作
一、目录结构
开发一个Chrome扩展,项目目录大致如下,下文统一以标准命名介绍。
- background.js:后台脚本
- content.js:内容脚本
- manifest.json:插件配置文件(清单列表)
- popup.html 和 popup.js:插件弹窗页面及其脚本
二、配置清单(manifest.json)
- 该文件必须位于项目的根目录下。
- 必需的键只有 “manifest_version”、“name” 和 “version”。
- 开发过程中支持注释 (//),但在将代码上传到 Chrome 应用商店之前,必须先移除这些注释。
基本配置如下
{
"manifest_version": 3,
"name": "My Chrome Extension",
"version": "1.0",
"permissions": ["storage", "activeTab", "scripting"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}
Chrome官方文档中明确说明,v2已经弃用了。
三、沙盒环境
Chrome 插件的每个模块(background.js、popup.js、content.js)都运行在自己的沙盒环境中,意味着它们的 JavaScript 变量和函数是相互隔离的,避免了相互干扰。唯一的通信方式是通过 Chrome 提供的 API(如 chrome.runtime.sendMessage
或 chrome.storage
)。
- background.js:运行在插件的后台,它运行在独立的沙盒环境中,无法与网页内容直接交互。
- popup.html:弹窗页面,用户点击浏览器工具栏的插件图标时弹出。
- popup.js:只能在弹窗页面存在时运行,弹窗页对应的脚本。
- content.js:内容脚本,运行在网页的环境中,可以直接访问网页的 DOM。
浏览器运行环境:与普通的网页脚本不同,Chrome 插件模块无法直接访问网页的全局变量和 DOM,必须通过 content.js 作为桥梁进行交互。同时,插件可以使用 chrome 提供的扩展 API,而网页中的 JavaScript 代码则没有这些权限。
四、通信方式
1. chrome.runtime API
由于处于不同沙盒环境,只能通过chrome.runtime API
或 chrome.storage
通信
chrome.runtime.sendMessage
发送消息,接收两个参数
- message: 要发送的消息内容,可以是一个对象。
- callback (可选): 当消息成功发送或遇到错误时调用的回调函数。
chrome.runtime.sendMessage({ type: "test", message: "Hello World!" }, (response) => {
console.log("回应:", response);
});
chrome.runtime.onMessage.addListener
监听消息,参数为回调函数,其参数:
- message:
chrome.runtime.sendMessage
发送的数据(第一个参数)。 - sender:发送消息的上下文,包含有关消息来源的信息,如 tab、url、扩展id。
- sendResponse:返回消息(chrome.runtime.sendMessage中callback可以接收)
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
console.log("接收到的消息:", message);
sendResponse({ type: "responseData", data: "返回数据" })
});
2. chrome.storage
不同于 localStorage
,chrome.storage
是异步的,所有存储操作都会接收一个回调函数,以处理操作完成后的逻辑。
chrome.storage
主要有两个存储区域:
- chrome.storage.local:在本地存储数据,不同步到其他设备,存储容量最大为 5MB。
- chrome.storage.sync:同步数据到 Chrome 登录账户的所有设备,存储容量为 100KB,适合存储轻量配置或设置。
基本用法
存储数据
// 存储数据
chrome.storage.local.set({ key: "value" }, function() {
console.log('数据已存储');
});
// 或者使用多个键值对
chrome.storage.local.set({ key1: "value1", key2: "value2" }, function() {
console.log('多组数据已存储');
});
获取数据
// 获取存储的数据
chrome.storage.local.get(['key'], function(result) {
console.log('获取到的值:', result.key);
});
// 获取多个键值对
chrome.storage.local.get(['key1', 'key2'], function(result) {
console.log('key1 的值:', result.key1);
console.log('key2 的值:', result.key2);
});
删除数据
// 删除指定键的数据
chrome.storage.local.remove('key', function() {
console.log('数据已删除');
});
// 删除多个键的数据
chrome.storage.local.remove(['key1', 'key2'], function() {
console.log('多个数据已删除');
});
清除所有数据
// 清除存储中的所有数据
chrome.storage.local.clear(function() {
console.log('所有数据已清除');
});
测试
进入扩展程序管理页,或者直接访问chrome://extensions/
确保开启开发者模式
直接将文件夹拖入即可
注意:background.js不同于content.js,他并不与页面共享相同的 JavaScript 环境,所以需要单独打开一个控制台。
点击检查视图,会弹出background.js对应的控制台
v2版本的扩展有所不同,名为background.html,新项目不建议使用v2。
五、案例演示
案例中,content使用chrome.runtime API
通信,popup使用chrome.storage
获取数据,实际场景中可以使用任意通信方式,没有限制,流程如下:
- content 判断该页面是否需要扩展运行
- 如果需要则通知 background 发送请求
- background 将返回数据通过
chrome.runtime API
交予 content,然后content将该值赋给页面中input元素 - background 将返回数据保存到
chrome.storage
供 popup 渲染
content.js
console.log("content.js - 开始运行...")
const tag = localStorage.getItem("tag")
console.log("标识:", tag)
if (tag) {
console.log("该页面需要使用插件,准备接收消息...")
// 通知 background.js 可以继续执行接口调用
chrome.runtime.sendMessage(
{ type: "pageCheck", valid: true, tag },
(response) => {
console.log("收到消息:", response)
if (response.type === "responseData") {
const inputElement = document.getElementById("test-chrome-extensions")
if (inputElement) {
inputElement.value = response.data.name
}
}
}
)
} else {
console.log("该页面不需要使用插件,不执行扩展逻辑。")
// 通知 background.js 不用调用接口
chrome.runtime.sendMessage({ type: "pageCheck", valid: false, tag })
}
background.js
console.log("background.js - 开始运行...")
// 模拟请求并返回数据
function fetchData(tag) {
console.log("标识为", tag) // 可能需要该标识作为请求参数
const responseData = { name: "田本初", age: 23 }
return new Promise((resolve) => {
setTimeout(() => {
resolve(responseData)
}, 1000) // 模拟接口异步返回
})
}
// 监听 content.js 发来的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "pageCheck") {
if (message.valid) {
console.log("页面需要接口调用...")
fetchData(message.tag).then((response) => {
chrome.storage.local.set({ data: response }) // 存储数据
sendResponse({ type: "responseData", data: response }) // 发送消息
console.log(
"接口调用成功,向content发送消息,并将data存储到storage供popup使用",
response
)
})
// 返回 true 表示异步使用 sendResponse,否则content.js 不会等待异步返回结果
return true
} else {
console.log("页面不需要接口调用...")
return
}
}
})
popup.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Popup</title>
<script src="popup.js"></script>
<style>
.container {
width: 600px;
height: 600px;
background-color: #efefef;
}
</style>
</head>
<body>
<div class="container">
<div id="show_name"></div>
<div id="show_age"></div>
</div>
</body>
</html>
popup.js
console.log("popup.js - 开始运行...")
// 获取数据
chrome.storage.local.get(["data"], (result) => {
if (chrome.runtime.lastError) {
console.error("获取数据时发生错误:", chrome.runtime.lastError)
return
}
console.log("获取到的值:", result)
// 处理数据
const data = result.data
// 更新 popup.html 中的内容
const nameDiv = document.getElementById("show_name")
const ageDiv = document.getElementById("show_age")
if (nameDiv && ageDiv && data) {
nameDiv.textContent = `姓名:${data.name}`
ageDiv.textContent = `年龄:${data.age}`
}
})
六、分析manifest.json
上文提过一次 manifest.json
,通过案例后详细分析一下清单:
{
"manifest_version": 3,
"name": "My Chrome Extension",
"version": "1.0",
"permissions": ["storage", "activeTab", "scripting"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}
- manifest_version:Manifest 文件的版本,官方已经不再支持发布v2版,目前都是3。
- name:插件名
- version:版本号,后续更新时必须修改
- permissions:权限,本文只用到了
storage
- activeTab:允许扩展在用户点击扩展图标时访问当前活动标签页的内容
- scripting:允许扩展使用 chrome.scripting API 注入脚本到网页中
- background:在后台运行,负责处理扩展的主要逻辑和事件
- action:用户点击扩展图标时,会显示指定的弹出页面
- content_scripts:定义要注入的内容脚本及其匹配规则
-
matches:定义哪些网页 URL 匹配规则,以决定哪些页面将注入指定的内容脚本。
"http://\*/\*":匹配所有 HTTP 协议的网页。 "https://\*/\*":匹配所有 HTTPS 协议的网页。 "*://example.com/*":匹配 example.com 域下的所有网页。 "*://*.example.com/*":匹配 example.com 域下的所有子域网页。
-
修改matches为"matches": ["https://www.baidu.com/"],
可以发现只有在该域名下才执行了 content.js
配置扩展图标,其中action下的default_icon对应工具栏中的图标,icons对应管理扩展页面中的图标。
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
更多详细使用请参考:官方文档