组件库的版本锁定:升级前先冻住

去年十一月的一个周二,早上九点,测试群里有人发了一张截图:日期选择器点开之后,月份那一行变成了英文。前一天下午我们刚把内部组件库从 3.7.4 升到 3.8.0,改动说明写的是修几处样式。

三个前端项目一起升的,升完各自跑了一遍构建,都是绿的。测试复现了两个小时,只有在其中一个项目里能复现,另外两个正常。同一份代码,同一个版本号,出来三种结果。

问题查了三天。结论先讲:升级之前先冻结,冻结不是不升,是把谁能升、升到哪、什么时候升写进配置,让依赖树在一段时间里只有一个答案。

一、一次小版本升级,三天的排查

1.1 三个人各装了一个版本

周一上午我们三个人各自在本机装依赖。老周先装,装完提交了锁文件;小陈后装,本地有缓存,锁文件没动;我装的时候顺手执行了一次更新,把自己那份锁文件覆盖了上去。

三份锁文件在三个分支上,合并的时候只报了文本冲突,谁也没细看。冲突解决完,锁文件里组件库的版本号是新的,子依赖的版本号还是旧的,两份记录被压成了一份。

这类冲突很隐蔽。文件能合并,构建能过,只有运行时才露头。

1.2 依赖树里的两份副本

出问题的项目里,表单子包依赖核心包的 3.7 系列,我们直接装的核心包是 3.8。包管理器没有把两份合成一份,而是在依赖目录里放了两份,一份在顶层,一份嵌在表单子包下面。

日期控件是从表单子包里引出来的,它拿到的是嵌进去的那一份,语言包还是旧的。我们改的顶层那份,根本没被用上。

打印依赖树才看清这件事。层级有四层,核心包出现了两次,版本号差一个小版本。这个差异在构建日志里看不出来,构建只告诉你能不能通过。

我们把这次的依赖树截了一份,存进文档当反例。新人入职讲依赖管理,先看这张图,比讲十分钟浮动范围管用。图上看得很清楚:同一个包名,两个路径,两个版本号,谁都没报错。

1.3 真正花时间的地方

找原因只用了一个下午,剩下两天半在处理另外两个项目。要确认它们有没有同样的问题,要确认旧锁文件里的版本组合能不能回滚,还要跟测试解释为什么构建通过不代表没问题。

事后复盘,升级动作本身只花了二十分钟。风险全在三个人三台机器装出三种结果这件事上,而不在升级本身。

二、锁文件锁住了什么

2.1 锁文件的边界

锁文件记录的是某一次安装的结果:每个包的确切版本、下载内容的摘要、依赖关系。它保证同一份锁文件装出来的树是一样的。

它不保证的事有三件:新装一个包时会不会顺带抬高别人的版本;不同包管理器对同一份锁文件的解读是否一致;子依赖里的浮动范围会不会在下一次安装时被重新解析。

我们踩的是第三件。锁文件里原本有核心包的两条记录,一条 3.7.6,一条 3.8.0,合并时只留下一条,安装时按浮动范围重新解析,解析出了新的组合。

2.2 传递依赖的浮动

组件库自己的清单文件里,子包之间通常写一个带脱字符的范围。这个范围对组件库的维护者是方便,对我们的项目是风险,因为范围意味着同一次安装可能得到不同的结果。

处理办法是在项目这一层把这个范围收掉。项目里显式声明子包的版本,再用覆盖字段把整棵树里所有同名包按到同一个版本上。

收范围这件事要在项目里做,不能指望组件库改。组件库有十几个使用方,各自的发布节奏不同,让它收范围等于让它替所有人做决定。

收完之后有个副作用要提前说清:项目里显式声明的版本越多,升级时改动的地方越多。所以声明只写我们真正用到的子包,没直接引用的交给覆盖字段兜住,清单不至于膨胀。

2.3 把冻结写进配置

配置分三块:直接依赖写死版本号,不写范围;覆盖字段把所有子依赖按到同一版;引擎字段锁住运行环境和包管理器的版本,避免不同人用不同解析器。

json 复制代码
{
  "name": "console-web",
  "private": true,
  "engines": {
    "node": "20.11.1",
    "npm": "10.2.4"
  },
  "dependencies": {
    "@ui/core": "3.8.2",
    "@ui/form": "3.8.2",
    "@ui/table": "3.8.2",
    "dayjs": "1.11.10"
  },
  "overrides": {
    "@ui/core": "3.8.2",
    "dayjs": "1.11.10",
    "@ui/form": {
      "@ui/core": "3.8.2"
    },
    "@ui/table": {
      "@ui/core": "3.8.2",
      "dayjs": "1.11.10"
    }
  },
  "scripts": {
    "freeze": "npm ci --ignore-scripts",
    "verify": "node scripts/check-tree.js --expect 3.8.2"
  }
}

覆盖字段是这套配置的核心。它不管依赖从哪一层引进来,只要名字对上,就按这里写的版本来,解析器不会再去做范围匹配。

配完之后我们加了一个检查脚本,遍历安装后的树,把每个组件的版本收集起来,出现两个不同版本就返回非零退出码。这一步放在流水线里,构建之前跑,二十秒左右。

