我写了一个 Chrome 扩展,让任何网站都能"换皮":架构、踩坑与设计实录
目录
- 缘起:为什么自己写一个"换皮"扩展
- 需求拆解:从"能改样式"到"用起来舒服"
- Chrome 扩展架构:四个文件就够
- 核心实现:按域名注入自定义 CSS
- Manifest V3 适配:那些文档没明说的限制
- 三个让我卡了半天的坑
- 交互设计:让"改样式"这件事本身不累
- 结尾:工具型扩展的"克制"哲学
1. 缘起:为什么自己写一个"换皮"扩展
我有一批每天要打开的网站:某个文档站、某个内部系统、某个资讯站。内容都不错,但 UI 各有各的"不舒服"——
- 文档站字号 14px,长文看着费眼;
- 内部系统主色是高饱和蓝,盯半小时就累;
- 资讯站广告位比正文还显眼。
我试过现成的样式定制扩展,但要么功能多到打开就劝退,要么默认把样式同步到云端账号——我只想改自己电脑上几个站的样式,不想注册不想上传。
于是目标定了:做一个纯本地的 Chrome 扩展,按域名存自定义 CSS,访问对应网站时自动注入,让我浏览时更舒服。
2. 需求拆解:从"能改样式"到"用起来舒服"
和 AI 协作的第一步,我把"能改样式"拆成了可验收的清单:
| 诉求(人话) | 拆解后(可验收) |
|---|---|
| 能改样式 | 提供编辑框,保存自定义 CSS 并注入到当前网站 |
| 按网站分别存 | 以域名为 key 存到 chrome.storage,不同站点互不干扰 |
| 能开关 | 每条样式有启用/禁用开关,不必删除就能临时关掉 |
| 不丢 | 数据存本地,重装浏览器可导出/导入 JSON 备份 |
| 用着不累 | 弹出面板默认显示当前域名,一键进入编辑,保存即生效 |
这张表直接决定了扩展的骨架——功能不多,但每一项都做到位。
3. Chrome 扩展架构:四个文件就够
Chrome 扩展听起来复杂,其实最小可用版本就四个文件:
my-style-extension/
├─ manifest.json // 扩展元数据与权限声明
├─ content.js // 注入到页面、应用 CSS 的脚本
├─ popup.html // 点击扩展图标弹出的小面板
└─ popup.js // 面板逻辑:编辑、保存、开关
manifest.json 是核心声明(Manifest V3 版本):
{
"manifest_version": 3,
"name": "本地样式定制",
"version": "1.0.0",
"permissions": ["storage", "activeTab", "scripting"],
"host_permissions": ["<all_urls>"],
"action": { "default_popup": "popup.html" },
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_start"
}
]
}
心得:扩展开发的门槛其实不在"写代码",而在"搞清楚每个权限到底要不要声明"。host_permissions 和 scripting 的区别、activeTab 和 <all_urls> 的取舍,是初学者最容易绕晕的地方。
4. 核心实现:按域名注入自定义 CSS
核心逻辑只有两段:存的时候按域名存,注入的时候按当前域名取。
popup.js 保存(按当前域名写入 storage):
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
const domain = new URL(tab.url).hostname;
chrome.storage.local.get('rules', ({ rules = {} }) => {
rules[domain] = { css: editor.value, enabled: true };
chrome.storage.local.set({ rules });
});
});
content.js 注入(页面加载时取当前域名的规则):
const domain = location.hostname;
chrome.storage.local.get('rules', ({ rules = {} }) => {
const rule = rules[domain];
if (rule && rule.enabled && rule.css) {
const style = document.createElement('style');
style.textContent = rule.css;
document.head.appendChild(style);
}
});
就这么几行,扩展就能跑了。Chrome 扩展的"魔法"其实很朴素:content script 是注入到页面的 JS,chrome.storage 是本地 KV 存储,两者一组合就是"按域名持久化样式"的完整闭环。
5. Manifest V3 适配:那些文档没明说的限制
我从 V2 起步,迁到 V3 时踩了几个文档没明说的点:
browser_action改名action,page_action被合并进action。V2 的扩展不改这部分直接装不上;chrome.tabs.executeScript被废弃,改用chrome.scripting.executeScript,且需要在permissions里加"scripting";- 远程代码执行被禁。V3 不允许 content script 引入远程 JS,所有逻辑必须打包进扩展。这对"纯本地"扩展反而是好事——逼着你把代码写干净。
心得:Manifest V3 的核心变化其实就一句话——更安全、更本地、更不可远程操控。对一个"本地改样式"的扩展来说,V3 的限制反而是天然契合的。
6. 三个让我卡了半天的坑
坑一:注入时机决定一切。 一开始 run_at 用默认的 document_idle,结果某些站点在 document_idle 之前就已经渲染完并应用了自己的样式,我注入的 <style> 虽然在 DOM 里,但被站点后续 JS 动态注入的样式覆盖。改成 document_start 后,我的 <style> 提前插入,配合 !important 才稳定生效。教训:注入样式类扩展,run_at 选 document_start,再用 !important 兜底。
坑二:CSS 优先级博弈。 网站的样式表会写 !important,你的也写 !important,最后比的是选择器特异性。我一开始用 body { ... } 这种低特异性选择器,被站点秒杀。后来养成习惯:自定义样式一律用高特异性选择器 + !important,比如 html body div.main .content { ... !important },胜率才上来。
坑三:CSP 限制动态注入。 某些站点设了严格的 Content-Security-Policy,直接 document.head.appendChild(style) 会被拦。解决办法是用 chrome.scripting.insertCSS API 注入——这是扩展特权 API,不受页面 CSP 约束。教训:能走扩展 API 就别走 DOM 注入,少踩一半坑。
7. 交互设计:让"改样式"这件事本身不累
扩展本身也要好用,否则就是"为了改样式又给自己添麻烦"。我的 popup 面板设计原则只有一条:打开就能改,改完即生效。
- 打开 popup 自动显示当前域名和已保存的样式;
- 编辑框默认聚焦,
Ctrl+S保存并立即注入到当前页(不用刷新); - 顶部一个"启用/禁用"开关,临时关掉某站样式不用删;
- 底部"导出/导入"按钮,备份和迁移用。
心得:工具型扩展的交互衡量标准是"完成一次定制需要几次点击"。我的目标是 3 次以内:打开扩展 → 改 CSS → Ctrl+S。每多一次点击,就多一个不再使用的理由。
8. 结尾:工具型扩展的"克制"哲学
做完这个扩展,我对"工具型产品"有了一个更清晰的认知:
- 功能克制:我没做"样式市场"“一键应用别人分享的样式”“云端同步”——不是做不到,是每加一个功能,主流程就重一分;
- 数据本地:所有样式存在
chrome.storage.local,不发起任何网络请求。隐私是这类工具的底线; - 可迁移:导入导出 JSON 是从第一天就有的,而不是最后补的——数据主权要写进架构,不是写进营销文案。
如果你也想做类似的小工具,我的建议是:从一个自己每天都会用的场景出发,把"用着不累"当成和"功能正确"一样硬的需求。Chrome 扩展的门槛比想象中低,四个文件就能跑;真正难的是"克制住加功能的冲动"。
工具小而准,价值往往不在多,而在"每次打开都恰好够用"。
- 点赞
- 收藏
- 关注作者
评论(0)