上下文模式(Context-Mode)工程实践:设计原则、核心实现与避坑指南
1. 从“上下文模式”说起:一个被低估的工程概念
第一次听到“context-mode”这个词,很多人会下意识地把它归到某个具体框架的配置项里,觉得无非是个开关或者枚举值。但如果你在工程一线待过几年,尤其是在做系统设计、状态管理、或者跨模块协作的时候,就会发现“上下文模式”其实是一个贯穿始终的底层思维模型。它决定了你的系统在什么条件下切换行为、以什么粒度传递信息、以及如何在复杂场景下保持一致性。
我最早接触这个概念是在做前端状态管理的时候。当时项目里有一个需求:同一个组件在“编辑态”和“预览态”下要呈现完全不同的交互逻辑。最直觉的做法是写两个组件,或者用一堆 if-else 判断。但这样做的后果是代码迅速膨胀,维护成本飙升。后来团队里一位资深工程师提出了“上下文模式”的思路——把“当前处于什么模式”抽象成一个独立的上下文对象,所有组件都从这个上下文里读取自己的行为策略。这个改动让代码量减少了将近四成,而且新增模式时只需要扩展上下文定义,不需要动业务组件。
这就是 context-mode 的核心价值:它把“在什么情况下做什么事”这个判断逻辑,从散落各处的条件分支里抽离出来,集中到一个可管理、可扩展的上下文层。听起来简单,但真正用好它,需要理解几个关键维度。
这篇文章适合谁看?如果你正在做以下任何一件事,那 context-mode 的思路大概率能帮到你:正在设计一个需要支持多种运行模式的系统;正在为频繁变化的需求而头疼,每次改动都要动很多地方;正在做跨团队协作,需要一套统一的“行为约定”;或者你只是单纯想提升代码的可维护性和可读性。我会从设计思路、核心细节、实操落地、问题排查几个层面展开,尽量把我在实际项目中踩过的坑和总结的经验都倒出来。
2. 内容整体设计与思路拆解
2.1 为什么需要“上下文模式”而不是简单的条件判断
先来还原一个典型场景。假设你在做一个表单系统,需要支持三种模式:新建、编辑、只读。最直接的做法是在每个字段组件里判断当前模式:
if (mode === 'create') { // 可编辑,空值 } else if (mode === 'edit') { // 可编辑,有初始值 } else if (mode === 'readonly') { // 不可编辑,展示值 }这种写法在只有两三个模式、三五个字段的时候没问题。但当模式增加到七八种,字段增加到几十个,每个字段在不同模式下的行为又有细微差异时,灾难就开始了。你会发现自己在一个巨大的条件分支网里挣扎,改一个模式的行为要翻遍所有组件,而且很容易漏掉某个角落。
context-mode 的思路是把“模式”本身变成一个一等公民。你定义一个上下文对象,里面包含了当前模式以及该模式下所有相关的行为策略:
const formContext = { mode: 'edit', fields: { name: { editable: true, required: true, validator: nameValidator }, age: { editable: true, required: false, validator: ageValidator }, // ... }, actions: { submit: { visible: true, label: '保存' }, cancel: { visible: true, label: '取消' } } };然后所有组件都从这个上下文里读取自己的配置。这样做的好处是:模式的定义和模式的使用解耦了。新增一个模式,只需要新增一份上下文配置,不需要改动任何组件代码。组件只负责“根据上下文渲染”,不负责“判断当前是什么模式”。
这个思路背后的设计原则是“关注点分离”和“开闭原则”的结合。判断逻辑集中管理,扩展时对修改封闭、对新增开放。我在多个项目里验证过,当模式数量超过三个、或者模式之间的差异维度超过两个时,context-mode 带来的收益就开始明显超过它的抽象成本。
2.2 上下文模式与其他方案的对比选型
在决定采用 context-mode 之前,有必要把它和其他常见方案做个对比。我整理了一个表格,基于我在实际项目中的体验:
| 方案 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| 条件判断散落各处 | 模式少、差异小 | 实现简单,无抽象成本 | 难维护,易遗漏,扩展性差 |
| 继承/多态 | 模式差异体现在行为上 | 符合面向对象直觉 | 类层次容易过深,灵活性受限 |
| 策略模式 | 算法可互换 | 职责清晰 | 偏重算法替换,对“状态+行为”的组合支持不够 |
| 状态机 | 状态流转复杂 | 流转关系明确 | 偏重流转,对同一状态下的多维度配置支持弱 |
| context-mode | 多模式、多维度配置 | 集中管理,易扩展,组件无感知 | 需要前期设计上下文结构 |
从表里可以看出,context-mode 最适合的场景是:同一套组件需要在多种模式下呈现不同行为,且模式之间的差异是多维度的(不只是某个字段的显隐,而是多个字段的编辑性、校验规则、动作按钮等组合变化)。
选型时还要考虑一个因素:团队对抽象层的接受度。context-mode 引入了一个额外的上下文层,如果团队里有人不熟悉这种模式,可能会觉得“多此一举”。我的经验是,在项目初期就把它讲清楚,最好配一个简单的示例,让大家看到“新增模式时只需要加一份配置”的便利,接受度会高很多。
2.3 上下文模式的核心设计原则
设计一个好的 context-mode 方案,需要遵循几个原则。这些原则是我从多次重构中总结出来的,每一条都对应着曾经踩过的坑。
原则一:上下文是只读的。上下文对象一旦创建,就不应该在运行时被修改。如果需要切换模式,应该创建一个新的上下文对象,而不是修改现有的。这样做的好处是避免了“状态污染”——某个组件修改了上下文,导致其他组件行为异常。我在早期项目里就犯过这个错误,一个子组件为了临时禁用某个字段,直接改了上下文里的配置,结果另一个依赖同一上下文的组件也受到了影响,排查了半天才找到原因。
原则二:上下文的结构要扁平化。不要把上下文设计成深层嵌套的结构。扁平化的结构更容易查找、更容易 diff、也更容易做类型定义。如果确实需要分组,用命名约定或者独立的子上下文,而不是嵌套对象。
原则三:上下文要包含“默认值”和“覆盖值”两层。默认值定义了所有模式的公共行为,覆盖值定义了特定模式的差异。这样新增模式时,只需要写差异部分,公共部分自动继承。这个设计在模式数量多的时候尤其有用,能大幅减少重复配置。
原则四:上下文的创建要有统一的入口。不要在各个地方手动拼装上下文对象,而是通过一个工厂函数或者配置中心来创建。这样能保证上下文的一致性,也方便做校验和日志。
3. 核心细节解析与实操要点
3.1 上下文对象的结构设计
上下文对象的结构设计是整个方案的基础。设计得好,后续扩展轻松;设计得不好,用不了多久就会变成新的技术债。我推荐的结构是这样的:
const context = { meta: { mode: 'edit', version: '1.0', createdAt: '2024-01-15T10:00:00Z' }, data: { // 当前模式下的数据快照 }, behaviors: { // 行为配置,按领域分组 field: { ... }, action: { ... }, validation: { ... } }, extensions: { // 预留的扩展点 } };meta部分存放上下文的元信息,比如当前模式、版本号、创建时间。这些信息在调试和日志里非常有用。data部分存放当前模式下的数据,注意这里的数据应该是只读的,如果需要修改数据,应该通过专门的 action 来触发,而不是直接改上下文。behaviors是核心部分,按领域分组存放行为配置。extensions是预留的扩展点,用于存放一些实验性的或者特定场景的配置。
这个结构的关键在于分层清晰。元信息、数据、行为、扩展各占一层,互不干扰。我在实际项目里见过有人把数据和行为混在一起,结果每次改行为配置都要小心不要碰到数据,非常别扭。
3.2 模式切换的触发机制与边界处理
模式切换是 context-mode 里最容易出问题的地方。常见的触发方式有几种:用户操作触发(比如点击“编辑”按钮)、数据状态触发(比如数据加载完成后自动进入只读模式)、外部事件触发(比如收到某个通知后切换模式)。
不管哪种触发方式,都需要处理几个边界问题。第一个是切换时的数据一致性。如果从编辑模式切到只读模式,未保存的修改怎么办?我的做法是在切换前触发一个beforeModeChange钩子,让业务方有机会处理未保存的数据。第二个是切换的幂等性。如果连续两次切换到同一个模式,不应该产生副作用。这需要在切换逻辑里加一个判断:如果目标模式和当前模式相同,直接返回。第三个是切换的原子性。模式切换应该是一个原子操作,要么完全切换成功,要么保持原状。这要求上下文对象的创建和替换是原子的,不能出现“切了一半”的中间状态。
function switchMode(currentContext, targetMode) { if (currentContext.meta.mode === targetMode) { return currentContext; // 幂等 } const hookResult = triggerHook('beforeModeChange', { from: currentContext.meta.mode, to: targetMode }); if (hookResult === false) { return currentContext; // 钩子阻止了切换 } const newContext = createContext(targetMode); triggerHook('afterModeChange', { context: newContext }); return newContext; }这段代码展示了切换逻辑的核心骨架。注意beforeModeChange钩子可以返回 false 来阻止切换,这给业务方提供了一个“拦截”的机会。比如在编辑模式下有未保存的修改时,可以弹窗询问用户是否放弃修改,用户选择“取消”就阻止切换。
3.3 上下文与组件的通信方式
上下文和组件之间的通信方式决定了这套方案的易用性。常见的有三种:属性传递、依赖注入、全局状态管理。
属性传递最直接,父组件把上下文作为 prop 传给子组件。优点是显式、易追踪;缺点是如果组件层级深,需要逐层传递,很啰嗦。依赖注入通过框架提供的机制(比如 React 的 Context、Vue 的 provide/inject)来传递,组件在需要的时候自行注入。优点是避免了逐层传递;缺点是依赖关系不够显式,调试时不太直观。全局状态管理把上下文放到全局 store 里,任何组件都可以访问。优点是方便;缺点是容易造成滥用,组件和上下文的耦合变得隐蔽。
我的建议是:优先用依赖注入,在组件层级浅或者需要显式控制传递范围时用属性传递,尽量避免全局状态管理。全局状态管理看起来方便,但会让“谁依赖了上下文”变得难以追踪,在大型项目里是维护噩梦。
还有一个细节:组件读取上下文时,应该只读取自己需要的部分,而不是把整个上下文都注入进来。这样做的好处是减少不必要的重渲染,也让组件的依赖关系更清晰。比如一个只关心“提交按钮是否可见”的组件,应该只读取behaviors.action.submit.visible,而不是整个behaviors对象。
3.4 上下文的序列化与持久化
在某些场景下,上下文需要被序列化保存,比如保存到本地存储、或者通过网络传输到另一个环境。这时候就需要注意几个问题。
第一个是循环引用。上下文对象里如果包含了函数或者对 DOM 的引用,序列化时会出问题。我的做法是在上下文里只存放纯数据,函数和引用通过 ID 或者名称来间接引用。第二个是版本兼容。上下文的结构可能会随着版本迭代而变化,反序列化时需要做版本检查和迁移。我通常会在meta里放一个version字段,反序列化时根据版本号决定是否需要迁移。第三个是安全性。如果上下文来自外部输入,需要做校验和过滤,防止注入攻击。这一点在跨环境传输时尤其重要。
function serializeContext(context) { return JSON.stringify({ meta: context.meta, data: context.data, behaviors: context.behaviors }); } function deserializeContext(json) { const raw = JSON.parse(json); if (raw.meta.version !== CURRENT_VERSION) { raw = migrateContext(raw); } return raw; }序列化时只保留meta、data、behaviors三部分,extensions里的内容如果是实验性的,可以选择不序列化。反序列化时先做版本检查,如果需要迁移就调用迁移函数。迁移函数应该是一个纯函数,输入旧版本上下文,输出新版本上下文。
4. 实操过程与核心环节实现
4.1 从零搭建一个上下文模式的原型
光说理论不够,我带你从零搭一个可运行的原型。这个原型模拟一个“订单管理”场景,支持三种模式:查看、编辑、审核。每种模式下,订单字段的编辑性、校验规则、可用操作都不同。
第一步,定义上下文的类型。如果你用 TypeScript,这一步能帮你省下大量调试时间:
interface OrderContext { meta: { mode: 'view' | 'edit' | 'audit'; version: string; }; data: { orderId: string; customerName: string; amount: number; status: string; }; behaviors: { fields: Record<string, FieldBehavior>; actions: Record<string, ActionBehavior>; }; } interface FieldBehavior { editable: boolean; required: boolean; visible: boolean; validator?: string; } interface ActionBehavior { visible: boolean; enabled: boolean; label: string; }类型定义清楚之后,创建上下文的工厂函数就很好写了:
function createOrderContext(mode: string, data: any): OrderContext { const baseBehaviors = { fields: { orderId: { editable: false, required: true, visible: true }, customerName: { editable: true, required: true, visible: true }, amount: { editable: true, required: true, visible: true }, status: { editable: false, required: false, visible: true } }, actions: { save: { visible: false, enabled: false, label: '保存' }, submit: { visible: false, enabled: false, label: '提交' }, approve: { visible: false, enabled: false, label: '通过' }, reject: { visible: false, enabled: false, label: '驳回' } } }; const modeOverrides = { view: { fields: { customerName: { editable: false }, amount: { editable: false } }, actions: { save: { visible: false }, submit: { visible: false } } }, edit: { actions: { save: { visible: true, enabled: true }, submit: { visible: true, enabled: true } } }, audit: { fields: { customerName: { editable: false }, amount: { editable: false } }, actions: { approve: { visible: true, enabled: true }, reject: { visible: true, enabled: true } } } }; return { meta: { mode, version: '1.0' }, data, behaviors: deepMerge(baseBehaviors, modeOverrides[mode] || {}) }; }这段代码的关键在于deepMerge。它把基础行为和模式覆盖行为合并在一起,生成最终的上下文。基础行为定义了所有模式的公共部分,模式覆盖只写差异部分。这样新增一个模式时,只需要在modeOverrides里加一项,写清楚和基础行为的差异即可。
4.2 组件如何消费上下文
上下文创建好之后,组件需要消费它。我以 React 为例,展示一个字段组件的实现:
function OrderField({ name, context }) { const behavior = context.behaviors.fields[name]; const value = context.data[name]; if (!behavior.visible) { return null; } return ( <div className="order-field"> <label>{name}</label> {behavior.editable ? ( <input value={value} required={behavior.required} onChange={(e) => handleFieldChange(name, e.target.value)} /> ) : ( <span>{value}</span> )} </div> ); }这个组件完全不关心当前是什么模式,它只从上下文里读取behavior和value。模式切换时,上下文变了,组件自动重新渲染,行为随之改变。这就是 context-mode 的威力:组件是模式无关的,模式的变化通过上下文传递。
操作按钮组件也是类似的逻辑:
function OrderActions({ context }) { const actions = context.behaviors.actions; return ( <div className="order-actions"> {Object.entries(actions).map(([key, behavior]) => { if (!behavior.visible) return null; return ( <button key={key} disabled={!behavior.enabled} onClick={() => handleAction(key, context)} > {behavior.label} </button> ); })} </div> ); }按钮组件遍历所有 action,根据visible和enabled决定是否渲染和是否可点击。新增一个操作时,只需要在上下文的actions里加一项,按钮组件不需要任何改动。
4.3 模式切换的完整流程与钩子机制
模式切换的完整流程需要包含钩子机制,让业务方有机会在切换前后做一些处理。我实现了一个简单的钩子管理器:
const hooks = { beforeModeChange: [], afterModeChange: [] }; function registerHook(name, fn) { if (hooks[name]) { hooks[name].push(fn); } } function triggerHook(name, payload) { for (const fn of hooks[name] || []) { const result = fn(payload); if (result === false) { return false; } } return true; }注册钩子的方式:
registerHook('beforeModeChange', ({ from, to }) => { if (from === 'edit' && hasUnsavedChanges()) { return confirm('有未保存的修改,确定要离开吗?'); } return true; }); registerHook('afterModeChange', ({ context }) => { console.log(`模式已切换到 ${context.meta.mode}`); trackEvent('mode_change', { mode: context.meta.mode }); });beforeModeChange钩子返回 false 可以阻止切换,这给业务方提供了拦截能力。afterModeChange钩子用于做一些副作用,比如日志、埋点、通知等。
4.4 参数计算与性能优化
上下文模式在性能上有一个潜在问题:每次模式切换都会创建一个新的上下文对象,如果上下文很大,创建和合并的开销可能不可忽略。我做过一个测试,上下文包含 100 个字段配置时,deepMerge的耗时大约在 2-3 毫秒,对于大多数场景是可以接受的。但如果字段数量达到 1000 个,耗时就会上升到 20-30 毫秒,这时候就需要优化了。
优化的思路有几个。第一个是惰性合并。不要一次性合并所有字段,而是按需合并。组件读取某个字段时,如果该字段还没有合并,再触发合并。第二个是缓存。如果同一个模式被多次切换,可以缓存合并结果。第三个是结构共享。合并时尽量复用未修改的部分,而不是深拷贝整个对象。
const contextCache = new Map(); function createOrderContext(mode, data) { const cacheKey = `${mode}:${JSON.stringify(data)}`; if (contextCache.has(cacheKey)) { return contextCache.get(cacheKey); } const context = doCreateContext(mode, data); contextCache.set(cacheKey, context); return context; }缓存策略要注意失效问题。如果data变了,缓存 key 也会变,所以不会命中旧缓存。但如果data很大,JSON.stringify本身的开销也不小。这时候可以用一个版本号或者哈希值来代替完整的序列化。
5. 常见问题与排查技巧实录
5.1 上下文模式落地时的典型问题速查表
在实际项目中落地 context-mode,我遇到过不少问题。整理成一张速查表,方便你对照排查:
| 问题现象 | 可能原因 | 排查思路 | 解决方案 |
|---|---|---|---|
| 模式切换后组件没更新 | 上下文引用没变,或者组件没订阅上下文 | 检查上下文是否创建了新对象;检查组件是否从上下文读取 | 确保切换时创建新对象;确保组件正确订阅 |
| 某个字段行为不符合预期 | 模式覆盖没写对,或者合并顺序有问题 | 打印合并后的上下文,对比预期 | 检查覆盖配置的层级和字段名 |
| 切换模式时数据丢失 | 切换时没有保留数据,或者数据被覆盖 | 检查切换逻辑是否传递了 data | 在切换时显式传递 data |
| 性能下降明显 | 上下文过大,合并开销高 | 测量合并耗时,检查上下文大小 | 启用缓存、惰性合并或结构共享 |
| 钩子没触发 | 钩子注册顺序问题,或者钩子被覆盖 | 检查钩子注册代码,打印钩子列表 | 确保钩子注册在切换之前;避免重复注册 |
| 序列化失败 | 上下文包含函数或循环引用 | 检查上下文内容,尝试序列化 | 只存放纯数据;用 ID 引用函数 |
这张表里的每一条都是我实际踩过的坑。比如“模式切换后组件没更新”这个问题,我遇到过一次,排查了很久才发现是因为切换逻辑里直接修改了原上下文的属性,而不是创建新对象。React 的浅比较没检测到变化,所以没触发重渲染。改成创建新对象后问题就解决了。
5.2 独家避坑技巧与经验总结
除了上面这些通用问题,还有一些技巧是我在多次实践中总结出来的,常规文档里不会写。
技巧一:给上下文加一个“调试模式”。在开发环境下,给上下文对象加一个__debug属性,记录上下文的创建时间、创建位置、合并历史等信息。当出现问题时,这些信息能帮你快速定位。生产环境下把这个属性去掉,避免性能开销。
技巧二:用快照测试保护上下文结构。上下文的结构一旦确定,就不应该轻易改变。我通常会给每个模式的上下文写一个快照测试,任何结构变化都会导致测试失败,提醒开发者确认是否有意为之。这能有效防止“不小心改坏了某个模式”的情况。
技巧三:上下文变更要有日志。每次模式切换、每次上下文创建,都打一条日志。日志里包含模式名称、时间戳、触发来源。在排查“为什么突然变成这个模式了”这类问题时,日志是唯一的线索。
技巧四:不要过度设计。context-mode 不是银弹。如果只有两个模式、差异也很小,直接用条件判断可能更简单。我见过有人为了“架构优雅”而强行引入上下文模式,结果增加了不必要的复杂度。判断标准很简单:当模式数量超过三个,或者模式之间的差异维度超过两个时,再考虑引入。
技巧五:上下文的字段命名要统一。我见过一个项目,有的地方用editable,有的地方用canEdit,有的地方用isEditable。这种不一致在合并和读取时非常容易出错。建议在项目初期就定好命名规范,并且用 TypeScript 的类型检查来强制约束。
5.3 从问题排查到预防:建立上下文健康检查机制
排查问题固然重要,但更好的做法是预防问题。我在项目里建立了一套“上下文健康检查”机制,在开发和测试阶段自动运行,提前发现潜在问题。
健康检查的内容包括:结构检查,确保上下文的结构符合预期,没有缺失或多余的字段;类型检查,确保每个字段的类型正确;一致性检查,确保模式覆盖里的字段在基础行为里都有定义;完整性检查,确保所有模式都能正确创建上下文。
function healthCheck(context) { const issues = []; // 结构检查 if (!context.meta || !context.behaviors) { issues.push('上下文缺少必要字段'); } // 一致性检查 for (const field of Object.keys(context.behaviors.fields)) { if (!BASE_FIELDS.includes(field)) { issues.push(`未知字段: ${field}`); } } // 完整性检查 for (const mode of ['view', 'edit', 'audit']) { try { createOrderContext(mode, {}); } catch (e) { issues.push(`模式 ${mode} 创建失败: ${e.message}`); } } return issues; }这个健康检查函数可以在 CI 里运行,每次提交代码时自动检查。发现问题尽早修复,比等到线上出问题再排查要划算得多。
5.4 上下文模式的扩展与演进策略
context-mode 不是一成不变的,随着业务发展,它也需要演进。演进时要注意几个策略。
策略一:向后兼容。新增字段时给默认值,删除字段时先标记废弃、保留一段时间。这样旧代码不会因为上下文结构变化而崩溃。策略二:版本化。上下文的meta.version要随着结构变化而递增,反序列化时根据版本做迁移。策略三:渐进式迁移。不要一次性把所有模式都迁移到新结构,而是逐个模式迁移,每迁移一个就验证一个。策略四:保留回退路径。如果新结构出了问题,要能快速回退到旧结构。这要求新旧结构在一段时间内共存。
我在一个项目里经历过一次上下文结构的大改版。当时因为业务需求变化,需要把“字段行为”从扁平结构改成按分组嵌套的结构。我们采用了渐进式迁移:先在新代码里用新结构,旧代码继续用旧结构,通过一个适配层来兼容。迁移持续了大约两个月,期间没有出现大的问题。如果当时选择一次性全量迁移,风险会大很多。
6. 上下文模式在不同场景下的变体与取舍
6.1 前端表单场景下的轻量级实现
前端表单是 context-mode 最常见的应用场景。在这个场景下,我倾向于用轻量级的实现,不引入额外的状态管理库,直接用 React 的 Context 或者 Vue 的 provide/inject。
轻量级实现的关键是减少抽象层级。不要搞太复杂的钩子机制,不要搞太复杂的合并逻辑。表单场景的模式通常比较固定,差异也比较明确,直接用对象展开来合并就够了:
const context = { ...baseContext, ...modeOverrides[mode], behaviors: { ...baseContext.behaviors, ...modeOverrides[mode].behaviors } };这种浅合并的方式在表单场景下足够用,而且性能更好。只有在嵌套层级深、需要精细控制合并行为时,才需要引入深合并。
6.2 后端服务场景下的上下文传递
后端服务里也有 context-mode 的应用,比如一个 API 需要根据调用方的角色返回不同的字段。这时候上下文通常通过请求头或者 token 来传递。
后端场景的特点是上下文需要在多个服务之间传递。这时候上下文的序列化和反序列化就很重要。我通常会把上下文的关键信息放在请求头里,比如X-Context-Mode: admin,然后在网关或者中间件里解析成完整的上下文对象。
function contextMiddleware(req, res, next) { const mode = req.headers['x-context-mode'] || 'default'; req.context = createContext(mode, req.user); next(); }中间件模式的好处是上下文创建逻辑集中在一处,所有后续的处理函数都可以直接从req.context读取。新增模式时只需要改中间件,不需要改每个处理函数。
6.3 跨端场景下的上下文同步
跨端场景(比如同时有 Web、移动端、小程序)下,上下文需要在不同端之间同步。这时候最大的挑战是不同端的能力差异。比如 Web 端支持某个操作,但小程序端不支持。这时候上下文里需要包含“端能力”的信息。
我的做法是在上下文里加一个capabilities字段,记录当前端支持的能力。组件在渲染时同时检查behaviors和capabilities,只有两者都满足时才渲染。
const context = { meta: { mode: 'edit', platform: 'web' }, capabilities: { fileUpload: true, richText: true, camera: false }, behaviors: { ... } };这样同一份上下文配置可以在不同端复用,端差异通过capabilities来隔离。新增端时只需要定义该端的capabilities,不需要改行为配置。
6.4 什么时候不该用上下文模式
最后说说什么时候不该用 context-mode。第一种情况是模式极少且差异极小。比如只有“新建”和“编辑”两个模式,差异只是初始值不同,那直接用参数传递就够了,没必要引入上下文。第二种情况是模式差异是动态的、不可枚举的。如果模式的行为取决于运行时的复杂计算,无法预先定义成配置,那上下文模式就不合适,应该用策略模式或者直接写逻辑。第三种情况是团队对抽象层接受度低。如果团队里大多数人都不熟悉这种模式,强行引入会导致沟通成本高、维护困难。这时候可以先从小范围试点,积累经验后再推广。
判断标准可以总结成一句话:当“模式”是一个稳定的、可枚举的概念,且模式之间的差异可以用配置来描述时,context-mode 是合适的;否则,考虑其他方案。
我在实际项目中的体会是,context-mode 最大的价值不在于它有多高深,而在于它把“在什么情况下做什么事”这个判断逻辑变得显式和可管理。它让模式的变化从“散弹式修改”变成“集中式配置”,这在需求频繁变化的项目里能省下大量时间。但也要注意不要过度设计,简单的场景就用简单的方法,工具要匹配问题,而不是反过来。