摘要
腾讯基础开发中心负责腾讯文档相关业务,面临多仓库开发效率低、维护成本高的问题。文章讲述了针对老旧工程架构的改造实践,包括 npm 包自动化发布、组件库构建优化、大仓脱困尝试以及防止代码劣化的措施等。
腾讯文档前端工程架构面临的问题
腾讯基础开发中心负责腾讯文档除编辑器外的大部分业务,涉及众多的 npm 包、CDN 组件和 application 服务,散落在多个业务仓库。随着业务和人员增长,旧架构下开发效率低,如一个小业务需求可能涉及多个仓库,发布测试包等流程繁琐,耗时久。且多仓库基础设施维护成本高。
npm 包仓库自动化发布系统的构建
发布困难的原因
有个近 140 个 npm 包的仓库,基建老旧,采用老旧 webpack 构建,发布繁琐混乱。文档虽有但改动仍耗时,且存在流程不规范导致的代码不同源问题,例如有同学在特性分支直接发 latest 版本不合入主干,离职后部分代码丢失。还有依赖混乱脆弱、幽灵依赖等问题。
借助现代科技实现快速发布
- pnpm 打造安全稳定依赖环境:2024 年选用 pnpm 作为包管理工具,其有远超 npm 的依赖安装速度,workspace 特性对多包仓库支持好,Workspace protocol 可让仓库内 npm 包依赖使用本地代码,降低依赖复杂度。将 link - workspace - packages 设为 false,只有显式设置依赖版本为 workspace:才启用 Workspace protocol 使用本地依赖,减少黑盒逻辑。且 pnpm workspace 天然依赖隔离,结合特殊 node_modules 结构设计,可彻底避免幽灵依赖。在切换包管理工具时,通过脚本迁移依赖,使用 depcheck 检查子包依赖,查询根目录 lock 文件中的实际安装版本写回子包 package.json,删除根目录依赖。
- 构建系统支持级联构建发布:多包仓库自动化发布难点在于包之间的依赖关系。以前架构中包间依赖写版本从源下载,修改代码后需多次构建和更新版本。改造后包间依赖使用本地代码,但带来依赖级联构建问题。引入构建系统概念,前端有多种流行构建系统,综合考虑选择 Nx。使用 Nx 后构建独立出来,可配置命令间依赖关系,Nx 通过分析依赖拓扑图编排发布任务链。如修改 A 包代码,运行
nx affected publish
命令,Nx 会按顺序构建并发布相关包。 - 测试版本和正式版本发布
- 测试版本发布:期望开发无感知且避免版本冲突,通过
publish:beta
命令自动更新版本发布,核心是取当前分支和时间戳更新 version,再用 pnpm publish 发布。由于使用 Workspace protocol,只能用 pnpm publish,发布时会替换 workspace:为真实版本号。 - 正式版本发布:需要遵循 semver 规范。业界有通过解析 commit msg 分析版本(如 lerna)和开发同学手动生成 changeset 文件(如选用的 changeset)两种方式。选择 changeset 是因为它是 pnpm 与 turborepo 官方推荐,且让开发同学更有掌控感。开发同学在本地运行命令生成 changeset 文件说明每个包更新版本和 changelog,在流水线中消费此文件更新版本和 changelog 并合回主干。
- 测试版本发布:期望开发无感知且避免版本冲突,通过
- 优化 npm 开发体验:如果依赖关系层级深,开发体验差。开发阶段可直接引用源码提升速度,通过 pnpm 的 publishConfig 实现,如 package.json 中
main
字段开发时写src/index.ts
,发布时配置为dist/index.js
。
优化组件库的构建体积与速度
npm 打包最佳实践
老旧的 webpack 构建设置使 npm 包产出 cjs 格式 bundle 且无 external,体积大。经研究,external 所有依赖后可降低单包编译时间,加持 Nx 多线程并发构建,全量构建时间从 7min 降到 2min,且流水线按需构建受影响包,多数情况 1min 内可从 push 到发布测试包。基于此总结出发布 npm 包的最佳实践并搭建仓库脚手架,还采取渐进式构建升级,提供两套构建器供开发同学验证功能后合入主干。
进一步提升速度
- 多线程并发提速构建:docs - component 仓库使用 CDN 加载组件资源,复用依赖困难,随着组件增多构建速度和成功率降低,甚至出现 OOM 问题。选用 Nx 和 pnpm 改造,将每个组件视为独立 lib 包,解决 OOM 问题,使用 Nx 并发构建组件,但全量构建速度仅有小幅度优化。
- 提速依赖安装:之前仓库架构下,npm 安装依赖通过 copy 根目录 package.json 和 lock 文件到 docker 安装依赖后上传云端,下次流水线判断文件是否变更决定是否复用镜像。但 pnpm workspace 架构下每个子包有独立 package.json,不能仅通过根目录文件完成依赖安装。调试发现 pnpm 会在流水线中将 pnpm - store 安装在根目录,可选择用 docker volumn 缓存 pnpm - store 和 node_modules/.pnpm,再手动执行 pnpm install。但 docker volumn 不能跨构建机,又发现 pnpm fetch 命令,结合之前方案得出新方案:将 pnpm - lock.yaml 文件 cp 到 docker,执行 pnpm fetch 下载依赖,上传 docker 镜像,每次流水线运行时 lock 文件无变更就下载此 docker,将 node_modules/.pnpm copy 到对应目录,执行离线 install 重建依赖树,可使流水线依赖安装时间大部分降低到 20s 以内。
- 进一步提速 CI:DC 组件仓库组件间无依赖关系,Nx 改造后有单组件构建能力,可按需和缓存来降低每次流水线构建数量。
- 按需构建:Nx 的 affected 能力可得出有变更的 npm 包进行发布,但 DC 仓库构建时生成包含所有组件 js 的 json 文件用于加载组件,存在特性分支、发布分支与不同分支对比的问题,还存在重复构建问题。通过分析得出不同分支对比策略,且为避免组件依赖的 lib 包重复构建,采用 Nx 的远程缓存能力。Nx 可在子包粒度缓存,官方推荐 Nx - cloud 作为远程缓存云端但司内业务不能用,调研发现 nx - remotecache - s3 可在内网搭建远程缓存,实现跨构建机缓存,提升开发全流程速度。
多仓库困境与大仓脱困尝试
多仓库的困境
维护着七个仓库,一个小需求可能涉及三四个仓库,开发流程复杂低效。开发命令风格各异,开发模式下更新代码困难,各仓库基建老旧,同步代码难,这些都是多仓库带来的问题。
大仓的搭建与问题解决
- 基础设施选择:大仓选择 pnpm workspace + nx + changeset 作为基础设施,其中 changeset 用于仓库内 npm 包发布给外部。
- 大仓中的持续集成设计:腾讯文档前端服务接入统一流水线模板,其设计针对单一服务仓库,大仓迁移后不想单独维护 CI 代码,于是将单仓根目录的配置文件下降到每个 APP 子包中,开发一个 Nx 执行器,通过 oci open api 触发流水线启动自定义事件并传递当前服务配置文件作为环境变量。在大仓 push 流水线中执行
pnpm nx affected oci - feature
命令,Nx 分析有代码更改的子仓并运行 oci - feature 命令。大仓的流水线部署要按需部署服务,在特性分支对比策略获取需要部署的服务,发布分支中根据不同类型分支采用不同部署策略。 - 依赖管理
- 统一版本策略:将所有依赖安装到子仓 package.json 后,根目录依赖对需要统一版本的包(如内部组件库、react 等)仍有作用,将这类包提升到根目录,在子仓 package.json 中保留版本为*的引用,根目录用 pnpm overrides 覆写版本。但只有 APP 类型子仓可用此策略,npm 包需在 package.json 写明白依赖版本,CI 中用 syncpack 检查版本统一。
- 依赖使用规范:通过一系列 lint 插件规范依赖使用,如
@Nx/eslint - plugin
禁止子包间源码引用,eslint - plugin - import(no - extraneous - dependencies)
禁止使用未声明的依赖,no - restricted - imports
禁止使用某些依赖并给出替换建议。
- 无损迁移仓库:迁移代码时要保持 git 记录,操作步骤为筛选源仓库目录与 git 记录、修改文件夹名字、clone 代码到大仓中、合并目录。
阻止代码劣化的措施
粗糙体积检查的问题
站点体积影响加载速度,需要监控体积变化,但最初的体积检查方案问题多多。扫描 js 体积数据不准确,应关注首屏加载资源体积变更;检查易有误差,因为多人提交代码会更新基线体积;部署流水线无法阻断合入,效果有限;体积增长来源难分析,开发同学无法自查问题。
更优雅的体积检查实现
使用 bundle - status 工具进行产物分析,官方提供本地运行的插件与 ci,但效率低。优化方案为每次代码合入主干后,运行 bundle - status 的 baseline 模式,将当前 commit 的 baseline.json 重命名为{commit hash}.json 储存到 cos 中,流水线检查前通过 git merge - rebase 命令查找共同祖先节点获取其体积数据作为基线体积,对比超出红线限制则将对比产出的 html 上传到 CDN 并输出到 MR 评论区和体积检查群中方便开发同学排查。同时借助 Nx 远程缓存能力将体积检查移到 MR 流水线中,工蜂可配置跳过未成功的流水线检查。
总结
- 依赖管理方面:pnpm 解决幽灵依赖问题,syncpack 和 pnpm overrides 进行依赖版本管理,Workspace protocol 软链仓库间依赖,使用 docker voluem 提速 CI 依赖安装。
- 构建系统任务编排方面:基于 Nx 自动编排任务依赖关系,利用 Nx 的按需构建和远程缓存能力避免重复多余任务。使用 oci 的 open api 与流水线模板结合,通过 nx 触发不同子仓流水线实现大仓流水线设计。
- 防止代码劣化方面:用 bundle - status 进行体积检查,主干中每个 commit 储存体积数据,mr 中对比分析,分析 HTML 上传 CDN 辅助排查。前置 lint 检查阻止不符合预期代码合入。文章尽量基于开源代码,降低系统复杂度。