腾讯文档前端工程架构改造实践,腾讯文档 编程

腾讯文档前端工程架构改造实践 👉目录 1 老旧的工程架构让业务开发走得越来越慢 2 上百个 npm 包仓库的自动化发布系统 3 优化组件库的构建体积与速度 4 多仓库带来困境重重,决心尝试大仓脱困 5 如何阻止

您所需要做的就是简单地配置命令之间的依赖关系,并告诉Nx 发布任务依赖于构建任务,而构建任务又依赖于子依赖的构建任务。

{

.

\’目标默认\’: {

\’构建\’: {

\’dependsOn\’: [\’^build\’] //^表示该子依赖的任务

},

\’发布\’: {

\’dependsOn\’: [\’构建\’]

}

}

}

Nx可以通过分析整个仓库的依赖关系拓扑来编排整个发布任务链。修改A包中的代码后,只需运行“nx受影响的发布”命令即可。 Nx自动执行一系列发布任务。

基本上它与我上面绘制的流程图相符,但是依赖项是在包构建后立即释放的,而不是像上面流程图中那样在最后释放。这显然效率更高。这是实际的任务执行。

那么当我们说上面神秘的代码受到nx影响是什么意思呢?通过检查代码变化及其上游依赖包来分析代码发生变化的子包,核心是受到影响。例如,如果更改lib10,则受影响的范围是:

Nx受影响的发布意味着在所有受影响的子包上运行发布命令。由于您更改了包A 中的代码,其依赖链接上游的所有包都会受到影响,因此它们都会暴露一次。

了解如何在此处自动进行发布,并只需在管道中运行受影响的pnpm i pnpm nx 即可完成以前需要一小时手动干预的任务。不过,还有一小部分尚未实施。这是已经考虑并最终确定的事情——版本更改。

测试版

通过发布测试版本,我们希望开发者完全透明,避免多分支开发时的版本冲突。所以核心逻辑就是使用publish:beta命令自动更新版本。点击当前分支和时间更新版本,运行pnpm public进行发布。注意,因为使用了workspace协议,所以发布时workspace:会被替换为实际的版本号。

正式发布

与测试包不同,你可以不假思索地更新测试版本号,官方发布的包版本号为MAJOR.MINOR,并且必须遵循semver 规范。修补

如果发生不兼容的API 更改,请升级主版本号。

当以向后兼容的方式添加功能时,更新次要版本号。

如果您有向后兼容的错误修复,请更新补丁版本。

换句话说,管道必须向开发人员提供此信息以协助发布。一种方法是通过解析提交消息来分析应该发布哪些版本。简单来说,开发者在本地运行一个命令,生成一个文件,指示每个包需要更新的版本和更新日志,然后在管道中使用这个文件来更新版本和更新更新日志。文件。

为什么选择changeset?除了是pnpm和turborepo官方推荐的版本控制工具之外,版本发布更加透明,给开发者更好的控制,我认为这样做是有必要的。 Lerna 如何解析提交信息以确定要发布的版本超出了大多数开发学生的范围。此外,在多包场景下,每次提交可能对应多个包,这对于开发同学来说可能难以理解。这次你想发布什么包?因为变更集通过运行本地命令显式生成变更集文件,所以手动输入的变更日志比提交的信息更有意义。 git的流程是:

生成的临时文件是md文档。当然,您也可以手动更改。

mr 管道检查每个受影响的包是否有相应的变更集文件。如果不是,则管道被阻塞并且无法合并。

合并到主干后,运行changeset version来使用这个md文件并更新包版本。这包括A包本身的版本和C包的版本,C包依赖于A包。(当然,使用workspace协议时,pnpm发布总是使用当前仓库中的版本。)不需要更新更改集中的版本,因为您要将其替换为新版本,然后更新更改日志文件,最后将代码更改合并回主干。

此时,有了Nx 和变更集的支持,CI 流程就变得非常简单了。 pnpm i pnpm nx 受影响的公共软件包版本只有一行。还有另一个命令pnpm i pnpm Changeset version pnpm nx 影响了publish:latest。从此开发同学不再需要担心发布,只需要专注于代码开发。同时,流程控制和权限恢复,让代码合并后才能发布最新版本。主干本质上确保了源的多个版本之间的代码同步。整体流程是:

开发者只需考虑蓝色部分,CI自动完成其余部分。仓库CI只需要考虑版本和Nx命令调用。与转换前的过程相比,开发人员需要担心的节点数量从16个节点减少到2个节点。

优化:

2.3 进一步优化npm开发体验

01

在开发过程中我们意识到,依赖太多不仅会导致级联发布变得繁琐,而且会因为互相依赖产品而导致开发变得困难,必须一一构建。即使有Nx To 支持(开发任务依赖于子依赖构建任务),你仍然需要等到完整的链接依赖运行完构建。我怎样才能创建一个?实现更流畅的开发体验。

