前端自动化打包实践

前言 分三部分:gulp介绍自动化打包脚本编写;gerrit介绍工作流;gitlab-ci介绍平台搭建。每部分都会结合做过的两个项目进行总结,希望对大家有所帮助。
gulp 第一个项目用gulp是因为在当时它还是比较“新”的,用一下尝尝鲜。第二个项目还用它,是因为那段时间在学习函数式编程,接触了Monadic,想到能与这个概念匹配的例子就gulp,所以想用它的同时深入了解一下这个概念,当然最主要的原因是用起来熟悉顺手。
Monadic编程概念有点抽象,但gulp的编程范式却能直观的展现。

const { src } = require('gulp') src('glob格式').pipe(func1).pipe(func2).pipe(func3)

这个“.pipe(func1).pipe(func2).pipe(func3)”就算是Monadic了,其实和promise的then是一个道理。
Promise.resolve().then(func1).then(func2).then(func3)

他们之所以能链式调用,就是为func1、func2、fnc3...这些函数按规定返回了相同的接口(对象)。
// gulp 大致这样 var through = require('through2'); function func1() { return through.obj(function(file, enc, cb) { // through.obj返回的对象有pipe方法 // 调用cd就表示完成了 }) } // promise 大致这样 Promise.prototype.then = function(func) { let result = func() if(result.then && typeof result.then == 'function') { return result } else { return Promise.resolve(result) } }

