



















最近因为看了下 Chrome 里面置入的小模型,顺手又看了下 Chrome 扩展。
之前其实也接触过一些浏览器扩展,但是一直没有特别系统地看过它的结构,感觉好像就是写一个 html,然后再配几个 js 文件就可以了。
但是真正写起来以后,会发现 Chrome 扩展里面有几个不同的运行环境,尤其是 popup 和 content script,如果没有分清楚,后面调用网页内容或者操作页面 DOM 的时候就容易有点混乱。
这次先做一个比较简单的例子,点击扩展按钮以后,给当前页面加一个背景色,再在右下角显示一个提示。
代码放在这里:
source/code/chrome-extension-demo
目录结构如下:
1 | chrome-extension-demo/ |
这几个文件的作用比较简单:
1 | manifest.json 扩展的配置文件 |
首先看下 manifest.json。
1 | { |
这个文件相当于 Chrome 扩展的入口配置。
manifest_version 就是使用的配置版本,现在一般使用 3。
1 | "action": { |
这个配置表示点击浏览器右上角的扩展图标以后,打开 popup.html。
下面这个是权限:
1 | "permissions": [ |
activeTab 表示用户主动点击扩展以后,扩展可以临时访问当前的标签页。
这里的“临时”还是比较重要的,它并不是给扩展永久访问所有网站的权限,而是在用户操作扩展以后,允许它访问当前页面。
然后下面是 content_scripts:
1 | "content_scripts": [ |
这里配置了需要注入的脚本和匹配的网页。
<all_urls> 表示所有普通网页都可以匹配,content.js 会被注入进去。
run_at 表示什么时候注入脚本,这里使用的是 document_idle,也就是页面基本加载完成以后再执行。
官方文档中也比较推荐使用这个时机,因为一般扩展不需要在页面最开始加载的时候就执行。
不过 <all_urls> 的权限比较大,如果是真正发布的扩展,最好改成具体的网站范围,例如:
1 | "matches": [ |
接着看下弹窗页面。
1 | <!doctype html> |
这里就是一个很普通的 HTML 页面。
比较容易混淆的地方是,虽然它看起来也是一个页面,但是这个页面并不是当前打开的网站,而是扩展自己的页面。
比如在 popup.js 中:
1 | document.querySelector('#status') |
这里的 document 指的是 popup.html。
如果我们在这里修改:
1 | document.body.style.backgroundColor = 'red'; |
修改的是扩展弹窗,而不是当前打开的网站。
真正要修改网页,需要通过 content script 来完成。
先看下点击按钮以后的代码。
1 | const markButton = document.getElementById('mark'); |
首先是这一段:
1 | const [tab] = await chrome.tabs.query({ |
chrome.tabs.query 用来查询浏览器中的标签页。
这里传入了两个条件:
1 | active: true |
表示查询当前窗口中处于激活状态的标签页。
因为返回结果是一个数组,所以这里直接使用数组解构取第一个:
1 | const [tab] = ... |
拿到当前标签页以后,真正重要的是下面这段:
1 | chrome.tabs.sendMessage( |
这里是给当前标签页发送消息。
消息内容只是一个普通的 JavaScript 对象:
1 | { |
可以把它理解成告诉网页里的 content.js:
请执行一个叫做
MARK_PAGE的操作。
扩展的不同部分之间不能直接共享变量,一般就是通过这种消息来通信。
下面是真正修改网页的代码。
1 | chrome.runtime.onMessage.addListener( |
这一段是监听消息:
1 | chrome.runtime.onMessage.addListener(...) |
当 popup.js 发送消息以后,这个监听器就会被调用。
首先判断消息类型:
1 | if (message.type !== 'MARK_PAGE') { |
因为以后可能还会有很多操作,比如:
1 | { |
或者:
1 | { |
所以一般会通过 type 来区分不同的消息。
然后这里:
1 | document.body.style.backgroundColor = '#fff7cc'; |
此时的 document 就不一样了。
因为这段代码运行在网页中,所以这里的 document 是当前网站的 DOM,可以直接修改网页背景。
接着创建一个提示框:
1 | const marker = document.createElement('div'); |
设置一些样式以后:
1 | document.body.appendChild(marker); |
就把它插入到了当前网页中。
最后:
1 | sendResponse({ |
把执行结果返回给 popup.js。
于是整个调用链就是:
1 | 点击 popup 中的按钮 |
这里其实是 Chrome 扩展里最重要的一个概念。
popup.js 和 content.js 虽然都是 JavaScript,但是运行环境不一样。
可以简单看成:
1 | 扩展自己的环境 |
所以在 popup.js 里面:
1 | document.body |
拿到的是弹窗页面。
在 content.js 里面:
1 | document.body |
拿到的是当前网站页面。
这也是为什么 popup.js 不能直接修改网页,必须通过消息把操作交给 content.js。
Chrome 这里还有一个隔离环境的概念。
content script 可以访问网页的 DOM,但是它定义的 JavaScript 变量不会直接和网页脚本混在一起。
比如网页中有一个变量:
1 | const userName = 'page'; |
扩展的 content.js 中也有:
1 | const userName = 'extension'; |
一般不会因为变量名相同就互相覆盖。
不过它们仍然共享网页的 DOM,所以 content.js 还是可以:
1 | document.querySelector(...) |
相关说明可以看 Chrome Content Scripts。
打开:
1 | chrome://extensions |
然后:
chrome-extension-demo 目录如果修改了 manifest.json,需要在扩展管理页面重新加载扩展。
如果修改了 content.js,一般还需要重新加载扩展并刷新网页,因为脚本是在网页加载时注入的。
如果在下面这些页面中点击扩展:
1 | chrome://extensions |
可能会出现:
1 | 当前页面暂时不能注入脚本 |
这通常不是代码写错了,而是浏览器内部页面本身不允许扩展注入脚本。
所以这段错误处理还是需要有的:
1 | if (chrome.runtime.lastError) { |
当前例子里面,content.js 收到消息后马上就返回了结果。
如果中间需要进行异步操作,例如请求接口:
1 | chrome.runtime.onMessage.addListener( |
这里的:
1 | return true; |
是比较容易漏掉的。
它表示当前消息还不会马上响应,需要保留通信通道,等异步请求结束以后再调用 sendResponse。
具体可以看 Chrome Message Passing。
这次在 manifest.json 中同时配置了:
1 | "permissions": [ |
以及:
1 | "content_scripts": [ |
这两个配置实际作用不完全一样。
content_scripts 负责决定 content.js 自动注入哪些页面。
activeTab 则表示用户主动操作扩展以后,可以临时访问当前标签页。
如果扩展不需要监听所有页面,只在用户点击扩展时执行代码,也可以使用:
1 | "permissions": [ |
然后通过 chrome.scripting.executeScript 动态注入代码。
当前这个例子为了简单,还是使用静态的 content_scripts。
Chrome 扩展最开始看起来就是一些 HTML、CSS 和 JavaScript 文件,但是它和普通网页还是有一些区别。
比较重要的是先理解这几个东西:
1 | manifest.json 负责配置 |
这次的例子虽然只是给网页换了个背景色,但是从点击按钮到修改网页,已经把 Chrome 扩展最基本的一条链路走通了。
后面如果要做网页翻译、页面摘要、读取选中文本,基本也都是在这个结构上继续扩展。
这里使用content相当于是一个持久化的脚本注入,如果只是临时要抓取一下网页内容
可以直接在popup里调用
1 | await chrome.scripting.executeScript |
这种相当于是每点一次调一次,那么参数上下文这些每次都要重新传
而通过content的话相当于是有个server注入在网页里
可以通过消息的形式让它做对应的处理
各有优势,主要是学习下chrome扩展的这些机制
而对于之前的mv2版本也是类似的,只不过是之前可以直接注入整个content.js用来执行
所以看起来像是mv2可以直接在popup里执行tab网页的dom操作
实际上并不是,而是通过注入整个content.js来执行的
整体来说还是一样,所以chrome扩展的大致执行机制就是这样
在自己实现的时候可以自行选择方式
而如果通过background.js的方式的话
比如点击 popup 中的按钮,让当前页面变色,有两种方式。
简单方式:
1 | popup.js |
如果需要通过后台统一处理:
1 | popup.js |
popup.js
1 | document |
background.js
1 | chrome.runtime.onMessage.addListener( |
content.js
1 | chrome.runtime.onMessage.addListener( |
这里有两次消息传递:
1 | popup.js |
为什么需要 background?
如果只是一个按钮点击操作,实际上不需要 background:
1 | popup.js → content.js |
已经够了。
但是下面这些场景就比较适合加入 background:
1 | 用户按快捷键 |
或者
1 | content.js 读取网页内容 |
因为 popup 会随时关闭,不能依赖它长期存在。
MV3 中不再使用传统的 background.js 页面,而是使用扩展 Service Worker:
1 | { |
调用关系没变
1 | popup |
Service Worker 是事件驱动的,不会一直运行。没有事件时可能被 Chrome 暂停,后续事件到来时再重新启动,因此不能把重要状态只放在全局变量中,应该放到 chrome.storage 中。
另外,MV3 的扩展 Service Worker 本身也不能访问网页 DOM。About extension service workers
这样就大致知道了chrome开发的主体逻辑
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。