Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
aui opened this issue Apr 28, 2017 · 7 comments

Comments

@aui
Copy link
Member

aui commented Apr 28, 2017

近两年由于技术的发展,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

@fuxun2008
Copy link

fuxun2008 commented May 6, 2017

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

@aui
Copy link
Member Author

aui commented May 6, 2017

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

@reuwi
Copy link

reuwi commented Sep 7, 2020

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

@aui aui changed the title 基于 Git、Svn 的 Commit 实现可增量构建的前端持续集成解决方案 基于 Git、Svn 变更检测实现可增量构建的前端持续集成解决方案 Oct 22, 2020
@aui
Copy link
Member Author

aui commented Oct 28, 2020

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

@stephenzhao
Copy link

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

@aui
Copy link
Member Author

aui commented Aug 21, 2022

按照包维度进行对比的

@stephenzhao
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants