在上一篇文章中,我们介绍了Chrome插件的页面如何写,以及各个组件之间是如何来通信的,得到了不少朋友的积极反馈,大家对Chrome插件的相关内容也都比较感兴趣,也存在着相当大的应用市场;本文就结合项目开发中遇到的的一些实际问题,分享一些开发经验。
从V2升级到V3
上一篇文章写的时间比较早,使用的还是V2版本的插件,而现在Chrome最新的插件版本也来到的V3,而且V2插件也不能继续在Chrome商店里面发布上架了;因此很多朋友吐槽得比较多的就是,上一篇文章中介绍的插件版本太老了;因此本文我们先来看下如何从V2升级到V3,以及两个版本存在着哪些区别。
首先Chrome浏览器是从88版本开始支持V3,因此开发之前,首先确定一下自己的浏览器版本是否高于这个版本;第一步,就是修改manifest.json
文件,将我们的插件版本号从2改到3。
| { "manifest_version": 3, }
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
注意:这里改的是manifest_version
,而不是version
字段。
权限配置升级
在V2版本中,host权限和其他的权限配置一般都统一的放在permissions
字段中,而其他一些可选权限则在optional_permissions
:
| { ... "permissions": [ "tabs", "bookmarks", "https://www.xieyufei.com/", ], "optional_permissions": [ "unlimitedStorage", "*://*/*", ] }
|
permissions
列出的权限是插件被安装前
所需要的;而optional_permissions
列出的一些权限,是插件在安装时不需要的,在安装之后
可能会要求的权限。
在V3版本中,权限配置更加精细化,我们需要把主机权限独立到单独的host_permissions
和optional_host_permissions
字段中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| { ... "permissions": [ "tabs", "bookmarks" ], "optional_permissions": [ "unlimitedStorage" ], "host_permissions": [ "https://www.xieyufei.com/", ], "optional_host_permissions": [ "*://*/*", ] }
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
web_accessible_resources
web_accessible_resources
字段用来控制外部访问插件中的资源,比如content-script脚本或者popup页面中需要展示展示图片资源;在V2版本中,直接定义一个资源列表,那么所有网站都能访问这些资源了:
| { "web_accessible_resources": [ "images/*", "style/extension.css", "script/extension.js" ], }
|
而来到V3版本,我们需要配置一个对象数组,对象中通过resources和matches更加精细化的配置了哪些外部网站可以访问哪些资源文件。
| { "web_accessible_resources": [ { "resources": [ "style/extension.css", "script/extension.js" ], "matches": [ "https://*.xieyufei.com/*" ] } ], }
|
假设我们有一张图片资源在以下插件目录下:
| extension-files/ manifest.json content-script.js images/ banner.png
|
我们想让content-script.js来在页面呈现图片的地址,需要在manifest.json声明可以被访问到:
| { "web_accessible_resources": [ { "resources": [ "images/banner.png" ], "matches": [ "*" ] } ], }
|
然后在content-script.js中调用Chrome插件的chrome.runtime.getURL函数
来获取图片的地址,图片的地址看起来可能是这样的:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
| chrome-extension://<extension-UUID>/images/banner.png
|
这里的extension-UUID
并不是插件的ID,而是一个随机生成的唯一id。
我们在匹配资源文件的路径时,面对多个文件匹配,也可以使用通配符:
| { "web_accessible_resources": [ { "resources": [ "images/*.png" ], "matches": [ "*" ] } ], }
|
background后台
background后台的升级也是Chrome插件更新的重要特性之一,使用了Service Worker替代了原来的Background page。在V2版本中,我们使用background.scripts可以配置多个js,或者使用background.page配置一个后台页面:
| { "background": { "scripts": ["js/script1.js", "js/script2.js"], "persistent": true }, }
|
persistent: true
指定了脚本一直在后台运行,直到插件被禁用或者卸载,这样就导致占用了大量的内存;因此V3废弃了scripts和page;如果我们还是指定这两者,Chrome就会报下面错误,直接就不让我们运行插件了,
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
| 错误 The "background.scripts" key cannot be used with manifest_version 3. Use the "background.service_worker" key instead. 无法载入清单。
|
V3版本升级改用了service_worker
字段代替原来scripts和page,确保插件不会一直占用浏览器的资源,仅在需要时才运行,从而节省资源:
| { "background": { "service_worker": "js/background.js" }, }
|
service_worker
字段不是一个数组,只支持字符串格式。
同时V3版本升级也让background.js支持了模块化开发,我们可以在里面直接import本地的方法,让我们能够不用依赖打包的方式进行模块化开发,使用方式也很简单,在background添加type
属性即可:
| { "background": { "service_worker": "js/background.js", "type": "module" }, }
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
我们在background.js中就可以使用import导入本地模块:
| import { add } from "./utils.js"; chrome.runtime.onInstalled.addListener(() => { console.log("测试插件已经安装", add(2, 4)); });
export function add(a, b) { return a + b; }
|
同时,由于background不再支持page页面配置background.html,因此也无法调用window对象上的XMLHttpRequest
来构建ajax请求;也就是说我们不能像V2版本一样,在background.html中使用jQuery的$.ajax来发送请求了,而是需要使用fetch函数
来获取接口数据。
由于service workers是短暂的,在不使用时会终止,这意味着它们在整个浏览器插件运行期间会不断的启动、运行和终止,也就是不稳定的;因此我们可能需要对V2中background.js的代码逻辑进行一些改造,以往我们会习惯将一些数据直接存储到全局变量,比如像下面这样:
| let saveUserName = "";
chrome.runtime.onMessage.addListener(({ type, name }) => { if (type === "set-name") { saveUserName = name; } });
chrome.action.onClicked.addListener((tab) => { console.log(saveUserName, "saveUserName"); });
|
当我们运行项目时发现,全局变量saveUserName在某些情况下获取到的数据变成空字符串,存储的数据直接消失了;笔者在项目调试中刚开始经常会遇到这种神奇的问题,调试的值跟实际的值不一样,随之消失的还有笔者的信心。
因此在V3中,需要对这种全局存储的变量数据进行改造,改造的方式也很简单,就是将数据持久化保存到storage中,需要用到的地方随用随取:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
| chrome.runtime.onMessage.addListener(({ type, name }) => { if (type === "set-name") { chrome.storage.local.set({ name }); } });
chrome.action.onClicked.addListener(async (tab) => { const { name } = await chrome.storage.local.get(["name"]); chrome.tabs.sendMessage(tab.id, { name }); });
|
actions升级
有小伙伴也许发现了,我们上面使用了chrome.action.onClicked
来注册点击事件,而不是原来的chrome.browserAction.onClicked
。
由于历史原因,之前将插件的图标分为pageAction
和browserAction
,两者的区别在于browserAction始终都显示,更像我们现在的插件图标逻辑;而pageAction则比较特殊,只有当某些特定的页面打开时才会显示图标。
而V2版本两者的区分界限已经较为模糊了,区别不是很大;但是在manifest.json中配置还是有区分,常用的就是browser_action:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
| { "page_action": { ... }, "browser_action": { "default_popup": "popup.html" } }
|
升级到V3版本,直接统一为同一个action,不需要再区分:
| { "action": { "default_title": "插件标题", "default_popup": "popup.html", "default_icon": { "16": "/images/get_started16.png", "32": "/images/get_started32.png", }, "icons": { "16": "/images/get_started16.png", "32": "/images/get_started32.png", } }, }
|
需要注意的是:如果注册了popup.html的页面,则chrome.action.onClicked
点击事件注册后并不会被执行。
我们在绑定chrome.action
事件的地方也需要进行统一:
| chrome.browserAction.onClicked.addListener(tab => { ... }); chrome.pageAction.onClicked.addListener(tab => { ... });
chrome.action.onClicked.addListener(tab => { ... });
|
CSP
内容安全策略(Content Security Policy,简称CSP),是在manifest.json中配置的,用于限制扩展可以从哪些源加载代码,比如script标签可以从哪些域名地址加载CDN,或者禁止eval()
等可能不安全的函数;在V2版本中,默认是一个字符串配置:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
| { "content_security_policy": "default-src 'self'" }
|
升级到V3版本,content_security_policy
字段依然被保留,支持另外两个属性:extension_pages和sandbox:
| { "content_security_policy": { "extension_pages": "default-src 'self'", "sandbox": "..." } }
|
default-src 'self'
表示默认所有类型的引用文件(js文件、html文件)都是应该在插件包内的;如果我们想要支持从某个域名地址引入js文件,在V2中我们会看到下面的写法:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
| { "content_security_policy": "script-src 'self' https://xieyufei.com; object-src 'self'" }
{ "content_security_policy": "script-src 'self' https://*.xieyufei.com; object-src 'self'" }
|
但V3中不支持这样的写法,不允许从某个域名地址引入文件。
API调用升级
我们在调用chrome API的地方,也有一些需要进行升级改造的,比如上面的chrome.action:
| chrome.browserAction.onClicked.addListener(tab => { ... }); chrome.pageAction.onClicked.addListener(tab => { ... });
chrome.action.onClicked.addListener(tab => { ... });
|
在获取资源地址的时候,也需要将chrome.extension.getURL
替换成chrome.runtime.getURL
:
| chrome.extension.getURL("images/img.png");
chrome.runtime.getURL("images/img.png");
|
V3中,执行script-content的api函数executeScript也从tabs
,升级到了scripting
;因此我们还需要在manifest.json中添加scripting
权限才能调用;同时,执行的脚本也从原来的单个文件,变成可以接收多个文件:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
| chrome.tabs.executeScript( tab.id, { file: 'content-script.js' } );
chrome.scripting.executeScript({ target: {tabId: tab.id}, files: ['content-script.js'] });
|
insertCSS()
和removeCSS()
也从tabs
升级到了scripting
。
| chrome.tabs.insertCSS(tab.id, injectDetails, () => { });
const insertPromise = await chrome.scripting.insertCSS({ files: ["style.css"], target: { tabId: tab.id } });
|
service worker异步返回数据
我们在实际项目中,有时候会需要service worker异步返回一些数据,比如请求接口后返回一些接口数据等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| chrome.runtime .sendMessage({ type: 'get-status', }) .then((res) => { })
chrome.runtime.onMessage .addListener(async ({ type }, sender, sendResponse) => { if (type === 'get-status') { fetch('XXX/list.json').then(res=>{ sendResponse(res) }); } })
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
上面的代码中在content-script.js发送消息到background中,虽然这里我们虽然是在then中返回了res,或者使用async/await;但是很遗憾,在content-script.js接收到的res还是undefined,我们需要对background代码进行改造
| chrome.runtime.onMessage .addListener(async ({ type }, sender, sendResponse) => { if (type === 'get-status') { fetch('XXX/list.json').then(res=>{ sendResponse(res) }); return true; } })
|
在onMessage回调函数里返回true,告诉Chrome我们想要异步发送响应。
插件和原生页面通信问题
我们有时候会遇到需要插件去和原生Web页面进行通信情况,这里的原生Web页面页面指的并不是content-script.js或者popup.html页面,一般也是我们开发的网站页面;比如在原生Web页面页面中,需要判断是否安装了插件,没有安装插件的话显示下载插件的跳转链接;或者点击原生页面上的某一个按钮,将数据保存到插件中来等等,就需要涉及插件和原生Web页面页面的通信问题。
这里有几种实现通信的方式,第一种最简单的方式就是通过隐藏的dom节点,比如安装插件后,通过content-script.js在页面上放置一个隐藏的dom,将插件信息放到放到dom节点上,这样的缺点也很明显,只能传输一些简单的数据,且不能进行双向通信。
第二种方式,通过插件的id,从原生Web页面想插件发送消息,首先需要配置在manifest.json中配置externally_connectable
字段,来声明哪些Web页面可以通过这种方式,和插件建立链接:
| { "externally_connectable": { "matches": ["https://*.fill-you-web-url.com/*"] }, }
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
externally_connectable还可以指定ids
字段,用来指定需要通信的其他Chrome插件;配置完成然后就可以在我们的Web页面里添加发送消息的代码了:
| const extensionId = "iodjapnffldobobfdaoobinimjofgejm";
chrome?.runtime?.sendMessage( extensionId, { type: "pageMsg", msg: "hello i am from origin", }, (response) => { console.log("res data", response); } );
|
这里如果我们没有配置上面的externally_connectable字段,浏览器是不会在我们的页面上注入chrome.runtime.sendMessage
方法的,因此我们需要对这个函数进行异常判断,否则页面就会报错。
| chrome.runtime.onMessageExternal.addListener( (request, sender, sendResponse) => { if (request.type === "pageMsg") { sendResponse('res msg'); } else { sendResponse("received"); } } );
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
第三种方式,我们可以通过window.postMessage
进行通信,window.postMessage
一般用在多个页面之间通信,当然,我们的content-script.js和原生Web界面是同源的,更能直接通信了;两者的发送方式和接收方式在代码上都是一样的,这里也不再进行区分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| window.addEventListener('message', (ev) => { if (ev.source != window) { return; } if (ev.data) { const { type, saveData } = ev.data; } })
const clickSend = ()=>{ window.postMessage( { type: 'myTestPostMsg', saveData: { title: 'XXX', version: 'QQQ' }, }, '*' ); }
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
这样我们不需要获取插件的ID也能通信了,不过我们在监听message消息时会看到各种各样插件或者页面之间传递的消息,因此我们对传输数据的命名方式上差异化,可以定义一些独特的前缀,避免和其他页面产生不必要的冲突。