资讯动态

开发 context-mode:用规则引擎实现浏览器场景切换

发布时间:2026/10/28 3:02:53 来源:尧图企业网站定制
这个名为 context-mode 的项目最初源于我自己每天在浏览器里的真实痛点写代码的时候需要打开 GitHub、Stack Overflow、本地文档做运营分析的时候要切到数据后台、投放平台、行业资讯站到了专注写作时又恨不得把社交网站全部屏蔽掉。我的工作节奏经常在两三个领域之间切换每次切换都得手动关掉一批标签页、再打开另一批一天重复十几次非常消耗精力。后来我想明白了一个道理人真正需要的不是一次性收藏一批网址而是能按“当下正在做什么”来组织浏览器状态。于是就有了 context-mode——一个给浏览器做“场景模式切换”的个人工具项目。它会记录用户当前隶属于哪个工作上下文context在改变上下文时自动执行一组可配置的规则关闭不属于该上下文的标签页、打开该上下文需要的主要站点、调整扩展的屏蔽策略。规则完全自己定义切换只用一个快捷键几秒钟内整个浏览器就能从“写代码状态”变成“做数据分析状态”。这篇文章会把整个项目的设计思路、Manifest V3 架构、状态同步机制、调试过程中踩到的坑以及最后沉淀下来的扩展方向按我实际做完的顺序逐步写清楚。如果你也想自己构建一个类似的“上下文切换器”或者只是对浏览器扩展的状态管理机制感兴趣应该能从里面直接找到可复用的思路和代码骨架。1. context-mode 究竟在优化什么从浏览器使用习惯到上下文模型很多开发者听见“context-mode”这个名字第一反应是编辑器里的什么高亮功能或者是终端工具里的某种模式显示。其实我这里说的 context指的就是“用户当前心智中的任务场景”。做技术调研和做内容创作需要的信息入口完全不同两种状态下大脑的注意力分配也完全不同。浏览器本身不知道你此刻在干什么它只会忠实地保留你之前打开的 30 个标签页这也正是效率损耗的来源。1.1 不做简单的“网址分组”而做“规则驱动的状态切换”我最早的想法确实很朴素把常用网站按工作类型收藏成文件夹需要的时候一键打开一组。做过一轮原型后我发现这种方式只解决了“打开”的问题没解决“关闭”的问题。如果你从开发模式切到运营模式浏览器里还挂着十几个 GitHub、API 文档的标签页它们不会因为你打开了另一个文件夹就自动消失。久而久之标签页越来越多内存和注意力都被拖着走。所以 context-mode 的核心设计调整成一个简单的规则引擎每个上下文由一组“进入规则”构成规则包含三部分分别是 open打开 URL、close关闭 URL 匹配的标签页、block临时拦截匹配站点的访问。切换上下文的时候后台脚本统一处理这三类动作。这个模型虽然多花了一些开发时间但实际用起来比单纯的分组收藏干净得多因为它是用“当前我要什么状态”来驱动浏览器状态而不是用“我之前存过什么”来驱动。1.2 每个上下文应该包含的信息粒度在设计配置文件的时候我一开始把上下文定义成一个大 JSON里面乱七八糟地塞了标题、颜色、图标、站点列表、备注。真正在代码里写规则引擎的时候才发现信息太多反而让匹配逻辑变得复杂。后来我把上下文的数据结构收敛成了这样{ id: code-1350479032, name: coding, matchPatterns: [https://github.com/*, https://stackoverflow.com/*], openTabs: [https://github.com/, https://mail.google.com/], closedPatterns: [https://news.ycombinator.com/*, https://twitter.com/*], blockedPatterns: [https://www.bilibili.com/*] }matchPatterns 用于判断“当前属于哪个上下文”openTabs 是这个上下文要主动打开的页面closedPatterns 是切换时需要关闭的页面blockedPatterns 则交给扩展拦截。这种设计让每个上下文的行为非常可预测我只需要关心“这个模式下浏览器应该长成什么样”而不需要关心浏览器现在具体有哪些标签页。每切换一次都有一次明确的重置动作思绪不会在残留页面之间漂移。1.3 为什么把“场景识别”优先做成手动切换我身边有朋友问能不能完全自动感知比如检测到你打开了 IDE 就自动切到 coding 模式检测到你在看数据平台就自动切到 analysis 模式。从工程角度这是可行的但在真实工作流里有个很烦的问题误触。你打开一个页面可能是临时查个东西结果扩展自作主张地把一套完整环境都改了反而是更大的打扰。所以 context-mode 的第一版原则是状态识别的主动权先交给用户用快捷键或点击弹窗手动切换规则引擎只负责执行不负责猜。这样也方便排查问题切换出错了你知道是自己规则写的有问题而不是某个“智能算法”出了什么玄学故障。等规则足够稳定后续可以考虑加入“基于活动标签页 URL 的半自动建议”但第一版我坚决不加。2. 架构拆解Manifest V3 下三个核心模块的职责划分老一代的 Chrome 扩展会用常驻后台页面background page来保存全局状态Manifest V3 全面转向了 service worker 之后一个比较大的变化是后台代码不再是永远活着的浏览器会在事件触发时唤醒它空闲时又随时把它杀掉。这个机制直接影响了 context-mode 的整体架构设计——因为“切换上下文”是一个需要读取规则、计算差异、执行动作的完整流程每一步都不能假设内存里还有上一次留下的临时数据。2.1 三种脚本各司其职避免逻辑堆在同一个文件里我把项目拆成了三个主要的部分后台 service worker 负责规则的加载和切换动作的执行popup 负责当前上下文的展示和手工切换按钮content script 只在需要拦截被 block 规则命中的页面时才会被注入。这样的好处是每个模块的生命周期和职责边界非常清楚。后台模块的核心逻辑长这样// background.js import { loadContexts, resolveCurrentContext, applyContext } from ./context-engine.js; import { RULE_STORAGE_KEY } from ./constants.js; chrome.runtime.onInstalled.addListener(async () { const storage await chrome.storage.local.get(RULE_STORAGE_KEY); if (!storage[RULE_STORAGE_KEY]) { await chrome.storage.local.set({ [RULE_STORAGE_KEY]: defaultContextConfig() }); } }); chrome.commands.onCommand.addListener(async (command) { if (command ! toggle-context) return; const current await resolveCurrentContext(); const contexts await loadContexts(); const next contexts.find((item) item.id ! current?.id) || contexts[0]; await applyContext(next); });这个结构最开始是写在一个文件里的写了两百多行之后我发现一个函数里既有 storage 读取又有 tabs 操作还有规则运算很难做单测也很难定位问题。后来我把规则的计算动作哪个标签页要关、哪个标签页要开拆到了 context-engine.js 里后台文件只保留事件绑定和调度。这种“计算与执行分离”的方式在扩展开发里特别值得推荐。2.2 service worker 事件驱动模式下的状态恢复策略MV3 的 service worker 有一个很典型的场景扩展在后台执行到一半比如正在遍历当前窗口的全部标签页结果 30 秒的 CPU 时间预算用完了service worker 被强制终止下次事件触发时一切重新来过。为了避免这种“做到一半被打断”的尴尬我把每次切换动作拆成了两个阶段先在 context-engine 里算出完整的 action plan要开哪些标签页、要关哪些标签页再统一交给执行器。action plan 是可以序列化的普通 JSON如果 worker 中途被销毁我可以在 chrome.storage.session 里存一份 plan恢复后继续。这个方案实现起来也不复杂但是对整体稳定性提升很大。另外要注意的一个细节是 chrome.storage.session 这个存储区域在 MV3 里作用很特殊它只保存在内存中不会同步到磁盘而且有 10MB 的容量限制正好适合存放“本次切换的临时状态”。我在项目里用 session 区域存当前方案的结果用 local 区域存规则配置用 chrome.storage.onChanged 监听规则变化三层分开逻辑非常清爽。3. 上下文规则的生命周期从配置写入到触发执行这个项目核心的复杂度其实不在 UI而在“规则什么时候生效、怎么生效”。我花了比较多时间打磨的是规则的生命周期也就是一个规则改完之后从写入存储到真正影响用户浏览器状态之间经历的完整过程。很多用户说自己的扩展“改了配置没反应”绝大多数情况下就是因为没有理解生命周期。3.1 配置修改的两条路径popup 表单与配置文件导入context-mode 提供了两种方式修改规则。第一种是在扩展弹窗里直接编辑当前上下文的名称、匹配模式和 URL 列表改动后立即写入 chrome.storage.local第二种是提供一个 JSON 配置的导入导出功能适合批量增删规则。这两种路径最后都会触发同一条数据流修改 → 写入 storage → 触发 chrome.storage.onChanged 监听 → 后台重新计算当前上下文 → 生效。我比较推荐把“写配置文件”这条路径保留下来虽然做导出导入功能多花了一些时间但调试排查的时候非常好用。你可以先把一个复杂的规则集在本地编辑好然后一次性导入扩展不用在弹窗的输入框里一条条地填。而且配置文件可以被版本管理换电脑的时候迁移状态非常方便。3.2 切换动作的执行顺序为什么先关后开执行过程中有一个顺序问题值得专门说说。如果你同时要关闭若干标签页和打开若干新标签页先做哪一步对最终效果的影响其实很大。我的实现是先计算关闭列表再执行关闭操作最后统一打开新页面。先关后开的原因很实际新打开的标签页会排在标签栏最右侧如果当前窗口里堆积着一二十个旧标签页打开的新页面会被挤到很靠后的位置用户很难一眼看到它们。先把不需要的标签页清掉再打开新页新页面就会出现在一个相对靠前且聚焦的位置。对于需要快速进入工作状态的场景来说这种视觉顺序上的体验差别很明显。执行关闭动作的时候我还特意加了一个保护机制跳过当前存活标签页里 pinned 的标签页因为固定的标签页通常是用户刻意保留的常用工具比如邮箱或者常用后台不应该被某个上下文规则误杀。其实这个判断在原生的 tabs API 里很简单一个 pinned 属性就能搞定但如果不注意很容易让人觉得你的扩展“乱关标签页”。3.3 拦截规则放在哪一层更合理blockedPatterns 的拦截功能第一版是直接放在 background 模块里用 chrome.tabs.query 配合 URL 检查来实现的切换上下文时扫描现有标签页发现命中 blockedPatterns 的标签页就自动关闭。这个方法虽然能处理掉已经打开的站点但没法阻止用户在切换上下文之后再去手动打开被屏蔽的站点。第二版我在 content script 里加了一个更接近实时拦截的机制页面加载前判断当前活动标签页所在上下文的 blockedPatterns 是否包含即将访问的 URL如果命中就直接把页面重定向到一个说明页面。这里用到了 declarativeNetRequest 也能做的能力不过考虑到规则变更频繁且需要动态判断“当前上下文”我最终选择用 content script 配合存储数据来实现逻辑更直观也更容易调试。4. 弹窗交互与可视化反馈状态是否切换成功一目了然工具类扩展最忌讳的是“动作执行了但用户完全不知道发生了什么”。切换上下文本来是一个改变浏览器全局状态的动作如果缺少清晰的界面反馈你会下意识地想“我刚才到底切过去没有”这种不确定性很影响使用信心。所以 popup 部分我没有做得很花哨但要信息清晰。4.1 弹窗就是一个上下文状态仪表盘popup 的整体布局分三段最上方是当前上下文名称和一句描述中间是当前上下文的规则统计比如“将打开 5 个标签页关闭 3 个标签页拦截 2 个站点”下方是切换到其他上下文的按钮列表。统计信息的计算不在 popup 里重复做而是由后台在切换时把结果写入 chrome.storage.sessionpopup 通过 storage.session.get 读取后渲染。我这么设计主要是为了避免让 popup 去拉取全部标签页从而影响打开速度。popup 本身是一个轻量页面如果每次打开都要执行一大堆标签页查询交互延迟会非常明显。切换按钮点击之后popup 不会立刻关闭而是显示一个短暂的“切换中”状态。等后台完成全部操作通过 storage.onChanged 监听到 session 数据变化后popup 再更新为完成状态。虽然这在纯技术上看是很简单的事件监听但真实体验差别很大尤其是在你按错快捷键本想快速纠正的时候一个明确的“已切到 analysis”提示能避免不少误操作。4.2 快捷键带来的切换速度提升我后来专门把切换动作绑定到了快捷键上。在 manifest.json 的 commands 字段里注册了一个 toggle-context 命令比如设置成 AltC 或 CtrlShiftSpace这样连点开弹窗的步骤都省了。快捷键触发的好处不只是快它还能起到节奏感的作用按下快捷键就像给浏览器发了一个“换挡”信号操作路径比鼠标点击更短也更容易养成习惯。使用快捷键的时候对 UI 反馈的要求更高因为快捷键触发不会自动弹 popup。我专门在切换完成后利用 chrome.notifications.create 发一条轻量通知内容就是“已切换到 coding 上下文关闭 4 个标签页打开 2 个标签页”。这个通知不会长期存在但它能让你在视线不离开键盘的情况下确认状态确实变了。5. 从零开始调试扩展时最容易忽略的四个边界问题开发 context-mode 的过程中调试占的时间比写功能本身还多。这里我把最有代表性的四个问题单独写一节它们每一个都在真实运行环境中出现过而且如果不把原理想清楚很难定位。5.1 service worker 被挂起导致的“切换无响应”我第一次测试的时候上下文切换偶尔不生效多点是几次又好了非常像随机 bug。后来查明原因service worker 处于休眠状态popup 通过 chrome.runtime.sendMessage 发送切换消息时浏览器启动了 worker 来接收消息理论上应该能正确执行。但我的代码里有个异步函数在 worker 激活早期走了异常路径导致切换动作没有完成。问题是这个异常只出现在 worker 被冷启动后的前几个事件里等 warm 状态时就正常了。解决办法倒不复杂在后台模块的全局入口尽早绑定 chrome.runtime.onMessage 监听同时在函数体内部给所有异步操作加上 try/catch 和日志。MV3 的 worker 不像老背景页那样有个稳定的全局变量错误日志可能还没来得及 console.log 出去就被终止了所以排查这类问题时我习惯把关键日志写到 chrome.storage.session 里奔溃后还能读出来看。5.2 竞态条件连续切换两个上下文只生效了最后一个这个问题的根子是异步事件没有加锁。用户如果在短时间内连续按两次快捷键第一次的 applyContext 还在遍历标签页面第二次的 applyContext 又启动了两个动作交错执行很容易出现“前一个动作刚关闭的标签页被后一个动作重新打开”的乱局。我不想引入复杂的事务机制所以用了一个简单的 promise 队列所有切换动作排队执行前一个完成后一个才开始。实现也不复杂用一个数组保存当前正在运行的 promise每次 applyContext 都等它 finish 后再执行下一个。上下文切换不是高频操作队列机制带来的延迟完全可以忽略但能彻底消除乱序执行带来的标签页抖动。5.3 URL 匹配模式不是简单的字符串 includes我在设计 matchPatterns 时犯了新手错误直接用 URL.includes(domain) 来判断当前标签页属于哪个上下文。结果一个明显的副作用就是包含 github.io 的页面也会被误判为 GitHub 主站访问 google.com 时把 googleusercontent 的图片资源也算成匹配。后来我统一改用浏览器原生的 match pattern 规则也就是在声明 permissions 和 content_scripts 时用的那种表达式。自己解析 match pattern 很简单但如果想统一处理各种极端输入更稳的方案是用 chrome.permissions 或 chrome.declarativeNetRequest 内置的能力来校验 URL或者在代码里引入一个解析 match pattern 的小库。我项目里的数据量不大所以自己写了一个小型校验函数把协议、域名、路径拆开匹配避免通配符和特殊字符导致误判。5.4 标签页“消失”后恢复不了因为 session 不保留历史最开始我在切换上下文时直接调用 tabs.remove 关闭标签页。为什么后来加了一个关闭保护提示因为做过一次误操作之后我就明白了tabs.remove 是永久性的如果当前上下文规则写错了或者匹配模式写得太宽你一天攒下来的几十个工作标签页会在一瞬间全部被关掉。为了避免这个灾难性后果我给 context-mode 增加了一个双保险所有的关闭动作在真正执行前会先把被关标签页的 URL 列表存入 chrome.storage.session同时在弹窗里保留一个“一键恢复上一个上下文”的按钮。即便发生误操作你也可以在几秒钟内把刚才的标签页重新打开。对工具来说“撤销”能力给我的安全感可能比高效的切换本身更大。6. 后续演进与真实使用建议从个人工具走向通用工具context-mode 做到现在这个版本我已经在日常生活里连续用了几周了。它给我省下的时间不是每次切换那一两分钟那么简单更重要的是省下的注意力标签栏不再时刻处于“几十个页面乱糟糟”的状态浏览器窗口本身变成了一个可预期的、随上下文变化的工具台而不是信息堆填区。6.1 自动感知上下文可以做成可选增强前文我说第一版不做自动感知但实际用久了之后自然会有新的需求。有些规则其实是高度可预测的比如当活动标签页变成了某个特定域名开头的时候基本可以断定进入了对应的上下文状态。我给后续版本预留了一个接口context-engine 里增加一份“observer 配置”监听当前活动标签页的变化当 URL 命中某个上下文且该上下文标记为 auto 时候自动提示用户是否切换。这个功能目前还在开发中但我已经在规则文件里预留了 manual / auto 字段。用户可以在配置里决定某个上下文是手动触发还是自动触发这样既保留了对突发事件的控制权也不会因为要频繁手动切换而感到烦躁。自动和手动的边界是这种工具未来最值得打磨的地方。6.2 规则导入导出与团队共享把配置文件当成文档管理很多人可能会觉得一个浏览器扩展的规则配置个人的东西弄一套导入导出是不是过度设计。但在我实际工作和协作的场景里我发现不同岗位之间的“互联网使用习惯”其实有相当大的共性前端开发的 context 配置和运维工程师的 context 配置共享度很高。团队里如果有人把一套还不错的 context 规则集整理出来其他人导入之后稍加调整就能用。我把默认规则文件放在了项目目录的 contexts.default.json 里并且在扩展设置页提供了“导出当前配置”“从文件导入配置”两个按钮。配置文件本身是纯 JSON可以被 Git 管理、被在线文档评论、被 Copy 到新电脑。对多人协作而言这比自己打开设置面板一项项配置要友好得多。6.3 使用 context-mode 后我调整了哪些习惯最后聊一点真实感受。刚开始使用的时候我花了很多时间设计规则想把每个上下文都做得“完美”把每个细节都考虑到。后来发现规则过细反而会让大脑在切换上下文时产生过重的思考负担——因为你在切换之前还得想想“我到底该用哪个模式”。工具的价值在于降低决策成本而不是增加另一层决策。我对自己的使用方式做了这样的收敛只保留两个核心上下文工作coding模式默认打开代码仓库、文档、技术社区和内部工具生活focus模式默认关闭所有社交娱乐站点、打开在线文档和阅读列表。临时打开的页面不属于任何上下文不进规则也不会被杀掉。这个设计足够简单也足够稳定。如果你也想自建类似工具我建议从最小可用的两三个上下文开始先用起来再根据真实痛感逐步增加规则而不是一开始就试图覆盖所有生活场景。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