👉目次
1 老旧的工程架构让业务开发走得越来越慢
2 上百个 npm 包堆栈的自动化发布体系
3 优化组件库的构建体积与速率
4 多堆栈带来逆境重重,刻意实验大仓脱困
5 怎样克制代码劣化
6 总结
腾讯底子开发中心负责维护着腾讯文档除编辑器外的大部门业务, 包罗 90+ npm 包与 170+ 的 CDN 组件,另有六个 application 服务,散落在七个业务堆栈中。随着业务量和开发同砚的渐渐增多,底子办法的不完满导致导致开发服从越来越低,一个业务需求须要高出两三个堆栈是常事。代码只须要写一行,发布测试包,更新版本,摆设环境这些反而须要一小时,大伙苦不堪言。而且多堆栈的底子办法维护也本钱越来越高,怎样进步多项目标开发服从,低落维护本钱,成为了急需办理的标题。
01
老旧的工程架构让业务开发走得越来越慢
须要管理的地方在那边,只有弄清晰病症才气够有效对症下药,我们通过 review 开发全流程,发现标题重要是这几个方向:
- 多 npm 包手动发布服从低下且不安全。
- 基建分散且老旧,构建速率和效果都不尽如人意。
- npm 包体积巨大,一个小功能引入 300kb,被接入方频仍吐槽。
- 上卑鄙依靠涉及项目多,更新依靠摆设环境流程繁琐。
- 体积连续增长,在天然熵增的天下中,怎样克制软件连续劣化。
下面聊聊我们是怎样办理这些标题,阅读完全文你能相识到:
- 上百个 npm 包的堆栈怎样实现全自动发布流。
- 怎样将 170+ 组件堆栈的构建时间低落到 2min。
- 真正当代的前端大仓实践履历,怎样举行大仓依靠管理,大仓中怎样搭建连续集成体系。
- 怎样优雅的将体积查抄与 CI 流程团结,连续监控
站点巨细。
02
上百个 npm 包堆栈的自动化发布体系
2.1 为什么这么难
我们有个堆栈维护着快要 140 个 npm 包,但是其基建却险些无人维护,接纳的还是多年前的一套老旧 webpack 构建,发布繁琐且杂乱。每位开发在改 bug 改到这个堆栈的时间都苦不堪言,纵然有文档的存在,一行小小的改动却动辄须要半天,典范的半天是如许度过的:
相比流程繁琐更为严肃的是流程不规范导致的代码差异源标题,有同砚为了快速验证,在自己的特性分支直接发 latest 版本而没有合入主干,导致别人再次发布 latest 版本的时间丢失了代码:
以致碰到过一次这种环境下该同砚去职了,只生存在他本地的那部门代码永久的消散了,只能按照业务逻辑重写一遍代码。
依靠杂乱脆弱,全部的 npm 包共用根目次下的依靠,升级依靠牵一发而动满身,很难评估依靠升级的影响范围,布满不安全感。幽灵依靠(由于 npm 会将整个依靠树打平放在一起,以是代码可以用到许多没有实际写入 package.json 中的依靠,很轻易有预期之外的依靠变更)标题频出,莫名其妙的依靠变更导致现网白屏也不是没有出现过,很须要一个安全可控的依靠环境
2.2 当代科技助力,发布安心又快速
我们的目标实在很明白,须要一套自动化发布的体系。开发只须要专注于业务代码开发,而权限,发布等等都交给体系。颠末调研和对比分析,我们选用了 pnpm + Nx + changeset + oci 这套技能栈来落地 npm 包的自动发布流。
pnpm 打造安全稳固的依靠环境
2024 年选用 pnpm 作为包管理工具应该无需多言,除了远超 npm 的依靠安装速率,其开箱可用的 workspace 特性做到了对多包堆栈最好的支持, Workspace protocol 可以让堆栈内的 npm 包之间的依靠直接使用本地代码,而不是从源上下载,确保堆栈内的依靠始终为最新的代码,极大的低落了依靠关系的复杂度。
同时我们以为应该把 link-workspace-packages 设置为 false,此时只有显式的把依靠版本设置成 workspace: 之后才会启用 Workspace protocol 使用本地的依靠,低落黑盒逻辑。(没有设置此字段时,只要版本匹配都会软链到本地)
而更为告急的一点是,与 npm 的 workspace 特性差异,pnpm workspce天然就是依靠隔离的,每个子包中都会基于其 package.json 安装一个 node_modules, 代码只能只能使用自己和其父目次 package.json 中列出来的依靠,而无法访问到别的子包的依靠,团结 pnpm 的特别 node_modules 布局计划,也克制了使用子依靠,实现彻底克制幽灵依靠标题。
在切换包管理工具过程中,我们使用一个脚原来举行依靠的迁徙:
- 通过 depcheck 查抄子包内使用到的依靠。
- 然后查询其在根目次 lock 文件中的实际安装版本。
- 再写回子包的 package .json 中。
- 删撤消根目次下的依靠。
每个 npm 包只须要关心自己子目次下的依靠变更。依靠变更安全又可控。
构建体系支持级联构建发布
各人都知道只是单包堆栈做自动化发布很简朴,接入 CI 之后在流水线内里构建完成之后使用公共账户 publish 一次即可,但是多包堆栈没那么简朴。难点在于须要关心包之间的依靠关系,好比这是实际业务开发中的一个很常见的一个依靠关系:
在从前的架构中,包之间依靠都是直接写版本,然后从源中下载,我们修改 A 的代码,须要举行构建,发布,然后再更新 A 的新版本到 B 中,再重复一次更新 B 的新版本到 C。在颠末 Workspace protocol 改造之后,包间依靠使用的始终为本地代码,不再须要关心依靠版本变更的标题,但是如许也带来了一个新的标题,须要举行依靠的级联构建。
由于每个包都依靠其依靠的产物,以是须要按次序的构建包 A->B ->C,如许能确保全部代码都是最新的,然后举行发布
在 CI 中我们须要一个工具,自动化的分析出包之间的依靠关系,举利用命的编排。这就须要引入一个新的概念「build system」。构建体系顾名思义就是在构建工具之上层的,负责使命调治的体系,前端比力盛行的就有 Nx,Turborepo,Rush,Lage 等,综合思量社区生态、接入本钱、使用心智负担之后,我们选择了 Nx。
通过使用 Nx,在依靠隔离之后,我们将构建也完全独立了出来。把原来的多 entry 单 webpack 实例的构建模式,拆分成了多 webpack 实例的模式,每个子包都可以独立的举行构建。
只须要简朴的设置上下令之间的依靠关系,告诉 Nx publish 使命依靠于 build 使命,而 build 使命又依靠于子依靠的 build 使命:
- {
- ...
- "targetDefaults": {
- "build": {
- "dependsOn": ["^build"] // ^ 代表其子依赖的任务
- },
- "publish": {
- "dependsOn": ["build"]
- }
- }
- }
复制代码 Nx 通太过析整个堆栈的依靠拓扑图就可以编排挤整个发布使命链。当我们修改了 A 包的代码之后,只须要简朴的运行一条下令「nx affected publish」,Nx 就会自动的运行一系列使命来举行发布:
可以看到根本和我们上面画的流程图划一,次序的举行依靠的构建,同时并行地举行发布,不外它是在一个包构建完成之后立即举行发布,而不是如上文流程图一样末了再举行发布,这显着服从更高,这是实际的使命实行环境:
那么上文提到的机密代码 nx affected publish 到底是什么意思呢,核心是 **affected,**其意义就是「受影响的」,Nx 会通过查抄代码变更,来分析出有代码变更的子包和其上游依靠包,好比我们更改了 lib10,那么受影响的范围如下:
Nx affected publish 的意思就是全部受影响的子包中运行 publish 下令,我们修改了 A 包的代码,其依靠链路上游全部包都受到了影响,以是全部发布了一次。
看到这里怎样举行自动化发布实在已经呼之欲出了,我们只须要在流水线中运行 pnpm i && pnpm nx affected publish ,即可完成之前大概须要人工操纵一小时的使命,但是另有一小块没有思量完满,那就是版本的变更。
测试版本的发布
对于测试版本的发布,我们的渴望是开发完全无感知,同时可以大概克制多分支开发时的版本辩论,以是我们通过一个下令 publish:beta 来自动的举行版本更新与发布,其核心逻辑就是取当前分支和时间戳更新 version,然后运行 pnpm publish 举行发布,值得注意的是,由于使用了 Workspace protocol,以是只能使用 pnpm 举行 publish,它会在发布的时间把 workspace: 更换成真实的版本号。
正式版本的发布
对于正式版本的发布,预期则差异,不像测试包只须要无脑更新一个测试版本号,正式包的发布须要依照 semver 规范,给定一个版本号 MAJOR.MINOR.PATCH
- 当举行不兼容的 API 更改时,升级 MAJOR 版本号。
- 在以向后兼容的方式添加功能时,更新 MINOR 版本号。
- 在举行向后兼容的错误修复时,更新 PATCH 版本。
也就是说流水线须要开发同砚来提供这个信息辅助发布,业界重要有两种方式,第一种是通过分析 commit msg 来分析须要发布的版本,代表是 lerna,另有一类就是我们选用的 changeset, 简朴来说就是开发同砚在本地运行下令天生一份文件阐明每个包须要更新的版本和 changelog,在流水线内里斲丧这份文件来举行版本更新和更新 changelog 文件。
为什么选择了 changeset,除了是 pnpm 与 turborepo 官方保举的版本管理工具以外,我们以为版本发布这个变乱应该更透明,让开发同砚更有掌控感。Lerna 是怎样通过 commit 信息分析出须要发布的版本是绝大多数开发同砚不明白的,而且在多包的场景下,每个 commit 大概对应多个包,开发同砚大概很长一段时间都搞不清晰我此次合入会发布哪些包,而 changeset 通过本地运行下令显式的天生 changeset 文件,让流程更加可控,手动输入的 changelog 也肯定水平上比 git 中的提交信息更故意义,流程如下:
天生的暂时文件就是一份 md 文档,固然你也可以手动举行更改:
在 mr 流水线中我们会查抄每个受影响的每个包是否都具有对应的 changeset 文件,没有则会阻断流水线不允许合入。
在合入主干之后运行 changeset version 斲丧这份 md 文件,更新包版本,这包罗 A 包自己的版本,与依靠了 A 包的 C 包中依靠的版本(固然如果是使用 workspace 协议,则无需 changeset 来更新版本,由于 pnpm publish 的时间总会使用当前堆栈中的版原来举行更换),然后更新 changelog 文件,末了将代码变更合回主干。
至此,完备的自动发布流水线就完成了,在 Nx 与 changeset 的支持下,CI 流程变得非常简朴,测试包发布就一行下令 pnpm i && pnpm nx affected publish:beta,而正式包发布也就多了一个下令 pnpm i && pnpm changeset version && pnpm nx affected publish:latest。今后开发同砚再也无需关心发布,只须要专注于代码开发,同时由于流程上的管控和权限收归,只有代码合入主干之后才气发布 latest 版本,从根本上包管了多版本之间的代码同源。总体的流程如下图:
此中只有蓝色部门是开发须要关心的部门,别的都是 CI 自动完成的,绝大部门的复杂度都封装在 Nx 内部,堆栈 CI 只须要关心版本与调用 Nx 下令。与改造前的流程对比,开发同砚须要关心的变乱从 16 个节点低落到了 2 个!
优化后:
2.3 进一步优化 npm 开发体验
在开发过程中我们发现,如果依靠关系的层级太深,不但是级联发布比力贫苦,开发起来同样也是,由于相互之间依靠的都是产物,以是须要一个一个地举行构建,纵然有 Nx 的加持(dev 使命依靠子依靠的 build 使命),也须要等待全链路的依靠跑完一遍构建,dev 模式形同虚设,怎样才气打造更丝滑的开发体验呢。
实在标题的关键在于引用的都是产物,以是须要等待构建,但是在开发模式下真的须要使用构建产物吗?答案是否定的,在开发阶段我们完全可以直接引用源码,如许就只须要启动 C 包的 dev 模式,通过源码直接引用到 A B 包须要的代码,享受最为快速的开发体验,但是我们同时也须要包管其在 publish 之后,引用关系是精确的。
使用 pnpm 的 publishConfig 可以实现这个目标,其效果是在 publish 之后可以更换部门 package.json 中的字段。以是 package.json 中的 main 字段可以直接写上 src/index.ts,通过 publishConfig 设置 main 字段为 dist/index.js,如许就可以实如今开发阶段引用源码,而发布之后是引用产物了。相比与直接在构建中通过 alias 改写引用路径,如许可以大概让全部维护同砚一眼就看到实际的代码引用关系,克制出现标题欠好排查。
- {
- "name": "foo",
- "version": "1.0.0",
- "main": "src/index.ts",
- "publishConfig": {
- "main": "dist/index.js",
- "typings": "dist/index.d.ts"
- }
- }
复制代码 03
优化组件库的构建体积与速率
3.1 npm 打包的最佳实践
颠末改造之后,各人发布都爽了,但是却发现发布产物却是不尽如人意,经常有品类同砚找过来说,为什么这个 npm 包这么大?经太过析产物发现,由于老旧过期的 webpack 构创建置,npm 包只会产出 cjs 格式的 bundle 后产物,而且险些没有举行任何 external,单 js 体积经常能到达几百 kb。颠末一系列的研究,终极乐成的让 npm 包接入品类带来的体积增长从 300kb 低落到靠近 0,详细的过程可以阅读:我是怎样把同一顶部栏接入 ppt 的体积影响低落到 0 kb(https://docs.qq.com/aio/DSmR5a0RKUVVFVlFT?p=MRJrw32LpHTT3RhSuZoebH)。
在 external 了全部的依靠之后,构建就能跳过依靠只须要处置处罚其源码,大大低落了单包编译时间,加持上 Nx 真正的多线程并发构建,全量构建时间从 7min 低落到了 2min。而在流水线中始终只须要按需构建受影响的包,从 push 到发布测试包大多数环境下只须要1min 以内。
通过这一系列预研与实践,总结出发布一个 npm 包的最佳实践:怎样打包一个当代化的 npm 包(https://docs.qq.com/doc/DSlhYdnJMbk1vandi),基于此最佳实践,搭建了堆栈脚手架,开发同砚无需阅读冗长的文档,只须要使用一条下令就能新建出符合最佳实践的 npm 包。
然而堆栈中一百多个 npm 包,并不是全部都是我们在维护的,如果贸然全量的更换了全堆栈的构建,由于不熟悉业务场景都没办法举行功能验证,以是我们接纳了渐进式的构建升级。在上文 pnpm + nx 的改造底子上,每个 npm 包已经可以独立拥有自己的依靠和构建设置,以是我们继续提供了两套差异的构建器在堆栈中,负责维护的开发同砚可以使用下令一键实现构建的更换升级,举行功能验证之后才合入主干,包管稳固性。
3.2 快,要再快一点!
docs-component 堆栈也是文管维护着的一个组件堆栈,与 npm 堆栈差异的点在于,其使用 CDN 的方式加载组件资源,由于不颠末品类的构建,以是很难复用依靠,无法举行 external,导致了其构建的压力天然就比 npm 包堆栈的更大。随着组件数目上升,构建的速率和乐成率越来越低,直到某一天,单 webpack 构建 170+ 组件的 OOM 标题导致构建再也无法乐成了。
多线程并发提速构建
在有了 sc 堆栈的履历后,我们同样选用了Nx 与 pnpm 来举行堆栈的改造,将每个组件都视为一个独立的 lib 包,拥有独立的构建脚本与依靠关系,如许一来单个 node 线程只须要负责一个组件的构建,彻底的办理了构建 OOM 的标题,纵然未来200,300 组件也没有任何标题,使用 Nx 的本事,可以并发使用真正多线程举行组件的构建,但是此时全量构建速率仅有小幅度的优化,推测大概与 common 目次重复编译和多 webpack 实例自己也有肯定的性能斲丧有关。
提速依靠安装
在之前的堆栈架构下,使用 npm 安装依靠,整个组件堆栈就只有一组 package.json 与其 lock 文件,将这两个文件 copy 到 docker 中,举行依靠安装后上传到云端,在下次流水线运行的时间,判定 package.json 与 lock 文件是否有变更,没有的话直接复用前次打包出来的 docker 镜像即可。
但是在 pnpm workspace 的架构下行不通了,由于此时每个子包下都有独立的 package.json,也就是说我们堆栈 100 多个组件就有 100 多个 package.json文件,似乎没有办法仅通过根目次下的 package.json 来完成依靠安装。
在调试过程中偶然发现,在流水线中,pnpm 会将 pnpm-store 安装在项目根目次下(pnpm 全部的依靠都会储存在这里,依靠安装的时间只须要举行 copy 与软链),以是我们选择使用 docker volumn 直接缓存 pnpm-store 与 node_modules/.pnpm ,流水线中再手动实行一次 pnpm install,由于 pnpm-store 中已经有前次流水线中下载下来的依靠了,部门环境下只须要几秒钟就可以大概安装完依靠。
但是 docker volumn 有个标题就是并不能跨构建机 ,我们构建呆板是一个集群,还是有一些环境下没有完备的缓存,须要举行下载,继续调研下来发现了 pnpm fetch 这个神奇的下令,它可以根据单个 lock 文件,把全部依靠下载到 node_modules/.pnpm 中,然后再实行 pnpm install –offline,重修依靠树,团结之条件到的 docker + npm 缓存方案可以得出如许一套方案:
- 将 pnpm-lock.yaml 文件 cp 到 docker 中。
- 实行 pnpm fetch,下载 node_modules/.pnpm。
- 上传 docker 镜像。
- 每次流水线运行的时间,只要 lock 文件没有变更就下载这个 docker,将 node_modules/.pnpm copy 到对应的目次。
- 实行离线 install 重修依靠树。
这套方案下来,流水线中的依靠安装时间大部门时间可以低落到 20s 以内。
进一步提速 CI
在 DC 这个组件堆栈中,组件之间实在并没有依靠关系,大多数环境下开发同砚修改的实在只是某一个组件,在颠末 Nx 改造之后,具备了单组件构建的本事,以是很天然就想到能不能低落每次流水线中须要构建的数目呢,如果每次都只构建开发同砚修改了的那一个组件,速率天然就上来了,重要的思绪就是两个方向:按需和缓存。
按需构建:
如果有认真阅读上文的话,就还会记得 Nx 的一项本事叫做 affected,在 npm 堆栈中的流程非常简朴,每次和主干举行对比即可得出有变更的 npm 包然后举行发布。但是 DC 堆栈中要轻微复杂一点,由于它是一个把组件发布到 CDN 的组件堆栈,以是在构建的同时天生了一份包罗全部组件 js 的 json 文件用于提供给加载器加载组件,这就导致了两个标题:
- 由于此堆栈须要走发布流程发布这份 json 文件,以是存在 feature 分支,release 分支与 hotfix 分支,差异性子的分支就须要区分对比分支了,在特性分支中很轻易想到与主干分支举行对比即可,但是发布分支还与主干分支对比符合吗?
- 在堆栈改在之前,这个 json 文件是通过单个 webpack 构建时直接基于中心产物信息天生的,改造之后每个组件都有了独立的构建流程,以是酿成了每个组件只负责产出自己的一部门 json 信息,构建流程竣事之后再获取全部组件的 json 信息举行一次拼接,那么在按需构建之后,怎样获取这部门没有构建的组件 json 信息呢?
实在这两个标题办理的关键就只有一句话,想想这次代码是须要更新谁人环境的这份 json 文件。
根据这句话分析,特性分支的代码终极是为了合入主干,发布分支的代码终极是为了发到现网,以是就可以大概得出结论 :
特性分支与主干举行对比,发布分支与现网分支举行对比。
那是否每次推送都须要和目标分支举行对比呢,答案是否定的。由于如果始终与目标分支举行对比的话,也是存在重复构建的。好比前次 commit 我修改了A 组件,这次 commit 我修改了 B 组件,预期应该是前次推送代码的时间已经更新过 json 信息中 A 组件的 js 信息了,此次只须要构建 B 组件更新其 json 信息,但是每次都与主干对比的话会发现这次推送依然触发了 AB 两个组件的构建,实际上是不须要的。以是当前分支已经存在的话,直接与前次 push 的 commit 对比即可得到最小的变更范围,如许就能得出终极的对比方案:
- 特性分支
- 新分支:与主干举行对比,取主干的 json 信息举行拼接,将当前分支 json 信息生存起来。
- 已存分支:与上一次 push 推送的节点举行对比,取当前分支的 json 信息举行拼接。
- 发布分支
- 新分支:与现网分支举行对比,取现网的 json 信息举行拼接,将当前分支 json 信息生存起来。
- 已存分支:与上一次 push 推送的节点举行对比,取当前分支的 json 信息举行拼接。
如许的战略下就实现了每次最小的构建范围吗,实在另有优化空间。在这套战略下确实每次都只构建了当前 push 所修改的组件,但是实在堆栈内另有一部门 common lib 包,组件的构建会依靠于这些 lib 包的构建。
存在以下构建依靠关系。
- 第一次 push:a->b->A。
- 第二次 push:a->b->c->B。
在前文的战略下,克制了重复构建 A 包,但是ab 两个 lib 包并没有代码变更而重复构建了多次,怎样办理这个标题呢,答案就是「长途缓存」
盘算缓存
Nx 可以在子包的粒度上举行缓存,也就是说每个 lib 包和组件包都拥有独立的缓存,通过简朴的设置即可实现 lib 包 a,b 复用缓存,只须要真正构建组件 A。查阅官方文档发现 Nx 的缓存设置储存在 node_modules/.cache/Nx-cache下,通过上文提到的使用 docker volumn 挂载上理论上就可以实如今 ci 中复用缓存。
但是实践下来发现缓存的掷中率并没有很高,通太过析发现我们的CI 是运行在一个构建集群上,每次启动流水线的时间按照负载分配一台呆板实利用命,但是 docker volumn 只能在本机的多次 ci 中复用,而无法跨构建机。
要怎么样实现跨构建机的缓存呢,那就得提到 Nx 的另一项本事「长途缓存」。
比方 webpackjest 等使命的缓存都是生存在本地的,判定缓存是否掷中只能从本地获取信息,而 Nx 支持将缓存信息上传到云端,在使命开始的时间优先从云端获取信息判定是否掷中缓存,如果掷中的话从云端获取产物信息,没有话再走本地缓存的判定逻辑,盘算完成之后再将产物信息上传到云端和本地缓存中。
然而 Nx 官网是保举使用 Nx-cloud 作为长途缓存的云端(也是其付费点)出于信息安全思量,司内的业务肯定是不能使用的。颠末调研我们发现 nx-remotecache-s3 可以用来在内网搭建长途缓存,简朴来说它可以使用腾讯云 cos 作为云端存储,从而实现安全的长途缓存。实现了跨构建机的缓存体系,以致未来可以实现 ci 与本地开发复用缓存,团队间复用缓存,新分支编译冷启动也能到达秒级。
只要在 CI 构建过组件,在本地开发时都不须要再次重复构建,同事 A 构建过的组件,全团队都可以复用到!做到不止是提速 CI,真正的全开发流程提速。
在长途缓存的参加下,根本实现了按需构建,绝大多数 CI 中构建都可以大概低落到 3min 以内,与全量构建动辄十几分钟优化了 80% 以上。而且这个数据不会随着组件数目增长而低落,如今 170 个组件是两分钟,未来 1700 个组件也是两分钟!
04
多堆栈带来逆境重重,刻意实验大仓脱困
随着一些优化本事的落地,各人的开发体验已经提速了非常多,但是最根本的标题还是没有办理,那就是堆栈太多了!在谁人时间,我们这边维护着两个 CDN 组件堆栈,两个 npm 包堆栈,三个 APPLICAtion 堆栈一共七个堆栈,为何会造成云云杂乱的汗青配景不谈,要同时维护这么多堆栈着实让开发同砚不堪重负。在极限环境下,一个小需求就会要涉及到三四个堆栈,开发流程如下:
一次 npm 包的代码更新须要涉及到四个堆栈,这显着是低效且不公道的。不但是摆设环境,开发的时间更加痛楚,各个堆栈开发下令各有各的风格,光是记取四个堆栈的 dev 下令就须要斲丧不少脑细胞,而在开发模式下将 npm A 的代码更新更新到 APP 堆栈 A 的开发环境更是难于登天。每个堆栈各自都是两三年前的基建老旧不堪,纵然是故意维护,这么多堆栈同步代码也是一件难事 这些本质都是由于多堆栈带来的标题,于是我们实验使用大仓(Monorepo)来办理。
4.1 大仓之路也没有那么简朴,标题逐个击破
大仓是不是就是简朴的将多仓的代码都 copy 在一起呢,答案肯定是否定的,如果只是单纯的复制代码到一起,那大概并享受不到大仓带来的长处,反而被不停扩大的堆栈规模拖累低落开发服从,以是我们须要使用一系列的底子办法本事来支持大仓下的开发摆设。
有了前面几个组件堆栈的实践履历,大仓的底子办法绝不夷由的选择了 pnpm workspace + nx +changeset,另有 changeset 是由于堆栈内的 npm 包如今还须要发布给外部使用。实在上文提到的几个组件堆栈在改造之后已经是某种意义上的大仓了,有许多大仓中须要办理的标题我们都已经有过履历,但是同样碰到了一些新的标题。
4.2 大仓中的连续集成计划
碰到的第一个新标题便是大仓下的流水线到底应该怎样计划呢?这里有一个条件就是腾讯文档前端服务都是接入了同一的流水线模板(通过 include 语法引用),然后使用一份设置文件来个性化定制功能,真正的流水线代码维护在一个同一堆栈内里,如下图:
但是其计划针对的都是单一服务的堆栈,没办法很好的支持大仓,岂非我们迁徙了大仓之后须要自己单独维护一套 ci 代码吗?如许会有很高的维护本钱并不划算,能不能继续基于这套计划来实现大仓的连续集成呢,答案是可以的:
- 将原先处于单仓根目次的设置文件降落到每个 APP 的子包中,包罗一系列的 CI 所需文件,其路径都在设置文件中举行修改,每个服务都有独立的设置。
- 开发一个 Nx 实行器(可以明白为一个 script 下令),核心逻辑就是通过 oci open api 触发引用的模版堆栈对外袒露的各种流水线启动自界说变乱,同时读取当前服务的设置文件当做环境变量转达已往,覆盖掉默认的设置文件读取活动
- 如许一来,对于运行中的流水线来说它感知到的 Monorepo 堆栈就是一个单服务堆栈,由于它收到的构建下令和产物路径等都是某个服务单独设置好的,无需模板堆栈任何修改即可完成接入。
- 在大仓的 push 流水线中实行雷同 pnpm nx affected oci-feature 的下令,Nx 会分析有代码更改的子仓,然后运行其 oci-feature 下令,而 oci-* 下令就是 2 提到的实行器,负责触发各种范例的流水线
总体流程如下:
另有很告急的一点就是,大仓下的流水线摆设肯定不能每次都全服务摆设,纵然我们如今的计划中是多流水线并行跑,我们须要可以大概实现按需摆设服务,没有跳着看的同砚大概就会发现,这个标题我们在 CDN 组件堆栈中已经有过解法,在特性分支中:
- 第一次推送的分支与主干举行对比,获取须要摆设的服务。
- 非初次推送的分支与前次推送的 commit 举行对比,获取须要摆设的服务。
但是作为须要摆设发布的子服务与单纯的组件还略有差异,我们还须要关心在发布分支中的服务摆设体现:
- 每周拉起的 C 端 release 分支。
- 稳妥起见,我们选择手动控制,分支添加服务后缀,显式的控制须要摆设的服务(以是会有多服务多发布分支)。
- 每三个月拉起的私有化 release 分支 (以是只有一条发布分支)。
- 新分支全量摆设。
- 非初次推送与前次推送的 commit 举行对比,获取须要摆设的服务。
通过 Nx,我们极大的低落了上层流水线的复杂水平,不须要繁杂的去取 diff 文件举行手动对比,然后区分各种范例的子仓触发差异的流水线。核心逻辑非常同一,在受影响的项目中实行一个下令而已,基于此逻辑,我们的上层流水线中逻辑非常的薄,流水线完全不须要关心到底有多少个子仓,其各是什么范例,有什么功能,只须要一条下令即可完成所须要做的变乱:
- pnpm nx affected oci-feature publish:beta
复制代码 这个下令的意义很轻易明白,就是在我们对比战略下得出有变更的项目,然后「并行的实行」它们的 「oci-feature」「publish-beta」。如许形貌大概不太精确,由于具有 oci-feature 的子仓就是前端服务,而有 publish:beta 的子仓则是一个个 npm 包,两个下令不会共存,Nx 会在受影响的项目中探求这两个下令,只实行其存在的,这就是为什么流水线不须要关心子仓的范例。
4.3 依靠管理
大仓下的另一个难点在于怎样举行依靠的管理,由于堆栈内的服务数目增多,依靠数目也变得更加的巨大,怎样计划一套公道的依靠管理体系,可以有效克制多实例标题与重复打包标题。
同一版本战略
将全部的依靠安装到的子仓的 package.json 中之后,那么根目次的依靠是否可以大概完全删除呢,实在也另有其作用。由于确实有一些依靠须要全部子仓举行版本同一,好比内部的组件库,大概 react 这种,那么这类须要同一版本的包我们会提升到根目次,同时在子仓的 package.json 中生存一份版本为 * 的引用,在根目次使用 pnpm overrides 覆写成须要的版本。
- {
- "dependencies": {
- "foo": "^1.0.0"
- },
- "pnpm": {
- "overrides": {
- "foo": "$foo"
- }
- }
- }
复制代码- {
- "dependencies": {
- "foo": "*"
- }
- }
复制代码 值得注意的是,只有 APP 范例的子仓可以大概使用这种战略,由于 npm 包须要保持发布后可用,须要在 package.json 中写明白依靠版本,这时间就得依靠在 CI 中使用 syncpack 来查抄版本同一了。
依靠使用规范
同时通过一系列的 lint 插件来规范依靠的使用:
- @Nx/eslint-plugin :克制子包之间直接通过源码引用。
- eslint-plugin-import(no-extraneous-dependencies):克制使用没有在当前 package.json 中声明的依靠,防止直接在根目次写依靠。
- no-restricted-imports: 克制使用某个依靠,好比:
- "no-restricted-imports": ["error", {
- "paths": [{
- "name": "lodash",
- "message": "请使用 lodash-es,更有利于 tree shaking"
- }, {
- "name": "moment",
- "message": "请使用 dayjs 替换~"
- }]
- }]
复制代码 4.4 怎样无损迁徙堆栈
大仓搭建起来了,另有个标题就是要怎样迁徙代码呢,如果只是简朴的 copy 文件夹,那么全部的汗青提交记载都丢失了,最可骇的是全部 bug 都会算在你的头上,那么怎样保持 git 记载迁徙堆栈到大仓内里呢,按照如下操纵即可。
- # 1.筛选源仓库需要的目录与git记录
- git-filter-repo --path packages/ --to-subdirectory-filter ark-module --tag-rename '':'ark-' --force
- # 2.修改文件夹名字
- cd packages && for dir in */; do git mv "$dir" "ark-${dir}"; done
- # 3.clone 代码到大仓中
- git remote add "ark" ../ark &&
- git fetch "ark" &&
- git merge "ark"/feature/ark_to_mono_1009 --allow-unrelated-histories --no-ff -m "update ark repo"
- # 4.合并目录
- git mv ark-module/packages/* packages/
复制代码 05
怎样克制代码劣化
每次举行的优化专项就像一次 ICU,可以大概短暂的修复好许多标题,但是这个天下天然就是熵增的,代码堆栈肯定会随着业务代码的膨胀而不停劣化,我们须要一些「一样平常体检」,来连续维护堆栈的康健,最好可以大概实现再也不须要走进 ICU。
5.1 粗糙的体积查抄标题多多
站点体积很大水平上的影响了网站加载的速率,以是很有须要控制整站的体积,只是颠末一两次站点减包是远远不敷的,没有监控 的话过一段时间体积又很轻易增长上去了,我们须要本事来监控 每次 MR 带来的体积厘革,严肃时阻断合入,克制体积的连续膨胀。一开始为了快速可用,我们的方案很简朴:
- 主干中扫描 js 体积,将 js 体积上传到某平台,作为基线体积。
- mr 的摆设流水线中顺带查抄产物 js 体积,与平台上的产物体积举行对比,超出红线设置则报错。
但是跑了一段时间发现碰到的标题太多了。
1. 扫描 js 体积得到的数据并不精确。
我们须要关心的数据实际是首屏加载的全部资源体积变更,好比 dynmic import 的 js 实际上是可以肯定水平的上放过的,而首屏的图片资源大概不测引入了某个样式库的全量 CSS 则是须要偏重关注的。
2. 查抄很轻易有偏差。
由于我们的开发同砚许多,而主干只有一份基线体积数据, A 同砚提交 MR 查抄的时间,已经有 BCD 同砚把代码合入主干,更新了基线体积,那么查抄出来的体积变更就并不实际是 A 同砚的 MR 造成的,由于基线体积此时已经有别人的代码导致变更了,每次都得一遍遍告诉开发同砚 rebase 主干后再重试,非常的浪费时间。
3. 摆设流水线实际无法阻断合入,效果有限。
有许多履历证实,MR 流水线的运行时间大概会大幅的增长代码合入的时间,由于开发自己也会由于等待流水线而忘记合入代码,更别说点开帮 CR 代码的同事了。而体积查抄肯定须要走一遍构建,以是我们为了低落团体的流水线耗时,直接把体积查抄内置在构建流程之后,摆设测试环境的流水线中,而不是单独的在 MR 流水线中。
但是工蜂的特性决定了只有 MR 流水线挂了的时间才气够阻断合入,以是纵然摆设流水线报错了体积非常增长也有许多同砚由于各种标题不举行处置处罚而合入主干,并没有到达我们的目标,怎么才气分身流水线速率和查抄效果呢?
4. 体积增长的泉源很难分析,开发同砚被阻断了合入只能来找我?
由于只是简朴的对比了 js 体积,在碰到体积增长报错的时间,开发同砚并不知道是什么缘故原由导致的,于是每一个报错的 MR 终极都会找到我,开发没有办法自查标题地点,我的人力都被斲丧在了资助各人分析体积标题中。
而且并不是全部的体积增长都须要拦阻,只故不测的大要积依靠引入大概不测环境导致的大面积重复打包标题才是须要处置处罚的,正常的需求开发引入的体积增长又得允许合入。我们是用一个 curl 下令来控制红线体积,以是经常须要判定允许合入之后,本地跑一个下令之后,让开发同砚 rebuild 流水线,非常低效。
5.2 怎样实现更优雅的体积查抄
怎样才气实现一套真恰好用的体积查抄体系,解放人力,同时最大水平的低落流程与开发同砚的负担?颠末调研,发现了这个工具很符合我们的需求,bundle-status 是一个举行产物分析的工具,可以通过与某次基线体积数据对比,得出详细的变更分析图:
通过看这个页面,很轻易就能看得出体积变更的缘故原由是什么,包罗重复打包标题也可以大概辅助举行分析,同时它可以大概区分首屏 js,媒体资源,CSS 等。但是他们官方只提供了插件与 ci 用于在本地运行,总不大概每个人碰到标题要他在本地切换反复分支,跑反复构建,再得出这个图吧,那样服从也太低了,怎样才气与流程团结提供丝滑流畅的体验呢?我们是如许计划的。
优化对比基线体积数据
针对上文提到的偏差标题,要怎样克制他人提交的 commit 的干扰?实在核心是怎样获取到精确的基线体积,优化方案是在每次代码合入主干之后,运行 bundle-status 的 baseline 模式, 将当前 commit 的 baseline.json 重定名为 {commit hash}.json 储存到 cos 中,如许每个 commit 都有一份对应的体积数据。
在流水线中举行查抄之前,通过 git merge-rebase 下令查找当前分支与主干的共同先人节点,也就是当前分支拉出来的谁人 commit 节点。
通过其 commit hash 获取这个 commit 节点的体积数据作为基线体积,如许对比出来的体积厘革就是始终复合预期的。
通过对比,超出红线限定的就会把其对比产出的的 html 上传到 CDN 中,然后通过流水线插件输出到 MR 品评区和体积查抄群中,方便开发同砚自助排查标题。
长途缓存助力 MR 流水线提速
在上文提到过的 Nx 长途缓存的本事资助下,我们将体积查抄流程移到了 MR 流水线中,体积查抄依靠于构建使命,而绝大多数环境下构建在提 MR 之前就已经跑过许多次了,也就已经缓存到了我们的长途缓存中。
MR 流水线中只须要 NX 从长途缓存中获取构建输出的产物,然后跑体积查抄的下令就好,实际所需时间少于一分钟,而 mr 流水线就可以大概真正的阻断合入。
同时通过调研发现,工蜂可以大概通过设置直接在页面上跳过未乐成的流水线查抄,很适当作为体积查抄的逃生舱,再也不须要一次一次的让开发 rebuild 流水线了。
团体的流程计划如图:
06
总结
这是一篇很长的文章,初心就是向各人先容一下我们底子开发中心在前端大仓的一些实践,但是大概各人也注意到了,这篇长文中篇幅最少的反而是前端大仓这一节,最告急的缘故原由就是大仓中许多须要办理的标题已经在前文中写过了,这篇文章的脉络实在也和我们的实践履历是完全符合的,一开始只是想办理自动化发布的标题,但是过程中就碰到了许多大仓中会碰到的标题。遇山开山,遇水搭桥,到了某个节点忽然发现,心中不绝想做的谁人大仓似乎有本事搭建出来了,也是一个渐进式的过程。之前两个堆栈的实践履历为背面真正的 docs-monorepo 堆栈创建助力不少,我也渴望这些履历能分享出来,资助到每一位碰到过这些标题的前端团队。
好了,不说废话了,来一次脱水版的总结,我知道不少同砚是直接拉到末了只看总结的:
依靠管理:
pnpm 办理彻底幽灵依靠标题,共同 syncpack 通过 pnpm overrides 来举行依靠的同一版本管理,Workspace protocol 软链堆栈间依靠,始终使用最新的本地代码。使用 docker voluem 提速 ci 中的依靠安装。
使用构建体系举利用命编排:
基于 Nx 自动编排使命依靠关系,使用 Nx 的「按需构建」和「长途缓存」本事,永久不运行重复和多余的使命,这里的使命包罗发布npm 包、触发流水线、构建、单测、lint 查抄等。
使用 oci 的 open api 与原有的流水线模板团结,通过 nx 触发差异子仓流水线,实现大仓的流水线计划。
防止代码劣化:
使用 bundle-status 举行体积查抄防止不测体积增长:主干中每个 commit 储存一份体积数据,mr 中获取源节点的体积数据举行对比分析,分析 HTML 上传 CDN 辅助排查标题。
前置使用 lint 克制出现不符合预期的代码,共同在流水线中查抄不允许不符合要求的代码合入主干。
同时回看全文会发现,根本上没有什么所谓自研工具,一方面是人力所限,另一方面就是我认同全部的代码本质都是技能债,都是须要维护本钱的,以是我的理念就是只管基于开源的代码,使用社区先辈的工具,用只管少的代码实现我们的目标,从而低落体系的复杂度,工程化的代码不应该是自研的黑盒,而是可以最大水平的可以让每位开发同砚一起共建的阳光玻璃房。渴望这些实践履历可以大概资助到每一位读到这的同砚,同时也感谢在这个过程中资助到我的每一位同事,给于我时机与支持的 leader 们。
-End-
原创作者|刘固
你以为前端步伐员还应该把握哪些技能呢?接待品评留言。我们将选取1则品评,送出腾讯云开发者定制眼罩1个(见下图)。7月10日中午12点开奖。
📢📢接待参加腾讯云开发者社群,享前沿资讯、大咖干货,找爱好搭子,交同城挚友,更有鹅厂雇用时机、限量周边好礼等你来~
(长按图片立即扫码)
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金 |