第一个项目编写打包步骤时大致是下边这样的,步骤多了靠看代码很难缕清步骤之间的关系。
var gulp = require('gulp') gulp.task('步骤1', function() { // 步骤1 }) gulp.task('步骤2', function() { // 步骤2 }) gulp.task('default', ['步骤1', '步骤2'], function() { // 步骤1和步骤2是并行运行的,步骤1步骤2都结束之后进行这个步骤 })

第二个项目用了gulp新的版本,在API层面上解决了这个问题,并行用parallel,串行用series。
const { parallel, series } = require('gulp') function 步骤1() { // 步骤1 } function 步骤2() { // 步骤2 } function 步骤3() { // 步骤3 } exports.default = series(parallel(步骤1, 步骤2), 步骤3) // 步骤1和步骤2是并行运行的,步骤1步骤2都结束之后进行步骤3

假如步骤1和步骤2有相同的部分,只是某些参数不同,咋办?学的函数式又派上用场了,高阶函数用上。
const { parallel, series } = require('gulp') exports.步骤 = function(参数) { return function 步骤() { // 步骤1 步骤2 相同的部分,用参数来区分不同 } } const 步骤1 = 步骤(参数1) const 步骤2 = 步骤(参数2) function 步骤3() { // 步骤3 } exports.default = series(parallel(步骤1, 步骤2), 步骤3) // 步骤1和步骤2是并行运行的,步骤1步骤2都结束之后进行步骤3

用高阶函数可以把类似步骤封装起来,而且很自然的就想到模块化了。
所以第二个项目就做了模块化:比如需要操作两个git库,就涉及git的pull和push;比如不同阶段都需要操作文件,就涉及文件的移动和删除。
gulp模块化需要把gulpfile.js文件改成目录名,入口文件是这个目录下的index.js,其他模块则放到这个目录下随意命名了。
// gulpfile.js/git.js exports.pull = function(参数) { return function pull() { // 拉代码 } } exports.push = function(参数) { return function push() { // 可以把 add commit push 写到一起 } } // gulpfile.js/file.js exports.move = function(参数) { return function move() { // 文件从一个目录移动到另一个目录 } } exports.del = function(参数) { return function del() { // 删除文件 } }

当然我觉的模块化做的比较好的地方就是config和task的分离,在入口文件组合config和task,组成具体task。
// 配置 gulpfile.js/config.js const config = { // 配置 } export.getConfig = function() { // 通过方法获得配置 } export.getCombine = function() { // 通过方法获得动态配置 } // 任务 gulpfile.js/task.js const { parallel, series } = require('gulp') exports.task = function(name, config, combine) { const arr1 = [...] // 根据 config, combine 生成任务1 if(name == '任务1') { return series(...arr1) } const arr2 = [...] // 根据 config, combine 生成任务2后续的任务 if(name == '任务2') { return series(...arr1, ...arr2) } return series(...) // 根据 config, combine 任意组合的任务 } // 入口 gulpfile.js/index.js const { getConfig, getCombine } = require('./config') const { task } = require('./task') const config = getConfig() const combine = getCombine(参数) // 根据不同任务放开相应注释 // exports.default = task('任务1', config, combine) // 简单的任务1 // exports.default = task('任务2', config, combine) // 复杂的任务2

第一个项目遇到了一个问题,就是有的步骤gulp插件做不到,自己也没有能力完成插件的开发,因此gulp不能串起来所有步骤,最后是用命令行脚本串起来的。
# mac或linux用shell gulp --参数 # 混淆压缩 zip -参数 # 打zip包 java -jar --参数 # jdbc 修改数据库版本 gulp ftp # ftp到服务器 # windows用batch gulp --参数 # 混淆压缩 haozip.exe -参数 # 打zip包 java -jar --参数 # jdbc 修改数据库版本 gulp ftp # ftp到服务器

第二个项目也同样面临这样的问题,但是这次却找到了解决办法。
首先需要在命令行运行的步骤可以在nodejs里运行,原来nodejs自己本身就支持,不需要npm别的包。
const { execSync, exec } = require('child_process') execSync('命令', 配置) exec('命令', 配置)

其次是这次用的gulp版本对异步任务的支持,只要任务返回stream、promise、event emitter、child process或observable就可以了,如果是上述命令行任务,按照他们接口的范式编写代码就知道这任务啥时候执行完毕,好执行下个任务。
gerrit 在多人开发的情况下,还需要工作流来管理代码的合并。
第一个项目的工作流大致是这样的:一个需求一个分支,修改bug也算需求,每个测试环境一个分支,测试环境分支是需求分支merge在一起的。之所以采用这样管理方式,是因为有的需求在开发,但是不知道什么时候上线,或者有的需求遇到特殊情况延期上线,这样每次上线只要确定了上线的内容,merge一下对应分支就行了,准生产分支这样做几乎没问题。但是平时的测试分支一边开发一边merge,有冲突时有的人在测试分支解决了,但是在自己分支上没有修改记录,再重新组合时还得解决冲突,而且随着时间的推移,测试分支会越来越复杂,甚至出现每个人自己分支运行没问题,只有测试分支有问题,把测试分支删了,重新merge就没问题了,这个问题一直没有解决,每次都是删了重新组合。
第二个项目的工作流大致是这样的:每个月份上线内容在一个特性分支上开发,拉别人的代码的方式是衍和(rebase),这样减少了分支的关系的复杂度,不会出现第一项目那样需要删了重新组合的情况,而且push代码是受gerrit限制的,这就是为啥用它来做标题了,具体来说本地代码不能直接push到远程分支,而是发布到gerrit,发布的代码是需要经过审核通过,才能提交到远程分支。
第二个项目刚开始是用工具操作衍和和发布的,但是自动打包也要把这些步骤写到脚本里,gulp那部分介绍的“child process”虽然就几行代码,我可是花费了我不少心血的,同样下边的脚本虽然也是短短几行,但也是花费了我不少心血的。
# 衍和 git pull --rebase --progress "origin" # 把代码发布到 gerrit git push --progress origin HEAD:refs/for/refs/heads/分支

这个“HEAD:refs/for/refs/heads/”是默认的,也可以在gerrit里配置(没实际操作过,我猜的)。
这两个工作流都有各自特点,第二个工作流虽然分支的复杂度小,但是搭建复杂度较高(需要搭建+1的自动审核)和管理耗费的人力较多(多了一步+2的人工审核)。第一个工作流虽然有时需要删了重新组合,但是这个问题出现概率小,适合3-5人的小团队。我个人认为这两种的结合的工作流比较不错,对于存量功能用第二种,而对于增量功能还是用第一种比较好,新功能风险高,如果不能按期完成交付,可以先不合到特性分支,等达到可交付了再合。而且增量功能分支有了,也可以尽早将公共的模块代码提交分享出去,而不用担心会影响特性分支的交付。
gitlab 自动化打包脚本有了,工作流有了,接下啦就是最关键的了,怎样自动打包?
其实原理就是远程仓库添加git-hooks,远程仓库某些分支标签的某些动作如push就会触发git-hooks关联的自动化平台,自动化平台收到信号,开始执行对应的自动化脚本。当然了不止这一种触发方式,还有定时任务等等,这些不在这篇文章讨论范围。
第一个项目用的是 gitblit + genkins
首先要下载jar包:http://mirrors.jenkins.io/war...
然后启动:java -jar jenkins.war --httpPort=8080
在浏览器输入:http://localhost:8080
配置密码:按照页面指引找到initialAdminPassword文件,复制出密码
首次配置插件:首次可以先跳过插件安装,进入jenkins后再配置
配置插件:在Manage Jenkins/Manage Plugins/Advanced/Update Site里填写
http://mirrors.tuna.tsinghua....
安装插件:在Manage Jenkins/Manage Plugins/Available里找即可
自动打包必不可少的插件必须安装:git、nodejs,如果需要写流水线脚本,需要安装pipeline相关插件(这个项目没有用)
那么远程仓库和jenkins怎么触发自动打包?这个就是git-hooks
点击New Item填入项目名称,点击ok
在Source Code Management里选择Git输入远程仓库url
点击“添加“后输入用户名密码
选择输入的用户名密码
在Build Triggers设置触发器,这个在设置gitblit的时候会用到(令牌随意输就行)
在Build里填写要触发的脚本
点击保存
进入gitblit的groovy目录
编辑jenkins.groovy文件,在jenkinsUrl输入Build Triggers设置触发器,生成的地址
保存之后重启gitblit
进入gitblit管理页面,点击编辑,选中Receive
选中jenkins.groovy,移动到Selected,点击保存
这样gitblit push代码后就会触发Build里填写的脚本
可以参考:
https://www.jianshu.com/p/9a3...
第二个项目用的是 gitlab
gitlab runner下载:https://docs.gitlab.com/runne...
gitlab安装:在下载好的目录下运行
gitlab-runner install gitlab-runner start

.gitlab-ci.yml:存放于项目仓库的根目录,包含了项目自动化如何运行的描述语句
stages: - buildbuild-gulp: tags: - 标签 stage: build script: - gulp only: - web

pipeline:就是一个分成不同stage的job的集合;

stage:是job的一个逻辑上的划分,如“stage: build”;

job:就是Runner要执行的指令集合,必须包含 script,“build-gulp:...” 就是 job,可以自定义多个;

stages:定义了作业执行的顺序,默认包含build、test和deploy三个stage;

script:是一段由Runner执行的shell脚本;

only:web:在gitlab页面上按run pipline的时候执行;

tags:需要执行的Runner标签,需要在“首页=>setting=>CI/CD=>Runners=>Specific Runners”添加;

可以参考:
https://zhuanlan.zhihu.com/p/...
【前端自动化打包实践】https://juejin.cn/post/701814...

    推荐阅读