惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
S
SegmentFault 最新的问题
V
V2EX
阮一峰的网络日志
阮一峰的网络日志
C
Cisco Blogs
博客园 - 叶小钗
P
Privacy International News Feed
Jina AI
Jina AI
Apple Machine Learning Research
Apple Machine Learning Research
T
Threatpost
IT之家
IT之家
博客园 - 聂微东
Know Your Adversary
Know Your Adversary
Help Net Security
Help Net Security
罗磊的独立博客
I
Intezer
S
Schneier on Security
博客园_首页
C
CERT Recently Published Vulnerability Notes
雷峰网
雷峰网
Cisco Talos Blog
Cisco Talos Blog
宝玉的分享
宝玉的分享
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Webroot Blog
Webroot Blog
TaoSecurity Blog
TaoSecurity Blog
MyScale Blog
MyScale Blog
P
Privacy & Cybersecurity Law Blog
T
The Exploit Database - CXSecurity.com
PCI Perspectives
PCI Perspectives
Security Latest
Security Latest
H
Heimdal Security Blog
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
Y
Y Combinator Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Microsoft Security Blog
Microsoft Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
SecWiki News
SecWiki News
The GitHub Blog
The GitHub Blog
A
Arctic Wolf
A
About on SuperTechFans
aimingoo的专栏
aimingoo的专栏
T
Threat Research - Cisco Blogs
Engineering at Meta
Engineering at Meta
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

rxliuli blog

旅行 - 东福山岛 旅行 - 新加坡 - 越南 旅行 - 马来西亚 - 下 旅行 - 马来西亚 - 上 Browser Extension Dev - 08. 发布 Chrome Web Store Browser Extension Dev - 06. 按需注入脚本 Browser Extension Dev - 05. 存储和配置 Browser Extension Dev Extra - User Script 介绍 Browser Extension Dev - 04. Background Script Browser Extension Dev Extra - 如何找到锁定滚动的元素 Browser Extension Dev - 03. 注入 UI Browser Extension Dev - 02. 使用 WXT Browser Extension Dev - 01. 介绍基本概念 Chrome => Firefox 扩展移植的那些坑 发布 Safari 扩展到 iOS 应用商店 再游新疆 -- 自驾 实践: 使用 Hono 开发全栈应用 Web 流式写入文件 在构建时而非运行时编译 Markdown 在 Web 中解压大型 ZIP 并保持目录结构
Browser Extension Dev - 07. Popup UI
rxliuli · 2026-01-21 · via rxliuli blog

前言

在上一章 Browser Extension Dev - 06. Inject Script on Demand 中,我们介绍了按需为网页注入脚本执行自定义的功能,还实现了一个简单的复制网页主要内容为 Markdown 的扩展。在这一章中,我们将继续实现一个 Popup 弹窗,用于显示页面主要内容转换得到的 Markdown,并支持在复制之前进行预览和编辑。

首先,需要明确 Popup 是什么?
之前我们已经接触过 Content Script 注入网页的 UI 和 Options 配置页面。Popup 类似于 Options 页面,独立运行,但权限相比 Background/Options 更加受限。通常而言,它和 Content Script UI 的应用场景非常接近,都是显示一些当前网页相关的内容,但它也有一些独有的适用场景:

  • 安全与隔离,网站无法以任何方式主动访问 Popup UI,它们完全由浏览器的不同线程/进程进行隔离。Content Script 注入的任何 UI 都有可能被网页检测出来,这就是网页能够检测是否使用了广告拦截器的原因之一。
  • 不受普通网站影响,例如一个定时刷新的扩展,可以自动刷新当前页面,我们肯定不希望每次刷新网页之后都重新注入并显示操控面板。
  • 无需内存清理,注入 Content Script 很难完全清理内存,这在普通网页不会出现问题,但在 SPA 网页可能会导致问题,复杂的(换句话说,使用了很多 npm 包的)JavaScript 代码真的到处都是内存泄漏。而 Popup 在关闭后就彻底销毁了,下次会再次重建。
  • 可以在特权页面打开,例如 https://chromewebstore.google.com/,所有扩展的 Content Script UI 都会在这个网站禁用,但可以打开 popup 并且获得当前标签页的 URL,这在特定场景很有用,例如用于下载扩展 zip 文件的工具

Content Script UI 则有其他几个优势

  • 更大的 UI 区域:Popup UI 受限于面板宽度,无法制作全屏面板
  • 更容易与网站本身高度集成,例如需要添加符合网站外观的按钮时
  • 更容易控制和修改网站本身,例如希望拦截网络、监听并修改 DOM、或者拦截脚本的特定代码执行之类的 – Popup 可以结合 Background Script 注入脚本做到,但没有那么灵活

接下来,让我们接着之前的实现继续完善吧。

参考 Chrome 官方文档 https://developer.chrome.com/docs/extensions/develop/ui/add-popup

思考

现在面临一个问题:如何在 Popup 中获取页面的内容?
答案是无法直接获取,需要通过 Background Script 中转,大致流程如下:

Popup → Background → executeScript(inject.js) → 返回 markdown → Popup 显示

但是等等,scripting.executeScript 可以有返回值吗?当然可以,它支持同步和异步返回值,但返回值必须是可结构化克隆的。

参考 Chrome scripting API 关于 Promise 返回值的官方文档 https://developer.chrome.com/docs/extensions/reference/api/scripting#promises

实现

首先添加一个 popup 页面,在 entrypoints/popup 下添加 index.html 和 main.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- entrypoints/popup/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Popup</title>
<style>
html,
body,
#root {
margin: 0;
padding: 0;
width: 600px;
height: 800px;
font-size: 16px;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>
1
2
3
4
5
6
7
// entrypoints/popup/main.ts
const root = document.getElementById('root')!

root.innerHTML = `
<h1>Popup UI</h1>
<p>This is a placeholder for the popup UI.</p>
`

在浏览器中加载扩展之后,点击 action 可以看到弹窗出现了。

1769333824214.jpg

修改注入的脚本 Inject Script

在实现通信部分之前,需要修改一下之前注入的 Inject Script,不再复制 Markdown 到剪切板,而是使用 return 返回给调用者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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) {
return service.turndown(article.content)
// const markdown = service.turndown(article.content)
// await navigator.clipboard.writeText(`# ${article.title}\n\n${markdown}`)
// alert('Article copied as Markdown!')
} else {
return null
// alert('Failed to parse the article.')
}
})

下面开始实现 Popup 与 Background Script 的通信部分,由于 Chrome 原生的通信 API 使用起来非常痛苦,这里使用一个浅包装 @webext-core/messaging

安装依赖

1
pnpm i @webext-core/messaging

然后在 lib/messager.ts 中定义接口

1
2
3
4
5
6
// lib/messager.ts
import { defineExtensionMessaging } from '@webext-core/messaging'

export const messager = defineExtensionMessaging<{
getMarkdown: () => string | null
}>()

然后在 Background Script 定义实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
messager.onMessage('getMarkdown', async (ev) => {
const tabs = await browser.tabs.query({
active: true,
currentWindow: true,
})
if (!tabs[0].id) {
throw new Error('No active tab found')
}
const result = await browser.scripting.executeScript({
target: { tabId: tabs[0].id },
files: ['/inject.js'] as PublicPath[],
}) // 执行脚本并获取返回值
return result[0].result as string | null
})

最后在 Popup 中调用,出于简化考虑,这里直接使用 pre 渲染了 Markdown,我们将在下一步引入所见即所得的 Markdown 编辑器。

1
2
3
4
5
6
7
import { messager } from '@/lib/messager'

const root = document.getElementById('root')!
const md = await messager.sendMessage('getMarkdown')
const pre = document.createElement('pre')
pre.textContent = md as string
root.appendChild(pre)

1769333889436.jpg

添加 markdown 编辑器

由于并未使用 react,所以这里直接使用一个 vanilla JS 实现的 markdown 编辑器 easymde

还是先安装依赖。

1
pnpm i easymde

然后在 Popup 中使用它。

1
2
3
4
5
6
7
8
9
10
11
12
import 'easymde/dist/easymde.min.css'
import { messager } from '@/lib/messager'
import EasyMDE from 'easymde'

const root = document.getElementById('root')!
const textarea = document.createElement('textarea')
root.appendChild(textarea)
const md = await messager.sendMessage('getMarkdown')
new EasyMDE({
element: textarea,
initialValue: md as string,
})

现在就可以看到最终的效果了。

1769333846824.jpg

总结

在这一章,我们介绍了 Popup 的应用场景、Popup 与 Background Script 的通信、以及从网页获取数据的功能与实现。在下一章,我们终于要发布插件了,我将演示如何将插件发布到 Chrome Web Store,以便让其他人也能使用开发的扩展。

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

完整代码:https://github.com/rxliuli/browser-extension-dev-examples/tree/main/packages/07-popup-ui