Zhupi222
首页关于项目技术文章联系方式

为什么要把多套部署差异做成 Recipe

2026-07-21T00:00:00.000Z约8分钟
从照片确认页的场地差异讲起,记录一套桌面应用为什么引入 Recipe,以及远程配置上线后怎样处理缓存和素材加载顺序。
Electron前端架构配置系统项目复盘

这套桌面拍照应用会安装在多个场地。拍照、确认、生成和打印等主要流程相同,但每个场地对页面有自己的要求。

以照片确认页为例,有的场地允许用户重拍,有的场地会隐藏重拍按钮;背景图和提示文字也可能不同。还有一些差异会影响流程,例如是否跳过确认页、打印前是否增加提示。最初只有一两个场地时,我们直接在组件里判断当前场地,再决定显示什么。

场地继续增加后,这种做法开始影响维护。一次普通的按钮修改,需要同时检查其他场地是否会进入同一个判断。调整一张背景图也要修改代码、重新打包,然后把新版本更新到设备上。场地之间的差异本来只是一组图片、文字和开关,却一直跟着应用代码发布。

Recipe 就是在这个阶段进入项目的。

为什么只拆组件还不够

组件拆分可以让单个文件变短,却无法消除场地差异。即使把每个场地写成独立组件,新场地上线时仍要修改前端代码;公共流程修复后,还要确认多个组件是否都同步更新。

这个项目的情况更适合保留一套公共流程,再把可变化的部分单独保存。应用启动时先确认当前场地,然后读取该场地对应的配置。页面组件仍然是同一份,只是拿到的背景图、文案、按钮位置和流程开关不同。

我们把这份配置称为 Recipe。这个名字可以理解成“当前场地怎样组装这套应用的说明书”。

一份 Recipe 里有什么

Recipe 最后被分成四部分。tokens 保存组件使用的样式和素材名;cssVars 保存全局颜色等 CSS 变量;slots 指定页面某个位置使用哪个组件;behavior 保存会改变流程的开关。

把它换成一份简化示例,会更容易理解:

const recipe = { tokens: { photoConfirm: { backgroundImage: 'confirm-background.png', confirmButtonText: '使用这张照片', }, }, slots: { photoConfirmFooter: 'ConfirmActions', }, behavior: { allowRetake: false, skipPhotoConfirm: false, }, };

照片确认页不再判断自己属于哪个场地。它只读取 photoConfirm 对应的配置。流程代码也不需要知道场地名称,只检查 allowRetake 或 skipPhotoConfirm。

新增场地时,大部分工作变成准备一份新的 Recipe。公共组件继续复用,场地之间也不会互相引用名称。

Recipe 怎样到达设备

设备启动后,会根据项目 ID 从服务端获取 Recipe。成功获取的 JSON 会缓存在 Electron 的用户目录中。写缓存时先保存成临时文件,完成后再重命名,避免程序中途退出留下半份 JSON。

下一次启动如果没有网络,应用会读取这份缓存。新设备既没有网络也没有缓存时,则使用安装包内附带的默认 Recipe。整个读取顺序如下:

服务端 Recipe 请求失败 → 上一次保存的 Recipe 没有缓存 → 安装包内的默认 Recipe

因此,默认 Recipe 也要保持完整。增加新的配置字段时,除了更新服务端 JSON,还要给默认 Recipe 补上一个可用值。

流程开关为什么不和默认值混在一起

页面样式和流程开关采用了不同的覆盖方式。

如果远程 Recipe 少了某个样式字段,页面可以继续使用默认背景或默认 className。这样至少能够正常显示。流程开关则不能这样处理。假设远程配置遗漏了 allowRetake,而本地默认值恰好是 true,设备可能开放一个场地原本不允许的操作。

所以,只要服务端提供了 behavior,应用就使用整份远程 behavior,不再把本地字段补进去。远程配置需要写得更完整,但排查流程时只需查看一个对象。

const remoteBehavior = recipeData?.behavior; if (remoteBehavior) return remoteBehavior; return resolveUiRecipe(appConfig).behavior;

第一次上线后,配置和素材没有同时准备好

Recipe JSON 很小,通常很快就能获取。背景图和视频需要另外下载到设备,速度要慢得多。

第一版代码拿到 Recipe 后,立即把素材名转换成本地 static URL。此时 ZIP 可能还在下载,组件已经开始读取不存在的文件,页面偶尔会出现空图。

后来我们把远程 tokens 的启用时间推迟到素材初始化完成。下载期间继续使用安装包内的默认页面,initDone 变成 true 后再切换到远程 Recipe。

if (!recipeData?.tokens || !assetResolver || !initDone) { return resolvedLocalTokens; } const remoteTokens = resolveAssets(recipeData.tokens, assetResolver); return { ...resolvedLocalTokens, ...remoteTokens };

这个修改解决了空图,但页面仍可能先显示默认主题,再突然换成场地主题。加载遮罩随后也被延长,覆盖 Recipe 请求和素材同步的整个过程。用户看到页面时,配置和文件都已经准备好。

Storybook 负责编辑和检查

Recipe 字段多起来后,直接修改 JSON 很容易写错,也很难提前看到页面效果。项目在 Storybook 中增加了 Recipe 编辑功能,可以切换配置、调整字段、查看对应页面,然后导出 JSON。

早期的编辑面板都写在当前项目中,包含大量状态同步和导出代码。后来这些通用功能被移到共享工具,项目只保留自己的字段类型和默认值。一次迁移删除了约 550 行项目内的连接代码,后续又移除了旧的 behavior 和 slots 面板。

Storybook 只负责编辑和预览。导出的 Recipe 仍需交给后端保存,再由设备启动时获取。

最后的结果

改造完成后,多个场地继续使用同一套页面和拍照流程。场地差异集中在各自的 Recipe 中,修改图片、文字或已有开关时,不需要继续往公共组件里添加场地判断。

远程请求失败时,设备可以读取缓存;缓存也不存在时,还有默认 Recipe。素材尚未下载完成时,远程页面不会提前出现。此前分散在组件、配置文件和素材路径中的几类问题,现在有了固定的处理位置。

这套系统仍有需要完善的地方。远程 behavior 要求内容完整,运行时校验还可以做得更明确。Recipe 版本和素材版本目前也没有组成一个统一版本,排查时需要分别确认。

Recipe 适合这个项目,是因为多个场地长期共享同一套流程,并且差异需要独立维护。如果每个场地的流程完全不同,或者页面只会部署一次,继续使用同一套 Recipe 反而会增加复杂度。

上一篇桌面设备的素材同步:首次全量、增量阈值与失败回退

相关

桌面设备的素材同步:首次全量、增量阈值与失败回退
2026-07-18T00:00:00.000Z约9分钟
Electron资源同步离线运行