crx实操
[toc]
来源:https://juejin.cn/post/7035782439590952968
简单小插件
Chrome插件并没有很严格的项目结构要求,比如src、public、components等等,因此我们如果去看很多插件的源码,会发现每个插件的项目结构,甚至项目下的文件名称都大相径庭;
但是在根目录下我们都会找到一个manifest.json文件,这是插件的配置文件,说明了插件的各种信息;它的作用等同于小程序的app.json和前端项目的package.json。
我们在项目中创建一个最简单的manifest.json配置文件:
| 1 | { | 
实际导入时,要把注释都删掉。
我们经常会点击右上角插件图标时弹出一个小窗口的页面,焦点离开时就关闭了,一般做一些临时性的交互操作;在配置文件中新增browser_action字段,配置popup弹框:
| 1 | { | 
此时目录结构如下:

popup.html
| 1 | 
 | 
导入后效果:

为了用户方便点击,我们还可以在manifest.json中设置一个键盘快捷键的命令,通过快捷键来弹出popup页面:
| 1 | { | 
每次更新文件后,浏览器端需要重新加载下:

快捷键好像不支持Alt:

后台background
我们的插件安装后,popup页面也运行了;但是我们也发现了,popup页面只能做临时性的交互操作,用完就关了,不能存储信息或者和其他标签页进行交互等等;
这时就需要用到background(后台),它是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的;它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面。
background也是需要在manifest.json中进行配置,可以通过page指定一张网页,或者通过scripts直接指定一个js数组,Chrome会自动为js生成默认网页:
| 1 | { | 
效果如下:

但是控制台,没有在当前页面打印,因为两者不是一个页面。
我们给background设置一个监听事件,当插件安装时打印日志:
background.js
| 1 | // background.js | 

点击后chrome会弹出一个新的,针对background.js页面的控制台
拓展:
- 直接打印chrome对象  - 所调用的方法,都是在chrome对象上的 
storage存储
我们在插件安装时在storage中设置一个值,这将允许多个插件组件访问该值并进行更新操作:
| 1 | //background.js | 
chrome.declarativeContent用于精确地控制什么时候显示我们的页面按钮,或者需要在用户单击它之前更改它的外观以匹配当前标签页。
这里调用的chrome.storage和我们常用的localStorage和sessionStorage不是一个东西。
值得注意得是,我们需要在manifest中给插件注册使用的权限,否则chrome对象中,将没有storage和declarativeContent
| 1 | { | 
再次查看背景页的视图,我们就能看到打印的日志了;
既然可以存储,那也能取出来,我们在popup中添加事件进行获取,首先我们新增一个触发的button:
popup.html
| 1 | 
 | 
我们再创建一个popup.js的文件,用来从storage存储中拿到颜色值,并将此颜色作为按钮的背景色:
popup.js
| 1 | let changeColor = document.getElementById("changeColor"); | 
效果:

如果需要调试popup页面,可以在弹框中右击 => 检查,在DevTools中进行调试查看。
获取浏览器tabs
现在,我们获取到了storage中的值,需要逻辑来进一步与用户交互;更新popup.js中的交互代码:
| 1 | // popupjs | 
chrome.tabs的API主要是和浏览器的标签页进行交互,通过query找到当前的激活中的tab,然后使用executeScript向标签页注入脚本内容。
manifest同样需要activeTab的权限,来允许我们的插件使用tabs的API。
| 1 | { | 
效果如下:

颜色选项页面
现在我们的插件功能还比较单一,只能让用户选择唯一的颜色;我们可以在插件中加入选项页面,以便用户更好的自定义插件的功能。
在程序目录新增一个options.html文件:
options.html
| 1 | 
 | 
然后添加选择页面的逻辑代码options.js:
| 1 | let page = document.getElementById("buttonDiv"); | 
上面代码中预设了四个颜色选项,通过onclick事件监听,生成页面上的按钮;当用户单击按钮时,将更新storage中存储的颜色值。
options页面完成后,我们可以将其在manifest的options_page进行注册:
| 1 | { | 
重新加载我们的插件,点击详情,滚动到底部,点击扩展程序选项来查看选项页面。或者可以在浏览器右上角插件图标上右击 => 选项。
此时可以选择背景色,效果如下:


我的理解,options.html提供了一个,可针对插件配置的一个页面
插件功能进阶
通过上面一个简单的小插件,相信大家对插件的功能和组件都有了一个大致的了解,知道了每个组件在其中发挥的作用;
但这还只是插件的一小部分功能,下面我们对插件每个部分的功能以及组件做一个更深入的了解。
使用background管理事件
background是插件的事件处理程序,它包含对插件很重要的浏览器事件的监听器。
background处于休眠状态,直到触发事件,然后执行指示的逻辑;
一个好的background仅在需要时加载,并在空闲时卸载。
background监听的一些浏览器事件包括:
- 插件程序首次安装或更新为新版本。
- 后台页面正在监听事件,并且已调度该事件
- 内容脚本或其他插件发送消息
- 插件中的另一个视图(例如弹出窗口)调用runtime.getBackgroundPage
加载完成后,只要触发某个事件,background就会保持运行状态;在上面manifest中,我们还指定了一个persistent属性:
| 1 | { | 
persistent属性定义了插件常驻后台的方式;
当其值为true时,表示插件将一直在后台运行,无论其是否正在工作;
当其值为false时,表示插件在后台按需运行,这就是Chrome后来提出的Event Page(非持久性后台)。
Event Page是基于事件驱动运行的,只有在事件发生的时候才可以访问;
这样做的目的是为了能够有效减小插件对内存的消耗,如非必要,请将persistent设置为false。
persistent属性的默认值为true
alarms
一些基于DOM页面的计时器(例如window.setTimeout或window.setInterval),如果在非持久后台休眠时进行了触发,可能不会按照预定的时间运行:
| 1 | let timeout = 1000 * 60 * 3; // 3 minutes in milliseconds | 
Chrome提供了另外的API,alarms:
| 1 | chrome.alarms.create({delayInMinutes: 3.0}) | 
browserAction
我们知道了browser_action字段用来配置popup的页面,在其他的一些文档中还给出了page_action字段的配置,不过page_action并不
是所有的页面都能够使用;
不过随着Chrome的版本更新,这两者的功能也越来越相近;在Chrome 48版本之后,page_action也从原来的地址栏中移出来,和插件
放在一起;笔者在配置page_action的时候没有发现有什么比较大的区别,因此下面以browser_action为主。
在browserAction的配置中,我们可以提供多种尺寸的图标,Chrome会选择最接近的图标并将其缩放到适当的大小来填充;如果没有提
供确切的大小,这种缩放会导致图标丢失细节或看起来模糊。
| 1 | { | 
也可以通过调用browserAction.setPopup动态设置弹出窗口。
| 1 | chrome.browserAction.setPopup({popup: 'popup_new.html'}); | 
Tooltip
要设置提示文案,使用default_title字段,或者在background.js中调用browserAction.setTitle函数
| 1 | chrome.browserAction.setTitle({ title: "New Tooltip" }); | 
如果两者都设置了,以函数设置的显示为准。
Badge
Badge(徽章)就是在图标上显示的一些文本内容,用来详细显示插件的提示信息;
由于Bage的空间有限,因此最多显示4个英文字符或者2个函数;
badge无法通过配置文件来指定,必须通过代码实现,设置badge文字和颜色可以分别使用browserAction.setBadgeText()和
browserAction.setBadgeBackgroundColor():
| 1 | chrome.browserAction.setBadgeText({ text: "new" }); | 
效果如下:

content-scripts
content-scripts(内容脚本)是在网页上下文中运行的文件。通过使用标准的文档对象模型(DOM),它能够读取浏览器访问的网页的详细
信息,对其进行更改,并将信息传递给其父级插件。内容脚本相对于background还是有一些访问API上的限制,它可以直接访问以下
chrome的API:
- i18n
- storage
- runtime:- connect
- getManifest
- getURL
- id
- onConnect
- onMessage
- sendMessage
 
内容脚本运行于一个独立、隔离的环境,它不会和主页面的脚本或者其他插件的内容脚本发生冲突,当然也不能调用其上下文和变量。假
设我们在主页面中定义了变量和函数:
| 1 | <html lang="en"> | 
由于隔离的机制,在内容脚本中调用add函数会报错:Uncaught ReferenceError: add is not defined。
内容脚本分为以代码方式或声明方式注入。
代码方式注入
对于需要在特定情况下运行的代码,我们需要使用代码注入的方式;
在上面的popup页面中,我们就是将内容脚本以代码的方式进行注入到页面中:
| 1 | chrome.tabs.executeScript(tabs[0].id, { | 
或者可以注入整个文件:
| 1 | chrome.tabs.executeScript(tabs[0].id, { | 
声明式注入
在指定页面上自动运行的内容脚本,我们可使用声明式注入的方式;
以声明方式注入的脚本需注册在manifest文件的content_scripts属性下。它们可以包括JS文件或CSS文件。
| 1 | { | 
声明式注入除了matches必须外,还可以包含以下字段,来自定义指定页面匹配:
| Name | Type | Description | 
|---|---|---|
| exclude_matches | 字符串数组 | 可选。排除此内容脚本将被注入的页面。 | 
| include_globs | 字符串数组 | 可选。 在 matches 后应用,以匹配与此 glob 匹配的URL。旨在模拟 @exclude 油猴关键字。 | 
| exclude_globs | 字符串数组 | 可选。 在 matches 后应用,以排除与此 glob 匹配的URL。旨在模拟 @exclude 油猴关键字。 | 
声明匹配URL可以使用Glob属性,Glob属性遵循更灵活的语法。
可接受的Glob字符串可能包含“通配符”星号和问号的URL。星号*匹配任意长度的字符串,包括空字符串,而问号?匹配任何单个字符。
| 1 | { | 
将JS文件注入网页时,还需要控制文件注入的时机,由run_at字段控制;
首选的默认字段是document_idle,但如果需要,也可以指定为 “document_start” 或“document_end”。
| 1 | { | 
三个字段注入的时机区别如下:
| Name | Type | Description | 
|---|---|---|
| document_idle | string | 首选。 尽可能使用 “document_idle”。浏览器选择一个时间在 “document_end” 和window.onload 事件触发后立即注入脚本。 注入的确切时间取决于文档的复杂程度以及加载所需的时间,并且已针对页面加载速度进行了优化。在 “document_idle” 上运行的内容脚本不需要监听 window.onload 事件,因此可以确保它们在 DOM 完成之后运行。如果确实需要在window.onload 之后运行脚本,则扩展可以使用 document.readyState 属性检查 onload 是否已触发。 | 
| document_start | string | 在 css 文件之后,但在构造其他 DOM 或运行其他脚本前注入。 | 
| document_end | string | 在 DOM 创建完成后,但在加载子资源(例如 images 和 frames )之前,立即注入脚本。 | 
消息通信
尽管内容脚本的执行环境和托管它们的页面是相互隔离的,但是它们共享对页面DOM的访问;如果内容脚本想要和插件通信,可以通过
onMessage和sendMessage
| 1 | // contentScript.js | 
更多消息通信的在后面我们会进行详细的总结。
contextMenus
contextMenus可以自定义浏览器的右键菜单(也有叫上下文菜单的),主要是通过chrome.contextMenusAPI实现;在manifest中添加权限来开启菜单权限:
| 1 | { | 
通过icons字段配置contextMenus菜单旁边的图标
我们可以在background中调用contextMenus.create来创建菜单,这个操作应该在runtime.onInstalled监听回调执行:
| 1 | chrome.contextMenus.create({ | 
效果如下:

如果我们的插件创建多个右键菜单,则Chrome会自动将其折叠为一个父菜单。
contextMenus创建对象的属性可以在附录里面找到;
我们看到在title属性中有一个%s的标识符,当contexts为selection,使用%s来表示选中的文字;我们通过这个功能可以实现一个
选中文字调用百度搜索的小功能,效果如下:
(没出来效果,暂时跳过)
contextMenus还有一些API可以调用:
| 1 | // 删除某一个菜单项 | 
override
覆盖页面(override)是一种将Chrome默认的特定页面替换为插件程序中的HTML文件。
除了HTML之外,覆盖页面通常还有CSS和JS代码。
插件可以替换以下Chrome的页面。
- 书签管理器 - 当用户从 Chrome 菜单中选择书签管理器菜单项或在 Mac 上从书签菜单中选择书签管理器项时出现的页面。您也可以通过输入 - URL chrome://bookmarks来访问此页面。 
 
- 历史记录 - 当用户从 Chrome 菜单中选择历史记录菜单项或在 Mac 上从历史记录菜单中选择显示完整历史记录项时出现的页面。您也可以 - 通过输入URL chrome://history来访问此页面。 
 
- 新标签 - 当用户创建新标签或窗口时出现的页面。您也可以通过输入 URL chrome://newtab来访问此页面。
 
需要注意的是:单个插件只能覆盖某一个页面。例如,插件程序不能同时覆盖书签管理器和历史记录页面。
在manifest进行如下配置:
| 1 | { | 
如果我们覆盖多个特定页面,Chrome加载插件时会直接报错:
storage
用户在操作时,会产生一些用户数据,插件需要在本地存储这些数据,在需要调用的时候再拿出来;
Chrome推荐使用chrome.storage的API,该API经过优化,提供和localStorage相同的存储功能;不推荐直接存在localStorage中,两者主要有以下区别:
- 用户数据使用chrome.storage存储可以和Chrome的同步功能自动同步。
- 插件的内容脚本可以直接访问用户数据,而无需background。
- chrome.storage可以直接存储对象,而localStorage是存储字符串,需要再次转换
如果要使用storage的自动同步,我们可以使用storage.sync:
| 1 | chrome.storage.sync.set({key: value}, function() { | 
当Chrome离线时,Chrome会将数据存储在本地。
下次浏览器在线时,Chrome会同步数据。即使用户禁用同步,storage.sync仍将工作。
不需要同步的数据可以用storage.local进行存储:
| 1 | chrome.storage.local.set({key: value}, function() { | 
如果我们想要监听storage中的数据变化,可以用onChanged添加监听事件;
每当存储中的数据发生变化时,就会触发该事件:
| 1 | // background.js | 
devtools
用过Vue或者React的devtools的童鞋应该见过这样新增的扩展面板:
DevTools可以为Chrome的DevTools添加功能,它可以添加新的UI面板和侧边栏,与检查的页面交互,获取有关网络请求的信息等等;
它可以访问以下特定的API:
- devtools.inspectedWindow
- devtools.network
- devtools.panels
DevTools扩展的结构与任何其他扩展一样:它可以有一个背景页面、内容脚本和其他项目。
此外,每个DevTools扩展都有一个DevTools页面,可以访问DevTools的API。
配置devtools不需要权限,只要在manifest中配置一个devtools.html:
| 1 | { | 
devtools.html中只引用了devtools.js,如果写了其他内容也不会展示:
| 1 | 
 | 
新建devtools.js
| 1 | // devtools.js | 
这里调用create创建扩展面板,createSidebarPane创建侧边栏,每个扩展面板和侧边栏都是一个单独的HTML页面,其中可以包含其他资源(JavaScript、CSS、图像等)。
DevPanel
DevPanel面板是一个顶级标签,和Element、Source、Network等是同一级,在一个devtools.js可以创建多个;在Panel.html中我们先
设置2个按钮:
Panel.html
| 1 | 
 | 
panel.js中我们使用devtools.inspectedWindow的API来和被检查窗口进行交互:
| 1 | // panel.js | 
eval函数为插件提供了在被检查页面的上下文中执行JS代码的能力,而getResources获取页面上所有加载的资源;
我们找到一个页面,然后右击检查打开调试工具,发现在最右侧多了一个DevPanel的tab页,
效果如下:

点击我们的调试按钮,那么日志在哪里能看到呢?
我们在调试工具上右击检查,再开一个调试工具,这个就是调试工具的调试工具。
最终两个调试工具的效果如下:
(实操中,在panel面板中,右击时并没有出来检查菜单栏)
Sidebar
回到devtools.js,我们使用createSidebarPane创建了侧边栏面板,并且设置为sidebar.html,最终呈现在Element面板的最右侧:

有几种方法可以在侧边栏中显示内容:
- HTML内容。调用setPage以指定要在窗格中显示的 HTML 页面。
- JSON数据。将JSON对象传递给setObject.
- JavaScript表达式。将表达式传递给setExpression
通过JS表达式,我们可以很方便进行页面查询,比如,查询页面上所有的img元素:
| 1 | chrome.devtools.panels.elements.createSidebarPane( | 
效果如下:

另外,我们可以通过elements.onSelectionChanged监听事件,在Element面板选中元素更改后,更新侧边栏面板的状态;
例如,可以将我们关心的一些元素的样式进行实时展示在侧边面板,方便查看:
| 1 | var page_getProperties = function () { | 
备注:浏览器自带的devtools已经有这样的功能了
notifications
Chrome提供chrome.notifications的API来推送桌面通知;同样也需要现在manifest中注册权限:
| 1 | { | 
在background调用创建即可
| 1 | // background.js | 
webRequest
通过webRequest的API可以对浏览器发出的任何HTTP请求进行拦截、组织或者修改;
可以拦截的请求还包括脚本、样式的GET请求以及图片的链接;
我们也需要在manifest中配置权限才能使用API:
| 1 | { | 
权限中还需要声明拦截请求的URL,如果你想拦截所有的URL,可以使用*://*/*(不过不推荐这么做,数据会非常多),如果我们想以
阻塞方式使用Web请求API,则需要用到webRequestBlocking权限。
比如我们可以对拦截的请求进行取消:
| 1 | chrome.webRequest.onBeforeRequest.addListener( | 
组件消息通信
不同组件之间经常需要进行消息通信来进行数据的传递,我们来看下他们之间是如何进行通信的:
background和popup通信
background和popup之间的通信比较简单,在popup中,我们可以通过extension.getBackgroundPage直接获取到background对
象,直接调用对象上的方法即可:
| 1 | // popup.js | 
而background访问popup上则通过extension.getViews来访问,不过前提是popup弹框已经展示,否则获取到的views是空数组:
| 1 | //background.js | 
background和内容脚本通信
在background和内容脚本通信,我们可以使用简单直接的runtime.sendMessage或者tabs.sendMessage发送消息,消息内容可以是
JSON数据
从内容脚本发送消息如下:
| 1 | // content-script.js | 
而从后台发送消息到内容脚本时,由于有多个标签页,我们需要指定发送到某个标签页:
| 1 | // background.js | 
而不管是在后台,还是在内容脚本中,我们都使用runtime.onMessage监听消息的接收事件,不同的是回调函数中的sender,标识不同的发送方:
| 1 | chrome.runtime.onMessage.addListener( | 
长链接
上面的runtime.sendMessage和tabs.sendMessage都属于短链接,所谓的短连接,就是类似于HTTP请求,如果接收方不在线,就会
出现请求失败的情况;但有些情况下,需要持续对话,这时候就需要用到长链接,类似于websocket,可以在通信双方之间进行持久链
接。
长链接使用runtime.connect或tabs.connect来打开长生命周期通道,通道可以有一个名称,以便区分不同类型的连接。
| 1 | // content-script.js | 
从background向内容脚本发送消息也类似,不同之处在于需要指定连接的tab页,将runtime.connect改为tabs.connect。
  在接收端,我们需要设置onConnect的事件监听器,当发送端调用connect进行连接时触发该事件,以及通过连接发送和接收消息的port对象:
| 1 | //background.js | 


