chrome V3插件入门到放弃,Plasmo不完全使用指南

没有插件的浏览器是没有灵魂的。今天来近距离感受一下chrome的灵魂

开始之前了解一下灵魂chrome插件的版本。

chrome V3插件入门到放弃,Plasmo不完全使用指南

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」

比较小的变动

Host Permissions

在V2中,有两种方法为你的api或任何主机获得权限,要么在 permissions 数组或 optional_permissions 数组。

{
  "permissions": ["https://xxxx.com/*"]
}

在V3中,所有主机权限现在都单独存在一个新数组中,该数组的键为 host_permissions。主机权限不再与其他权限一起添加。

{
  "host_permissions": ["https://xxx.com/*"]
}

Actions

在V2中,分为 browser_actionpage_action

感兴趣的可以在MDN插件开发文档里面看一看。

在V3中,都统一合并为 action 。参考文档:API-action

content_security_policy 变动

在V2的manifest.json 的 content_security_policy 配置是一个字符串类型。升级到 V3 后变成了一个对象类型。详细的变更看文档会更加清晰:content_security_policy 参考文档

web_accessible_resources

详细变更参考 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下载到插件包中,改改资源引入就好~

Promises

V3 现在原生支持 Promise。许多常用 API 现在都支出,最终所有合适的 API 都会支持 Promise。

如果使用 callback,就不会返回 Promise,优先执行 callback。

较大的变动

将 Background Scripts 改造成 Service Workers

在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"] },

网络拦截,使用新的declarativeNetRequest来修改请求

这个变动非常的大,在本文后面详细讲这一块的内容。而且 MDN 文档还没更新 declarativeNetRequest 相关的内容,等下要找个新文档来看

弃用的API

在查看 MDN 文档时会有相关的提示

chrome V3插件入门到放弃,Plasmo不完全使用指南

使用chrome官方文档时的提示

chrome V3插件入门到放弃,Plasmo不完全使用指南

查阅官方文档时,那些标签也能帮助到我们。
Promise 标签:支持 Promise
<=MV2 标签:该API仅在V2前支持
>=MV3标签 :该API在V3后支持
Deprecated 标签:已废弃的 API

使用 Plasmo 开发

都2022年了,或许每次开发一些新东西的时候你都会在想:

好麻烦啊,上github找找有没有现成的。
好像都还可以,收藏吃灰,下次在开发把

不要下次了!就这次把,强烈推荐 Plasmo

官网: plasmo
github: PlasmoHQ/plasmo

官方自己的介绍(说的非常的朴素,我一个路人都觉得这功能写少了)
chrome V3插件入门到放弃,Plasmo不完全使用指南

作为一个过来人的感受,我只能说用 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 仓库找到相关的模版

chrome V3插件入门到放弃,Plasmo不完全使用指南

Plasmo 有一个很方便的地方在于:我开发 popup 的页面,我只需要有一个叫 popup.(tsx | vue) 的文件,开发background,只需要有一个 background.ts 文件。

这些作为对应的入口文件我们只需要按命名规范写好(甚至可以写成 popup/index.vue ),剩下的 manifest.json 配置就交给 Plasmo

从安装脚手架到现在,我们都没见到 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": [
					"<all_urls>"
				]
			}
		],
		// 针对哪些页面生效
		"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"]
//   }
// ]

好问题,去example找找模版就知道了 example/with-many-content-scripts。这里提供了多个 content.ts 的示例,这样就能针对不同页面注入不同的 content.ts

框架也提供了 自定义 manifest.json 的能力。更多的配置可以看 官方文档: plasmo customization 这部分

Plasmo 提供的一些库和功能

  1. @plasmohq/storage

参考文档 https://docs.plasmo.com/framework-api/storage

@plasmohq/storage 是一个来自 plasmo 的实用程序库,它抽象了浏览器扩展可用的持久存储 API。当扩展存储 API 不可用时,它会回退到本地存储,允许在弹出窗口 - 选项 - 内容 - 背景之间进行状态同步。

官网还说了一句,如果使用了这个库,配置会自动把 storage 的权限加上

我觉得还是挺好的,这样依赖抹平了不同平台之间存储的差异,也做了保底方案

  1. 一些比较特殊的标记符 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插件开发文档,好在国内还可以访问 MDN ,API这一块同步的还是比较快的,甚至有些页面有中文翻译了,平时查API可以到这里查

也会真的发生有兼容性问题,毕竟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 V3插件入门到放弃,Plasmo不完全使用指南

在chrome中,能直接使用 chrome.tabs.captureVisibleTab

当然还有随处可见的这样的判断

chrome V3插件入门到放弃,Plasmo不完全使用指南

提供了 @plasmohq/storage 抹平各个平台的存储api差异,还提供了快捷的方式然我们更新本地存储的内容

处理兼容性问题从来都不是一件容易的事情,搞不好开发人员都处理的很头大,所以更加别指望框架能自动处理。

总的看下来 --target 好像更多是用于发布到不同平台的时候有用,而不是帮我们处理不同浏览器的兼容问题(我的插件不发布到商店去,所以暂时找不到用途)

Plasmo 的介绍就到这里了。我也没开发什么出名的插件(很惭愧)都是处理公司需要的内容,所以可能还有很多好玩的功能没发掘到

Plasmo 还能一键发布到各个平台之类的功能,等着你们自己去探索了

如何应对 MV3 版本中“较大的变动”

一开始介绍的时候有提到版本变动有较小的,还有2个较大的。在我看来较小的变动可能只是改一下配置,不用影响太多业务逻辑代码就能运行的。

而较大的变动就影响挺大的

background 变动的影响

说一个场景,比如我们都很熟悉的浏览器拦截插件,或者其他的插件,下面都有角标。关键是这些角标是根据当前的域名记录的。

chrome V3插件入门到放弃,Plasmo不完全使用指南

怎么做到的呢?依赖 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 收到的内容后存储到本地去。

【非常严重】网络请求拦截的变动 declarativeNetRequest

只能感叹一句,V2 版本的拦截请求还是很好用的。

推荐一篇教程: 小茗同学: 【干货】Chrome插件(扩展)开发全攻略 这里面的的攻略很多都没过时,除了上面说的改动其他都很值得参考学习。我也是看这个入的门

可以看到教程的 8.6. webRequest 看看v2版本的拦截网络请求写法

之前是通过声明 webRequestwebRequestBlocking 等权限来进行网络请求的拦截。不过现在声明了也没用了 issues/1163

都要改为 declarativeNetRequest 来拦截

虽然新版的API也能拦截请求,修改head头之类的操作
但是,这些操作都没有回调!!(V2版本是有回调的,猫抓就是基于回调才抓的请求地址)

不过进过一通瞎找,找到另外一个文档(MDN还没更新 declarativeNetRequest 的内容)

onRuleMatchedDebug

chrome V3插件入门到放弃,Plasmo不完全使用指南

这个方法有这么一段话

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.XMLHttpRequestwindow.fetch 方法就能拦截到了

推荐直接学习: YGYOOO/ajax-interceptor 这里面的代码

唯一的问题可能就是 content.js 注入的速度没有页面发起请求的快,就会有几条漏网之鱼。

最后

这篇文章主要还是想介绍下 Plasmo。个人感觉用下来还是挺好用的

至于chrome 插件要升级到 MV3 最严重的其实还是网络请求相关的。其余的应该都还好(最起码有解决方案)

讲了那么多其实没有讲到一些开发的技巧类东西,主要是一些需要注意的坑。所以汇总一下链接方便查找学习

文档类

入门教程推荐

插个题外话,如果你既没有魔法,又想看原汁原味文档(挺好的,很有追求)

可以上 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

插件开发介绍就到这了,如果你有好的插件记得也推荐给我

发表回复