前端CI/CD思考(以小程序为例)

1,662 阅读15分钟

本文首发于本人CSDN博客​Tank_in_the_street

在谈论CI/CD这个东西的时候,先来聊聊这个东西是个什么意思。

CI,是英文Continuous Integration的简称,即持续集成。而CD,是英文Continuous Deployment的简称,即持续部署。

持续集成的意思即:是指多名开发者在开发不同功能代码的过程当中,可以频繁的将代码行合并到一起并切相互不影响工作。

而持续部署的意思是:基于某种工具或平台实现代码自动化的构建、测试和部署到线上环境以实现交付高质量的产品,持续部署在某种程度上代表了一个开发团队的更新迭代速率。

这么说的话好像有点深奥,那么结合起来怎么理解呢?那就先从传统的部署方法讲起吧。

传统的小程序部署和测试流程,需要开发人员把代码推上gitlab,然后测试同学拉取小程序指定分支的代码,配置好相应的appid等参数,才能进行测试。整套流程效率低下,对测试人员要求比较高,要求熟悉微信开发者工具。手动的参数配置也容易出错,导致测试测了半天竟然测错了版本。

而新的CI/CD所要解决的就正是这些弊端,从代码分支自动合并到傻瓜式自动化部署,其目的就是为了提升效率,降低错误的发生。而这也是我近期在做的一件事,不过由于刚开始接触公司业务,对公司业务不是非常熟悉,所以暂时没弄CI,转而把精力放在CI之前的代码预检测和CD的工作上。

一、代码预提交检测

在搞CI之前,肯定得先统一各位成员的代码格式。eslint这个东西也仅仅只是作为一个校验工具,成员不遵守规则,还是可以把代码提交到gitlab上。要是一些代码规范不一样,比如声明变量未使用,没加分号,这些也只是小事情。要是出现重复命名多个变量,甚至把报错代码强行推上去,那么整个代码仓库将会被这条记录给污染了。

怎么办?这个时候就得通过husky来让各位成员强行遵守了。husky这个包可以捕获git在不同阶段的钩子,通过给这些钩子配置一些方法,就能够达到我想要的目的。

单纯靠这个插件可不行,这个插件只是能捕获git不同阶段的钩子,对于哪些文件需要处理还不清楚。这个时候就得用lint-staged插件了。lint-staged是个文件过滤器,专门过滤git缓存区里的文件,可以通过配置要过滤的文件后缀名来获得想要的文件。具体的huskyrc文件如下配置:

{ 
    "hooks": { 
        "pre-commit": "lint-staged" 
    } 
}

虽然有了这两个插件,但是总感觉还是缺了点什么。没错,目前还缺了个能够自动格式化代码的插件。而这里选用的插件是prettier。prettier是一个多语言的代码格式化工具,prettier比eslint强大在它是可以对全量代码进行格式化。那它配合lint-staged的使用是这样的:

{ 
    "src/**/*.{js,jsx,ts,tsx}": ["prettier --write", "eslint"] 
}

这里语句后面有eslint,是防止当prettier出现问题的时候,代码没有进行格式化就提交上去,留一个eslint是防止这种问题的出现。

配合这些工具,代码预提交检测就可以了。

二、CI

CI的话则需要考虑的东西很多,比如当前团队的一个git的工作流是怎么样的。由于我所在的技术团队git的工作流的Gitflow,所以,持续集成的话不可能对master持续集成,而是应该对release分支进行持续集成。

不过由于Gitflow是个有十年历史的工作流思路,面对如今要求持续交付而应运而生的持续集成,多少有点力不从心。我这里就避开这个坑,暂时没有在CI上过多的做工作,转而对CD做更多的工作。

关于Gitflow不太适合持续集成的文章,可以看Gitflow工作流的创始人写的一篇文章:A successful Git branching model 他本人更推荐GitHubFlow。

三、CD

这里才是我最近比较着重研究的地方,关于对持续部署的现阶段落地和未来要去做的东西的思考。持续部署这个东西对于测试来说算是解放了他们很多的劳动力,能够更加专注的去做他们的本职工作了。

CD的构建工作是这样的,运用的工具是Jenkins,一开始在Jenkins里新建一个任务,并填写相应名称。

然后填写项目描述,最重要的就是要填写要拉取的代码地址和配置token。

代码地址的配置只需要在Repository URL填好地址即可,而Credentials这一栏,则需要填写个人在gitlab的ssh,查git的ssh百度即可,我这里就不多做笔墨了。

之后在General上,添加参数,选择Git Parameter。在Git Parameter为了和上面源码管理里的$branch保持一致,name这里得填branch,然后选择默认的分支。

小程序是分上传和预览的,这两个功能集成到jenkins上则需要用小程序官方提供的插件miniprogram-ci。小程序的官方文档里已经对这套插件的使用方法说的很明白了,感兴趣的可以查看这份文档:miniprogram-ci概述

我们只需要把这个插件安装到项目上,配置一个属于自己的运行文件,即可。

// 小程序CI集成上传预览
const ci = require('miniprogram-ci');
const { appid, qrcodeOutputDest, robot, branch, type, desc, projectPath, privateKeyPath, version, setting } = require('./constants');

