前端CICD
前端CICD
CICD 概念
前端 CICD(Continuous Integration and Continuous Deployment)是指前端开发中的持续集成和持续部署流程。它是一种自动化的工作流程,旨在将代码的集成、构建、测试和部署过程自动化,以提高开发团队的效率和产品的质量。
前端 CICD 的目标是实现以下几个方面的自动化:
- 代码集成:将多个开发人员的代码合并到主分支或开发分支,确保代码的一致性和兼容性。
- 构建:将源代码转换为可在生产环境中运行的静态文件,例如将 TypeScript 转换为 JavaScript、将 SCSS 转换为 CSS、将模块打包等。
- 测试:自动运行各种类型的测试,例如单元测试、集成测试和端到端测试,以确保代码的质量和功能的正确性。
- 部署:将构建好的静态文件部署到服务器、CDN 或其他托管服务上,使得应用程序能够在线上环境中运行。
为什么要做CICD?
提升代码质量、提高开发效率、加速产品交付以及确保应用安全。
- 提升代码质量:通过自动化的测试过程,CI/CD 可以确保每次代码变更都经过严格的验证,从而确保代码质量。这有助于减少人为错误,并及早发现潜在的问题。
- 提高开发效率:CI/CD 自动化了构建、测试和部署流程,使得开发人员能够更专注于编写代码,而不是手动执行这些繁琐的任务。这不仅可以节省时间,还可以减少因手动操作而引入的错误。
- 加速产品交付:通过自动化的持续集成和持续部署,CI/CD 能够大大缩短软件的开发周期。代码变更一旦通过测试,就可以立即部署到生产环境,从而加快产品上市的速度。
- 提高可预测性和可靠性:CI/CD 通过持续的自动化测试,提供了对软件质量的实时反馈。这使得团队能够更准确地预测项目的进度和潜在风险,从而提高项目的可靠性。
- 促进团队协作:CI/CD 可以帮助团队成员更好地协作,因为每个人都可以看到代码变更的实时状态以及测试结果。这有助于减少沟通成本,提高团队的工作效率。
- 确保应用安全:通过自动化的安全测试和漏洞扫描,CI/CD 可以帮助团队及时发现并修复安全问题,从而确保应用的安全性。
- 支持持续反馈和迭代:CI/CD 提供了一个快速反馈循环,使得团队能够更快地响应市场和用户的反馈,进行迭代和优化。
CICD 方案
- 自定义脚本:使用自定义的脚本和工具实现前端 CICD 流程,例如使用 Shell 脚本、Node.js 脚本或其他构建工具(如Grunt、Gulp)来执行代码的构建、测试和部署。
- 持续集成工具:使用专门的持续集成工具,如Jenkins、Travis CI、CircleCI、GitLab CI/CD等,配置和管理前端 CICD 流程。这些工具提供了可视化界面、插件和集成能力,使得配置和管理更加方便。
- 云原生方案:使用云原生的解决方案,如AWS CodePipeline、Google Cloud Build、Azure DevOps等。这些平台提供了托管的构建和部署服务,可以与云服务(如AWS S3、Azure Blob Storage)和容器平台(如Docker、Kubernetes)集成,实现全自动化的前端 CICD 流程。
- 版本控制托管平台集成:结合代码托管平台(如GitHub、GitLab、Bitbucket)的特性,利用其提供的 Webhooks、Actions、Pipelines等功能,实现前端 CICD 的集成和自动化。
以下主要对比我在实践中尝试的几种方案:
Node CI/CD | Jenkins | GitLab CI/CD | |
---|---|---|---|
平台支持 | 多平台 | 多平台 | Unix, Windows, macOS, 支持Go的平台 |
集成度 | 较低 | 中等 | 高(与GitLab平台紧密集成) |
配置方式 | 自定义脚本 | Web界面和脚本 | YAML配置语言 |
插件支持 | 有限 | 丰富的插件生态系统 | 支持插件和扩展 |
自动化程度 | 依赖自定义脚本 | 自动化构建、测试和部署 | 高度自动化,包括自动缩放和实时日志记录 |
分布式构建 | 依赖自定义实现 | 支持分布式构建 | 支持多台机器并行构建 |
社区支持 | 依赖于Node.js社区 | 庞大的社区和活跃的开发者社群 | 开源项目,有活跃的社区支持 |
使用门槛 | 较高(需要编写和维护自定义脚本) | 中等(需要一定的配置经验) | 低(友好的YAML配置和强大的集成功能) |
Node CICD
考虑到团队前端人数较少,项目数量不是非常多,服务器资源和其他资源较少,而使用 持续集成工具 需要一个服务器来执行构建和测试任务,因此在团队初期我们使用自定义脚本部署方案部署前端代码。
使用 Node 自定义脚本部署方案,可以在开发者本地执行 CICD 流程,不需要额外的服务器资源,适合资源不足的团队。
以下介绍自定义脚本部署方案实现过程:
所需资源
资源 | 作用 | 备注 |
---|---|---|
静态资源服务器 | 部署前端构建的静态资源 | 可以使用 nginx 部署 |
FTP | 上传静态前端资源到服务器 | FTP 服务和账号 |
node 环境 | 构建脚本和上传 |
使用技术
插件 | 作用 |
---|---|
husky | 设定 git hook , 在执行分支操作时自动执行 shell 脚本 |
shell 脚本 | 根据分支自动执行相对应环境 npm script 脚本 |
npm script | 自动构建脚本命令 |
ftp-deploy | 上传静态资源 |
部署规范
部署流程一般会通过结合 git
分支操作进行规范流程,在我们项目中,设定了以下部署规范:
- 测试分支
test
代码构建产物只能推送到 测试服,其他代码构建产物不能直接推送到测试服; - 正式分支
master
、prod
代码构建产物只能推送到 正式服,其他代码构建产物不能直接推送到 正式服;正式分支通常设置为保护分支,不能直接在正式分支提交代码,必须提交 PR 流程;
通过以上规则,主要避免以下情况:
- 通过设置测试分支
test
构建产物只能推送测试服,可以避免在多人开发不同模块时候竞争使用测试服资源,相互覆盖对方代码的情况;在该规则情况下,所有要上测试服的代码必须先合并到测试分支test
才能上测试服。 - 正式分支
master
、prod
代码构建产物只能推送到 正式服,可以避免将其他分支代码推送到正式环境;
具体分支流程规范,可以参考我的这篇文章:Git 规范
实现方案
总体流程
- 本地切换分支到
test
; - 合并开发分支代码,推送代码到远程仓库;
- 触发
git hooks
钩子,执行 shell 脚本; - shell 脚本检查分支,执行对应分支的
npm script
脚本; - 本地构建生成静态资源;
- 上传静态资源;
- 同步代码到 git 仓库。
下面将详细介绍每一个流程中实现方案
Git Hooks
Git hooks是Git版本控制系统中的脚本钩子,它们允许你在特定的Git操作(如提交、推送、合并等)发生时触发自定义脚本。
通过使用Git hooks,你可以在这些关键操作之前或之后执行自定义的脚本,以实现一些自动化、验证或其他定制化的操作。
Git hooks有两种类型:客户端钩子(Client-side hooks)和服务器端钩子(Server-side hooks)。
- 客户端钩子:客户端钩子在开发者本地执行,用于控制提交、合并等操作。以下是常见的客户端钩子:
pre-commit
:在执行提交操作之前触发。可以用于运行代码风格检查、单元测试等操作,确保提交的代码符合规范。pre-push
:在执行推送操作之前触发。可以用于运行集成测试、代码质量检查等操作,确保推送的代码符合要求。prepare-commit-msg
:在生成提交信息之前触发。可以用于自动填充提交信息,如添加分支名、问题号等。post-commit
:在提交操作完成后触发。可以用于触发通知、更新依赖等操作。
- 服务器端钩子:服务器端钩子在代码推送到远程仓库时执行,用于控制接受或拒绝推送,以及执行其他自定义操作。以下是常见的服务器端钩子:
pre-receive
:在接收推送操作之前触发。可以用于验证提交的代码、检查权限等操作,拒绝不符合要求的推送。post-receive
:在接收推送操作完成后触发。可以用于触发部署、通知等操作。
要配置Git hooks,你需要在Git仓库的.git/hooks/
目录下创建相应的钩子脚本文件。这些脚本文件默认是被禁用的,你需要将它们重命名为去掉.sample
后缀,并添加相应的逻辑。
Husky
Husky 是一个用于在 Gi 仓库中设置和管理 Git hooks 的工具。它可以帮助你更方便地配置和使用Git hooks,并确保这些钩子在团队中的各个开发环境中一致地运行。
Husky 提供了一个简单的命令行接口,可以在项目中安装和配置Git hooks。
它的优点之一是使用简单的配置文件,而不需要手动创建和管理钩子脚本文件。
Husky支持常见的客户端钩子,如pre-commit、pre-push等。
使用Husky的主要步骤如下:
安装Husky:在项目根目录中运行以下命令,使用npm或者yarn安装Husky:
npm install husky --save-dev # 或者 yarn add husky --dev ```
配置Git hooks:在
package.json
文件中添加一个husky
字段,用于配置Git hooks。你可以指定要运行的钩子以及相应的命令。例如,以下配置示例在提交之前运行ESLint检查:"husky": { "hooks": { "pre-push": "..." //值为你要执行的 npm script 脚本 } }
创建 husky 文件:在根目录下创建一个
.husky
文件夹,该文件夹下面包含_
文件夹和pre-push
钩子文件:在
./husky/_
文件夹下创建husky.sh
shell 文件:#!/bin/sh if [ -z "$husky_skip_init" ]; then debug () { if [ "$HUSKY_DEBUG" = "1" ]; then echo "husky (debug) - $1" fi } readonly hook_name="$(basename "$0")" debug "starting $hook_name..." if [ "$HUSKY" = "0" ]; then debug "HUSKY env variable is set to 0, skipping hook" exit 0 fi if [ -f ~/.huskyrc ]; then debug "sourcing ~/.huskyrc" . ~/.huskyrc fi export readonly husky_skip_init=1 sh -e "$0" "$@" exitCode="$?" if [ $exitCode != 0 ]; then echo "husky - $hook_name hook exited with code $exitCode (error)" fi exit $exitCode fi
创建 Git 钩子: 在
./husky
目录下创建pre-push
钩子shell 脚本,该文件脚本会在git push 之前执行#!/bin/sh . "$(dirname "$0")/_/husky.sh" # 代码推送前根据当前分支进行自动构建(仅对 prod 分支和 test 分支进行构建) # doc: https://typicode.github.io/husky/#/?id=install function current_branch () { branch='' testBranch='test' masterBranch="prod" cd $PWD if [ -d '.git' ]; then output=`git describe --contains --all HEAD|tr -s ''` if [ "$output" ]; then branch="${output}" fi fi if [ $branch == $testBranch ] then echo "自动构建并上传 test 分支代码" npm run build:test elif [ $branch == $masterBranch ] then echo "自动构建并上传 prod 分支代码" npm run build:prod fi } current_branch
该脚本主要执行以下操作:
- 获取当前分支;
- 检查当前分支是否为测试分支,是则执行构建测试脚本;
- 检查当前分支是否为正式分支,是则执行构建正式脚本;
npm Script
npm script是npm提供的一种功能,允许你在package.json
文件中定义和运行自定义的命令脚本。
通过使用npm script,你可以在项目中方便地运行各种任务和操作,例如构建项目、运行测试、启动开发服务器等。
在package.json
文件中,有一个特殊的字段"scripts"
,其中可以定义一组键值对,每个键表示一个自定义的命令,而对应的值则是要运行的命令脚本。
添加构建脚本
在 package.json
文件的 script
字段添加如下脚本:
"scripts": {
"build:test": "cross-env NODE_ENV=test vite build --mode test && esno ./build/script/postBuild.ts && npm run deploy:test",
"build:prod": "cross-env NODE_ENV=production vite build --mode production && esno ./build/script/postBuild.ts && npm run deploy:prod",
"deploy:test": "cross-env NODE_ENV=test && node ./deploy/index.ts",
"deploy:prod": "cross-env NODE_ENV=production && node ./deploy/index.ts",
},
脚本说明:
脚本 | 作用 |
---|---|
npm run build:test | 本地构建测试分支代码并上传资源到测试服 |
npm run build:prod | 本地构建正式分支代码并上传资源到正式服 |
npm run deploy:test | 上传资源到测试服 |
npm run deploy:prod | 上传资源到正式服 |
FTP 上传资源
在本地构建资源完成,将在根目录下生成 /dist
静态资源文件夹,然后通过上传资源脚本将该文件夹所有文件上传到静态资源服务器中。
上传脚本放置统一文件夹下面管理 /deploy
:
/deploy
-- config.ts // ftp-deploy 服务器脚本配置
-- gitCheck.ts //上传规范检查脚本,用于强制限制正式服只能上传 prod 分支代码;测试服只能上传 test 分支代码;
-- serverConfig.ts // 命令行交互脚本,用于获取 shell 交互中输入的用户名称和密码
-- utils.ts // 工具函数,获取环境变量
-- users.ts // 用户 ftp 账号密码配置,仅仅保存在本地,不能上传到 Git 仓库中,要添加到 .gitignore 文件
发布脚本思路
- 本地检查分支,强制限制正式服只能上传 prod 分支代码;测试服只能上传 test 分支代码;
- 上传代码:
实现代码:
const FtpDeploy = require("ftp-deploy");
const ftpDeploy = new FtpDeploy();
const DeployConfig = require("./config.ts");
const gitCheck = require("./gitCheck.ts")
/**
* 上传文件函数
*/
async function upload() {
const config = await DeployConfig.getFtpDeployConfig();
ftpDeploy
.deploy(config)
.then((res) => console.log('finished:', res))
.catch((err) => console.log(err))
ftpDeploy.on('uploading', function (data) {
console.log('total file count being transferred: ', data.totalFilesCount) // total file count being transferred
console.log(data.transferredFileCount, ' of files transferred') // number of files transferred
console.log('partial path with filename being uploaded:', data.filename) // partial path with filename being uploaded
})
ftpDeploy.on('uploaded', function (data) {
console.log('uploaded: ', data) // same data as uploading event
})
ftpDeploy.on('upload-error', function (data) {
console.log('upload error: ', data.err) // data will also include filename, relativePath, and other goodies
})
}
if (gitCheck()) {
upload()
}
git 分支检查:强制限制 test 分支构建产物上传到 测试服, prod 分支构建产物上传到 正式服,在 /deploy/gitCheck.ts
:
/**
* 上传规范:
* 1. 正式服只能上传 prod 分支代码;测试服只能上传 test 分支代码;
*/
const fs = require('fs')
const path = require('path')
const utils = require('./utils.ts');
// 获取当前 git 分支名称
function getCurrentBranchName(p = process.cwd()) {
const gitHeadPath = `${p}/.git/HEAD`
return fs.existsSync(p)
? fs.existsSync(gitHeadPath)
? fs.readFileSync(gitHeadPath, 'utf-8').trim().split('/')[2]
: getCurrentBranchName(path.resolve(p, '..'))
: false
}
/**
* git 分支检查
* @returns
*/
function getCheck() {
// 获取当前 git 分支名称
const currentBranchName = getCurrentBranchName()
// 上传的服务器环境与 git 分支对应表: key 为上传的服务器环境, value 对应的 git 分支代码
const envtoGitbranchMap = {
'test': 'test',
'production': 'master'
}
if (envtoGitbranchMap[utils.getNodeEnv()] !== currentBranchName) {
console.error('请检查分支代码!!! 测试服仅能上传 test 分支代码, 正式服仅能上传 master 分支代码!!')
return false
}
return true
}
module.exports = getCheck
shell 交互获取用户账号密码: 在 /deploy/serverConfig.ts
// const OUTPUT_DIR = require("../build/constant.ts").OUTPUT_DIR;
const path = require('path')
const inquirer = require('inquirer');
async function getInput() {
const info = {
user: null,
password: null
}
await inquirer
.prompt([
{
type: 'input',
name: 'user',
message: '请输入用户名,按回车确认:',
validate: function (val) {
return val ? true : "请输入非空字符串";
}
},
{
type: 'input',
name: 'password',
message: '请输入密码,按回车确认:',
validate: function (val) {
return val ? true : "请输入非空字符串";
}
}
])
.then((answers) => {
info.user = answers.user
info.password = answers.password
})
return info
}
//获取上传配置信息
const getConfig = async function () {
let users = {
user: null,
password: null
}
try {
users = require("./users.ts");
} catch (err) {
console.log('用户信息文件不存在 ', err)
} finally {
if (!users.user || !users.password) {
users = await getInput()
}
}
return {
user: users?.user,
password: users?.password,
host: {
test: "测试服 ftp 地址",
production: "正式服 ftp 地址"
},
port: {
test: "测试服 ftp 端口号",
production: "正式服 ftp 端口号",
},
localRoot: path.resolve(__dirname, `../dist`),
remoteRoot: {// 远程静态资源文件路径
test: "测试服 ftp 目录",
production: "正式服 ftp 目录",
}
};
}
module.exports = getConfig
获取 FTP 配置
const deployConfig = require("./serverConfig.ts");
const utils = require('./utils.ts');
module.exports.getFtpDeployConfig = async function getFtpDeployConfig() {
const config = await deployConfig()
return {
user: config.user[utils.getNodeEnv()], // 服务器登录账号
password: config.password[utils.getNodeEnv()], // 服务器密码
host: config.host[utils.getNodeEnv()], // 服务器地址
port: config.port[utils.getNodeEnv()], // ftp的服务器端口
localRoot: config.localRoot, // 上传的文件
remoteRoot: config.remoteRoot[utils.getNodeEnv()], // 远程服务器文件存储路径
include: ['*', '**/*'],// 这将上传除了点文件之外的所有文件
// 排除sourcemaps和node_modules中的所有文件
exclude: ["dist/**/*.map", "node_modules/**", "node_modules/**/.*", ".git/**"],
deleteRemote: true, // 如果为true,则在上传前删除目的地的所有现有文件
forcePasv: true, // 主动模式/被动模式
sftp: false, // 使用 sftp协议 或 ftp协议
};
};
总结
特点
自动化部署方案需要考虑各自团队规模,团队成员习惯,规范等,个人经验总结,自定义脚本方案适用于以下特点:
- 前端团队规模较小:管理方便,能统一团队成员习惯;
- 资源较少:自定义脚本方案通常在开发人员本地打开构建,最后只要上传到测试服即可,因此不需要单独的服务器资源做自动化打包部署工具;
- 不能自动回退特定版本:版本发布频率较低,自定义脚本方案不能自动回退特定版本,需要手动回退版本,因此比较难自动快速回退特定版本,需要人工手动回退;
- 需要统一管理版本发布人员,由于本地脚发布方案是任何团队都可以在本地构建发布,因此需要有一个人单独管理正式服发版版本。
优点
- 无需服务器, 开发人员本地构建项目,适用于服务器资源较少的团队,但发版速度受限于开发人员本地构建速度;
- 发布流程简单快速, 本地构建即可发布,不同成员发布时候不会导致竞争服务器发布资源,开发人员直接将代码合并到测试服,本地构建完成直接上到测试服即可,多个开发人员构建可以并行,相互不影响。
- 灵活性和自定义能力: 根据项目需求自定义发布流程和操作。你可以使用git hooks和shell脚本来实现各种自定义逻辑和操作,如分支合并检查、环境选择和本地打包。这种灵活性允许你根据特定项目的要求进行精确的控制和处理。
- **直接控制和可视化: **允许你直接在本地执行和监控发布流程。你可以通过git hooks和shell脚本将构建和部署过程与本地开发环境集成在一起,使整个过程更加可视化和直观。
- 快速迭代和验证: 能够快速迭代和验证发布流程。通过本地合并分支和推送来触发发布流程,你可以更快地检查和验证构建结果,减少了与持续集成工具中的构建和部署的等待时间。
缺点
- 每个开发者本地需要保存一份 FTP 账号,容易导致 FTP 账号密码泄露,引发安全问题。
- 不能快速回退特点版本
- **依赖开发者环境: ** 自定义脚本方案依赖于开发者本地环境的配置和设置。如果你的团队中有多个开发者,每个开发者的配置可能会有所不同,这可能导致发布流程的一致性问题。
- 可扩展性有限: 自定义脚本方案在规模化或多人协作项目中可能会遇到一些限制。当项目变得庞大且团队规模扩大时,手动执行和管理发布流程可能会变得更加复杂和困难。
- 缺乏自动化和集中化: 自定义脚本方案在自动化和集中化方面相对较弱。虽然使用git hooks和shell脚本可以部分自动化发布流程,但整个流程仍然依赖于开发者手动触发和执行。此外,你需要在每个开发者的本地环境中设置并维护发布流程,这可能增加了管理和维护的工作量。
持续集成工具(如 Jenkins)的方案相对于自定义脚本方案有以下优点:
- 自动化和集中化: 持续集成工具提供了自动化和集中化的发布流程管理。你可以配置构建和部署的自动化流程,减少了手动干预的需求,并将流程集中管理在持续集成工具中。
- 可扩展性和并行处理: 持续集成工具可以轻松处理大型和多人协作项目的发布流程。它们支持并行处理和分布式构建,可以处理多个分支和多个环境的同时构建和部署。
- 环境隔离和一致性: 持续集成工具提供了环境隔离和一致性,确保每次构建和部署在相同的环境中进行。这有助于减少由于开发者本地环境差异引起的问题,并提供更可靠和可重复的结果。
然而,持续集成工具也有一些限制和不足,例如学习和配置的复杂性、依赖外部工具和服务、额外的基础设施要求等。
Jenkins 部署
Jenkins
Jenkins 是一款开源 CI&CD 软件,用于自动化各种任务,包括构建、测试和部署软件。
Jenkins 支持各种运行方式,可通过系统包、Docker 或者通过一个独立的 Java 程序。
在前端 CICD 中,使用 jenkins 主要执行以下操作:
- 代码集成:将多个开发人员的代码合并到主分支或开发分支。
- 测试:在测试服自动运行各种类型的测试,例如单元测试、集成测试和端到端测试,以确保代码的质量和功能的正确性。
- 构建:在测试服打包构建代码,生成静态资源。
- 部署:将构建好的静态文件部署到服务器、CDN 或其他托管服务上,使得应用程序能够在线上环境中运行。
除以上主要流程,Jenkins 还可以给发布版本打标签,记录软件发布版本,也可以根据发布版本快速回退版本。
部署流程
- 本地开发测试完成,合并到测试分支,提交代码;
- 触发
git hooks
的钩子,在钩子中执行 Eslint 检查、测试等; - 提交后触发git 仓库中的 webhook 钩子,webhook 通知 Jenkins 开始构建任务;
- Jenkins 开始构建:
- 同步仓库代码,合并分支,打版本标签
- 构建
- 测试
- 部署,上传资源到测试服
大多数最基本的持续交付 Pipeline 至少会有三个阶段:构建、测试和部署;
Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
stages {
stage('Build') {
steps {
echo 'Building'
}
}
stage('Test') {
steps {
echo 'Testing'
}
}
stage('Deploy') {
steps {
echo 'Deploying'
}
}
}
}
参考资料