ci-task-runner icon indicating copy to clipboard operation
ci-task-runner copied to clipboard

基于 Git、Svn 变更检测实现可增量构建的前端持续集成解决方案

Open aui opened this issue 7 years ago • 7 comments

近两年由于技术的发展,Web 前端可以通过编译工具来实现 HTML、CSS、JS 所做不到的事情,从而覆盖更多的业务,从技术角度实现更多的价值。社区也在不断的创造更好的编译、构建工具,例如目前大红大紫的 Webpack。正因为这些先进的工具,我们工作效率得到了前所未有的提升。当然,我们也需要面对它们所带来的一些问题:构建速度越来越慢,导致发布速度越来越慢。尤其是在使用持续集成系统来构建的项目中,这个问题越严重。

解决构建慢的问题有很多途径,比如常见的手段是优化构建工具的配置,网上也有很多这样的实践经验文章,这些优化手段大多都是针对具体的工具、本地开发构建进行的,如果使用持续集成服务器进行构建,社区缺乏一些简单可靠增量构建解决方案。针对于此,我给大家分享我们前端团队(厦门欢乐逛)的实践成果:基于 Git、Svn 的 Commit 实现可增量构建的前端持续集成解决方案

背景

大约是 2014 年的时候,我们在 Git 服务器上通过 Githooks 、Grunt 实现了一个复杂的前端增量构建系统:提交代码到对应分支后服务器会自动进行增量构建、增量发布。这套系统这在当时看来自动化程度已经很高了,解决了本地构建、发布所带来的效率以及安全风险,版本发布非常快速。当时前端团队的构建与发布流程:

  1. 代码提交到开发分支:自动构建
  2. 代码提交到主干分支:自动发布

2014 ~ 2017 年之间,我们业务飞速发展,前端项目越来越多,构建这一块也被更先进的 Gulp 与 Webpack 代替,而之前基于 Grunt 设计的增量构建系统已经无法适应新业务与技术的需求,项目部署、团队协作的成本越来越非常高。这迫使我们思考如何实现一个不受具体构建程序约束、跨业务、支持增量构建与发布的标准化解决方案。

决定做这个事情之前,我们先将 Githooks 触发的构建与发布任务由持续集成系统代替,以让前端开发流程与工具标准化。

持续集成

“持续集成是一种软件开发实践。它倡导团队开发成员必须经常集成他们的工作,甚至每天都可能发生多次集成。而每次的集成都是通过自动化的构建来验证,包括自动编译、发布和测试,从而尽快地发现集成错误,让团队能够更快地开发内聚的软件”

以上是持续集成的概念,一个完整的持续集服务由以下几个系统组成:

  1. 一个自动构建过程,包括自动编译、分发、部署和测试等。对于前端项目,这里往往是 Gulp、Webpack、Mocha 等工具来实现。
  2. 一个代码存储库,即需要版本控制软件来保障代码的可维护性。如 Git 或 Svn 等。
  3. 一个持续集成服务器。如 Gitlab CI、Travis、Jenkins 等。

由于我们公司内部代码托管平台使用 Gitlab 搭建的,因此直接采用了 Gitlab 自带的 Gitlab CI 作为构建服务器,这样能够与目前工作流无缝整合在一起。

gitlab-ci

上图是 Gitlab Builds 模块的界面,CI 根据我们设置的 Git 分支策略,不断的为我们构建、发布测试环境、部署生产环境的代码。

增量构建

本地开发中,规模较大的项目一般会拆成多个模块,单独进行编译来提高构建速度,根据不同的参数来构建指定模块。

而在持续集成系统中,最初我们通过判断 Git 提交的消息的特殊标记来决定构建哪些模块,例如构建 “users” 模块:

git commit -m "[publish:users] 修复线上 BUG #456"

这种简单的开发约定可以让服务器做到精确构建,不足之处是需要人工介入,存在风险。例如:开发者修改了公共模块后,如果忘记构建依赖了它的业务模块,这很有可能引起线上故障。

理想的情况下,项目开发人员无需关注细节,只需要关注工作本身。测试、生产环境的构建与发布的细节应该完全由持续集成服务完成。

自动增量构建

为了实现完全自动化,我们使用检测文件修改的方式来触发增量构建,不同于本地开发中的 --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 要高效很多,并且能够让发布后的文件和版本库关联起来。更加重要的是它是成熟的解决方案,这对系统的稳定性至关重要。

