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

推荐订阅源

Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
月光博客
月光博客
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
大猫的无限游戏
大猫的无限游戏
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 聂微东
Apple Machine Learning Research
Apple Machine Learning Research
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
雷峰网
雷峰网
小众软件
小众软件
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 叶小钗
美团技术团队
宝玉的分享
宝玉的分享
Hugging Face - Blog
Hugging Face - Blog
阮一峰的网络日志
阮一峰的网络日志
A
About on SuperTechFans
Jina AI
Jina AI
D
Docker
Last Week in AI
Last Week in AI
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
Microsoft Azure Blog
Microsoft Azure Blog

rxliuli blog

App Store Connect 发布事故回顾 Safari 扩展 Popup 中的滚动迟缓问题 Safari 云签名在 GitHub Actions 中的两个陷阱 使用 GitHub Actions 全自动发布 Safari 扩展 旅行 - 东福山岛 旅行 - 新加坡 - 越南 旅行 - 马来西亚 - 下 旅行 - 马来西亚 - 上 Browser Extension Dev - 08. 发布 Chrome Web Store Browser Extension Dev - 07. Popup UI Browser Extension Dev - 06. 按需注入脚本 Browser Extension Dev - 05. 存储和配置 Browser Extension Dev Extra - User 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 - 04. Background Script
rxliuli · 2026-01-14 · via rxliuli blog

前言

在上一章 Browser Extension Dev - 03. 注入 UI 中,我介绍了如何向网页中注入自定义的 UI,同时还了解了如何使用 Shadow DOM、Tailwind CSS 和使用 npm 包。在本章,我将介绍 Background Script,这是扩展的核心元素之一。

首先,什么是 Background Script?

顾名思义,这是扩展可以在后台运行的脚本,与注入到网页的脚本有所不同,它有几个显著的特点:

  1. 可以访问所有扩展 API,在扩展的其他部分,例如 Content Script,可以访问的扩展 API 极其受限,例如无法访问 tabs API 来获取当前浏览器所有打开的标签页。
  2. 全局唯一,对于 Content Script 而言,在多个标签页中可能会被注入多次,但 Background Script 始终保持唯一,它不会同时存在多个。
  3. 按需启动,在 Manifest V3 之后,Background Script 更改为基于事件的模型,也就是说,没有事件传入时(例如,扩展可以监听新标签页的打开事件),它会自动休眠节省资源
  4. 无法使用 DOM API,这点不太明显,虽然 Background Script 确实在浏览器环境,可以使用有限的 Web API,但无法访问 DOM,尽管确实有几个替代选项(jsdom/Offscreen)
  5. 可以和扩展其他部分通信,Background Script 能与 Content Script、Popup Page 等部分通信,但其他部分之间却不能直接通信,所以需要 Background Script 中转

参考:
Background 简介:https://developer.chrome.com/docs/extensions/develop/concepts/service-workers(Manifest V3 后 Chrome 官方改名为 extension service workers,但通常还是习惯称呼为 Background Script)
Manifest V3 简介:https://developer.chrome.com/docs/extensions/develop/migrate/what-is-mv3

上面就是 Background Script 的几个关键点,接下来我将实现一个自动休眠不活跃标签页的扩展来演示它,下面是将会涉及到的 Tabs API,参考 https://developer.chrome.com/docs/extensions/reference/api/tabs

思考

首先,我们该如何定义不活跃的标签页?

从简单的角度来说,一个长时间没有访问的标签页就是不活跃的,例如最后一次访问标签页还是 30 分钟之前,那么应该可以认为是不活跃的了。如何知道标签页的最后访问时间呢?这就需要监听标签页相关的事件了,下面是涉及到的三个基本事件:

onCreated => 将标签页信息添加到扩展记录中
onRemoved => 从扩展记录的标签页列表中移除
onActivated => 更新标签页的最后访问时间

在长时间不活跃之后,我们可以自动冻结它,浏览器允许在不关闭标签页的情况下自动从内存中驱逐,再次访问时会自动重新加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Extension starts


Initialize existing tabs


┌─────────────────────────────────────────────────┐
│ Wait for events │◄──────┐
└─────────────────────────────────────────────────┘ │
│ │
├─── onCreated ──► Record new tab time ───────────►│
│ │
├─── onRemoved ──► Remove tab record ─────────────►│
│ │
└─── onActivated ──► Update access time │
│ │
▼ │
Check all tabs │
│ │
▼ │
Over 30 minutes? │
│ │ │
Yes │ │ No │
▼ └──────────────────►│
Freeze tab │
│ │
└─────────────────────────────┘

