前端工程化:云构建
作者: 发布于:

背景

通常个人在开发项目的时,都是在本地编写构建脚本对项目进行构建,这个脚本可能是 Gulp,可能是 Grunt, 可能是 webpack,也可能是其他的一些脚本,每次代码发布之前,都要对代码进行构建,代码仓库里面包含构建脚本和构建之后的代码。对于个人开发,这样做是没有问题的,但是涉及到多人开发或者团队开发就会有一定的问题。说是问题也不是问题只不过是会导致开发效率降低,构建错误的情况越来越多。

在本地对项目进行构建,通过脚手架工具来分发构建脚本对于团队开发来说有很多问题:

  • 构建脚本的开发维护者很难去持续优化,更新构建脚本
  • 构建脚本使用者对构建脚本的修改,改良不可复用
  • 每次发布之前都需要对项目进行构建,如果忘记构建将会导致发布失败
  • 同一个项目的开发者可能会有不同的构建脚本,极有可能会导致构建出错

我们把构建脚本从应用里面提炼出来,包装成单独 npm 模块,这样构建脚本(下文统称为构建器)就有了模块的一些特性:

  • 可分享: 任何人可以很方便发布一个构建脚本模块给任何人使用
  • 可修改: 如果你有更好的主意,可以 fork,加上自己想要的功能,并发布到 npm 平台上
  • 易维护: 模块可以由专人维护与更新

使用云构建后,本地不需要安装任何构建环境,这个对于一些新技术的推广是有好处的, 比如大家都知道,在 Windows 下, 安装 compass 不是一件轻松的事。而且对于构建脚本的更新也是很友好的,只需要更新云构建平台上的构建脚本即可。

使用云构建后仓库里面就不需要保存构建后的代码,这样有助于保持代码整洁,同时,在多人开发的时候,再也不会出现构建脚本冲突的情况。把云构建接入发布流程,每次提交发布时执行构建,这样就再也不会在发布之前忘了构建。而且服务器的性能更强大,对于比较大的项目能够更快执行构建,节省构建的时间。一线开发人员不需要去关心构建的问题,能够把更多的时间放到业务上,提高工作效率并。

历史和现状

Grunt;Gulp 等前端构建的概念是近几年才火起来的,其实淘宝前端团队早在 2011 左右就开始大规模对前端代码进行构建了(Git 也是在这个时候引入到团队内作为版本管理工具)。最初使用的构建引擎是 ant,基于 XML 描述构建规则,后来将 ant 的 build 任务放到了服务器端执行,再后来由于 ant 的扩展性和维护性太低直接改成了 shell 脚本(那个时候压缩代码还是用 YUICompress 和 Google Closure Compiler)。

再后来 Node.js 开始流行,基于 Node.js 的前端生产力工具开始如雨后春笋般涌现。团队内部开始使用 Grunt 构建前端代码(后续慢慢被 Gulp 和 webpack 替代),但依旧是在本机电脑执行构建,然后将构建后代码提交到仓库进行发布上线。14 年底开始构思并上线了第一版云端构建平台,开始逐步将前端代码的共建工作再次迁移到云端执行。

经过一年多时间的完善,云构建平台已经完全支撑起了团队内部乃至整个集团的前端代码构建任务,日构建任务量已达 1000+。并且构建服务还集成到了代码发布流程中和本地开发工具中,使前端开发前所未有的高效和轻松。

系统架构

