Chrome插件迁移至Manifest V3
Chrome插件商店即将全面停止对 manifest v2版本清单的支持,也就促使开发者们将逐渐将插件向MV3迁移。但迁移中会遇到的一些问题,比如清单格式的改变,由数组变成对象,或者由对象变成数组,不过这些问题也只是算格式上的不兼容而已,很好修改。而一些大麻烦比如MV3中CSP的策略更加严格,不仅限制了脚本注入,更是完全禁止使用
让注入的脚本(unsafe-inline)重新工作
我在content.js中生成了一个script标签,并手动将其注入到页面中,如下:
// content.js
const content = "alert('injected script')"
const script = document.createElement('script')
script.innerHTML += content
document.body.appendChild(script)
作为结果,你会发现:

对于这种情况,我们只需要将上面的js代码单独写到一个文件中,并在清单中声明此文件,方便Google进行代码审查,这样就满足MV3的要求了
// inject.js
alert('injected script')
// content.js
const script = document.createElement('script')
script.setAttribute('src', chrome.runtime.getURL('inject.js'))
document.body.appendChild(script)
// manifest.json
{
"manifest_version": 3,
...
"web_accessible_resources": [
{
"resources": ["inject.js"],
"matches": ["http://a.com/*", "https://a.com/*"]
}
]
...
}
使用沙盒(sandbox)让Vue重新工作
由于Vue使用了
使用沙盒的好处是很明显的,比如原先使用Vue写的页面不需要修改就可继续使用,减少迁移的工作量。但是坏处也很明显,比如说在沙盒中无法调用插件相关的api,但其实这个问题也非常好解决,我们只需要在原来的插件环境和隔离出的沙盒环境中,添加一个数据交换的中间层就可以了。

<!-- sandbox.html -->
...
<head>
...
<script src="scripts/vue-dev.js"></script>
</head>
<body>
<div id="app">
...
</div>
<script src="scripts/sandbox.js"></script>
</body>
...
<!-- warpper.html -->
...
<body>
<iframe id="sandbox" src="sandbox.html" frameborder="0"> </iframe>
<script src="scripts/wrapper.js"></script>
</body>
// manifest.json
{
"manifest_version": 3,
...
"sandbox": {
"pages": [
"sandbox.html"
]
}
...
}
接下来,我们在需要添加一个中间层,让
沙盒和父级通信
// sandbox.js
// 接收数据
window.addEventListener('message', function (event) {
const data = event.data
console.log(data)
})
// 发送数据
window.top.postMessage({ ... }, '*')
父级和沙盒通信
// wrapper.js
//接收数据
window.addEventListener('message', function (e) {
const data = e.data
console.log(data)
}
// 发送数据
const f = document.querySelector('#sandbox') as HTMLIFrameElement
f.contentWindow.postMessage({ ... }, '*')
由于发送和接收数据是异步进行的,所以我们要对上面的方法按照发布-订阅的设计模式进行封装,以便让业务代码不需要大幅度修改即可使用
譬如在MV2中的代码:
// sandbox.js
async mounted() {
const manifestData = chrome.runtime.getManifest()
...
}
对象
// sandbox.js
class MessageHub {
static map = {} as { [key: number]: (data?: MessageHubData) => void }
static seed = 0
static getMsgId() {
if (this.seed > 1000 * 1000) {
this.seed = 0
}
this.seed++
return this.seed
}
static sendMessage(id: number, message: { [key: string]: any }, fn?: (data?: MessageHubData) => void) {
if (id in this.map) {
throw Error('MessageHub id conflict')
}
this.map[id] = fn
window.top.postMessage({ ...message, id }, '*')
}
static {
window.addEventListener('message', function (event) {
const data = event.data as MessageHubData
const fn = MessageHub.map[data.id]
if (fn) {
fn(data)
}
delete MessageHub.map[data.id]
})
}
}
function getManifestData() {
return new Promise((resolve, reject) => {
MessageHub.sendMessage(MessageHub.getMsgId(), { fn: 'getManifestData' }, (data) => {
resolve(data.data)
})
})
}
new Vue({
...
async mounted() {
const manifestData = await getManifestData() as chrome.runtime.Manifest
...
}
})
// warpper.js
const f = document.querySelector('#sandbox') as HTMLIFrameElement
window.addEventListener('message', async function (e) {
const data = e.data
if (!data.fn) return
switch (data.fn) {
case 'getManifestData':
f.contentWindow.postMessage({ data: chrome.runtime.getManifest(), id: data.id }, '*')
break
...
default:
f.contentWindow.postMessage({ id: data.id }, '*')
break
}
})
如此,利用一个订阅–发布的中间件,我们不需要对代码进行大幅度修改,即可将插件迁移至MV3