上节课的思考题是容器化部署与容器化运行服务的差异点有哪些。这里我总结三个有代表性的供你参考:
在今天的课程里,我将带你分析一个基本的前端部署系统的工作流程、基本架构和主要功能模块的技术点。学习了这部分的内容之后,再结合之前几节课关于部署效率的内容,我们就可以基本掌握一个高效的前端构建部署系统的开发思路了。
要搭建一个自动化的构建部署系统,首先需要理解使用这个部署系统的工作流程。
在下图中,我演示了从用户提交代码到项目部署上线的整个过程中,部署系统与其他节点对接的流程示意图。
其中的主要环节如下:
除了核心的构建部署流程外,系统还需要具备可供用户正常使用的其他辅助功能流程:
以上就是一个基本的前端部署系统的工作流程。限于篇幅原因,课程里不再展开其中各个功能模块的具体细节,而主要介绍最核心的构建任务流程的相关技术点。
这部分主要介绍部署服务器环境准备、Webhook、任务队列等 6 个技术点,首先是部署服务器环境准备。
与普通的 Web 服务不同,用于项目构建部署的服务器需要具备构建部署流程所需的相关环境条件。在非容器化的情况下,如果所搭建的是分布式的服务,则需要尽量保证一些环境条件的一致,以便在不同项目使用和迁移时,保持过程和产物的稳定性。
需要保持一致的环境条件如下:
要实现用户提交代码后部署系统立即收到相关消息的功能,就需要事先在 CVS 系统(例如 Gitlab、Github 等)中创建 Webhook。具体流程如下:
在部署系统接收到 Webhook 传递的代码提交信息后,下一步就是根据提交信息创建构建记录,并执行构建任务。但是由于执行构建任务是耗时的,对于同一个项目而言,如果当前有正在执行的构建任务时,执行任务的工作目录是处于使用状态的,此时需要把这期间新创建的构建任务排入待执行队列中,等待当前任务执行完毕后,再从队列中获取下一个任务执行。即使使用容器化构建部署,构建任务在独立容器内进行,也需要对整个部署系统的同时执行任务数(Concurrency)设定限制。我们需要将超过限制数量的新增任务排入队列中,避免过多任务同时执行,耗尽集群计算资源。
在 NodeJS 中,有一些管理队列的工具可供选用,例如 Bull、Agenda 等。以 Bull 为例,下面的示例代码就演示了部署系统中创建队列、添加构建任务、任务处理、任务完成的流转过程。
// 创建任务队列
queue = new Queue(qname, {
redis: redisConfig,
})
queue.promiseDone = () => {}
queue.process(async (job, done) => {
const config = job.data
const task = new BuildTask(config) //创建并执行构建任务
queue.promiseDone = done //将任务完成函数赋值给外部属性,用于异步完成
})
return queue
}
export const queueJobComplete = async (id) => {
queue.promiseDone()
}
export const queueJobFail = async (id, err) => {
queue.promiseDone(new Error(err))
}
export const queueJobAdd= async (id, data) => {
queue.add(data, {
jobId: id, //jobId of queue
})
}
在之前的课程介绍过,部署系统中一次完整的构建任务大致可分为以下基本阶段:
这些阶段的划分可以起到以下作用:
和普通的 Web 服务不同,部署服务在对项目进行构建部署时,涉及许多命令行指令的调用。如下所示:
#依赖安装
npm install
#执行构建
npm run build
#产物打包
tar -zcf client.tar.gz dist/
在 NodeJS 程序中,这些调用需要通过子进程来完成,例如下面的代码:
import { spawn } from 'child_process'
export const spawnPromise = ({ commands, cwd, onStdout, onStderr }) => {
return new Promise((resolve, reject) => {
onStdout = onStdout || (() => {})
onStderr = onStderr || (() => {})
const subProcess = spawn('bash', { detached: true, cwd })
subProcess.on('close', (code, signal) => {
if (signal === 'SIGHUP') {
//abort callback immediately after kill
return reject()
}
if (code === 0) {
resolve('ok')
} else {
reject()
}
})
subProcess.stdout.setEncoding('utf8')
subProcess.stderr.setEncoding('utf8')
subProcess.stdout.on('data', onStdout)
subProcess.stderr.on('data', onStderr)
subProcess.stdin.on('error', (e) => {
notifySysError('subprocess stdin error', e)
reject(e)
})
commands.forEach((command) => {
subProcess.stdin.write(command + '\n')
})
subProcess.stdin.end()
})
}
我创建了一个 bash 的子进程,输入执行指令,然后监听输出信息和结束状态。通过这样的方式,即可控制各构件阶段指令的执行。
除了把构建过程划分成各执行阶段外,还需要定义一次构建任务的所有可能状态:
这 7 种状态中的后 4 种为终止状态。在部署系统中,需要将这些状态及时反馈到用户界面。
整个传递机制可以分为下面三个部分:
构建部署系统相对于我们日常比较熟悉的 B 端系统或 C 端 WebApp 而言,有一定的复杂性。但是只要理解了工作原理且掌握了整体架构,就可以按部就班地开发其中的各个模块,最后串接成一个功能完善、流程自洽的系统服务。所以本节课我们聊了两方面的内容:流程梳理和核心技术模块分析。
在流程梳理方面,首先你需要对构建部署的整体工作流程有一个比较清晰的认知,包括各服务间的对接、信息的传递等,其次掌握服务内部用户界面的各模块操作流程。在核心构建流程的模块分析方面,你需要了解操作层面的服务器环境的准备工作,代码架构层面的任务队列、构建任务阶段与状态拆分等。
希望通过这些内容,能让你对如何搭建高效的前端部署系统有一个初步印象。
到这里,我们的专栏就接近尾声了。下周还会更新一篇结束语,我会聊聊对开设课程的一些想法,包括对前端工程化领域的一些理解,以及对未来技术的展望。欢迎来听!
最后,我邀请你参与对本专栏的评价,你的每一个观点对我们来说都是最重要的。点击链接,即可参与评价,还有机会获得惊喜奖品!