let descInfo = `分支: ${branch}\r\n版本: ${version}\r\n描述: ${desc}`;

(async () => {
  const project = new ci.Project({
    appid: appid,
    type: 'miniProgram',
    projectPath: projectPath,
    privateKeyPath: privateKeyPath,
    ignores: ['node_modules/**/*','build/*','package.json','package-lock.json','gulpfile.js','**/*.less'],
  });

  if (type === 'preview') {
    const previewResult = await ci.preview({
      project,
      desc: descInfo,
      setting,
      robot,
      qrcodeFormat: 'image',
      qrcodeOutputDest: qrcodeOutputDest,
      onProgressUpdate: console.log,
    });
    console.log(previewResult);
  }
  else if (type === 'upload') {
    const uploadResult = await ci.upload({
      project,
      version: version,
      desc: descInfo,
      robot,
      setting,
      onProgressUpdate: console.log,
    });
    console.log(uploadResult);
  }
})();

由于区分了上传和预览类型,则需要在jenkin里添加多一个文本参数,来指定要选择的方式。这里的参数会注入到node的环境里,在process.argv那里便能捕获到pubtype。

除此以外,小程序的发包是按照当前团队情况来分不同的包。我目前团队情况是要区分正式包和测试包,所以又得在文本参数这里多加一个配置,来配置需要上传、预览什么样类型的包:

之后像构建机器人、appid等之类的参数也是用类似的方法,我就不一一阐述了。

最后也就到了最后一步了,就是配置构建的shell命令了。项目代码拉下来之后是没有node_module的,需要我们自己去安装node_module,这个时候就需要写shell命令了。

在构建那一栏里,添加执行shell这个构建步骤,首先得给项目安装依赖,由于在一般情况下,jenkin都是运行在公司内网的,公司内网很大程度上是不能翻墙的,所以还是老老实实的配置npm的代理:

node -v
npm -v
npm install --registry=https://registry.npm.taobao.org

还记得上面写的环境配置参数(env)吗,一般我们本地运行程序是运行npm run dev,这个命令在package.json文件里进行配置了,而到了生产环境上,则需要在package.json上配置生产环境的执行命令。

当然运行之后还要执行我们配置好的miniprogram-ci的运行文件,在这里传入我们在文本参数那里写好的配置参数,这样就差不多搞定了。

node -v
npm -v
npm install --registry=https://registry.npm.taobao.org
echo $env

npm run $env

node CI/index.js type=$pubtype branch=$branch desc=$desc version=$version robot=$robot

之后开发和测试的同学,在需要对某个分支进行上传或者预览的时候,填写一下配置即可:

由于在miniprogram-ci配置文件里qrcodeOutputDest参数指定了预览二维码图片的生成路径,只需按照配置的路径找该图片,下载到本地,用开发者工具的二维码编译即可在开发者工具里看到这个打包之后的项目。

四、遇到的坑

1. socket hang up

socket hang up可谓是把我折磨到死的问题。有时候你会发现自己的构建一时可以一时不行,都不知道是哪里出了问题。

关于这个问题的解决还是得从其原本意思出发。小程序执行上传和预览的时候都会调用一个接口,这个接口的域名是servicewechat.com,后面路由是根据你选的是上传或者预览而进行变化,再后面则带着一长串的参数,这些参数则是你在执行ci.preview或者ci.upload方法里所传入的参数。

这个域名是与小程序那边进行一个连接,将代码上传到小程序后台那边。如果是在本地电脑上执行,则很少会看到这个报错。但是如果是在jenkin里,因为jenkin所处的环境是公司内网环境,对于外网会进行一个限制。就拿我公司的jenkin来讲,整个jenkin服务配置的带宽只有5M,并且还分配给五台机器使用,默认的超时时间是一分钟,由于网络问题和项目代码过大,有时候代码不能在一分钟之内完成推送,这就导致 socket hang up的出现。

解决这个问题最简单粗暴的方法就是提升带宽、或者从项目代码入手,进行一些代码大小优化。甚至你都可以让运维同学把超时时间弄久一点,只不过这个方法有点治标不治本,毕竟带宽就这么大,还有五台机器都用这带宽,弄久一点超时时间只会白占带宽影响到别的正在部署的服务。

最后当然还是叫运维同学提升一下带宽,至于项目代码方面就得好好协商了。

当然网上也有别的方案,通过修改指定dns来实现,这个还是得看运维同学的配合,详细请看:小程序自动化部署方案在好大夫的落地

2. syscall name

一般出现这种问题的原因是在构建时走到安装依赖的时候手动取消jenkin部署进程导致的,因为安装依赖的时候node_modue里面已经产生了一个依赖的缓存文件,当你再次点击部署的时候则会触发这个错误。

解决这个错误的话得要看是哪个依赖产生了一个同名缓存文件,把他删掉之后在shell执行这个命令:

npm cache clean --force

然后再重新安装依赖。

其实预防这个问题很简单的,在shell执行构建任务的时候,只要项目第一次执行构建成功生成了node_modue文件之后,以后都可以不用再执行npm i 这个命令了。除非package.json文件发生变化。

