javascript-Chrome扩展实例(一)

Chrome扩展实例(一)

在Chrome扩展入门中,我们写了一个最简单的形式化Hello插件,这个插件除了点击之后显示写字符外,没有任何实际作用,在这一章节中我们将写一些具备简单功能的扩展。这些扩展需要通过后台、前台或二者配合实现,主要编程语言是JavaScript,使用Chrome浏览器提供的API。我们更希望在开发实例中逐步学习Chrome扩展的内容。

Chrome API文档:https://developer.chrome.com/docs/extensions/reference/

Service Worker后台服务

Service Worker是Chrome扩展在后台运行并监听和响应事件的服务程序,具有相当大的权限,几乎可以使用所有的Chrome API,作为扩展的支柱存在,稍微复杂一些的扩展都会需要service worker。在过去的版本中,service worker原名叫background算是非常贴切的名称了。

Service Worker是事件驱动的服务,平时处于后台静默或休眠状态,当注册在Chrome的API监听到相应事件时才会启动。Service worker注册并安装成功后,运行于浏览器后台,不受页面刷新的影响,可以监听和截拦作用域范围内所有页面的Web请求。

后台服务实例:将激活的Tab页面移至首位

我们下面用一个例子了解下插件的后台service worker是如何工作的。该例子的作用是将处于激活状态的tab页面移至首位,例如当我们点击某个页面时,该页面的标签页就会被移到首位。为了方便,该例子不会用到popupconetent等其他页面,只用后台的service worker。

首先必然是写配置清单manifest.json

1{
2  "name": "MoveToFisrt",
3  "description": "Demo Extension to move the activated tab to the first place",
4  "version": "1.0",
5  "manifest_version": 3,
6  "background": {
7    "service_worker": "background.js"
8  }
9}

清单中多出的配置项“background”就是用来指定service worker。manifes.json中使用的都是相对路径,因此我们在manifest.json同级目录添加background.js文件。其主要功能就是将激活的标签页移至首位。

 1//background.js
 2
 3//添加tab激活的事件监听器,事件触发后调用回调函数moveToFirstPosition
 4//该addListener的回调函数形式:(activeInfo: object) => void 
 5//activeInfo {tabID:tabID , windowId:windowId}
 6chrome.tabs.onActivated.addListener(moveToFirstPosition);
 7
 8//定义回调函数moveToFirstPosition
 9async function moveToFirstPosition(activeInfo) {
10  try {
11    // 将当前的tab移到首位即index:0。
12    await chrome.tabs.move(activeInfo.tabId, {index: 0});
13    console.log("Success.");
14  } catch (error) {
15    if (error == "Error: Tabs cannot be edited right now (user may be dragging a tab).") {
16      //如果标签无法移动,那么等50ms再尝试
17      setTimeout(() => moveToFirstPosition(activeInfo), 50);
18    } else {
19      console.error(error);
20    }
21  }
22}

这段js代码难度并不大,但是Chrome API有一个默认规则需要注意:

除非特殊说明,chrome.*的API都是异步的。因此调用后会立即返回,而不会等待异步的操作完成。如果需要对异步操作的结果进行处理,需要使用回调函数、Promiseasync...await...

所以,在使用Chrome API开发时,我们会经常见到async ... await..的形式。关于这种语法,可以参考ES 8中新增的规范。

现在我们可以在Chrome浏览器中载入这个扩展并尝试了!

这个例子中我们只是用了后台的service worker(background.js),该js脚本向Chrome的tab接口添加了一个事件监听器,当一个tab标签被激活后,会触发该事件监听器从而执行后台功能(moveToFirstPosition函数)。Chrome的后台脚本基本都是使用事件驱动的模式,这样可以降低扩展对内存的消耗。

扩展的权限管理