三、冻结窗口怎么开

3.1 一个版本号,三处对齐

冻结要先做的不是改配置,是选一个版本。三个项目共用一套组件,版本号必须一致,否则跨项目的组件行为对不上,联调时会互相怀疑。

我们把版本号写在一份共享清单里,三个项目的配置从清单里取。清单只有一行是人工维护的,其余由脚本生成。

第二步是同步锁文件。约定只有一个人能生成锁文件,其他人一律按锁文件安装,不执行更新命令。谁本地执行了更新,流水线会检测到锁文件变化并拦下来。

3.2 升级的四步

升级流程定成四步:先在隔离分支上升一个项目,跑全量回归;再升第二个项目,对比两个项目的截图差异;第三步升第三个;第四步统一放开。

每一步之间隔一天,留出观察时间。这个节奏看着慢,实际上一个小版本从决定到全量,五天能走完,比出问题再回滚快得多。

升级分支上要带三样东西:版本号变更、回归截图、依赖树检查结果。缺一样不合并。

截图对比这件事一开始没人愿意做,觉得是形式。做了两轮之后意见变了,因为组件的改动大多在细节上,表格行高差两像素,按钮的禁用态颜色浅一档,靠肉眼在测试环境里翻页面很容易漏。截图摆在一起,差异会被框出来。

3.3 回滚要能在十分钟内完成

回滚不是把版本号改回去那么简单,锁文件也要回到上一版,否则下次安装又会解析出新的组合。我们把每次升级前的锁文件打上标签存在仓库里,回滚就是检出标签、重新安装、重新构建。

演练过一次。从发现问题到线上恢复,用了九分钟,其中六分钟在等构建。这个数字要练,不练的话真出事时会在该改哪个文件上耗掉半小时。

四、冻结不是把版本钉死一辈子

4.1 版本债要记账

冻结时间长了,组件库的新功能用不上,修正类补丁也进不来。我们建了一张表,记每个组件当前版本、可用版本、冻结原因、解冻条件。

表里有一列是欠了多久。超过两个季度没升的,下一次迭代必须安排一次升级,不管有没有业务需求。这条规则执行得不算严,但至少让忘了升这件事有地方露头。

还有一列是影响面。组件被多少个页面用到,写清楚。影响面大的,升级前要多跑一轮回归;影响面小的,升级可以合并到例行迭代里。

4.2 什么时候解冻

解冻的触发条件有三种:组件库发了修正类补丁;我们要用的新功能只在新区间里;冻结超过一个季度。

三个条件里,补丁优先级高,当天升。新功能次之,跟着迭代走。到期解冻放在版本规划会上定,不单独插队。

解冻之后重新冻结,流程一样。冻结和解冻都不是一次性动作,是一套周期。真正让这套东西跑起来的,不是配置写得多严,是有人定期翻那张表。

常见问题

问:直接依赖写死版本号,会不会导致修正补丁进不来?

答:会,所以要配合版本债的表。写死只是把什么时候升的决定权从安装时挪到人手上,不是永不升级。补丁类升级优先级高,走当天的通道。

问:覆盖字段和锁文件同时用,会不会互相打架?

答:不会,两者管的事不同。覆盖字段管解析规则,锁文件管安装结果。改了覆盖字段之后要重新生成一次锁文件,之后一律按锁文件安装。

问:小团队只有两三个人,也需要这套流程吗?

答:人少可以省掉审批,但覆盖字段和锁文件由一个人生成这两条建议保留。人少的时候问题不是没人审,是没人记得谁改过。

问:怎么判断依赖树里有没有重复的包?

答:装完之后遍历一次,按包名分组,版本数大于一就报出来。做成脚本放进流水线,比出问题再翻目录省事。

相关推荐
QianhangQianping5 小时前
一块大屏的数据在浏览器里怎么变成画面:背景、子屏、前景三层结构
前端·低代码·架构·编辑器·数据可视化·大屏可视化
她的男孩7 小时前
低代码加字段后表结构怎么同步:预览安全 DDL,高风险变更再强制执行
java·spring boot·低代码
搭贝7 小时前
【无标题】
人工智能·低代码·ai·1024程序员节·搭贝
收纳过往1 天前
数据库加索引的时机:慢查询日志说话
android·开发语言·数据库·python·低代码·rxjava
昵称画1 天前
图文:一张十年前的车间系统架构图
低代码·系统架构·php·企业微信·rxjava·零售
驰骋工作流1 天前
流程驱动与菜单过程:两种系统设计出发点的差别驰骋BPM驰骋低代码低代码工作流引擎
低代码
得帆低代码PaaS2 天前
一张 Excel 台账什么时候该迁成应用:四个信号、五步迁移与权限设计
低代码·数据建模·权限管理·系统迁移·制造业
山东孙燕姿2 天前
设备台账重构:从一张表到三层模型
android·java·网络·低代码·重构·rxjava
她的男孩2 天前
Spring Boot 表单挂 Flowable 审批:设计器绑模型后,业务侧用注解把单子跑起来
spring boot·低代码·工作流引擎