或者你也可以在shell执行构建任务的时候,每次都删除node_module,然后再重新安装,也不会有这种问题的发生。

3. 预览的图片不能自动展示出来

预览的图片最后是会通过你在qrcodeOutputDest配置的地址,生成到该地址中,所以一般还得手动从jenkin项目中手动打开这个文件。

如何展示这个预览图片呢?网上很多方案都是要给jenkin安装插件,然后修改全局配置,把这个图片链接地址直接展示到构建历史上,比如这样:

思路很好,确实可以这样做。但是如果在公司项目上,除非整个jenkin都是新部署的,不然就为了你这个项目突然间改全局配置,在有无风险都不清楚的情况下贸然这样操作,出了问题那肯定准备提桶跑路的。

比较遗憾的是,要是通过上面所说的一整套流程构建一个项目的jenkin任务,在不通过插件的情况下是无法将图片展示出来的,必须推倒重来,用pipeline的方式新建一个项目的构建任务,而这也是我后期所要做的东西。

五、后期要补充的东西

我们先来回顾一下刚刚我们是怎么样生成一个构建任务的。

首先我们通过在jenkins里选择新建任务,然后点击这个框,一步一步的填写变量和shell命令来实现我们的CD工作流。

但是这么做也有他的一个弊端,首先第一个弊端就是刚刚说的,无法把预览的二维码直接放到构建历史里展示,得让测试通过生成地址去找二维码图片。如果有另一位同学在该项目里点了构建,则之前的二维码图片会被自动回收,测试同学可能都还没看到就不见了。

还有一个问题就是不够灵活。目前现在所看到的只是一个半自动的CD工作流,而我的目标则是全自动的CD工作流?何为全自动?

首先,在上面的部署中,由于配置的参数太多,得逐个填完之后,才能点击构建。其实,由于无法感知gitlab代码仓库里分支代码的变化,所以每次都要手动配置完之后才能点击构建部署。通过自由风格的软件项目,其实不能很好的对各个步骤阶段执行它对应的逻辑。

那该怎么办呢?pipeline(流水线)就能一劳永逸的解决上面所遇到的困难瓶颈。

pipeline是运行在jenkin上的一套工作流框架,不同于之前的配置,pipeline可以通过groovy脚本来实现更加灵活的配置,可以对gitlab的触发器做细粒度更细的操作。接下来的配置方法大致如下:

首先在jenkins里选择流水线任务。

然后点击构建触发器里的高级选项,将webhook复制下来,并选择需要监听的分支。我这里选择用正则的形式去匹配分支:

点击生成之后将secret token保存。之后打开gitlab,把配置好的地址和token粘贴上去,同时点击新建tag的时候也会触发构建器。

之后可以尝试在gitlab那里对目标分支进行打tag操作,如果在jenkins上能看到进行构建,则证明成功了。

之后则填写pipeline代码,pipeline代码是groovy脚本,可以对构建过程做出更加细腻的操作,由于还没落地实践,这里就给一段伪代码。

在写伪代码之前再多嘴一句,pipeline里是分声明式和脚本式,两者的区别从代码上是这样的:

// 声明式
pipeline {
    agent none 
    stages {
        stage('Example Build') {
            agent { docker 'maven:3.8.1-adoptopenjdk-11' } 
            steps {
                echo 'Hello, Maven'
                sh 'mvn --version'
            }
        }
        stage('Example Test') {
            agent { docker 'openjdk:8-jre' } 
            steps {
                echo 'Hello, JDK'
                sh 'java -version'
            }
        }
    }
}



// 脚本式
node {
    stage('Example') {
        if (env.BRANCH_NAME == 'master') {
            echo 'I only execute on the master branch'
        } else {
            echo 'I execute elsewhere'
        }
    }
}

而从官网上所说是这样的:

Scripted Pipeline, like Declarative Pipeline, is built on top of the underlying Pipeline sub-system. Unlike Declarative, Scripted Pipeline is effectively a general-purpose DSL [2] built with Groovy. Most functionality provided by the Groovy language is made available to users of Scripted Pipeline, which means it can be a very expressive and flexible tool with which one can author continuous delivery pipelines.

简单来说,就是脚本式更加的灵活,因为可以控制更细的节点。不过嘛看项目代码的实际情况来用。我这里用声明式就足够了。

详细的pipeline语法请查看官网文档:Pipeline Syntax

其他方案

jenkins pipeline这个方案能够很好的解决我们的问题,有同学可能会好奇有没有其他方案。那肯定是有的,比如社区比较火的一个是用tekton来实现CI/CD工作流,文档:tekton ,还有就是可以利用gitlab-CI自带的也能实现类似效果。

参考文档

1. 什么是GitFlow工作流?

2. Jenkins 配合 GitLab 实现分支的自动合并、自动创建 Tag

3. 什么是CI/CD

4. 嘴对嘴,手摸手 ,10分钟教你学会用 Jenkins +miniprogram-ci 自动生成微信小程序预览二维码

5. Jenkins Pipeline系列(一)—— 如何配置扩展共享库

6. Pipeline Syntax