序
对于前端开发而言,静态页面的部署是容易的。只需要将打包好的页面,放入已经配置好的 nginx
或者 apache
的静态资源目录下即可。然而,重复的、机械化的打包、上传、部署工作,是无意义且令人厌烦的。
自动化部署就是为了解决这一问题而存在的。
很多重量级的软件 Docker
、Jenkins
等,都提供了自动化部署的相关功能。但也为 自动化部署 流程,蒙上了一层神秘的面纱。其实,自动化部署并不是什么高深的技术,只要你掌握了一些基本的工具和原理,就可以轻松地实现自动化部署。
因此,本文仅以前端开发最熟悉的 NodeJS
为基础,攻略 自动化部署 ,并使之能够运用到实际的项目中。希望能对大家有所帮助。
部署流程
现在来回顾一下,前端页面的手动部署流程。
git
拉取最新代码 => npm
更新依赖 => 打包生成 dist
=> 登录服务器 => 上传文件到指定目录
打包生成 dist
, 通常是缓慢耗时、需要等待的。如果,打包出的文件数很多,我们还可能需要将文件 zip
压缩, 上传到服务器后再解压。
手动部署通常是涉及本机文件和服务器交互的。自动化部署 中,我们可以将上述步骤全权交由服务器。
服务器部署流程:
git
拉取最新代码 => npm
更新依赖 => 打包生成 dist
=> 移动 dist
内容到 nginx
静态资源
服务器相关依赖:
git
: 拉取源码node/npm
: 更新依赖与打包nginx
: 静态资源目录
Webhooks
你可能已经发现了,如果服务器实现了上述流程。开发者只需要告诉服务器,什么时候跑这个流程。这就是全部。
幸运的是 git
对于每个仓库,都提供了 webhooks
功能。简而言之,就是如果你对仓库做了某些操作,git
将通过 http
向你提供的地址发送操作信息(json).
要确保 git 平台 与 部署服务器 之间是可连通的,webhooks 才具有可行性
以 gitlab
为例:
-
trigger
: 这里选中了push events
,pattern
填入了master
. 当master
分支 提交事件发生时,触发该webhook
-
url
: 发送信息到http://192.168.110.130:3000/deploy/zz-platform-config
-
secret token
: 可以输入一个和部署服务器约定的hash
值, 用作校验。
这里演示使用的
gitlab
、部署服务器 都使用的是内网。当然,你也可以使用公网的配置,只要确保 git 平台 与 部署服务器 之间可连通。
如果你使用的也是内网环境,需要打开 webhooks
对本机地址的允许
服务端应用程序
接下来创建服务端应用程序,监听 3000
端口,接受 webhooks
将会发送的信息。
这里以 NestJS 为例
Controller
这个应用程序将部署在 192.168.110.130
服务器上
webhooks
操作信息将发送到 http://192.168.110.130:3000/deploy/zz-platform-config
整理一下思路之后,一个 DeployController
的实现如下:
ts
@Controller('deploy')
export class DeployController {
constructor (private readonly deployService: DeployService) {}
@Post('zz-platform-config')
async deployZzPlatformConfig (
@Body() body: GitlabPushEvent,
@Headers('X-Gitlab-Token') token: string,
) {
// 1. 校验 Secret token 是否正确
if (token !== '123456') return {
code: 401,
message: 'Unauthorized',
}
// 2. 通过 git 下载代码到本地 git clone
const { dir: codeDir } = await this.deployService.cuCode(body)
// 3. 更新代码的依赖 pnpm i
await this.deployService.uDependencies(codeDir)
// 4. 获取部署信息,如 打包命令名称、产物部署目录 等,
const deployConfigs = await this.deployService.rDeployConfigs(codeDir, body)
// 5. 根据部署信息,运行打包脚本,将打包产物拷贝至静态资源目录
for (const deployConfig of deployConfigs) {
await this.deployService.deploy(codeDir, deployConfig)
}
return {
code: 200,
message: 'ok',
}
}
}
Service
逐一分析实现之前,介绍一些重要的变量 \ 方法 \ 接口
ts
export const workRoot = '[当前部署应用程序的根目录]'
// 代码下载到 codes 目录下
export const codesRoot = path.resolve(workRoot, 'codes')
// webhook 发送的信息
export interface GitlabPushEvent {
project: {
// @example rd/platform/zz-platform-config
path_with_namespace: string
}
repository: {
// @example http://192.168.110.138:30080/rd/platform/zz-platform-config.git
git_http_url: string
}
}
ts
import { spawn } from 'node:child_process'
export const run = async (
script: string,
cwd?: string,
) => { // 使用这个方法, 调用脚本命令
return new Promise((resolve) => {
const [cmd, ...args] = script.split(' ')
// 在node中使用子进程运行脚本
const app = spawn(cmd, args, {
cwd,
stdio: 'inherit',
shell: true,
})
app.on('close', resolve)
})
}
run
是实现程序中运行 cmd
命令的重要方法。
更新代码
ts
/**
* 创建/更新代码
* @param body
*/
async cuCode (body: GitlabPushEvent) {
// 如果 codesRoot 不存在, 创建
if (!fs.existsSync(codesRoot)) {
fs.mkdirSync(codesRoot, { recursive: true })
}
// 检查仓库是否已经拉取过
const codeDir = path.resolve(
codesRoot,
body.project.path_with_namespace,
)
if (
// 文件夹存在
fs.existsSync(codeDir)
// 并且文件夹为空
&& !fs.readdirSync(codeDir).length
) {
// 清除该文件夹
fs.rmdirSync(codeDir)
}
if (
// 文件夹不存在
!fs.existsSync(codeDir)
) {
/**
* git clone
* @example
* git clone http://192.168.110.138:30080/rd/platform/zz-platform-config.git rd/platform/zz-platform-config
*/
await run(`git clone ${body.repository.git_http_url} ${body.project.path_with_namespace}`, codesRoot)
} else {
// 文件夹存在, 且不为空
// 放弃当前修改
await run('git reset --hard', codeDir)
// 拉取最新代码
await run('git pull', codeDir)
}
return {
dir: codeDir,
}
}
上述函数将会把目标仓库的源码下载到本地指定的目录中。函数返回了 dir
, 即源码的根目录,方便我们后续使用。
这是一个执行 cuCode
之后的工作目录,在这个示例中 dir
= codes/rd/platform/zz-platform-config
.
更新依赖
使用 pnpm
下载/更新依赖,需要部署环境中里提前安装 pnpm
ts
/**
* 更新依赖
*/
async uDependencies (codeDir: string) {
// 安装依赖
await run('pnpm i', codeDir)
}
部署配置
为了使,部署服务的通用性更强,考虑在目标仓库(源码)中,放入一个json
,来描述如何部署这个项目
json
{
"scripts": [
// 依次执行命令,最终生成打包产物
"build:skzz" // npm run build:skzz
],
"path": {
// 打包产物所在目录
"dist": "./app/dist/zz/zz-platform-config",
// 静态资源目录
"html": "/home/nginx/html/zz/zz-platform-config"
}
}
ts
/**
* 获取部署配置
*/
async rDeployConfigs (codeDir: string, body: GitlabPushEvent) {
let deployList = [{}] as DeployConfig[]
if (fs.existsSync(
path.resolve(codeDir, 'deploy.config.json'),
)) {
const jsonObj = JSON.parse(
fs.readFileSync(
path.resolve(codeDir, 'deploy.config.json'),
'utf-8',
),
)
if (Array.isArray(jsonObj)) {
deployList = jsonObj
} else {
deployList = [jsonObj]
}
}
return deployList.map(deployConfig => {
const scripts = deployConfig.scripts || []
const pathDist = deployConfig.path?.dist ?? './dist'
// prod: deployConfig.path?.html ??
const pathHtml = deployConfig.path?.html ?? path.resolve(workRoot, 'nginx/html', body.project.path_with_namespace)
return {
scripts,
path: {
// 从 dist 中取出所有文件
dist: path.resolve(codeDir, pathDist),
// 将取出的文件放入 nginx 的指定目录下
html: pathHtml,
},
}
})
}
可能一个目标仓库中,存在多个应用需要部署,例如文档和应用本身。这里处理成数组,增强健壮性。
部署
ts
async deploy (codeDir: string, deployInfo: DeployConfig) {
// 如果 dist 文件夹存在,清空文件夹
if (fs.existsSync(deployInfo.path.dist)) {
await fs.promises.rm(
deployInfo.path.dist,
{ recursive: true },
)
}
// 执行构建命令
for (const script of deployInfo.scripts) {
await run(`npm run ${script}`, codeDir)
}
// 如果 html 文件夹存在,重名文件夹【自动备份】
if (fs.existsSync(deployInfo.path.html)) {
await fs.promises.rename(
deployInfo.path.html,
deployInfo.path.html + '.' + new Date().toISOString(),
)
}
// 将 dist 下所有文件放入 html 下
consola.log('开始拷贝文件')
await fs.promises.cp(
deployInfo.path.dist,
deployInfo.path.html,
{
recursive: true,
},
)
consola.success('部署成功')
}
根据部署配置,执行命令,转移产物,即部署成功。
:::warning
这里展示了一个最基础的部署实现。当然,可以根据实际的需求,去调整代码。这一过程是自由的。
举个例子,如果认为 deployInfo.path.html
,完全由目标仓库决定是有安全风险的,则使用 静态 或者 静态+动态拼接 的方式决定。
:::
服务器中启动
安装必要依赖
-
git
-
node >= 18.0.x
-
pnpm -
npm i pnpm -g
配置git免登
推荐使用 SSH 密钥或访问令牌(Personal Access Token)来进行身份验证。
也有一种简单的设置,运行以下命令,设置全局凭证存储:
shell
git config --global credential.helper store
这会在用户主目录下的 .git-credentials 文件中存储明文的用户名和密码。请注意,这并不是最安全的方式,因为凭证以明文形式存储在文件中。
执行一次 Git 操作(如克隆或拉取),并在提示输入用户名和密码时输入你的 GitLab 用户名和密码。
接下来的 Git 操作将不再要求输入用户名和密码,因为它们已经存储在 .git-credentials 文件中。
但由于密码以明文形式存储在文件中,它并不是最安全的方式。
后台启动 nest 服务
shell
# 服务器中拉取 部署应用 安装依赖
# 后台启动 nest 服务
# 在命令末尾添加 & 可以将命令放入后台运行 日志默认放入当前文件夹
$ nohup npm run start &
# 查看日志
$ tail -f nohup.out
# 查看进程
$ ps aux | grep "npm"
服务启动后,接受到提交事件,开始打包部署。
结
本文涉及的代码@simple-deploy
通过本文的实践,我们可以看到,自动化部署并不是一个遥不可及的技术,而是通过合理利用现有工具和技术,结合适当的编程实践就能实现的过程。通过配置 webhooks
触发部署事件,服务器端应用程序能够自动处理从代码更新到部署的整个流程,包括代码拉取、依赖安装、打包、以及将打包好的文件放入资源目录。
这些理解和技能的转移,可以帮助我们更深入地理解自动化部署的本质,以及如何在更复杂的系统中实施自动化部署策略。对将来接触和使用 Docker
、 Jenkins
也有着重要的启示和补充意义。