在网络安全与数据隐私重要性日益凸显的当下,Chrome在V3版本的manifest.json中,着重考虑了安全与隐私,甚至到了不进行权限授予就不能使用大部分Chrome API的程度。当我们需要操作浏览器或用户的相关数据等Chrome API时,需要给予扩展相对应的权限(Permissions),从而帮助扩展程序或应用程序受到攻击时尽可能减小损失。扩展有四种类型的权限:

  1. permissions:包含API文档中特点的权限字段如(storage,geolocation
  2. optional_permissions:类似permissions。但是在运行时由用户决定是否给予权限,而非事先决定。
  3. host_permissions:通过匹配字段来匹配是否有相应权限。
  4. optional_host_permissions:类似host_permissions使用匹配字段。但是在运行时由用户决定是否给予权限,而非事先决定。

扩展程序或应用程序必须在清单文件manifest.json中的 permissions字段中声明所需要的权限,否则Chrome API会拒绝被调用。关于那些API需要什么权限的细节,可以参考Chrome扩展的权限的文档

自定义扩展使用permission的实例

下面我们要开发一个Chrome扩展,让用户可以改变当前网页的背景色,其中需要使用到存储权限。首先在该扩展的目录下创建manifest.json文件。

1{
2  "name": "Coloring",
3  "description": "Demo Extension to change background color!",
4  "version": "1.0",
5  "manifest_version": 3,
6  "background": {
7    "service_worker": "background.js"
8  }
9}

注意:此时我们未向其添加权限permission信息。目前为止,这个manifest.json文件和上个例子“将激活的Tab页面移至首位”并无太大区别。接下来,我们编写该扩展的background.js文件。

1const color = "#3aa757"; //绿色
2
3chrome.runtime.onInstalled.addListener(()=>{
4    //调用存储API,存储color变量。
5    chrome.storage.sync.set({color});
6    console.log(`[Coloring] default background color is set to: ${color}`);
7});

我们载入这个扩展,发现这个简单的脚本报错了:

chrome_extension_permission_error

错误原因是chrome.storage.syncundefined,这是因为Chrome还没将storage接口授权给扩展,因此扩展无法获得chrome.storage.sync.*的API。我们要做就是在manifest.json中添加permissions字段,并在其中添加storage权限。

 1{
 2  "name": "Coloring",
 3  "description": "Demo Extension to change background color!",
 4  "version": "1.0",
 5  "manifest_version": 3,
 6  "background": {
 7    "service_worker": "background.js"
 8  },
 9  "permissions":["storage"]
10}

重新加载扩展,可以看到background.js执行无误,并如预期那样在DevTools页面中输出日志。

1[Coloring] default background color is set to: #3aa757  background.js:6

这样我们就能在浏览器中存储了一个颜色数据,不过暂时也只是存储下,并没有实际的用途。

Tips:在重新载入扩展后,chrome://extensions/页面下的扩展依旧会显示有个错误按钮,那是因为扩展会保留之前的错误,而非重载之后依然有错,我们需要手动清除之前的错误信息。

使用扩展修改原网页

现在,我们需要将存储的背景颜色数据拿出来真正利用上。为了降低学习曲线,我们先只是用扩展工具栏上的popup页面来修改网页。结合上一篇文章《javascript-Chrome扩展入门》中popup页面的内容,我们增加popup页面与相关icon图标。首先,在manifest.json文件添加actionicon字段。

 1
 2{
 3  "name": "Coloring",
 4  "description": "Demo Extension to change background color!",
 5  "version": "1.0",
 6  "manifest_version": 3,
 7  "background": {
 8    "service_worker": "background.js"
 9  },
10  "permissions": ["storage"],
11  "action": {
12    "default_popup": "popup.html",
13    "default_icon": {
14      "16": "images/get_started16.png",
15      "32": "images/get_started32.png",
16      "48": "images/get_started48.png",
17      "128": "images/get_started128.png"
18    }
19  },
20  "icons": {
21    "16": "images/get_started16.png",
22    "32": "images/get_started32.png",
23    "48": "images/get_started48.png",
24    "128": "images/get_started128.png"
25  }
26}

其中,正如前一篇文章介绍的,action字段用于代表点击扩展工具栏中扩展图标的响应动作,default_popup是指点击图标后弹出的页面,本例中是popup.html这个页面。default_iconicon下都是不同尺寸的图标图片,这个例子是借用了谷歌Chromemanifest V2版本的素材,链接:https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/_archive/mv2/tutorials/get_started。(如果链接失效了,自己找一些图片换上去也行)

关于扩展的图标。最好使用PNG格式,兼容性最好,BMP、GIF、ICO和JPEG格式也可以使用,不过写本文时(2023年1月)尚不支持WebP和SVG格式。图标的尺寸最好分别提供16×16像素、32×32像素、48×48像素和128×128像素四种,如果尺寸不合适,Chrome浏览器将会尽力选用合适的尺寸(不保证效果)。

接下来就是编写popup.html页面。我们希望popup.html这个页面能够提供一个按钮,当我们点击按钮时,就能够将网页背景颜色替换成我们之前在background.js存储的绿色。显然,仅仅靠HTML是不可能的,还要借助javascript来实现。基础的HTML代码如下:

 1<!DOCTYPE html>
 2<html>
 3  <head>
 4    <style>
 5      /*按钮的样式*/
 6      button {
 7        height: 30px;
 8        width: 30px;
 9        outline: none;
10        margin: 10px;
11        border: none;
12        border-radius: 2px;
13        }
14
15        button.current {
16        box-shadow: 0 0 0 2px white, 0 0 0 4px black;
17        }
18
19        body {
20          background: black;
21        }
22    </style>
23  </head>
24  <body>
25    <button id="changeColor">B</button>
26  <script> alert('Add some codes here');// 可以在这里添加JS代码吗??? </script>
27  </body>
28</html>

分离网页元素和脚本

上面的HTML代码很简单,就只有一个button按钮。如果开发者觉得一个简单的js脚本,直接在上面的<script> ... </script>里添加就行,那么这个扩展必然是运行不起来的。因为根据Chrome extension的Content Security Policy(CSP),不允许我们使用内联javascript脚本(inline script)。我们可以试试,跑这个代码会报什么错误。

chrome_extension_CSP.png

在网页中,inline script是恶意注入的重灾区,因此处于安全考虑,Chrome extension在开发时禁止使用inline script。即使原生的CSP是可以添加unsafe-inline字段来允许inline script的运行,但是Chrome extension开发组还是完全禁止了unsafe-inlineunsafe-eval这些存在安全隐患方法的使用。如果应要开发者坚持要使用inline script,必须要添加noncehash这种额外的安全验证手段,其带来的代码复杂度往往还要超过单独把inline script改成独立的js文件。因此,除非是实在没法改,都不鼓励使用inline script。

因此,我们需要再单独建一个javascript文件popup.js来放脚本程序,同时修改popup.html中inline script为External JavaScript文件链接。

1<!-- no inline script 
2<script> alert('Add some codes here');// 可以在这里添加JS代码吗??? </script>
3-->
4<script src="popup.js"></script>

那在popup.js添加什么样的Javascript代码呢?第一,解析popup.html的dom树,得到button表单元素。第二,给button注册监听按钮点击(click)事件。第三,当发生click事件时,需要从浏览器的本地存储中取出background.js存储的颜色数据。第四,解析DOM树修改页面背景的CSS属性。接下来,我们在上面的popup.html中着手添加js代码:

 1//popup.js
 2//第一,解析dom树,得到button表单元素。
 3const button = document.getElementById("changeColor");
 4
 5//第二,注册监听按钮点击事件。
 6button.addEventListener("click", onClickFunction);
 7
 8async function onClickFunction(){//注意由于Chrome API都是异步的,因此这里用async函数
 9    //调用Chrome接口取出当前标签页
10    const [tab] = await chrome.tabs.query({active: true, currentWindow: true});
11    // 以当前标签页为上下文,执行setPageBackgroundColor函数
12    chrome.scripting.executeScript({
13        target: {tabId: tab.id},
14        function: setPageBackgroundColor,
15    });
16}
17
18function setPageBackgroundColor(){
19    //第三,从浏览器的本地存储中取出存储的颜色数据。
20    chrome.storage.sync.get("color", ({ color }) => {
21        //第四,修改页面背景的CSS属性。
22        document.body.style.backgroundColor = color;
23    });
24}

在上面的js代码中,我们使用到了另外两个Chrome API:chrome.tabs.querychrome.scripting.executeScript。它们分别来获取当前活动的tab页面和执行脚本命令,需要activeTab, scripting两种权限,因此我们再修改上述manifest.jsonpermissions字段,添加这两种权限:

1//....
2  "permissions": ["storage","activeTab", "scripting"],
3//....

之后重新载入扩展,点击扩展工具栏中的插件,弹出了一个带字母“B”(Button)按钮的小页面,点击该按钮,则会将网页背景颜色改成我们之前存储的绿色#3aa757。我们以常见的百度首页为例:

chrome_extension_changeColor.png

Tips: 请注意,插件通过给body标签设置样式来修改网页背景色。因此,如果网页背景色是由其他标签决定的,便无法生效。

扩展的上下文

说实话,第一次看到例子中popup.js代码有点迷糊,尤其是中间async function onClickFunction()函数,不知道为什么要有这一步操作,还得用chrome.tabs.query先找当前的tab页面,然后在麻烦地用chrome.scripting.executeScript来执行setPageBackgroundColor函数。如此大费周章,为什么不能直接在注册监听事件的时候就将回调函数设置为setPageBackgroundColor呢?

我们可以尝试下看看直接这么做有什么效果,修改popup.js中的时间监听代码:

1//....
2//第二,注册监听按钮点击事件。
3button.addEventListener("click", setPageBackgroundColor);//直接执行修改背景的函数
4//....

重新载入后,Chrome没有报错,点击工具栏扩展弹出的按钮,得到的效果如下图所示:

chrome_extension_changebg_error_context

哈,网页页面本身的背景颜色没变,改变的只有扩展popup.html那个小页面的背景颜色!这是因为:要考虑扩展执行时候的上下文啊。

首先,点击按钮"B"所执行的popup.js脚本是在popup.html中引入的,因此popup.js执行的上下文是popup.html页面,因此此时的setPageBackgroundColor函数中DOM解析的document.body.style.backgroundColor是指popup.html的背景色,而非原网页www.baidu.com的背景色。所以要改变js函数执行的上下文,我们要先用chrome.tabs.query({active: true, currentWindow: true});找到当前页面的上下文环境,然后用

1chrome.scripting.executeScript({
2        target: {tabId: tab.id}, // 指定函数执行的上下为tabID所代表的环境
3        function: setPageBackgroundColor,
4    });

来重定向函数function: setPageBackgroundColor执行时的上下文环境为target: {tabId: tab.id}所指代的上下文。这样修改背景颜色的效果才会作用到原网页中。

不过,我们也可以通过上下文的区别来提供更好的用户体验。比如修饰原有的按钮。为了让用户更直观的感受背景将要变换的颜色,我们可以提前将背景色放到按钮上,让用户感觉我点击该颜色的按钮,就可以将背景改成和按钮一样的颜色。其修改的popup.js代码如下,读者可借助下面的代码体会两种上下文的区别。

 1//popup.js
 2//第一,解析dom树,得到button表单元素。
 3const button = document.getElementById("changeColor");
 4
 5// 从storage取背景色并设到按钮上,以popup.html为上下文
 6chrome.storage.sync.get("color", ({ color }) => {
 7    button.style.backgroundColor = color;
 8  });
 9
10//第二,注册监听按钮点击事件。
11button.addEventListener("click", onClickFunction);
12
13async function onClickFunction(){//注意由于Chrome API都是异步的,因此这里用async函数
14    //调用Chrome接口取出当前标签页
15    const [tab] = await chrome.tabs.query({active: true, currentWindow: true});
16    // 以当前标签页为上下文,执行setPageBackgroundColor函数
17    chrome.scripting.executeScript({
18        target: {tabId: tab.id},//指定访问的网页为上下文
19        function: setPageBackgroundColor,
20    });
21}
22
23function setPageBackgroundColor(){
24    //第三,从浏览器的本地存储中取出存储的颜色数据。
25    chrome.storage.sync.get("color", ({ color }) => {
26        //第四,修改页面背景的CSS属性。
27        document.body.style.backgroundColor = color;
28    });
29}

效果如下:

chrome_extension_context

选项设置页面

目前改背景颜色这个扩展只支持将背景色设为绿色,这样很不灵活。我们可以给插件添加选项设置页面,实现更加丰富的功能,比如提供不同的背景色。

选项设置页面也可以看作是扩展的配置页面,是扩展灵活性的体现。在manifest.json中,需要添加option_page字段指定选项页面,我们在目录中新建options.html作为选项页面的HTML文件。完整的mamifest.json文件如下:

 1{
 2  "name": "Coloring",
 3  "description": "Demo Extension to change background color!",
 4  "version": "1.0",
 5  "manifest_version": 3,
 6  "background": {
 7    "service_worker": "background.js"
 8  },
 9  "permissions": ["storage","activeTab", "scripting"],
10  "action": {
11    "default_popup": "popup.html",
12    "default_icon": {
13      "16": "images/get_started16.png",
14      "32": "images/get_started32.png",
15      "48": "images/get_started48.png",
16      "128": "images/get_started128.png"
17    }
18  },
19  "icons": {
20    "16": "images/get_started16.png",
21    "32": "images/get_started32.png",
22    "48": "images/get_started48.png",
23    "128": "images/get_started128.png"
24  },
25  "options_page": "options.html"
26}

这时重新加载插件,如果扩展在扩展工具栏可见,则右键点击扩展图标,可以看到右键菜单中的选项菜单,点它就会打开我们添加的选项页options.html。如果扩展被收纳进扩展程序图标里,那么点击扩展程序图标,找到对应扩展右侧的三个竖点图标 $\vdots$,在下拉菜单中也可以找到选项入口。

chrome_extension_options

现在我们还没给options.html添加任何内容,因此现在打开选项页是一个空白页。我们给其添加上相应的Html代码(体会个意思就不搞太复杂了^_^):

 1<!DOCTYPE html>
 2<html>
 3  <head>
 4    <style>
 5      button {
 6        height: 30px;
 7        width: 30px;
 8        outline: none;
 9        margin: 10px;
10      }
11    </style>
12  </head>
13  <body>
14    <div id="buttonDiv">
15    </div>
16    <div>
17      <p>Choose a different background color!</p>
18    </div>
19  </body>
20  <script src="options.js"></script>
21</html>

popup.html类似,其主要功能也得依托Javascript实现,同时受限于CSP规则,js代码应该与Html代码分写在两个文件中,所以我们还要建立options.js实现具体功能。

为了方便示例,我们在选项页面中提供四种可选的背景色,用四个按钮表示。当我们点击选中的颜色的按钮时,会将对应的颜色存储到Chrome浏览器的存储空间chrome.storage.sync中。这样popup.html页面运行时就会从浏览器中选出选项页确定的颜色。其代码如下:

 1//options.js
 2
 3//设置四种颜色:绿、红、黄、蓝
 4const kButtonColors = ['#3aa757', '#e8453c', '#f9bb2d', '#4688f1'];
 5
 6//根据给定的颜色创建颜色按钮并注册监听事件
 7function constructOptions(kButtonColors) {
 8    //找到添加按钮的div
 9    let page = document.getElementById('buttonDiv');
10    //根据给定的颜色创建颜色按钮
11    for (let item of kButtonColors){
12        let button = document.createElement('button');
13        //设置按钮背景色
14        button.style.backgroundColor = item;
15        //为按钮注册监听事件,点击按钮则存储该颜色
16        button.addEventListener('click', ()=>{
17            chrome.storage.sync.set({color:item}, ()=>{
18                console.log('color is ' + item);
19            })
20        });
21        //添加元素
22        page.appendChild(button);
23    }
24}
25
26//运行创建按钮函数
27constructOptions(kButtonColors);

打开选项页面,就是四个不同颜色的按钮。

chrome_extension_optionpage.png

当我们点击蓝色按钮后,popup.html的按钮也会变成蓝色,点击后,网页的背景色也是蓝色。

chrome_extension_optionpage_blue.png

总结

这篇文章中,我们相对完整的实现了一个改背景颜色的Chrome扩展,包含了清单文件manifest.json,后台脚本background.js,功能页面popup.html和配置页面options.html。基本走通了Chrome扩展开发的普遍流程,其他扩展开发的步骤也是大同小异。同时文章中也介绍了常见权限问题、上下文问题、CSP问题等常见坑,也算是经验的积累。改颜色扩展完整的目录结构如下:

chrome_extension_my_color.png

该扩展主要参考了Chrome官方Manifest V2的一个教程,针对V3版本做了修改。

如果想把自己的扩展给更多人使用,可以将其打包上传的Chrome商店,发布自己的扩展,其步骤可参考Chrome教程https://developer.chrome.com/docs/webstore/publish/。如果只是小范围使用,可以直接把文件拷给别人就可以了。

参考文档

  1. Google官方文档https://developer.chrome.com/docs/extensions/mv3/
  2. MV2 教程老例子 https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/_archive/mv2/tutorials/get_started