注意:Tabs API 本身提供了 lastAccessed 字段查看标签页的最后访问时间,但这个字段在 Safari 浏览器并不支持,参考 https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/Tab#browser_compatibility

实现

由于在之前的章节中已经说明过如何初始化扩展,这里不再赘述初始化过程。

1
2
# init project
pnpm dlx wxt@latest init 04-background-script --template vanilla --pm pnpm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// wxt.config.ts
import { defineConfig } from 'wxt'

export default defineConfig({
manifestVersion: 3,
manifest: {
name: 'Auto Sleep Tabs',
description:
'Automatically puts inactive tabs to sleep to save memory and CPU.',
},
webExt: {
disabled: true,
},
})

监听标签页事件

首先打开 entrypoints/background.ts,可以看到 WXT 初始化的代码。

1
2
3
export default defineBackground(() => {
console.log('Hello background!', { id: browser.runtime.id })
})

让我们在函数中注册我们的监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default defineBackground(() => {
console.log('Hello background!', { id: browser.runtime.id })

const lastAccessed = new Map<number, number>()

browser.tabs.onCreated.addListener((tab) => {
if (!tab.id) {
return
}
lastAccessed.set(tab.id, Date.now())
})
browser.tabs.onRemoved.addListener((tabId) => {
lastAccessed.delete(tabId)
})
browser.tabs.onActivated.addListener((activeInfo) => {
lastAccessed.set(activeInfo.tabId, Date.now())
console.log('Tab activated:', activeInfo.tabId)
})
})

现在,使用 pnpm dev 启动开发模式,打开 chrome://extensions/,加载已解压的扩展并选择 .output/chrome-mv3-dev 目录,然后点击扩展的 service worker 链接打开 Background Script 的 DevTools Console 开始调试。

1768548027488.jpg
1768548056610.jpg

当我们添加一个新标签页时可以看到 Tab activated: 1207047510 相关的日志。

1768548143569.jpg

识别不活跃的标签页并自动冻结

然后我们需要在 onActivated 事件中检查已记录的标签页中是否有长时间未访问的标签页,如果发现就自动冻结它。在此之前,需要更新 wxt.config.ts 添加 tabs 权限,这是使用 browser.tabs.query API 所必须的。

1
2
3
4
5
6
7
8
9
import { defineConfig } from 'wxt'

export default defineConfig({
manifest: {
// other config...
permissions: ['tabs'],
},
// other config...
})

你可能注意到,下面查找标签页时包含了很多过滤条件,下面将会一一说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
browser.tabs.onActivated.addListener((activeInfo) => {
lastAccessed.set(activeInfo.tabId, Date.now())
console.log('Tab activated:', activeInfo.tabId)
autoDiscardTabs()
})
async function autoDiscardTabs() {
const Timeout = 30 * 60 * 1000 // 30 minutes
const tabs = (await browser.tabs.query({})).filter(
(tab) =>
tab.id && // 只查找包含 id 的普通标签页,某些特殊标签页可能不包含 id,例如浏览器调试窗口之类的
!tab.pinned && // 如果是固定标签页,则忽略
!tab.active && // 如果标签页还活跃,也就是说,一直呆在一个标签页里
!tab.audible && // 如果正在播放音视频,则忽略
!tab.frozen && // 如果已经被 Chrome 内置机制冻结了,则忽略
!tab.discarded && // 如果已经被手动冻结了,则忽略
lastAccessed.has(tab.id) && // 如果没有记录过这个标签页,则忽略
Date.now() - lastAccessed.get(tab.id)! > Timeout, // 如果最后访问时间距离现在已经超过 30 分钟,则认为满足条件
)
for (const tab of tabs) {
// 注意:discard 可能会失败,例如标签页正在被使用或已被关闭
await browser.tabs.discard(tab.id!)
console.log('Tab auto-discarded:', tab.id, tab.title)
}
}

将 Timeout 调整为 1ms,就可以方便的进行测试了。在切换标签页再切换回来之后,就能看到标签页自动刷新了,这意味着自动冻结功能确实生效了。

1768549353576.jpg

总结

现在,我们实现了基本的标签页自动冻结功能,这个实现非常粗糙,还有很多问题没有处理,如果感兴趣,你可以自行尝试解决下面几个问题:

  1. 如何让用户手动配置自动冻结时间,避免默认值不符合用户需求
  2. 如何解决用户长时间未使用浏览器,不触发任何事件导致无法休眠的问题
  3. 如何处理扩展启动时已存在的标签页(提示:browser.runtime.onStartup

在下一章,我们将介绍配置相关的 API 和 options 页面来解决配置问题。

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

完整代码:https://github.com/rxliuli/browser-extension-dev-examples/tree/main/packages/04-background-script