云构建系统由五部分组成:

  • 客户端(client
  • client 负责向云构建发起一个构建请求并获取构建后内容。
  • client 官方提供了一个已经封装好逻辑的 npm 模块,如果是基于 Node.js 的系统,可以直接使用。
  • client 支持将需要构建的代码直接上传给构架服务器或者仅提供一个 URL,由构建服务器自己从 URL 下载代码,官方更推荐后者。
  • 构建器(builder
  • builder 是构建任务的最终执行者,包含详细的构建业务逻辑
  • builder 是一个标准的 npm 模块
  • 构建服务器(workers
  • workers 是一组高性能服务器,每台服务器可以并发运 32 个构建任务。
  • workers 可以动态扩容;上线和下线
  • 构建路由(router
  • router 负责分发构建任务给 worker。
  • router 集成了 worker 负载监控功能,可以保证所有 worker 平均负载。
  • 数据展示和管理平台(web
  • web 展示所有构建过程中产生的数据
  • web 管理构建器和构建服务器

架构图:

运行一次构建任务的大概过程如下:

  1. app(需要调用云构建的各种系统)集成 client,并使用 client 提供的接口发起构建请求
  1. client 从 router 获取一个 worker 地址
  1. client 与 worker 建立 socket 连接,并向这个 worker 发起构建任务
  1. worker 实时输出构建日志信息给 client
  1. worker 完成构建后将构建结果返回给 client
  1. client 将构建结果返回给 app

为了减轻构建服务器的负载,整个构建过程中涉及到的文件上传下载服务都是通过文件中转服务来完成的。

abc.json

除了上面的五个部分,还有一个配置文件也是必不可少的:abc.json(a build config)。这个文件一般跟需要构建的内容放在一起发送给 worker。是一个标准的 JSON 文件,指定需要调用的 builder 和一些配置信息。

构建器(builder)

abc.json 和 builder 是整个云构建平台唯一可定制部分。

builder 是一个标准的 npm 模块,入口文件可以是一个 Grunfile.js 或者 gulpfile.js,当然也可以是你自己的 xx.js。如果是 Gulp 或者 Grunt 脚本,worker 会帮你运行这个脚本,如果是普通的 npm 包,wroker 会运行由 package.json 文件中指定的入口文件。

构建器编写注意事项

1,项目本身需要依赖一些外部的模块,例如 lodash,需要构建开始前需要自己安装相应的依赖,可以通过一个 Gulp 的 task 去执行,没有依赖则忽略。

3,针对特定项目的配置信息,可以在项目的配置文件(abc.json)中添加,然后在构建时通过读取配置文件获取。

4,云构建和本地构建有一定的区别。本地构建时,源码目录和构建好的代码的存放目录构建者都是明确的。而云端构建,构建脚本是由云构建平台来控制的,云构建也需要收集构建好的文件返回给客户端,因此待构建源码的目录(src)和构建好的代码的存放目录(dist)都是需要有云构建平台来指定,worker 在执行 builder 的时候会传递相关参数。

5,如果构建器本身需要安装依赖,package.json 的依赖需要是 dependencies,不能是 devDependencies。

构建器测试

构建器编写本身也需要一定的成本,而且在本地无法测试,如果构建器编写出错,而且已经发布,将会造成很大的问题,因此需要一个构建器的测试平台。

通过删除构建系统的大部分特性,而只保留最核心的功能,同时去除原系统的一些限制使构建器能够在上面正常运行。同时编写相应的测试命令行工具,形成整个构建器测试平台。

线上构建系统对构建器做了严格的限制,构建器必须要审核通过才能够发布上线。测试平台没有这些限制,方便构建器开发者更新测试。

遇到的问题和解决方法

1,HTTP 连接

最开始的时候 client 与 worker 都是通过 HTTP 进行通信,这样实现起来的确是很简单,系统也能正常运行。而且对于绝大多数构建任务来说是没有问题的。但是遇到一些比较大的项目,构建时间比较长的项目,问题就暴露出来了。由于构建时间比较长 HTTP 连接经常可能会被重置,既有可能因为 nginx 代理的问题导致,也有可能因为网络问题导致。

随着云构建系统越来越复杂,服务器的返回值,需要经过多层嵌套才能够返回给客户端,这对于系统的调试和错误处理带来了很多的不变,而且大大降低了代码的可读性。错误处理变得很复杂,可能会存在没有发现的 bug。

为了彻底解决这个问题,我们使用了 socket 来代替 HTTP,使用了 socket 的特性,构建时间即使再久也不会发生构建过程中通信中断的问题。而且只要构建发生错误,通过 socket 的事件机制立马就能够通知客户端。

2,文件上传

在最初的系统中,项目文件的上传是通过 client 直接把项目文件压缩打包上传到 worker,项目文件很大打包压缩上传的时间需要很久,而且文件过大,nginx 会返回 413 错误。当并发任务数量过多的时候,worker 负载过大,经常会因为上传下载占用过多的资源影响构建服务的正常进行。

通过把文件上传服务独立出来,建立单独文件中转服务,worker 只需要关注构建相关的问题,不再接收处理上传的文件,client 传递给 worker 的只是一个 URL 地址。构建完成后,worker 把构建好的代码打包压缩上传到文件中转服务,然后返回相应的地址给 client,client 通过地址拿到构建好的内容。所有文件处理都通过第三方去处理,文件部分的处理和构建过程完全独立。减少了系统的耦合程度。

3,负载均衡

随着云构建系统被使用的越来越多,构建任务经常需要等待,为了避免构建等待,加快构建速度,我们增加了构建服务器的数量。

多台构建服务器就需要有相应的任务分发机制,对任务进行分发保证构建任务不需要等待。因此增加了云构建路由服务(router)。云构建路由对任务进行分发,构建服务器有多台,在分发的时候要根据构建服务器上面正在运行任务的数量进行分发,确保任务能够以最快的速度运行。

同时还需要设定心跳机制,定时去检测构建服务器的情况,如果构建服务器出现异常能够及时报警,同时自动下线相应的构建服务器。最大程度的保证构建能够正常的运行,不会因为一台构建服务器出现故障而影响整个构建系统的正常运行。

展望

1,为了提高测试平台的稳定性和安全性,我们设想为每个构建器提供单独的沙盒运行环境,使用 docker 技术把构建器的运行环境和构建系统本身隔离开来,保证构建器运行过程的问题不会影响构建系统本身,使两者独立起来。这样做对于系统的安全性也会有很大的提高,限制了构建器本身的运行环境,即使构建器中存在一些危害构建系统的行为也不会影响到构建系统,这样极大的提高了安全性和稳定性。

2,目前云构建还只是拥有对代码进行构建的能力,完全可以把云构建平台进行通用化,成为一个通用 Node.js 任务运行平台,目前我们正在做这方面的尝试。