前言
在上一章 Browser Extension Dev - 05. Storage and Configuration 中,我们介绍了为扩展添加设置页面并使用 Storage API 保存和读取配置的功能。在这一章,我们将介绍按需注入脚本。这种方式完全不会拖慢网站运行速度,同时在 Chrome Web Store 安装时不会有任何安全警告提示。接下来我们将实现一个扩展:点击图标即可将网页主要内容复制为 Markdown。
思考
我们之前已经接触过 Content Script 注入和 Background Script 监听扩展图标点击。虽然尚未介绍,但两者可以进行消息通信。
有了上面的背景知识,你可能会想要这样做:
- 为所有网页注入 Content Script 并监听后台消息
- 点击扩展图标时在 Background Script 通知 Content Script
- 在 Content Script 中执行具体逻辑
这种方法有几个主要缺点:
- 默认为所有网页注入 Content Script 不仅会拖慢网站速度,还可能导致风险,因为注入过程对用户完全无感知,后续扩展更新可能引入漏洞
- 权限要求极高,安装扩展时会提示安全警告,说明这个扩展要求读取和修改所有用户访问的网站

而对于需要明确动作触发的场景,其实有更简单的实现方式:
- 点击扩展图标时在 Background Script 中向当前网页注入一段脚本
- 在脚本中执行具体逻辑
这样,需要的权限就从 ['<all_urls>'] 变成了 ['activeTab', 'scripting'],虽然权限数量变多了,但风险反而更低,必须由用户触发才能执行代码,所以安装扩展时不会有任何警告。例如:

这里涉及到的关键 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, }, })
|
然后再试一次,可以看到脚本确实被注入并正确执行了。

⚠️ 局限性:如果你希望注入的脚本能持久化(刷新或重新进入页面后仍自动运行),这是行不通的,仍然需要正确声明 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.') } })
|


总结
在本章中,我们实现了一个按需注入脚本的扩展,它不会影响网页正常运行,只在用户触发时才执行代码,真正做到即插即用。在下一篇,我们将继续完善这个扩展,使用 Popup 弹窗直接预览和编辑从当前页面复制的 Markdown。
如果有任何问题,欢迎加入 Discord 群组讨论。
https://discord.gg/VxbAqE7gj2
完整代码:https://github.com/rxliuli/browser-extension-dev-examples/tree/main/packages/06-inject-script-on-demand