设计系统组件弃用迁移:用法清单、Codemod 证明与分阶段下线
给组件加上 @deprecated 不等于消费者已经迁移。本文用版本化生命周期注册表、静态与运行时用法闭包、幂等 Codemod 和视觉/可访问性门禁,让旧组件可量化退出并随时回退。
本文要点
- 主题聚焦:React、TypeScript、开源
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
设计系统把 LegacyModal 标成 @deprecated,主仓库搜索结果已经为零,发布 major 后仍有微前端从二次封装、别名导入或旧 lockfile 加载它。另一类事故是 Codemod 把 Button kind="danger" 改成新 prop,类型检查通过,却丢了键盘焦点和 loading 时的可访问名称。设计系统与 Design Tokens解释 token、组件分层和一般版本策略;本篇只治理具体组件 API 从弃用声明到物理移除的全过程。
SemVer 要求不兼容的公共 API 变更提升 major,但 major 本身不会替消费者完成迁移。npm 的 deprecate 命令给版本附加提示,也不等于撤回版本,更不应拿来标记整个仍可用包。组件级生命周期需要比包版本更细的机器合同,并把源代码、构建产物和真实消费清单连起来。
建立 componentLifecycle.v1
设计系统仓库发布不可变 designSystem.componentLifecycle.v1。每个条目用稳定 componentId 标识,而非可能变化的 export 名,记录 package、symbol 与导出路径、ACTIVE/DEPRECATED/BLOCKED/REMOVED 状态、首次弃用版本、最早移除版本和日期、替代组件、prop 映射、行为差异、codemod digest、owner、支持矩阵及批准例外。registry 与 package tarball、类型声明、文档构建使用同一 release digest。
componentId: overlay.legacy-modal
state: DEPRECATED
deprecatedIn: 8.3.0
removalNotBefore: 10.0.0
replacement: overlay.dialog
codemod: sha256:09c...
exceptionsExpireAt: 2026-10-01
状态只能单向推进;误弃用通过新 registry generation 恢复 ACTIVE 的特殊纠正事件,旧记录不改写。reader 遇到未知状态、无替代项或已过期例外时返回 UNKNOWN 并转人工,不能按已迁移计数。移除日期和版本两个条件同时满足才可进入候选删除,避免发布日期变化偷走迁移窗口。
先冻结受管全集,再求消费闭包
先发布不可变 consumerScope.v1:枚举所有受管 repo ID、默认分支、workspace、包注册表、已发布微前端目录、明确排除项及 owner,并对规范化全集计算 scopeRoot。少一个仓库不是“零消费”,而是 scope 变更,必须重新批准。扫描源码、Storybook、测试、MDX、生成模板和已发布包依赖图,解析 ESM/CJS import、re-export、别名、JSX member expression 与包装器;动态加载无法证明时标记 UNKNOWN。每份 snapshot 固定 Git tree、lockfile、解析器、registry generation,所有结果组成 snapshotRoot。仓库依赖图改动规划提供包级候选边,这里还要定位到组件与 prop。
Codemod 使用 AST 与类型信息,先产生计划,再按 cohort 写入。它必须保留注释、key、ref 和事件顺序,对 JSX spread 无法判定的 prop 进入人工列表。连续运行两次时第二次 diff 必须为空;反向 adapter 或 Git revert 必须能恢复旧用法。jscodeshift 适合执行批量变换,但 matcher、parser、版本及输入摘要都要固定。AST Codemod 重构不变量约束通用变换,本迁移还增加组件语义验收。
类型通过只是第一层。每个批准场景同时运行旧、新组件:比较用户事件与状态转换、焦点进入和归还、ARIA role/name/state、键盘操作、RTL 与多主题截图、响应式布局和 bundle 变化。截图必须使用固定字体与浏览器,并为动态区域声明 mask;差异超阈值只能由带到期时间的人工批准解释。替代组件若故意改变行为,应在 registry 列出迁移决策,不能通过扩大截图容差隐藏。
专门攻击“搜索不到”的路径
故障夹具覆盖 barrel re-export、重命名 import、memo/forwardRef、高阶组件、JSX spread、动态 map、MDX、旧版微前端、两个 lockfile、Codemod 中断和新旧 CSS 同载。还要从 scope 偷删仓库、交换 prop 默认值、删掉 aria-describedby 并让消费者降级安装。门禁必须检出 scope 漂移、未知消费、部分迁移、行为差异和依赖回退。进入 BLOCKED 后,每个受管 consumer 的构建以最新 snapshot 为基线,任何新旧组件引用、旧版依赖或新例外都直接失败,不能只禁止设计系统仓库新增 export。
进入 BLOCKED 前要求:受管 snapshot 新鲜度不超过 24 小时;用法分类率 100%;Codemod 二次 diff 为 0;交互与可访问性断言 100%;未批准视觉差异和 UNKNOWN 为 0;连续两个 release 旧调用为 0;50 个代表仓库自动迁移成功率至少 98%。删除证明必须同时绑定批准的 scopeRoot、全库 snapshotRoot、registry generation 和待发布 package tarball digest;任一仓库、lockfile、registry 或 tarball 改变就使证明失效并重扫。REMOVED 后再用旧 fixture 证明构建明确失败,而非解析到错误导出。
发布按仓库 cohort 推进:先只警告,再提供兼容 adapter,随后对新代码阻断旧用法,最后才删除 export。若错误率上升、焦点回归出现一次、未知消费重新出现或自动迁移失败率超过 2%,将 registry alias 切回上一 generation、恢复旧 export 与匹配样式,并暂停 cohort;不要 unpublish 已发布包。依赖升级兼容门确保消费者回滚 lockfile 与制品一致,React 组件可访问性则为焦点和语义断言提供基线。这样弃用才是可观测的迁移合同,而不是一句最终无人再看的警告。
Related