你是否曾经对某个网页的功能感到不满?比如 Google 搜索页面上那个显眼的 AI Mode 按钮:

1767430067384.jpg

通过浏览器扩展,你可以让它彻底消失:

1767430033953.jpg

什么是浏览器扩展?

一般而言,浏览器扩展是一种修改用户浏览网页的方式,它赋予了网站使用者而不是开发者更多的权限。让有能力的用户可以自定义它们的网页浏览体验,现代浏览器提供了极其丰富的扩展 API,甚至能实现一些看起来需要 app 才能做到的事情,但它的基本出发点是让用户可以控制正在浏览的网页。

一些例子

  • 隐藏 Google AI 相关功能
  • 根据规则自定义网页重定向
  • 解除网页上的复制粘贴限制
  • 在浏览器后台运行定时任务

基本结构

1
2
3
4
5
6
manifest.json # 扩展入口文件
icon/*.png # 扩展图标
content-scripts/content.js # 注入到网页的脚本,可选
background.js # 后台脚本,可选
options.html # 配置页面,可选
popup.html # 点击扩展 icon 的弹出窗口,可选

创建 Manifest

现在,我们来创建一个基本的扩展,不使用任何包管理器、TypeScript、或者 Web 框架,只使用基本的 JavaScript 完成。在下一章节中,将会使用现代开发工具链(WXT)重写它,这里只是让我们对扩展的实际结构有个了解。作为例子,我们将会创建一个扩展来隐藏 google.com 中那个 AI Mode 按钮。

1767430067384.jpg

首先,创建一个 manifest.json,里面是一些基本信息,manifest_version 是一个固定值,代表使用扩展 API 的第三个主要版本。

1
2
3
4
5
6
{
"manifest_version": 3,
"name": "Hide AI Mode on Google Search",
"version": "0.0.1",
"description": "Hide the AI Mode button on Google Search pages."
}

实现 Content Script

然后我们需要创建一个 content-scripts/content.js 脚本(这个路径不是固定的,只是一般做法),里面实现在网页加载时自动隐藏 AI Mode 按钮。
基本思路也很简单

  1. 找到 google.com 中的 AI Mode 按钮的 CSS selector
  2. 在 google.com 加载时注入一个 js 脚本,自动隐藏它

分析获得 CSS selector

打开 google.com,然后打开 DevTools > Elements 可以看到 AI Mode 是一个 button 元素,但它看起来并没有可以作为稳定 CSS selector 的东西。

1767430093347.jpg

对于现代网站而言,class 通常被构建工具压缩的连亲妈都不认识了,所以需要一些跳脱的方法,例如 button 中包含的 SVG 似乎很适合作为一个选择器(网站上的 icon 改动频率并不高)。这样,借助 :has 子选择器和属性选择器,我们就可以组合出一个稳定的 selector 了。

1767430118046.jpg

1
button:has([d="M15.65 11.58c.18-.5.27-1.03.31-1.58h-2c-.1 1.03-.51 1.93-1.27 2.69-.88.87-1.94 1.31-3.19 1.31C7.03 14 5 12.07 5 9.5 5 7.03 6.93 5 9.5 5c.46 0 .89.08 1.3.2l1.56-1.56C11.5 3.22 10.55 3 9.5 3 5.85 3 3 5.85 3 9.5S5.85 16 9.5 16c.56 0 2.26-.06 3.8-1.3l6.3 6.3 1.4-1.4-6.3-6.3c.4-.5.72-1.08.95-1.72z"])

:has 指的是如果元素包含指定 selector 的子元素,就匹配它。对于这个 selector 而言,这意味着只有包含特定 AI Mode svg 图标的 button 按钮才会被匹配到。

实现 content script

接下来创建 content.js 脚本,由于我们只想隐藏这个按钮,最简单的方法就是注入一个 css 样式,例如

1
2
3
4
const style = document.createElement('style')
style.textContent =
'button:has([d="M15.65 11.58c.18-.5.27-1.03.31-1.58h-2c-.1 1.03-.51 1.93-1.27 2.69-.88.87-1.94 1.31-3.19 1.31C7.03 14 5 12.07 5 9.5 5 7.03 6.93 5 9.5 5c.46 0 .89.08 1.3.2l1.56-1.56C11.5 3.22 10.55 3 9.5 3 5.85 3 3 5.85 3 9.5S5.85 16 9.5 16c.56 0 2.26-.06 3.8-1.3l6.3 6.3 1.4-1.4-6.3-6.3c.4-.5.72-1.08.95-1.72z"]) { display: none; }'
document.head.appendChild(style)

在 Manifest 中声明

接下来,我们还需要在 manifest.json 中声明这个内容脚本,告诉浏览器要在哪些网站注入,这里我们只需要在 google.com 注入,所以需要修改为

1
2
3
4
5
6
7
8
9
{
// before config...
"content_scripts": [
{
"matches": ["https://www.google.com/"], // 只匹配首页,因为 AI Mode 按钮仅在首页出现
"js": ["content-scripts/content.js"]
}
]
}

调试扩展

接下来,我们需要在 Chrome 中加载这个扩展。

首先,打开 chrome://extensions/,并启用 Developer Mode

1767429435907.jpg

然后,使用 Load unpacked 按钮选择扩展目录,就能加载扩展到浏览器了。

访问 google.com,可以看到 AI Mode 按钮确实不见了,但会闪一下出现然后才消失,这意味着注入的脚本时机不够早,在注入的脚本执行之前,AI Mode 按钮就已经显示了,然后脚本注入才隐藏了按钮,这是一个基本的时序问题。幸运的是,可以简单调整 manifest 配置解决这个问题。

1
2
3
4
5
6
7
8
9
{
"content_scripts": [
{
"matches": ["https://www.google.com/"],
"js": ["content-scripts/content.js"],
"run_at": "document_start" // 在网页刚开始加载时就注入脚本,参考 https://developer.chrome.com/docs/extensions/reference/manifest/content-scripts#world-timings
}
]
}

然后在 chrome://extensions/ 重新加载扩展让修改生效。

1767429741359.jpg

接着就看到了一个错误 Uncaught TypeError: Cannot read properties of null (reading 'appendChild')

1767429844440.jpg

这是因为注入脚本的时机非常早,甚至连 document.head 标签都还没有渲染,可以修改为在 document.documentElement 注入 style 标签。

1
2
3
4
const style = document.createElement('style')
style.textContent =
'button:has([d="M15.65 11.58c.18-.5.27-1.03.31-1.58h-2c-.1 1.03-.51 1.93-1.27 2.69-.88.87-1.94 1.31-3.19 1.31C7.03 14 5 12.07 5 9.5 5 7.03 6.93 5 9.5 5c.46 0 .89.08 1.3.2l1.56-1.56C11.5 3.22 10.55 3 9.5 3 5.85 3 3 5.85 3 9.5S5.85 16 9.5 16c.56 0 2.26-.06 3.8-1.3l6.3 6.3 1.4-1.4-6.3-6.3c.4-.5.72-1.08.95-1.72z"]) { display: none; }'
document.documentElement.appendChild(style) // document.documentElement 代表页面的根元素,即 `<html></html>` 标签。

再次刷新扩展然后访问 google.com 可以看到 AI Mode 按钮不见了,而且也不再出现闪烁的情况。

1767430033953.jpg

添加图标

最后,这个扩展还缺少图标,我们将一张 128x128 的 png 图像放在 icon/128.png。
然后修改 manifest 添加 icons 设置即可。

1
2
3
4
5
{
"icons": {
"128": "icon/128.png"
}
}

再次刷新扩展,就可以看到扩展图标已经被正确加载了。

1767430968353.jpg

总结

这就是第一个基本扩展。通过这个例子,我们了解了扩展开发的几个核心概念:

  • Manifest 文件定义了扩展的基本信息和需要哪些权限
  • Content Script 可以注入到网页中,直接操作 DOM
  • 脚本注入时机会影响实际效果,需要根据场景选择 document_startdocument_enddocument_idle
  • 扩展的调试流程和普通网页开发类似,都可以在 DevTools 中查看错误

你可能注意到,直接编写原生 JavaScript 并手动管理文件有些繁琐。在下一章中,我们将使用 WXT 这个现代开发工具重构这个扩展,体验 TypeScript、热重载等特性。

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

完整代码:https://github.com/rxliuli/browser-extension-dev-examples/tree/main/packages/01-basic

参考

Google Chrome 扩展开发文档 https://developer.chrome.com/docs/extensions