CI & CD
在前端项目的构建部署流程里,除了使用构建工具执行构建之外,还有哪些因素会影响整个部署流程的工作效率?在部署系统中进行项目构建时,又会面临哪些和环境相关的问题和优化方案?
前端项目的部署流程
在前端项目中,通常可以把在一个全新环境下的代码部署过程分为以下几个环节:
- 获取代码:从代码仓库获取项目代码,并切换到待部署的分支或版本。
- 安装依赖:安装项目构建所需要的依赖包。
- 源码构建:使用构建工具对项目源代码进行构建,生成产物代码。
- 产物打包:将部署所需的代码(通常指的是构建后的产物代码,如果是部署 Node 服务则还需要其他目录与文件)打成压缩包。
- 推送代码:将待部署的文件或压缩包推送至目标服务器的特定目录下,如果是推送压缩包的情况,还需执行解压。
- 重启服务:在部署 Node 服务的情况下,在代码推送后需要进行服务重启。
本地部署相比部署系统的优势
对于使用部署系统的项目而言,除了重启服务这一步骤在普通静态服务部署中不需要执行外,上述其他环节通常是每次构建都需要经历的。
而如果使用本地开发环境进行部署,则可以根据情况对前两个环节进行简化:
- 在获取代码的环节中,本地开发环境已经包含了项目的本地代码,同拉取完整的代码仓库相比,直接获取更新内容并切换分支或版本的处理要更快一些。
- 在安装依赖的环节中,本地开发环境通常已包含了构建所需的最新依赖包,即使切换到待部署版本后发现依赖版本有变更,更新依赖包的时间也比在空目录下完整安装依赖包的时间更短。
此外,本地部署还有另外两点优势是使用部署系统所不具备的:
- 增量构建:在构建配置与项目依赖不发生变化的情况下,理论上,本地部署可以让构建进程长时间地驻留,以达到增量构建的效果。
- 快速调试:本地部署时,构建过程会直接在本地进行,因此有任何构建问题时可以第一时间发现并处理。相比之下,远程的部署系统则需要将一定的时间消耗在链路反馈和本地环境切换上。
因此,如果单从上面的部署环节来看,本地部署的效率一般优于部署系统,那么为什么在企业中通常不建议这样做呢?
本地部署的劣势
同远程部署系统相比,不管从安全性还是人员效率上看,本地部署都存在诸多问题:
流程安全风险
环境一致性
本地部署的第一个问题在于无法保证环境的一致性:
- 同一个项目,不同开发人员的本地环境(操作系统、NodeJS 版本等)都可能存在差异。
- 由于 NodeJS 语义化版本(Semantic Version)在安装时自动升级的问题,不同开发人员的本地 node_modules 中的依赖包版本也可能存在差异。
- 开发人员的本地环境和部署代码的目标服务器环境之间也可能存在差异。
这些差异会导致项目代码的稳定性无法得到保障。例如对于一个 Node 项目而言,在一个 NodeJS 低版本环境下构建的产物,在 Node 高版本环境下就有可能启动异常。
因此,如果项目都由开发人员各自在本地部署,无疑会降低项目的稳定性,增加部署风险。
而使用远程统一的部署系统,一方面避免了不同开发人员的本地环境差异性,另一方面,部署系统的工作环境也可以与线上服务环境保持一致,从而降低环境不一致的风险。
过程一致性
同环境一致性的问题相似,本地部署的第二个问题是无法保证部署过程的一致性。所谓过程的一致性,就是尽可能地让每次部署的流程顺序、各环节的处理过程都保持一致,从而打造规范化的部署流程。本地部署依赖人工操作,这就可能因为操作中的疏漏,导致过程一致性无法得到保障。尽管可以通过将部署流程写入脚本等方式减少人工误操作的风险,但是这和通过部署系统将完整处理过程写入代码的方式相比,仍然不够安全可靠。同时,系统可以记录每次部署操作的细节日志,便于当出现问题时快速解决。
工作效率问题
可回溯性
可回溯性的问题可以从日志和产物两方面来看。
- 日志:在部署过程中我们可能遇到各种问题,例如构建失败、单元测试执行失败、推送代码失败、部署后启动服务失败等。遇到这些问题时,需要有相应的日志来帮助定位。尽管在本地部署执行时也会输出日志,但是这些日志是临时的,查阅不便,且本地部署的日志至多只能保留当前一次的处理日志,如果希望对历史部署过程进行查看分析,更不能使用这种方式。
- 产物:通常,部署系统中会留存最近几次部署的构建产物包,以便当部署后的代码存在问题时能够快速回滚发布。而本地部署在项目的开发目录下执行,因此通常只会保留最近一次的构建产物,这就阻碍了上述快速回滚的实现。
相对的,一个规范化的部署系统,则可以记录和留存每一次部署操作的细节日志,以及保留最近若干次的部署代码包,因此在可回溯性上又胜一筹。
人员分工
工作效率的第二个问题是人员分工问题,这个问题又可以从以下几个侧面来分析:
首先部署过程需要耗费时间。在本地部署当前项目的某一个分支时,无法同时对该项目进行继续开发,往往只能中断当前的工作,等待部署完成。
在这个前提下,一个项目中的多名开发人员如果各自在电脑中进行部署,无疑增大了上述流程安全的风险系数。但反过来,如果一个项目里只有个别开发者的本地环境拥有部署权限,则所有人的部署需求都会堆积到一起,大大增加对有权限的开发者的工作时间的占用。如果不能及时响应处理,也会延误其他人的后续工作。
此外由于分工角色的不同,在许多情况下,部署流程会主动由测试人员而非开发人员发起。当部署在开发人员的本地环境中进行时,会像上面多人开发集中部署那样彼此影响,也增加了相应的沟通成本。
CI/CD
持续集成(Continuous Integration,CI)和持续交付(Continuous Delivery,CD),是软件生产领域提升迭代效率的一种工作方式:开发人员提交代码后由 CI/CD 系统自动化地执行合并、构建、测试和部署等一系列管道化(Pipeline)的流程,从而尽早发现和反馈代码问题,以小步快跑的方式加速软件的版本迭代过程。
这个过程通常是各系统(版本管理系统、构建系统、部署系统等)以自动化的方式协同完成的。而本地部署依赖人工操作,所以并不支持这种自动化的处理过程。
代码部署工具
在企业项目和开源项目中被广泛使用的几个典型部署工具,包括 Jenkins、CircleCI、Github Actions、Gitlab CI。
Jenkins
Jenkins 是诞生较早且使用广泛的开源持续集成工具。早在 2004 年,Sun 公司就推出了它的前身 Husdon,它在 2011 年更名为 Jenkins。下面介绍它的功能特点。
功能特点
- 搭建方式:Jenkins 是一款基于 Java 的应用程序,官方提供了 Linux、Mac 和 Windows 等各系统下的搭建方式,同时也提供了基于 Docker 的容器化搭建方式。此外,Jenkins 支持分布式的服务方式,各任务可以在不同的节点服务器上运行。
- 收费方式:Jenkins 是完全免费的开源产品。
- 多类型 Job:Job 是 Jenkins 中的基本工作单元。它可以是一个项目的构建部署流程,也可以是其他类型,例如流水线(Pipeline)。在 Jenkins 中支持各种类型的 Job:自定义项目、流水线、文件夹、多配置项目、Github 组织等。
- 插件系统:Jenkins 架构中内置的插件系统为它提供了极强的功能扩展性。目前 Jenkins 社区中共有超过1500 个插件,功能涵盖了继续继承和部署的各个环节。
- Job 配置:得益于其插件系统,在 Jenkins 的 Job 配置中可以灵活定制各种复杂的构建与部署选项,例如构建远程触发、构建参数化选项、关联 Jira、执行 Windows 批处理、邮件通知等。
- API 调用:Jenkins 提供了 Restful 的 API 接口,可用于外部调用控制节点、任务、配置、构建等处理过程。
CircleCI
CircleCI 是一款基于云端的持续集成服务,下面介绍它的功能特点。
功能特点
- 云端服务:由于 CircleCI 是一款基于云端的持续集成服务,因此无须搭建和管理即可直接使用。同时也提供了收费的本地化搭建服务方式。
- 收费方式:CircleCI 的云端服务分为免费与收费两种,免费版本一个账号只能同时运行一个 Job,同时对使用数据量、构建环境等有一定限制。而收费版本则提供了更多的并发构建数、更多的环境、更快的性能等。此外,如第一点所述,企业内部使用的本地化搭建服务方式也是收费的。
- 缓存优化:CircleCI 的任务构建是基于容器化的,因此能够缓存依赖安装的数据,从而加速构建流程。
- SSH 调试:它提供了基于 SSH 访问构建容器的功能,便于在构建错误时快速地进入容器内进行调试。
- 配置简化:在 CircleCI 中提供了开箱即用的用户体验,只需要少量配置即可快速开始构建项目。
- API 调用:CircleCI 中也提供了 Restfull 的 API 接口,可用于访问项目、构建和产物。
Github Actions
Github Actions(GHA)是 Github 官方提供的 CI/CD 流程工具,用于为 Github 中的开源项目提供简单易用的持续集成工作流能力。
功能特点
- 多系统:提供 Linux、Mac、Windows 等各主流操作系统环境下的运行能力,同时也支持在容器中运行。
- 矩阵运行:支持同时在多个操作系统或不同环境下(例如不同 NodeJS 版本的环境中)运行构建和测试流程。
- 多语言:支持 NodeJS、JAVA、PHP、Python、Go、Rust 等各种编程语言的工作流程。
- 多容器测试:支持直接使用 Docker-Compose 进行多容器关联的测试(而 CircleCI 中则需要先执行安装才能使用)。
- 社区支持:Github 社区中提供了众多工作流的模板可供选择使用,例如构建并发布 npm 包、构建并提交到 Docker Hub 等。
- 费用情况:Github Action 对于公开的仓库,以及在自运维执行器的情况下是免费的。而对于私有仓库则提供一定额度的免费执行时间和免费存储空间,超出部分则需要收费。
Gitlab CI
Gitlab 是由 Gitlab Inc. 开发的基于 Git 的版本管理与软件开发平台。除了作为代码仓库外,它还具有在线编辑、Wiki、CI/CD 等功能。在费用方面,它提供了免费的社区版本(Community Edition,CE)和免费或收费的商用版本(Enterprise Edition,EE)。其中社区版本和免费的商用版本的区别主要体现在升级到付费商用版本时的操作成本。另一方面,即使是免费的社区版本,其功能也能够满足企业内的一般使用场景,因此常作为企业内部版本管理系统的主要选择之一,下面我们就来了解 Gitlab 内置的 CI/CD 功能。
功能特点
- 与前面两款产品相似的是,Gitlab CI 也使用 yml 文件作为 CI/CD 工作流程的配置文件,在 Gitlab 中,默认的配置文件名为 .gitlab-ci.yml。在配置文件中涵盖了任务流水线(Pipeline)的处理过程细节:例如在配置文件中可以定义一到多个任务(Job),每个任务可以指定一个任务运行的阶段(Stage)和一到多个执行脚本(Script)等。完整的 .gitlab-ci.yml 配置项可参考官方文档。
- 独立安装执行器:与前面两款产品不同的是,Gitlab 中需要单独安装执行器。Gitlab 中的执行器 Gitlab Runner 是一个独立运行的开源程序,它的作用是执行任务,并将结果反馈到 Gitlab 中。开发者可以在独立的服务器上安装Gitlab Runner 工具,然后依次执行gitlab-runner register注册特定配置的 Runner,最后执行gitlab-runner start启动相应服务。此外,项目中除了注册独立的 Runner 外,也可以使用共享的或组内通用的 Runner。
当项目根目录中存在.gitlab-ci.yml 文件时,用户提交代码到 Git 仓库时,在 Gitlab 的 CI/CD 面板中即可看到相应的任务记录,当成功设置 gitlab-runner 时这些任务就会在相应的 Runner 中执行并反馈日志和结果。
依赖安装效率优化
五种前端依赖的安装方式
我们先来对比 5 种不同的前端依赖安装方式:
- npm:npm 是 NodeJS 自带的包管理工具,也是使用最广泛的工具之一。在测试时我们使用它的默认安装命令 npm install,不带额外参数。
- Yarn:Yarn 是 Facebook 于 2016 年发布的包管理工具,和 npm 5 之前的版本相比,Yarn 在依赖版本稳定性和安装效率方面通常更优(在 Github 中,Yarn 的 star 数量是 npm 的两倍多,可见其受欢迎程度)。在测试时我们同样使用默认安装命令 Yarn, 不带额外参数。
- Yarn with PnP:Yarn 自 1.12 版本开始支持 PnP 功能,旨在抛弃作为包管理目录的 node_modules,而使用软链接到本地缓存目录的方式来提升安装和模块解析的效率。在测试时我们使用 yarn --pnp,不带额外参数。。
- Yarn v2:Yarn 在 2020 年初发布了 v2 版本,它和 v1 版本相比有许多重大改变,包括默认使用优化后的 PnP 等。v2 版本目前通过 Set Version 的方式安装在项目内部,而非全局安装。测试时我们使用安装命令 Yarn,不带额外参数。
- pnpm:pnpm 是于 2017 年发布的包管理工具,和 Yarn 相同,它也支持依赖版本的确定性安装特性,同时使用硬连接与符号连接缓存目录的方式,这种方式相比于非 PnP 模式下的 Yarn 安装而言磁盘存储效率更高。测试时我们使用安装命令 pnpm install,不带额外参数。
依赖安装的基本流程
在对影响效率的问题进行分析之前,我们需要先了解一下前端依赖安装的基本流程阶段划分,这有助于分析不同场景下执行时间的快慢因素,排除各工具的细节差异。前端项目中依赖包安装的主要执行阶段如下:
- 解析依赖关系阶段:这个阶段的主要功能是分析项目中各依赖包的依赖关系和版本信息。
- 下载阶段:这个阶段的主要功能是下载依赖包。
- 链接阶段:这个阶段的主要功能是处理项目依赖目录和缓存之间的硬链接和符号连接。
那么如何获取执行时间呢?
如何获取执行时间
上面的几种安装方式中,npm 和 Yarn 在执行完成后的输出日志中会包含执行时间,而 pnpm 的输出日志中则没有。不过我们还是可以使用系统提供的 time 命令来获取,方法如下所示:
time npm i
time yarn
time pnpm i
如何获取执行日志
除了获取安装过程的执行时间外,如果需要进一步分析造成时间差异的原因,就需要从安装过程日志中获取更详细的执行细节,从中寻找答案。不同工具显示详细日志的方式也不同:
- npm:使用 npm 安装时需要在执行命令后增加***--verbose***来显示完整日志。
- Yarn v1:Yarn v1 版本(包括 Yarn --PnP)的实现方法和 npm 一样,即通过增加 --verbose 来显示完整日志。
- Yarn v2:Yarn v2 版本默认显示完整日志,可通过 --json 参数变换日志格式,这里使用默认设置即可。
- pnpm:pnpm 安装时需要在执行命令后增加 --reporter ndjson 来显示完整日志。
环境状态的五个分析维度
在确定了安装工具和分析方式后,我们还需要对执行过程进行划分,下面我一共区分了 5 种项目执行安装时可能遇到的场景:
场景名称 | Lock文件 | 历史安装目录 | 本地缓存 | 示例中日志名称 |
---|---|---|---|---|
纯净环境 | - | - | - | chean_install.log |
Lock环境 | Y | - | - | lock_install.log |
缓存环境 | Y | - | Y | cached_install.log |
无缓存的重复安装环境 | Y | Y | - | nocache_reinstall.log |
重复安装环境 | Y | Y | Y | cached_reinstall.log |
注 1:除了第一种纯净环境外,后面的环境中都存在 Lock 文件。因为 Lock 文件对于提供稳定依赖版本至关重要。出于现实场景考虑,这里不再单独对比没有 Lock 文件但存在历史安装目录的场景。 注 2: 为了屏蔽网络对解析下载依赖包的影响,所有目录下均使用相同注册表网址 registry.npm.taobao.org。 注 3:以下时间统计的默认设备为 MacOS,网速约为 20Mbit/s。
不同维度对安装效率的影响分析
Github Actions自动部署应用到自己的服务器
首先我们需要有自己的服务器环境,可以选择购买阿里云或腾讯云来使用。然后安装对应的Node或Nginx环境,简单安装可以使用宝塔面板来安装。首先安装宝塔面板
官方提供了命令来安装
然后会提供外网面板地址、用户名和密码等信息。
Github Actions是Github上一个类似于持续集成的功能,它允许你在一些节点上(如提交代码,特定时间等)触发一些操作。我们这里就利用它来实现自动部署应用到自己的服务器。
配置服务器密钥
- 生成密钥:ssh-keygen -m PEM -t rsa -b 4096 -C "xxxxxxxxx@xxx.com"
- 将公钥保存到 authorized_keys 文件中:cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
配置 SSH 公钥(将服务器公钥添加到GitHub账户的SSH)
配置GitHub SSH、Secrets,配置路径:当前仓库 -> Settings -> Secrets (这里配置的变量是 xxx.yml 文件中 secrets.你配置的名称
)
- REMOTE_HOST:服务器地址
- REMOTE_TARGET:服务器目录
- REMOTE_USER:服务器账户
- REMOTE_SSK_KEY:服务器私钥
然后在仓库的根目录,创建 .github/workflows 目录,在 .github/workflows 目录下 添加 xxx.yml 或 xxx.yaml 文件
name: GitHub Pages
# 在main分支发生push事件时触发。
on:
push:
branches:
- main
pull_request:
env: # 设置环境变量
TZ: Asia/Shanghai # 时区(设置时区可使页面中的`最近更新时间`使用时区时间)
jobs:
build: # 自定义名称
runs-on: ubuntu-latest # 运行在虚拟机环境ubuntu-latest
strategy:
matrix:
node-version: [16.x]
steps:
- name: Checkout # 步骤1
uses: actions/checkout@v1 # 使用的动作。格式:userName/repoName。作用:检出仓库,获取源码。 官方actions库:https://github.com/actions
- name: Use Node.js ${{ matrix.node-version }} # 步骤2
uses: actions/setup-node@v1 # 作用:安装nodejs
with:
node-version: ${{ matrix.node-version }} # 版本
# 生成静态文件
- name: Build
run: npm install && npm run build
- name: Deploy # 步骤3
uses: easingthemes/ssh-deploy@v2.1.2
env:
SSH_PRIVATE_KEY: ${{ secrets.REMOTE_SSK_KEY }}
ARGS: "-avz --delete"
SOURCE: "dist/"
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
REMOTE_USER: ${{ secrets.REMOTE_USER }}
TARGET: ${{ secrets.REMOTE_TARGET }}
最后在github中actions中查看构建