5

Chrome插件实战开发

 1 year ago
source link: https://xieyufei.com/2023/08/11/Chrome-Plugin-Practise.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

  在上一篇文章中,我们介绍了Chrome插件的页面如何写,以及各个组件之间是如何来通信的,得到了不少朋友的积极反馈,大家对Chrome插件的相关内容也都比较感兴趣,也存在着相当大的应用市场;本文就结合项目开发中遇到的的一些实际问题,分享一些开发经验。

从V2升级到V3

  上一篇文章写的时间比较早,使用的还是V2版本的插件,而现在Chrome最新的插件版本也来到的V3,而且V2插件也不能继续在Chrome商店里面发布上架了;因此很多朋友吐槽得比较多的就是,上一篇文章中介绍的插件版本太老了;因此本文我们先来看下如何从V2升级到V3,以及两个版本存在着哪些区别。

  首先Chrome浏览器是从88版本开始支持V3,因此开发之前,首先确定一下自己的浏览器版本是否高于这个版本;第一步,就是修改manifest.json文件,将我们的插件版本号从2改到3。

{
// "manifest_version": 2,
"manifest_version": 3,
// ...
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

注意:这里改的是manifest_version,而不是version字段。

权限配置升级

  在V2版本中,host权限和其他的权限配置一般都统一的放在permissions字段中,而其他一些可选权限则在optional_permissions

// V2
{
...
"permissions": [
"tabs",
"bookmarks",
"https://www.xieyufei.com/",
],
"optional_permissions": [
"unlimitedStorage",
"*://*/*",
]
// ...
}

  permissions列出的权限是插件被安装前所需要的;而optional_permissions列出的一些权限,是插件在安装时不需要的,在安装之后可能会要求的权限。

  在V3版本中,权限配置更加精细化,我们需要把主机权限独立到单独的host_permissionsoptional_host_permissions字段中:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

// V3
{
...
"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版本中,直接定义一个资源列表,那么所有网站都能访问这些资源了:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

// V2
{
// ...
"web_accessible_resources": [
"images/*",
"style/extension.css",
"script/extension.js"
],
// ...
}

  而来到V3版本,我们需要配置一个对象数组,对象中通过resources和matches更加精细化的配置了哪些外部网站可以访问哪些资源文件。

// V3
{
// ...
"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配置一个后台页面:

// V2
{
"background": {
"scripts": ["js/script1.js", "js/script2.js"],
// or "page": "background.html"
"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,确保插件不会一直占用浏览器的资源,仅在需要时才运行,从而节省资源:

//V3
{
"background": {
"service_worker": "js/background.js"
// 移除了 "persistent": true
},
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

service_worker字段不是一个数组,只支持字符串格式。

  同时V3版本升级也让background.js支持了模块化开发,我们可以在里面直接import本地的方法,让我们能够不用依赖打包的方式进行模块化开发,使用方式也很简单,在background添加type属性即可:

// manifest.json
{
"background": {
"service_worker": "js/background.js",
"type": "module"
},
}

  我们在background.js中就可以使用import导入本地模块:

// background.js
import { add } from "./utils.js";
chrome.runtime.onInstalled.addListener(() => {
console.log("测试插件已经安装", add(2, 4));
});

// utils.js
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的代码逻辑进行一些改造,以往我们会习惯将一些数据直接存储到全局变量,比如像下面这样:

// V2 background.js
let saveUserName = "";

// 其他页面,比如content-script或者popup中存储数据
chrome.runtime.onMessage.addListener(({ type, name }) => {
if (type === "set-name") {
saveUserName = name;
}
});

// 点击popup时展示数据
chrome.action.onClicked.addListener((tab) => {
// 这里saveUserName可能为空字符串
console.log(saveUserName, "saveUserName");
});

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  当我们运行项目时发现,全局变量saveUserName在某些情况下获取到的数据变成空字符串,存储的数据直接消失了;笔者在项目调试中刚开始经常会遇到这种神奇的问题,调试的值跟实际的值不一样,随之消失的还有笔者的信心。

消失了

  因此在V3中,需要对这种全局存储的变量数据进行改造,改造的方式也很简单,就是将数据持久化保存到storage中,需要用到的地方随用随取:

// V3 service worker
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

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  由于历史原因,之前将插件的图标分为pageActionbrowserAction,两者的区别在于browserAction始终都显示,更像我们现在的插件图标逻辑;而pageAction则比较特殊,只有当某些特定的页面打开时才会显示图标。

pageAction

pageAction

  而V2版本两者的区分界限已经较为模糊了,区别不是很大;但是在manifest.json中配置还是有区分,常用的就是browser_action:

// V2
{
"page_action": { ... },
"browser_action": {
"default_popup": "popup.html"
}
}

  升级到V3版本,直接统一为同一个action,不需要再区分:

// V3
{
"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事件的地方也需要进行统一:

// V2 
chrome.browserAction.onClicked.addListener(tab => { ... });
chrome.pageAction.onClicked.addListener(tab => { ... });

// V3
chrome.action.onClicked.addListener(tab => { ... });

  内容安全策略(Content Security Policy,简称CSP),是在manifest.json中配置的,用于限制扩展可以从哪些源加载代码,比如script标签可以从哪些域名地址加载CDN,或者禁止eval()等可能不安全的函数;在V2版本中,默认是一个字符串配置:

// V2
{
"content_security_policy": "default-src 'self'"
}

  升级到V3版本,content_security_policy字段依然被保留,支持另外两个属性:extension_pages和sandbox:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

// V3
{
"content_security_policy": {
"extension_pages": "default-src 'self'",
"sandbox": "..."
}
}

  default-src 'self'表示默认所有类型的引用文件(js文件、html文件)都是应该在插件包内的;如果我们想要支持从某个域名地址引入js文件,在V2中我们会看到下面的写法:

// 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:

// V2 
chrome.browserAction.onClicked.addListener(tab => { ... });
chrome.pageAction.onClicked.addListener(tab => { ... });

// V3
chrome.action.onClicked.addListener(tab => { ... });

  在获取资源地址的时候,也需要将chrome.extension.getURL替换成chrome.runtime.getURL

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

// V2
chrome.extension.getURL("images/img.png");

// V3
chrome.runtime.getURL("images/img.png");

  V3中,执行script-content的api函数executeScript也从tabs,升级到了scripting;因此我们还需要在manifest.json中添加scripting权限才能调用;同时,执行的脚本也从原来的单个文件,变成可以接收多个文件:

// V2
chrome.tabs.executeScript(
tab.id,
{
file: 'content-script.js'
}
);

// V3
chrome.scripting.executeScript({
target: {tabId: tab.id},
files: ['content-script.js']
});

  insertCSS()removeCSS()也从tabs升级到了scripting

// V2
chrome.tabs.insertCSS(tab.id, injectDetails, () => {
// callback code
});

// V3
const insertPromise = await chrome.scripting.insertCSS({
files: ["style.css"],
target: { tabId: tab.id }
});

service worker异步返回数据

  我们在实际项目中,有时候会需要service worker异步返回一些数据,比如请求接口后返回一些接口数据等:

// content-script.js
chrome.runtime
.sendMessage({
type: 'get-status',
})
.then((res) => {
// 对res处理
})

// background.js
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代码进行改造

// background.js
chrome.runtime.onMessage
.addListener(async ({ type }, sender, sendResponse) => {
if (type === 'get-status') {
fetch('XXX/list.json').then(res=>{
sendResponse(res)
});
// 这里添加了返回true
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页面可以通过这种方式,和插件建立链接:

// manifest.json
{
"externally_connectable": {
"matches": ["https://*.fill-you-web-url.com/*"]
},
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  externally_connectable还可以指定ids字段,用来指定需要通信的其他Chrome插件;配置完成然后就可以在我们的Web页面里添加发送消息的代码了:

// 插件ID
const extensionId = "iodjapnffldobobfdaoobinimjofgejm";

// 向Chrome扩展发送请求
chrome?.runtime?.sendMessage(
extensionId,
{
type: "pageMsg",
msg: "hello i am from origin",
},
(response) => {
console.log("res data", response);
}
);

  这里如果我们没有配置上面的externally_connectable字段,浏览器是不会在我们的页面上注入chrome.runtime.sendMessage方法的,因此我们需要对这个函数进行异常判断,否则页面就会报错。

// background.js 接收原生Web页面消息
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界面是同源的,更能直接通信了;两者的发送方式和接收方式在代码上都是一样的,这里也不再进行区分:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

// 页面初始化话进行监听
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消息时会看到各种各样插件或者页面之间传递的消息,因此我们对传输数据的命名方式上差异化,可以定义一些独特的前缀,避免和其他页面产生不必要的冲突。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK