【第923期】基于 Git、Svn 的 Commit 实现可增量构建的前端持续集成解决方案
前言
当看到这个标题的时候,我是被吸引的因为很多时候在持续集成方面都是运维在处理的,在项目场景上还没处理过这种的。今日早读文章由厦门欢乐逛 @ 糖饼分享。
正文从这开始~
近两年由于技术的发展,Web 前端可以通过编译工具来实现 HTML、CSS、JS 所做不到的事情,从而覆盖更多的业务,从技术角度实现更多的价值。社区也在不断的创造更好的编译、构建工具,例如目前大红大紫的 Webpack。正因为这些先进的工具,我们工作效率得到了前所未有的提升。当然,我们也需要面对它们所带来的一些问题:构建速度越来越慢,导致发布速度越来越慢。尤其是在使用持续集成系统来构建的项目中,这个问题越严重。
解决构建慢的问题有很多途径,比如常见的手段是优化构建工具的配置,网上也有很多这样的实践经验文章,这些优化手段大多都是针对具体的工具、本地开发构建进行的,如果使用持续集成服务器进行构建,社区缺乏一些简单可靠增量构建解决方案。针对于此,我给大家分享我们前端团队(厦门欢乐逛)的实践成果:基于 Git、Svn 的 Commit 实现可增量构建的前端持续集成解决方案。
背景
大约是 2014 年的时候,我们在 Git 服务器上通过 Githooks 、Grunt 实现了一个复杂的前端增量构建系统:提交代码到对应分支后服务器会自动进行增量构建、增量发布。这套系统这在当时看来自动化程度已经很高了,解决了本地构建、发布所带来的效率以及安全风险,版本发布非常快速。当时前端团队的构建与发布流程:
代码提交到开发分支:自动构建
代码提交到主干分支:自动发布
2014 ~ 2017 年之间,我们业务飞速发展,前端项目越来越多,构建这一块也被更先进的 Gulp 与 Webpack 代替,而之前基于 Grunt 设计的增量构建系统已经无法适应新业务与技术的需求,项目部署、团队协作的成本越来越非常高。这迫使我们思考如何实现一个不受具体构建程序约束、跨业务、支持增量构建与发布的标准化解决方案。
决定做这个事情之前,我们先将 Githooks 触发的构建与发布任务由持续集成系统代替,以让前端开发流程与工具标准化。
持续集成
“持续集成是一种软件开发实践。它倡导团队开发成员必须经常集成他们的工作,甚至每天都可能发生多次集成。而每次的集成都是通过自动化的构建来验证,包括自动编译、发布和测试,从而尽快地发现集成错误,让团队能够更快地开发内聚的软件”
以上是持续集成的概念,一个完整的持续集服务由以下几个系统组成:
一个自动构建过程,包括自动编译、分发、部署和测试等。对于前端项目,这里往往是 Gulp、Webpack、Mocha 等工具来实现。
一个代码存储库,即需要版本控制软件来保障代码的可维护性。如 Git 或 Svn 等。
一个持续集成服务器。如 Gitlab CI、Travis、Jenkins 等。
由于我们公司内部代码托管平台使用 Gitlab 搭建的,因此直接采用了 Gitlab 自带的 Gitlab CI 作为构建服务器,这样能够与目前工作流无缝整合在一起。
增量构建
本地开发中,规模较大的项目一般会拆成多个模块,单独进行编译来提高构建速度,根据不同的参数来构建指定模块。
而在持续集成系统中,最初我们通过判断 Git 提交的消息的特殊标记来决定构建哪些模块,例如构建 “users” 模块:
git commit -m "[publish:users] 修复线上 BUG #456"
这种简单的开发约定可以让服务器做到精确构建,不足之处是需要人工介入,存在风险。例如:开发者修改了公共模块后,如果忘记构建依赖了它的业务模块,这很有可能引起线上故障。
理想的情况下,项目开发人员无序关注细节,只需要关注工作本身。测试、生产环境的构建与发布的细节应该完全由持续集成服务完成。
watch
为了实现完全自动化,我们使用检测文件修改的方式来触发增量构建,不同于本地开发中的 --watch 模式,我们采用 Git 的 Commit ID 来实现。原因:持续集成系统需要明确的知道任务的成功与失败状态,而构建与编译工具自带的--watch 会导致进程常驻,无法获取运行结果。
gitCommit.watch('./users', (last, pre) => {
if (last.id !== pre.id) {
exec('cd users && webpack --color');
}
});
// [more code..]
gitCommit.watch() 方法会记录上一次的提交版本,对比新旧提交版本即可决定是否启动构建,从而实现增量构建。这种基于版本仓库的变更对比使用 md5 要高效很多,并且能够让发布后的文件和版本库关联起来。更加重要的是它是成熟的解决方案,这对系统的稳定性至关重要。
标准化
项目中通常都有自己的构建脚本,如果再添加增量构建逻辑这无疑会加剧构建脚本的复杂度、带来更高的成本。因此我们设计了一种描述增量构建的任务的配置格式,然后实现任务调度器来解析它们、运行任务,以实现对业务原有构建流程的解耦。例如:
{
"tasks": ["users", "photos"],
"program": "cd ${taskPath} && webpack --color"
}
tasks 是要观察的目标列表,它是文件或者目录;program 是它们发生变更后的处理命令;${taskPath} 被设计为一个变量,运行时解析到当前构建目标。
按需全量构建
在业务中,免不了需要全量构建的情况,例如:导航的替换需要重新构建所有业务模块。
这种情况下需要添加 dependencies 来描述依赖,以让任务调度程序能够处理依赖。例如公共模块 “common” 发生版本变更,就执行全量构建:
{
"tasks": ["common", "users", "photos"],
"program": "cd ${taskPath} && webpack --color",
"dependencies": ["common"]
}
如果 Npm 的模块发生版本变更也需要进行全量构建,将 package.json 添加到 dependencies 即可:
{
"tasks": ["common", "users", "photos"],
"program": "cd ${taskPath} && webpack --color",
"dependencies": ["common", "package.json"]
}
多进程加速
前端代码压缩是一个 CPU 密集型操作,非常耗时。而大部分前端构建工具都是单进程设计的,因此它们都无法利用多核心 CPU 资源。如果业务模块之间没有依赖关系,启动多进程可以加速运行它们。
给 tasks 设计一个并行运行任务的描述格式,例如使用二维数组:
{
"tasks": [["common"], ["users", "photos"] ],
"program": "cd ${taskPath} && webpack --color",
"dependencies": ["common", "package.json"]
}
遇到可并行的任务,任务调度程序可根据当前机器的 CPU 核心数启动对应的子进程数,实现多核加速。
我们在 4 核心 CPU 机器进行测试,启用多进程后全量构建效率将提高 300%。
成果
至此,我们用一个非常简单的技术方案实现了设计目标。由于任务调度器的职责非常简单,不对业务有侵入,因此我们很快速的在几个大项目中完成部署,和业务中原有的构建脚本配合完成增量构建与发布的任务。
上图是我们一个大型项目,仓库中有 600 个左右的 js 模块。采用增量构建后,持续集成系统从一个版本从代码提交、构建、发布通常一到两分钟即可完成。如果关闭增量构建,这个过程将是十分钟以上。
这个任务调度程序它在我们内部叫做 ci-task-runner,它的诞生是我们踩了 N 多坑的结果。在多个重要项目的生产环境稳定运行半年之后,我们决定将此作为团队第一个开源项目公布出来。
ci-task-runner 是一个标准 NodeJS 模块,它只做一件事情:观察文件或目录版本变更,启动对应处理程序。
因为简单,所以它非常灵活:
与 Grunt、Gulp、Webpack、Rollup 等编译、构建工具无缝连接
可以使用 Npm Scripts、Gitlab CI、Travis、Jenkins 等工具启动它
支持 Git 与 Svn 这两款版本管理工具
Github 主页:https://github.com/huanleguang/ci-task-runner
最后,具体要怎么融合进项目可以到gitlab 上看看。
关于本文
作者:厦门欢乐逛 @ 糖饼
原文:https://github.com/huanleguang/ci-task-runner