没有插件的浏览器是没有灵魂的。今天来近距离感受一下chrome的灵魂
开始之前了解一下灵魂chrome插件的版本。
Chrome 浏览器从88版本开始支持MV3啦(即Manifest Version 3),现在浏览器版本都100+了。而MV2(即Manifest Version 2)将会在2023年 退休
。所以今天要讲的就是MV3版本
后续的文章中,因为我没有魔法,所以贴出来的文档地址都是国内可以访问的文档(有条件的同学可以直接看谷歌的原文档 https://developer.chrome.com/docs/extensions/mv3/)
manifest.json 作为插件的配置清单最能体现相关的变动了 从manifest.json 参考文档 可以很清楚地看到配置升级其实主要加了2个 「action」和 「host_permissions」
在V2中,有两种方法为你的api或任何主机获得权限,要么在 permissions 数组或 optional_permissions 数组。
{
"permissions": ["https://xxxx.com/*"]
}
在V3中,所有主机权限现在都单独存在一个新数组中,该数组的键为 host_permissions。主机权限不再与其他权限一起添加。
{
"host_permissions": ["https://xxx.com/*"]
}
在V2中,分为 browser_action
和 page_action
。
browser_action
更多是负责插件的icon的切换等操作。参考文档: API-browserActionpage_action
更多是针对某个页面进行地址栏的操作 参考文档:API-pageAction感兴趣的可以在MDN插件开发文档里面看一看。
在V3中,都统一合并为 action
。参考文档:API-action
在V2的manifest.json 的 content_security_policy
配置是一个字符串类型。升级到 V3 后变成了一个对象类型。详细的变更看文档会更加清晰:content_security_policy 参考文档
详细变更参考 web_accessible_resources
// v2 写法
{
"web_accessible_resources": ["images/my-image.png"]
}
// v3 写法
{
// …
"web_accessible_resources": [
{
"resources": [ "test1.png", "test2.png" ],
"matches": [ "https://web-accessible-resources-1.glitch.me/*" ]
}, {
"resources": [ "test3.png", "test4.png" ],
"matches": [ "https://web-accessible-resources-2.glitch.me/*" ],
"use_dynamic_url": true
}
],
// …
}
以前一些功能可以依赖于网络请求动态加载,V3 则不允许这样的操作了
只能把以前通过链接加载的js下载到插件包中,改改资源引入就好~
V3 现在原生支持 Promise。许多常用 API 现在都支出,最终所有合适的 API 都会支持 Promise。
如果使用 callback,就不会返回 Promise,优先执行 callback。
在V2中,Background是可以通过 persistent
配置来确保页面时候需要 持久化
。而且还能支持 .html
"background": {
"scripts": ["background-script.js"],
"persistent": false
}
// 或
"background": {
"page": "background-page.html",
"persistent": false
}
很多小技巧都依赖于 html 这特性,把数据挂载在 background 的 window
对象上进行数据中转
V3 则是强制使用了 Service Workers,禁止了持久化。background只能使用js文件
"background": { "scripts": ["background.js"] },
这个变动非常的大,在本文后面详细讲这一块的内容。而且 MDN 文档还没更新 declarativeNetRequest 相关的内容,等下要找个新文档来看
chrome.extension.getExtensionTabs()
chrome.extension.getURL()
chrome.extension.lastError
chrome.extension.onRequest
chrome.extension.onRequestExternal
chrome.extension.sendRequest()
chrome.tabs.getAllInWindow()
chrome.tabs.getSelected()
chrome.tabs.onActiveChanged
chrome.tabs.onHighlightChanged
chrome.tabs.onSelectionChanged
chrome.tabs.sendRequest()
chrome.tabs.selected
在查看 MDN 文档时会有相关的提示
使用chrome官方文档时的提示
查阅官方文档时,那些标签也能帮助到我们。
Promise
标签:支持 Promise
<=MV2
标签:该API仅在V2前支持
>=MV3标签
:该API在V3后支持
Deprecated
标签:已废弃的 API
都2022年了,或许每次开发一些新东西的时候你都会在想:
好麻烦啊,上github找找有没有现成的。
好像都还可以,收藏吃灰,下次在开发把
不要下次了!就这次把,强烈推荐 Plasmo
官网: plasmo
github: PlasmoHQ/plasmo
官方自己的介绍(说的非常的朴素,我一个路人都觉得这功能写少了)
作为一个过来人的感受,我只能说用 Plasmo
很舒服~
想体验先安装Plasom,快速上手文档: getting-started
注意下自己的 pnpm 版本或者 npm 版本,我用的是pnpm
# 使用下面的命令进行项目初始化
pnpm dlx plasmo init
# OR npm v7
npm x plasmo init
这时候如果你是新手,建议直接从 PlasmoHQ/examples 找一个模版看一下他的目录结构,然后找到自己想要的功能进行开发
比如我想基于vue,开发一个 popup 的界面。可以在示例中直接找到 examples/with-vue
还有各种技术栈(React,svelte,tailwindcss,nextjs…)
不止 popup 页,还有 background, devtool, options 页面都能在 examples 仓库找到相关的模版
Plasmo 有一个很方便的地方在于:我开发 popup 的页面,我只需要有一个叫 popup.(tsx | vue)
的文件,开发background,只需要有一个 background.ts
文件。
这些作为对应的入口文件我们只需要按命名规范写好(甚至可以写成 popup/index.vue
),剩下的 manifest.json
配置就交给 Plasmo
从安装脚手架到现在,我们都没见到 manifest.json
文件,更加说明了这些入口不需要我们显示声明
虽然没有 manifest.json
,但是该要写的配置还是得写的
比如我们开发一个针对 http://xxxx.com
网页的插件,首先得申请权限 host_permissions
这部分配置写在了 package.json
中的 "manifest"
下。包括申请权限,注入资源都在 "manifest"
中去配置。
// package.json
{
// ...
"manifest": {
"permissions":["declarativeNetRequest"], // 获取拦截网络请求的权限
// 页面注入静态资源
"web_accessible_resources": [
{
"resources": [
"inject.js"
],
// 针对全部界面注入
"matches": [
""
]
}
],
// 针对哪些页面生效
"host_permissions": [
"https://xxxx.com/*",
"http://xxxx.com/*"
]
}
// ...
}
有个例外就是 content.ts (注入到网页的那部分内容)
因为 content.ts 对应的配置是 MDN文档:manifest.json/content_scripts
正常的配置应该是这样的
"content_scripts": [
{
"matches": ["*://*.mozilla.org/*"],
"js": ["content.js"]
}
]
因为 content.ts 是动态入口,也就是说 content_scripts[0].js
的内容是框架去生成的,而不是我们自己手动填的
这也就造成了 content_scripts
的配置只能是写在 content.ts
这个页面中。这样 Plasmo 才能既知道入口路径,也知道对应的配置
以下示例代码来自: with-content-script/content.ts
// file - content.ts
import type { PlasmoContentScript } from "plasmo"
// 进行 content_scripts 的配置
export const config: PlasmoContentScript = {
matches: ["https://www.plasmo.com/*"]
}
window.addEventListener("load", () => {
console.log("content script loaded")
document.body.style.background = "pink"
})
// 运行后出来的配置可能就是
// "content_scripts": [
// {
// "matches": ["https://www.plasmo.com/*"],
// "js": ["content.[hash].js"]
// }
// ]
content.ts
? 如果我想一个插件针对不用的站点做不同的操作呢?好问题,去example找找模版就知道了 example/with-many-content-scripts。这里提供了多个 content.ts
的示例,这样就能针对不同页面注入不同的 content.ts
了
框架也提供了 自定义 manifest.json 的能力。更多的配置可以看 官方文档: plasmo customization 这部分
参考文档 https://docs.plasmo.com/framework-api/storage
@plasmohq/storage 是一个来自 plasmo 的实用程序库,它抽象了浏览器扩展可用的持久存储 API。当扩展存储 API 不可用时,它会回退到本地存储,允许在弹出窗口 - 选项 - 内容 - 背景之间进行状态同步。
官网还说了一句,如果使用了这个库,配置会自动把 storage
的权限加上
我觉得还是挺好的,这样依赖抹平了不同平台之间存储的差异,也做了保底方案
url:
data-text:~
data-base64:~
这部分标记符可以在这些文档中找到 content-scripts、content-scripts-ui、assets
用起来就类似这样的:
import cssText from "data-text:~/contents/plasmo-overlay.css"
import someCoolImage from "data-base64:~assets/some-cool-image.png"
import myJavascriptFile from "url:./path/to/my/file/something.js"
这部分更多的可能是为了相对路径,或者引入一些特殊的内容。比如 data-text:~
这个就很有用,我可以在 .css
文件中更好的编写我的内容,然后通过 data-text:~
把文件的内容以 text 引入,用于我注入到页面上
url:
这个也是为了获取这个文件在打包后所处的位置。
比方说我们按正常模式写文件,写完后可能要给 content.js 动态注入到页面去,这时候可以动态创建script标签,
src = chrome.runtime.getURL('xxx.js')
不过因为我们这个是进过了 Plasmo 打包的,有可能对应的资源被加上了hash值,这时候 url:
就是获取文件的路径了(类似 chrome.runtime.getURL(‘xxx.js’) 的功能了)
在示例仓库 examples/with-devtools/devtools.tsx 就有这么一段代码:
import fontPickerHTML from "url:./panels/font-picker/index.html"
import fontPropertiesHTML from "url:./panels/font-properties/index.html"
chrome.devtools.panels.create(
"Font Picker",
null,
// See: https://github.com/PlasmoHQ/plasmo/issues/106#issuecomment-1188539625
fontPickerHTML.split("/").pop()
)
可以自己打印一下 fontPropertiesHTML
变量,其实是一个网页的路径。(使用.split(“/”)是为了处理一个bug,issuse链接也在备注里了,可以看看了解了解)
文档链接在都贴出来了,更多的用法就自己去摸索了
不管是运行 npm run dev 还是 run build,都会生成一个 build/xxxx
目录。里面就是存放着可以运行的chrome插件代码
默认是 chrome-mv3-dev
代表开发 chrome 插件,v3 版本,dev环境
当然你也可以用 --target
指定是开发 firefox 版本/开发 mv2版本, –target-flag 毕竟都不推荐开发 mv2 的东西了。就不细说了
运行 npm run dev 后,把 build/chrome-mv3-dev
这个文件夹拖到浏览器安装插件的位置,就能看到了。不知道怎么操作的建议看下文档: loading-the-extension
build/chrome-mv3-dev 目录下也有 manifest.json 文件,也就是我们在 package.json 里面 + content.ts 的配置,所有的配置都汇总在这里了。想看配置有没有生效看这里就行
插件打包,打包为zip
pnpm build -- --zip
# OR
npm run build -- --zip
# OR
plasmo build --zip
打包到firefox
plasmo build --target=firefox-mv2 --zip
build --target=firefox
的作用体现在哪里?
说实话我也没发现,可能是为了多区分开一个目录,或者 firefox 没升级到 mv3 版本,又或者是同样的配置 firefox 有细微差别,Plasmo就可以自动处理掉
至于代码兼容性
在开发过程中,我们都是用 chrome
作为插件API,比如 chrome.runtime.sendMessage
chrome.xxx.xxx
。正常来说不用特别的适配,写的话也按 chrome
来写即可开头也提到,我因为不能看外网的chrome插件开发文档,好在国内还可以访问
MDN
,API这一块同步的还是比较快的,甚至有些页面有中文翻译了,平时查API可以到这里查
browser
开头,兼容性可以看对应文档下面的表格。(如果你用browser
,在开发过程是没有智能提示的,毕竟我们装的ts包是 @types/chrome
)。也会真的发生有兼容性问题,毕竟chrome更新一直都很快的
判断运行环境: /packages/shared-utils/src/env.ts
// env.ts 节选代码
export const isBrowser = typeof navigator !== 'undefined'
export const target: any = isBrowser
? window
: typeof global !== 'undefined'
? global
: {}
export const isChrome = typeof target.chrome !== 'undefined' && !!target.chrome.devtools
export const isFirefox = isBrowser && navigator.userAgent.indexOf('Firefox') > -1
export const isWindows = isBrowser && navigator.platform.indexOf('Win') === 0
export const isMac = isBrowser && navigator.platform === 'MacIntel'
export const isLinux = isBrowser && navigator.platform.indexOf('Linux') === 0
根据环境,使用不同的处理方式,比如网页快照
/packages/app-frontend/src/features/timeline/composable/screenshot.ts
在chrome中,能直接使用 chrome.tabs.captureVisibleTab
当然还有随处可见的这样的判断
提供了 @plasmohq/storage 抹平各个平台的存储api差异,还提供了快捷的方式然我们更新本地存储的内容
处理兼容性问题从来都不是一件容易的事情,搞不好开发人员都处理的很头大,所以更加别指望框架能自动处理。
总的看下来 --target
好像更多是用于发布到不同平台的时候有用,而不是帮我们处理不同浏览器的兼容问题(我的插件不发布到商店去,所以暂时找不到用途)
Plasmo 的介绍就到这里了。我也没开发什么出名的插件(很惭愧)都是处理公司需要的内容,所以可能还有很多好玩的功能没发掘到
Plasmo 还能一键发布到各个平台之类的功能,等着你们自己去探索了
一开始介绍的时候有提到版本变动有较小的,还有2个较大的。在我看来较小的变动可能只是改一下配置,不用影响太多业务逻辑代码就能运行的。
而较大的变动就影响挺大的
说一个场景,比如我们都很熟悉的浏览器拦截插件,或者其他的插件,下面都有角标。关键是这些角标是根据当前的域名记录的。
怎么做到的呢?依赖 popup 的页面记录吗?
popup 几乎不可能,因为在我开发过程中,popup 在每次打开的时候其实都会重新运行一遍。同一个站点如果打开2次popup.tsx对应的组件就会在执行2次
所以这部分的数据就得留给 background.ts
或者 content.ts
去做
为了搞懂这其中的技巧,我看了一下 猫抓
这个插件的代码
以下代码节选自 猫抓
插件
// js/popup.js
var BG = chrome.extension.getBackgroundPage();
var tabid;
chrome.windows.getCurrent(function(wnd) {
chrome.tabs.getSelected(wnd.id, function(tab) {
tabid = tab.id;
var id = "tabid" + tab.id;
ShowMedia(BG.mediaurls[id]);
});
});
// js/background.js
//初始化
if (typeof mediaurls === 'undefined') {
var mediaurls = new Array();
}
// ...
// 中间的代码用了 chrome.webRequest.onResponseStarted 监听请求
// 然后筛选出 .m3u8 和 分析出对应的 .ts 文件,感兴趣的自己在看看
// ...
//标签更新,清除该标签之前记录
chrome.tabs.onUpdated.addListener(function(tabId, changeInfo) {
if (changeInfo.status == "loading") //在载入之前清除之前记录
{
var id = "tabid" + tabId; //记录当前请求所属标签的id
if (mediaurls[id])
mediaurls[id] = [];
}
});
//标签关闭,清除该标签之前记录
chrome.tabs.onRemoved.addListener(function(tabId) {
var id = "tabid" + tabId; //记录当前请求所属标签的id
if (mediaurls[id])
delete mediaurls[id];
});
可以看到,在 popup.js 里面获取了一个BG
因为 MV2的background是有window对象的。所以 BG 可以理解为 background.html 的 window对象 var BG = chrome.extension.getBackgroundPage();
从 window 对象中获取 mediaurls 参数,获取对应tab要显示的角标数,然后给到 popup 显示
如果 background 变成了一个 Service Worker ,那就不存在 window
对象了
解决方案就是改用通信的方式,popup发起一个sendMessage。background来监听,并且进行回调给popup
整体的思路还是用 background 来存储和转发消息,background 收到的内容后存储到本地去。
只能感叹一句,V2 版本的拦截请求还是很好用的。
推荐一篇教程: 小茗同学: 【干货】Chrome插件(扩展)开发全攻略 这里面的的攻略很多都没过时,除了上面说的改动其他都很值得参考学习。我也是看这个入的门
可以看到教程的 8.6. webRequest 看看v2版本的拦截网络请求写法
之前是通过声明 webRequest
、 webRequestBlocking
等权限来进行网络请求的拦截。不过现在声明了也没用了 issues/1163
都要改为 declarativeNetRequest 来拦截
虽然新版的API也能拦截请求,修改head头之类的操作
但是,这些操作都没有回调!!(V2版本是有回调的,猫抓就是基于回调才抓的请求地址)
不过进过一通瞎找,找到另外一个文档(MDN还没更新 declarativeNetRequest 的内容)
onRuleMatchedDebug
这个方法有这么一段话
Fired when a rule is matched with a request. Only available for unpacked extensions with the declarativeNetRequestFeedback permission as this is intended to be used for debugging purposes only.
当规则与请求匹配时触发。 仅适用于具有 declarativeNetRequestFeedback 权限的解压扩展,因为这仅用于调试目的。
注意是 解压扩展,意思就是必须是解压的包/zip包,并且声明了这个权限才能用。
如果你的插件是想发布到应用市场,或者生成 .crx
后缀的插件包,一样是用不了 onRuleMatchedDebug
滴(累了)
虽然background不能直接监听返回的内容,不过 devtool 面板可以啊 devtools/network。但是如果你想用devtool面板的API话,你得打开F12才能用 (累了*2)
所以目前拦截回调的这一块还没有想出非常通用的方案,或许这就是chrome口中的安全,隐私…
如果想粗暴点解决的话其实可以把要拦截的源文件下载下来,然后手动添加一个 window.postMessage(xxx) 主动给 content.ts 发消息,然后 content.ts 在转发到后台去
background 部分就拦截网络请求,redirect 到插件下载的源文件那边去(其实就是针对性很强针对某个网页的某个js可以这么搞)
如果是想篡改某些js的内容,而且自己会本地开一个服务的话,用 redirect 真的是很方便的
既然都讲到拦截了,顺便讲讲 如何拦截网页发出的请求。原理就是用 content.js
注入js,修改 window.XMLHttpRequest
和 window.fetch
方法就能拦截到了
推荐直接学习: YGYOOO/ajax-interceptor 这里面的代码
唯一的问题可能就是 content.js 注入的速度没有页面发起请求的快,就会有几条漏网之鱼。
这篇文章主要还是想介绍下 Plasmo。个人感觉用下来还是挺好用的
至于chrome 插件要升级到 MV3 最严重的其实还是网络请求相关的。其余的应该都还好(最起码有解决方案)
讲了那么多其实没有讲到一些开发的技巧类东西,主要是一些需要注意的坑。所以汇总一下链接方便查找学习
文档类
chrome原汁原味文档:https://developer.chrome.com/docs/extensions/mv3/)
MDN文档插件开发文档:https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions
热心网友同步的原文文档(MDN找不到的时候就来这里看看):https://sunnyzhou-1024.github.io/chrome-extension-docs/extensions/api_index.html
入门教程推荐
插个题外话,如果你既没有魔法,又想看原汁原味文档(挺好的,很有追求)
可以上 github https://github.com/GoogleChrome/developer.chrome.com
把整个 developer.chrome.com 搞下来(下面的命令不用我细说了把)
# 安装依赖的
npm run ci
# dev 后 打开 http://localhost:8080/ 就可以看到
npm run dev
# 如果你想同步一份到自己服务器,就运行把
npm run production && npm start
插件开发介绍就到这了,如果你有好的插件记得也推荐给我