前言

在上一章 Browser Extension Dev - 05. Storage and Configuration 中,我们介绍了为扩展添加设置页面并使用 Storage API 保存和读取配置的功能。在这一章,我们将介绍按需注入脚本。这种方式完全不会拖慢网站运行速度,同时在 Chrome Web Store 安装时不会有任何安全警告提示。接下来我们将实现一个扩展:点击图标即可将网页主要内容复制为 Markdown。

思考

我们之前已经接触过 Content Script 注入和 Background Script 监听扩展图标点击。虽然尚未介绍,但两者可以进行消息通信。

有了上面的背景知识,你可能会想要这样做:

  1. 为所有网页注入 Content Script 并监听后台消息
  2. 点击扩展图标时在 Background Script 通知 Content Script
  3. 在 Content Script 中执行具体逻辑

这种方法有几个主要缺点:

  1. 默认为所有网页注入 Content Script 不仅会拖慢网站速度,还可能导致风险,因为注入过程对用户完全无感知,后续扩展更新可能引入漏洞
  2. 权限要求极高,安装扩展时会提示安全警告,说明这个扩展要求读取和修改所有用户访问的网站

1768995880017.jpg

而对于需要明确动作触发的场景,其实有更简单的实现方式:

  1. 点击扩展图标时在 Background Script 中向当前网页注入一段脚本
  2. 在脚本中执行具体逻辑

这样,需要的权限就从 ['<all_urls>'] 变成了 ['activeTab', 'scripting'],虽然权限数量变多了,但风险反而更低,必须由用户触发才能执行代码,所以安装扩展时不会有任何警告。例如:

1768996707257.jpg

这里涉及到的关键 API 是 scripting.executeScript,顾名思义,用于执行自定义脚本。

参考 Chrome 官方 activeTab 指南:https://developer.chrome.com/docs/extensions/develop/concepts/activeTab

实现

在扩展图标点击时注入脚本

接下来在后台脚本中实现监听和注入部分。
首先还是更新 wxt.config.ts,添加所需权限以及空的 action 字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineConfig } from 'wxt'

export default defineConfig({
manifestVersion: 3,
manifest: {
name: 'Copy As Markdown',
description: 'Copy page content as Markdown',
permissions: ['activeTab', 'scripting'],
action: {},
},
webExt: {
disabled: true,
},
})

然后添加一个用于测试的注入脚本。与 Content Script 不同,这类脚本在 WXT 中需要使用 defineUnlistedScript 声明。

1
2
3
4
// entrypoints/inject.ts
export default defineUnlistedScript(() => {
alert('Injected script executed!')
})

然后在 background 中监听并注入它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { PublicPath } from 'wxt/browser'

export default defineBackground(() => {
browser.action.onClicked.addListener(async (tab) => {
if (tab.id) {
await browser.scripting.executeScript({
target: { tabId: tab.id },
// 这里的 /inject.js 是指构建之后的文件,如果你使用 pnpm build,就可以在 .output/chrome-mv3 中看到 inject.js 了
// 注:一开始这里可能会报 ts 类型错误,pnpm dev/build 启动之后 wxt 会正确扫描 entrypoints 并生成类型定义
files: ['/inject.js'] as PublicPath[],
})
}
})
})

除了 files 参数,还可以通过 func/args 直接传递函数和参数,适用于简单场景。参考:https://developer.chrome.com/docs/extensions/reference/api/scripting#type-ScriptInjection

当我们打开 google.com 然后点击扩展图标时,却发现没有反应。查看扩展详情页面,可以看到一个错误。

1
Uncaught (in promise) Error: Could not load file: 'inject.js'.

和之前 Browser Extension Dev - 03. 注入 UI 时一样,需要在 manifest 中增加 web_accessible_resources 配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { defineConfig } from 'wxt'

export default defineConfig({
manifestVersion: 3,
manifest: {
name: 'Copy As Markdown',
description: 'Copy page content as Markdown',
permissions: ['activeTab', 'scripting'],
action: {},
web_accessible_resources: [
{
resources: ['inject.js'],
matches: ['<all_urls>'],
},
],
},
webExt: {
disabled: true,
},
})

然后再试一次,可以看到脚本确实被注入并正确执行了。

1768998702041.jpg

⚠️ 局限性:如果你希望注入的脚本能持久化(刷新或重新进入页面后仍自动运行),这是行不通的,仍然需要正确声明 host_permissions 权限来持久化注入 Content Script,即使使用 scripting API 仍然受到权限模型的限制。

在注入脚本中实现功能

接下来实现读取网页主要内容,转换为 Markdown,然后复制到剪贴板的功能。借助 npm 生态,实现起来非常简单。

首先安装需要的依赖

1
2
pnpm i @mozilla/readability turndown
pnpm i -D @types/turndown

然后编写少量胶水代码即可完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Readability } from '@mozilla/readability'
import TurndownService from 'turndown'

export default defineUnlistedScript(async () => {
const service = new TurndownService({
headingStyle: 'atx',
codeBlockStyle: 'fenced',
})
const reader = new Readability(document.cloneNode(true) as Document) // 深度复制避免影响到原网页
const article = reader.parse() // 解析主要内容
if (article && article.title && article.content) {
const markdown = service.turndown(article.content) // 转换 HTML 为 Markdown
await navigator.clipboard.writeText(`# ${article.title}\n\n${markdown}`) // 复制
alert('Article copied as Markdown!')
} else {
alert('Failed to parse the article.')
}
})

1768999375770.jpg
1769003419253.jpg

总结

在本章中,我们实现了一个按需注入脚本的扩展,它不会影响网页正常运行,只在用户触发时才执行代码,真正做到即插即用。在下一篇,我们将继续完善这个扩展,使用 Popup 弹窗直接预览和编辑从当前页面复制的 Markdown。

如果有任何问题,欢迎加入 Discord 群组讨论。
https://discord.gg/VxbAqE7gj2

完整代码:https://github.com/rxliuli/browser-extension-dev-examples/tree/main/packages/06-inject-script-on-demand