其实问题的关键是所有的产品都被引用了,所以你必须等待构建。但开发模式下真的有必要使用构建产品吗?答案是不。使用这种方法,您只需启动C包的开发模式,然后通过源代码直接引用A和B包中您需要的代码即可。然而,为了享受最快的开发体验,您还应该确保发布后您的参考关系是正确的。

这可以使用pnpm 的publishConfig 来实现。这具有在发布后替换package.json 中的某些字段的效果。因此,直接将package.json的main字段写为src/index.ts,并通过publishConfig将main字段设置为dist/index.js,就可以在开发阶段参考源码了。产品发布后即可查看。与构建时直接通过别名重写引用路径相比,这可以让所有维护同学一目了然地看到实际的代码引用关系,避免出现难以排查的问题。

{

\’名字\’: \’嗯\’,

\’版本\’: \’1.0.0\’,

\’主\’: \’src/index.ts\’,

\’发布配置\’: {

\’主\’: \’dist/index.js\’,

\’打字\’: \’dist/index.d.ts\’

}

}

02

优化组件库构建量和速度。

3.1npm打包最佳实践

转换后,每个人都对发布感到满意,但我们发现产品发布并不令人满意。经常有同学问我这个npm包为什么这么大。在分析产品后,我们发现Webpack 构建配置已经过时,并且npm 包仅生成捆绑产品,几乎没有外部化。单个JS的体积往往达到数百kb。经过一番研究,我终于成功地将npm 包访问类别造成的大小增加从300kb 减少到接近0。详细过程请参考以下文章:我们如何将集成顶栏访问的大小影响从ppt 降低到0 kb(https://docs.qq.com/aio/DSmR5a0RKUVVFVlFT?p=MRJrw32LpHTT3RhSuZoebH)。

当所有依赖都外部化后,构建只需要跳过依赖并处理源代码,大大减少了单个包的编译时间。 Nx 真正的多线程并发构建将完整构建时间从7 分钟减少到2 分钟。 2分钟。您的管道应该只按需构建受影响的包。在大多数情况下,推送测试包并发布它只需要不到一分钟的时间。

通过这一系列的前期研究和实践,我们整理出了发布npm包的最佳实践:如何打包现代npm包(https://docs.qq.com/doc/DSlhYdnJMbk1vandi)。仓库脚手架就是基于这种最佳实践。开发人员可以使用单个命令创建遵循最佳实践的新npm 包,而无需阅读冗长的文档。

然而仓库里的100多个npm包并不是全部都是我们维护的。如果你过快地替换整个仓库构建,你将无法进行功能验证,因为你不熟悉业务场景。我们采取了逐步升级的方法。基于上面的pnpm + nx 转换,每个npm 包都可以独立地拥有自己的依赖项和构建配置,从而仍然在仓库内提供两组不同的构建器。负责维护的开发学生可以使用Command 1 键。允许构建替换和升级,只有在功能验证后才合并到主干以确保稳定性。

3.2 更快,更快!

docs-组件仓库也是Document Manager维护的组件仓库。与npm仓库的区别在于,它使用CDN来加载组件资源。由于没有类别构建,因此很难重用依赖项和组件。既然不能外部化,那么构建它的压力自然比npm包仓库要大。随着组件数量的增加,构建速度和成功率越来越低,直到有一天,170+组件的单个Webpack 构建遇到了OOM 问题,构建不再成功。

多线程并发加快构建速度

在获得了sc仓库的经验后,我还选择了Nx和pnpm来转换我的仓库。每个组件都被视为一个独立的lib包,具有独立的构建脚本和依赖项,并且应该只负责单个节点的线程。构建一个组件就彻底解决了构建OOM的问题。 Nx 功能允许您使用真正的多线程同时构建组件。这次的完整构建速度是微乎其微的。小的优化可能与公共目录的重复编译以及多个Webpack 实例本身的某些性能损失有关。

加快依赖安装速度

在之前的仓库架构中,我们使用npm来安装依赖,但是整个组件仓库只有一组package.json及其lock文件,我们接下来将这两个文件复制到Docker中,安装依赖并上传到云端。时间。检查管道是否已运行以及package.json和lock文件是否已更改。如果之前打包的Docker 镜像没有改变,则重复使用它。

但是,它不适用于pnpm 工作区结构。目前,每个子包都有独立的package.json,所以仓库中有100多个package.json文件,对应100多个组件。似乎没有办法只通过目录中传递根package.json来完成依赖安装。

在调试过程中,我偶然发现,在管道中,pnpm在项目的根目录下安装了pnpm-store(pnpm的所有依赖项都存储在这里,安装依赖项时只需复制和软链接)。使用Docker 卷时,直接缓存pnpm-store 和node_modules/.pnpm,并且管道再次手动运行pnpm install。 pnpm-store 已经在最后一个管道中下载了依赖项,因此安装可能只需要几秒钟。完全依赖。

然而,Docker 卷的问题是它们无法跨越构建机器。我们的构建机器是集群,缓存还没有完成,所以可能还是有需要下载的时候。经过进一步研究,我发现了神奇的命令pnpm。 fetch 可以基于单个锁定文件使用,将所有依赖项下载到node_modules/.pnpm,并运行pnpm install offline 来重建依赖项树。通过将其与上面提到的docker + npm 缓存解决方案相结合,可以实现这样的结果。解决方案:

将pnpm-lock.yaml 文件复制到docker 中。

运行pnpm fetch 以下载node_modules/.pnpm。

上传Docker 镜像。

除非锁定文件已更改,否则每次运行管道时,它都会下载此Docker 并将node_modules/.pnpm 复制到相应的目录。

执行离线安装以重建依赖关系树。

使用此解决方案,在大多数情况下,您可以将管道中依赖项的安装时间减少到20 秒以内。

CI进一步加速

在DC组件仓库中,组件之间实际上不存在依赖关系。大多数情况下,开发学生只在Nx 转换后更改特定组件,因此很自然地认为他们可以。减少每个管道需要创建的构建数量。如果开发学生只构建每次都会更改的组件,那么主要思想有两个方向:按需和缓存。

按需构建:

如果您仔细阅读上述内容,您可能还记得Nx 功能之一受到了影响。 npm 仓库中的进程每次与trunk 进行比较时都可以拾取修改后的npm 包。然后发布它。然而,DC Warehouse 有点复杂。由于这是一个组件仓库,将组件发布到CDN,因此在构建的同时会生成一个包含所有组件js的json文件,用于服务于loader。加载组件会引发两个问题:

这个仓库需要经过一个release流程来发布这个json文件,所以需要区分和比较feature分支、release分支和hotfix分支。但是比较发布分支和主干分支是否合适?

在仓库变更之前,这个json文件是构建单个Webpack时直接根据中间产品信息生成的。转换后,每个组件都有独立的构建过程,因此每个组件只输出自己的json信息。一旦构建过程完成,将检索并组合所有组件的json 信息。 那么按需构建后如何获取这部分未构建组件的json信息呢?

其实解决这两个问题的关键就一句话。考虑到此代码需要更新该环境中的json 文件。

分析这个语句可以看出,feature分支的代码是为了最终合并到主干,release分支的代码是为了最终在现网被发现,可以得出以下结论。

将功能分支与主干进行比较,将发布分支与实时分支进行比较。

那么每次推送都应该与目标分支进行比较吗?答案是否定的。这是因为如果不断地与目标分支进行比较,就会重复构建。例如,在上次提交中您更改了组件A,而在本次提交中您更改了组件B。这次我们推送代码的时候,期望json信息中A组件的js信息已经更新了。你只需要构建B组件来更新json信息,但是如果每次和trunk对比,你会发现这次push触发了两个组件AB的构建,其实这是没有必要的。因此,如果您当前的分支已经存在,您可以直接将其与上次推送的提交进行比较,以获得最小的更改范围并获得最终的比较计划。

特征分支

新建分支:与trunk对比,获取trunk的json信息进行拼接,并保存当前分支的json信息。

现有分支:与之前推送的推送节点进行比较,获取当前分支的json信息,并进行拼接。

发布分支

新建分支:与现有网络分支进行比较,检索并合并现有网络json信息,并保存当前分支json信息。

现有分支:与之前推送的节点进行比较,获取当前分支的json信息,并进行拼接。

这个策略每次都能达到最小构建范围吗?其实还有优化的空间。采用这种策略,每次只会构建当前推送改变的组件,但实际上仓库中有一些常见的lib包,构建组件依赖于构建这些lib包。

存在以下构建依赖项:

第一推:a-b-A。

第二次推动:a-b-c-B。

之前的策略避免了包A 的重复构建,但是两个lib 包ab 被多次构建,没有任何代码更改。解决这个问题的办法就是“远程缓存”。

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点开奖。

📢📢欢迎加入腾讯云开发者社群,享前沿资讯、大咖干货,找兴趣搭子,交同城好友,更有鹅厂招聘机会、限量周边好礼等你来~

(长按图片立即扫码)

#以上关于腾讯文档前端工程架构改造实践的相关内容来源网络仅供参考,相关信息请以官方公告为准!

原创文章,作者:CSDN,如若转载,请注明出处:https://www.sudun.com/ask/92953.html

Like (0)
CSDN的头像CSDN
Previous 2024年7月4日
Next 2024年7月4日

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注