Gitlab CI — 前端自动化构建及优化

7,530 阅读8分钟

现在前端花样越来越多了,今天我要谈的并不是什么新奇的东西了,自动化构建部署嘛,如果你是一个 Javaer 或者 运维同学,对于 Jenkins 之类的自动化部署肯定不陌生。不过也提到过了,作为一个前端,特别是业务驱动的开发来说,平时肯定是很少接触这些东西的。笔者也是最近才打算前端自动化部署上容器,然后学习看了看,接下来就把学习过程和小总结给大家分享分享,很粗浅,大牛直接右上角(左上角)关闭就可以了。

我写文章的宗旨就是,把自己走过的弯路总结给大家,避免你们踩坑,不图你们点赞,少喷就好。因为确实经历过很多次“文章千百万,实践就完蛋”的场景太多了,大家也不对读者负责,所以我的文章一般都会有代码和 Demo 示例,做不到最好但是至少如果你遇到过相同的问题,肯定能解决~

前置

  • Gitlab
  • Gitlab Runner
  • Docker

上面属于前置内容,Gitlab 就不用说了吧,一般公司内部使用的都是 Gitlab 仓库;Gitlab Runner,一般需要额外安装,预先编写的自动化构建部署的脚本(后面会介绍到)就是由它来运行的;最后,项目要打包成镜像部署到容器上,所以也需要 Docker。

因为是公司内部流程不是自己独自玩耍,所以让运维大大替你搞定就可以了,现在这个时代在公司使用应该都是默认会有的了。个人项目可能大概率应该是放在 Github 上的,小伙伴们可以选择 Github Actions,网上有很多教程。

新建一个项目

create-react-app 为例子简单介绍一下,自动化构建部署然后生成 Gitlab Pages。首先,我们新建一个项目推送到远程仓库:

.gitlab-ci.yml — 初始化

在使用 Gitlab CI 进行自动化构建部署的时候,需要新建一个 .gitlab-ci.yml 配置文件,里面是我们自动化构建的具体步骤的脚本,这里先简单的进行初始化一下,下面会详细介绍:

# 定义阶段 stages
stages:
  - build
  - deploy

# 定义 job - build 项目
job1 - build 阶段:
  # 开始之前需要安装依赖
  stage: build
  script:
    - echo "finish build stage"

# 定义 job
job2 - 发布到线上环境:
  stage: deploy
  script:
    - echo "finish deploy stage"

上面配置文件大体意思就是,此项目的自动构建大致分为两个阶段,分别是 build 和 deploy 阶段,并且每个阶段都会执行一些相应的任务,这里因为是初始化演示,只是简单的进行了两行文本输出,并且字段含义大家也不需要知道,下面会详细介绍。

将文件推送上去,查看效果:

从图中可以看出,成功推送到远程,接下来点击: CI/CD -> pipelines

从上图可以看到,项目的自动化构建就完成了,你可能会问了,这么简单?其实就是这么简单,虽然我将构建内容简化成了两行输出,但是整体流程其实是不变的,无非就是构建过程复杂化,增加脚本内容而已。接下来,就用此例子,一步一步加深了解 Gitlab CI。

Gitlab 中 YAML 相关概念解析

前面提到了,所有的构建部署内容都是写在 .gitlab-ci.yml 脚本里执行的,因此,此文件的内容写法以及各个关键字对应的意义,是首先要了解的。

官方文档在此,我只是官方文档的筛选搬运工,大家也可以自己去《有道词典》翻译。

关键字 Reqired 描述
image 使用 Docker 镜像
service 使用 Docker 服务
stages 定义构建阶段
types stages 别名(建议只使用stages,已经被废弃)
before_script 每个脚本任务执行之前执行的脚本
after_script 每个脚本任务执行之后执行的脚本
variables 定义变量,可以在构建阶段使用
cache 定义缓存文件,可以在后面阶段使用到

job

job.gitlab-ci.yml 里的顶级元素了,任何自动化部署都必须有的,个人认为可以将它分成任务,每个任务里至少包含一条脚本表明在此任务里要做的事情。可以定义多个 job 并且每一个 job 之间都是相互独立的,job 会被 Gitlab Runner 在自己的环境下执行。

# 定义 job1
job1:
  script: 
    - echo "i'm job1"
# 定义 job2
job2:
  script: 
    - echo "i'm job2"		

上面是最简单的示例,定义了两个 job,每个里面有一个脚本执输出一段字符串。

【注意】: 前面提到过,job 是顶级元素,它的命名也很宽泛,${string}:字符串 + 分号(不限中英文)即可,但是上面表格提到的各个关键字是不能被定义成 job名 的。

image && services

因为 Gitlab Runner 也是使用 Docker 进行构建的,因此可以在构建我们代码的时候也使用相应的镜像进行基础构建。对应字段就是 imageservices,先来看看官方给的简单示例:

# 基础镜像
image: ruby:2.1
# 使用镜像 service - postgres
services:
  - postgres

接下来我们看看我们自己的项目,我们是一个基于 create-react-app 构建的项目,所以依赖的肯定是 node 镜像,服务不需要可以不写,因为是可选项。

# 依赖镜像 node
image: node:10.16.0
# 定义阶段 stages
stages:
  - build
  - deploy

# 定义 job - build 项目
job1 - build 阶段:
  # 开始之前需要安装依赖
  stage: build
  script:
    - echo "finish build stage"

# 定义 job
job2 - 发布到线上环境:
  stage: deploy
  script:
    - echo "finish deploy stage"

上面,我们增加了镜像依赖,并且制定版本号是v10.16.0

stages

这个字段也挺重要的,我觉得可以翻译为为构建阶段,比如我们的项目分为两阶段,第一个阶段是 build — 编译打包,第二个阶段是 deploy — 发布上线。其实也就是对应两个 job,然后在下面对每一个阶段更具体化的描述。

stages 字段定义的几个阶段在 pipelines 构建过程中顺序是一致的,并且有如下规律:

  • 前一阶段完成,后一阶段执行

也就是说,build 成功了,deploy 才会执行。

  • 所有阶段全部成功,整个 pipelines 过程全部标记为 pass,整个构建才是成功
  • 其中一个阶段失败,pipelines 后续流程不会被执行,整个构建标记为失败。

具体执行过程我们可以在 pipelines 里查看到,如下图所示

script

这个也算是最重要的一个字段了,它表示由 Gitlab Runner 执行的一段 shell 脚本,在构建项目过程中肯定是要执行很多命令的,比如安装依赖、打包、部署等命令。还是以我们的项目为例,新增了 node 镜像,这就意味着可以执行 npm installnpm run build等命令了。

# 依赖镜像
image: node:10.16.0

# 定义阶段 stages
stages:
  - build
  - deploy

# 定义 job - build 项目
job1 - build 阶段:
  # 开始之前需要安装依赖
  stage: build
  script:
    - yarn install
    - yarn build
    - echo "finish build stage"

# 定义 job
job2 - 发布到线上环境:
  stage: deploy
  script:
    - echo "finish deploy stage"

在上面,我们在 job1 里新增了两个命令 yarn installyarn build两条命令,熟悉前端开发的应该都知道,项目生产之前要安装依赖和打包编译。接下来推送到远程仓库来看看效果:

  • 构建成功

  • 第一步的构建内容

从上方截图可以看的出来,推送上去的代码再次触发自动构建,构建成功并且第一步 build 阶段里面依次执行了我们编写的三个脚本内容,非常完美~

before_script 和 after_script

这两个字段含义就跟字面意义一致,before_script — 脚本执行之前执行的脚本,after_script — 脚本执行之后执行的脚本。

这里可能有人会说了,那么把 yarn install 安依赖的脚本命令放到 before_script里岂不是更加合适,这样 build 阶段只做 build 命令,更加贴切。答案是不行的,因为这两个命令是在外层,意思是每一个阶段之前都执行一次,也就是说,每一个 job 里都会先执行 before_script 然后再执行自己定义的 script 脚本,而事实上我们 yarn install 只需要执行一次。

我思来想去想找一个最佳场景去演示,但是感觉用到的场景确实很少而且也都不是很贴切,当然,如果只是为了了解功能,那么输出一个字符串就行了,这里我觉得国内可以在 before_script 前设置下淘宝镜像。

# 依赖镜像
image: node:10.16.0


before_script:
  - echo "======== before script ========"
  - npm config set registry https://registry.npm.taobao.org

after_script:
  - echo "======== after script ========"

# 定义阶段 stages
stages:
  - build
  - deploy

# 定义 job - build 项目
job1 - build 阶段:
  # 开始之前需要安装依赖
  stage: build
  script:
    - yarn install
    - yarn build
    - echo "finish build stage"

# 定义 job
job2 - 发布到线上环境:
  stage: deploy
  script:
    - echo "finish deploy stage"

来看看效果:

  • build 阶段

  • Deploy 阶段

可以看到确实如描述所言,这两个脚本每个构建阶段 — stage 都会执行。

only && except

接下来这两个关键字也很重要了,我们先来看一下例子:

  • 第一步,新建一个分支:branch-a
  • 第二步,修改内容推送到远程

先别管是否构建成功了,现在就出问题了。为啥?我们研究的是自动化构建部署上线,那么肯定是应该有上线规范的,你不能每个分支每次 push 到远程都触发构建吧,肯定不行啊。这时候,only 和 except 就派上用场了。

  • only:只允许符合条件的触发构建
  • except:除了某些内容,都会触发构建

这里为了演示,我约定的是,只有 master 的 push 会触发,只有 master 分支代码变化了,才会触发构建。

# 依赖镜像
image: node:10.16.0

before_script:
  - echo "======== before script ========"
  - npm config set registry https://registry.npm.taobao.org

after_script:
  - echo "======== after script ========"

# 定义阶段 stages
stages:
  - build
  - deploy

# 定义 job - build 项目
job1 - build 阶段:
  # 开始之前需要安装依赖
  stage: build
  script:
    - yarn install
    - yarn build
    - echo "finish build stage"
  only:
    - master

# 定义 job
job2 - 发布到线上环境:
  stage: deploy
  script:
    - echo "finish deploy stage"
  only:
    - master

【注意】:only 字段是需要每个 job 阶段都要单独定义的,因为不能保证你每个阶段对应的要求是什么。

将代码推送上去,已经不会触发自动构建了,我们再来验证一下,在 branch-a 分支提一个 MR 然后合并到 master,看看效果。

  • Merge Request

  • Merge && CI

可以看到,分支提交并不会触发 CI,而 Merge 到 master 之后会触发,达到预期。

关于 only 和 except 相关内容其实还有很多,比如特定分支,特定 tag 等等。这里就不做过多赘述,总之就是可以满足你任何复杂场景和操作,你自己去组合就好了。

variables

顾名思义,就是变量,我们可以预先定义好一些常用的变量,然后在 job 的脚本里使用它们,一个简单的例子:

variables:
    DOCKER_HUB_URL: "https://custom.dockerhub.com"
    
# 定义 job - build 项目
job1-build:
  # 开始之前需要安装依赖
  stage: build
  script:
    - yarn install
    - yarn build
    - echo $DOCKER_HUB_URL
    - echo "finish build stage"

除了自定义的变量之外,系统还内置了很多常量:具体查看这里

artifacts

这个参数也是十分重要的一环,它的作用是可以在当前 job 构建成功之后,将构建列表里的文件或者文件夹传递给其他的 job(不一定就是下一个 job),也就是说在两个 job 之间进行传递文件内容。

为什么说它重要呢,我们先来看看例子,我们在 job2 中新添加一个命令,查看当前构建目录:

# 定义 job
job2 - 发布到线上环境:
  stage: deploy
  script:
    - ls
    - echo "finish deploy stage"
  only:
    - master

输出结果如下:

我们会发现一个问题,在 job2 里获取的还是仓库 master 里的内容,这意味着什么,如果你了解 React 应该知道,create-react-app打包之后会生成一个 /build 文件夹,此文件夹里面的内容一般来说就是最终的上线内容。但是我们明明在 job1 里面 build 了,并且 job1 build 成功了,也就是说需要把 job1 build 成功过后的文件夹 /build 传递给 job2,所以此时也就用上了 artifacts

# 依赖镜像
image: node:10.16.0

before_script:
  - echo "======== before script ========"
  - npm config set registry https://registry.npm.taobao.org

after_script:
  - echo "======== after script ========"

# 定义阶段 stages
stages:
  - build
  - deploy

# 定义 job - build 项目
job1-build:
  # 开始之前需要安装依赖
  stage: build
  script:
    - yarn install
    - yarn build
    - echo "finish build stage"
  only:
    - master
  artifacts:
    paths:
        - build/

# 定义 job
job2-deploy:
  stage: deploy
  script:
    - ls
    - echo "finish deploy stage"
  only:
    - master
  dependencies:
    - job1-build
    
  • job1-build

Job1 新增的 artifacts 里面设置 paths: build/ 文件夹。

  • job2-deploy

Job2 新增 dependencies: job1-build,表示此 job 依赖 job1-build 传递过来的 artifacts 内容。

推送到远程查看效果:

从上图可以看出,job2 已经可以获取到 job1 构建成功之后生成的 /build 文件夹了。

.gitlab-ci.yml

最后的配置文件如下所示:

# 依赖镜像
image: node:10.16.0

before_script:
  - echo "======== before script ========"
  - npm config set registry https://registry.npm.taobao.org

after_script:
  - echo "======== after script ========"

# 定义阶段 stages
stages:
  - build
  - deploy

# 定义 job - build 项目
job1-build:
  # 开始之前需要安装依赖
  stage: build
  script:
    - yarn install
    - yarn build
    - echo "finish build stage"
  only:
    - master
  artifacts:
    paths:
        - build/

# 定义 job
job2-deploy:
  stage: deploy
  script:
    - ls
    #######
    # 这里可以对项目进行真实的发布
    #######
    - echo "finish deploy stage"
  only:
    - master
  dependencies:
    - job1-build
    

其实真实的自动化构建部署上线也就是在 job2-deploy 里面再新增更复杂的脚本而已,比如笔者做的就是打包 docker 镜像推送到 docker hub 然后通过 k8s 部署项目。当然,也可以增加 stage 和 job,具体细节每个人每个公司都不一样,大家可以自行修改。

项目构建流程体系原则

前面介绍完基本上前端利用 Gitlab UI 就可以进行简单的自动化部署上线了,只不过在公司多人开发的时候要规范一些,下面是我个人(个人看法,不喜勿喷)觉得比较规范化的合理流程。

  • 并行开发项目,每次上线 master 分支
  • 基于 master 分支新开分支开发(单人 && 多人)
  • 分支开发完毕准备上线前提 Merge Request,code-review 没问题后由 Masters 进行 Merge 到 master
  • master 分支改变触发 Gitlab CI 自动构建部署上线(上线失败回滚也是自动触发)

因此,Gitlab 项目配置应该是如下:

  • master 分支在新增完 .gitlab-ci.yml 之后设置不允许 push
  • master 分支只可以由管理员通过 Merge Request 进行代码的合并

相关优化

强调一点,这里的优化是指本人在实践中的优化,并不能代表通用型,所以大家可以按需采用并尝试。

  • npm -> yarn

这一点毋庸置疑,在前端开发体验中越来越多的项目已经从npm -> yarn,yarn 的安装速度以及缓存使用确实更适合 CI。

  • 镜像源设置
# 将镜像源设置成淘宝镜像
- yarn config set registry 'https://registry.npm.taobao.org'
# 如果你用到了 node-sass,比较特别,因为它经常安装失败,稳妥起见重新设置一下
- yarn config set sass_binary_site 'https://npm.taobao.org/mirrors/node-sass/'
  • 使用 gitlab-cicache

cache:
  key: your-project-name
  paths:
    - $(pwd)/.yarn-cache
    - node_modules/

并且安装命令变成 - yarn install --pure-lockfile --cache-folder $(pwd)/.yarn-cache

通过如下配置,就会在构建的时候对依赖包进行缓存,这样下次再次安装就会提速飞快~

下面是经过优化的一些截图:

  • 使用 npm 的时候:278s

  • 切换到 yarn: 196s

  • 增加缓存: 1s

当然了,这里的 1s 是指我们的依赖并没有发生变化的情况,如果发生变化,肯定是要重新安装的。但是总体来说,提速是很多的。

总结

很浅显的一篇文章,只是为了让大家能简单了解前端自动化构建,以及个人的一些小小总结。更深层次的比如Gitlab Runner和实际项目的Docker部署因为因人而异,也并不是所有人都用 Docker 部署前端,所以没过多介绍,我觉得作为入门了解 Gitlab CI 还 OK~