【Chrome】开发一个Chrome扩展以及常见问题的解决方案
前言
本文介绍开发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"
},
更多详细使用请参考:官方文档