通过 JSON 声明构建关系

项目中通常都有自己的构建脚本,如果再添加增量构建逻辑这无疑会加剧构建脚本的复杂度、带来更高的成本。因此我们设计了一种描述增量构建的任务的配置格式,然后实现任务调度器来解析它们、运行任务,以实现对业务原有构建流程的解耦。例如:

{
  "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"]
}

通过这种依赖声明依赖还有一个好处是:使团队所有人能够快速的了解模块修改导致的变更范围,例如测试同学可以通过 CI 日志准确知道关联模块的变更。

多进程并行加速构建

前端代码压缩是一个 CPU 密集型操作,非常耗时。而大部分前端构建工具都是单进程设计的,因此它们都无法利用多核心 CPU 资源。如果业务模块之间没有依赖关系,启动多进程可以加速运行它们。

tasks 支持并行运行任务的描述格式,使用二维数组即可启用并行构建:

{
  "tasks": [["common"], ["users", "photos"] ],
  "program": "cd ${taskPath} && webpack --color",
  "dependencies": ["common", "package.json"]
}

遇到可并行的任务,任务调度程序可根据当前机器的 CPU 核心数启动对应的子进程数,实现多核加速。

我们在 4 核心 CPU 机器进行测试,启用多进程后全量构建效率将提高 300% 以上。

成果

至此,我们用一个非常简单的技术方案实现了设计目标。由于任务调度器的职责非常简单,不对业务有侵入,因此我们很快速的在几个大项目中完成部署,和业务中原有的构建脚本配合完成增量构建与发布的任务。

ci-task-runner

上图是我们的一个大型项目,仓库中有 600 个左右的 js 模块。采用增量构建后,持续集成系统从一个版本从代码提交、构建、发布通常一到两分钟即可完成。如果关闭增量构建,这个过程将是十分钟以上。

publish

这个任务调度程序它在我们内部叫做 ci-task-runner,它的诞生是我们踩了 N 多坑的结果。在多个重要项目的生产环境稳定运行半年之后,我们决定将此作为团队第一个开源项目公布出来。

ci-task-runner 是一个标准 NodeJS 模块,它只做一件事情:观察文件或目录版本变更,启动对应处理程序。

因为简单,所以它非常灵活:

  1. 与 Grunt、Gulp、Webpack、Rollup 等编译、构建工具无缝连接
  2. 可以使用 Npm Scripts、Gitlab CI、Travis、Jenkins 等工具启动它
  3. 支持 Git 与 Svn 这两款版本管理工具

Github 主页:https://github.com/huanleguang/ci-task-runner

aui avatar Apr 28 '17 06:04 aui

你好,我是厦门美柚前端,看了你的文章,感觉思想挺不错的,和我们的流程也大体相似。 我看了还是不太明白gitlab-cli上面的配置,这个仓库现在是完整的吗?还是后续还会陆续会更新?可否把各个目录和重要配置文件都介绍一下~

fuxun2008 avatar May 06 '17 07:05 fuxun2008

gitlab ci 的配置网上文章应该挺多的,建议 google 。如果你看完 README.md 会发现我们这个任务调度器其实和 gitlab-ci 没太多关系

aui avatar May 06 '17 08:05 aui

想问下线上构建后的html文件要放在git版本控制里吗?如果要的话那每次构建完就要commit一下,这样就可能会有冲突,需要考虑解决冲突,如果不放,那么每次发布就只能是全量构建,应该没办法做到按需构建吧?

reuwi avatar Sep 07 '20 09:09 reuwi

@gaoshijun1993 构建后的代码不因该放在版本仓库中,这不是 git 仓库的职责

aui avatar Oct 28 '20 11:10 aui

请问能否说一下 如何做的增量构建? 我理解增量构建需要利用上一次构建的产物或者缓存. 但在CI的环境里面 这个怎么做到和 构建工具无关呢?

stephenzhao avatar Aug 21 '22 13:08 stephenzhao

按照包维度进行对比的

aui avatar Aug 21 '22 14:08 aui

我可以理解, 是每个包都有各自的 构建脚本. 当检测到某个包油变化 就进入这个包去执行构建对吗?

stephenzhao avatar Aug 29 '22 11:08 stephenzhao