前端业界现有的构建部署方案,常用的应该是 Jenkins,Docker,GitHub Actions 这些,而恰巧,我们公司现在就并存了前两种方案。既然已经有了稳定的构建部署方式,为什么还要自己做一套前端自己的构建平台呢?当然不是为了好玩啊,原因听我慢慢分析。 前端构建使用的时候可能会碰到各种各样问题,比如:
而这些问题,如果有了自己的构建平台,这都将不是问题,所以也就有了现在的——云长。
为何起名叫“云长“呢,当然是希望这个平台能像”关云长“一样,一夫当关万夫莫开。那云长又能给我们提供什么样的一些能力呢?
这当然是必备的基本能力了,云长提供了公司不同前端项目类型,例如 Pampas、React、Vue、Uniapp 等的构建能力。整个流程其实也并不复杂,开始构建后,云长的服务端,获取到要构建的项目名,分支,要部署的环境等信息后,开始进行项目的代码更新,依赖安装,之后代码打包,最后将生成的代码再打包成镜像文件,然后将这份镜像上传到镜像仓库后,并且将项目的一些资源静态文件都可以上传 CDN,方便前端之后的调用,最后调用 K8S 的镜像部署服务,进行镜像按环境的部署,一个线上构建部署的流程也就完成了。
如果是使用别人的构建平台, 很多前端自己想加入的脚本功能就依赖别人的服务来实现,而如果走云长,则可以提供开放型的接口,让前端可以自由定制自己的插件式服务。
比如这个线上构建打包的过程当中,就可以处理一些前文提到过的问题,痛点,例如:
公司现有的平台发布流程管控靠的是运维的名单维护,每个项目都会管理一个可发布人的名单,所以基本项目发版都需要发布人当晚跟随进行发布,而云长为了解决这个问题,提供了一个审核流的概念。
也就是当项目在预发环境测试完成之后,代码开发者可以提起一个真线的发布申请单,之后这个项目的可发布人会通过钉钉收到一个需要审核的申请单,可以通过网页端,或者钉钉消息直接操作,同意或者拒绝这次发布申请,在申请经过同意后,代码开发者到了可发布时间后,就能自己部署项目发布真线,发布真线后,后续会为这个项目创建一个代码的 Merge Request 请求,方便后续代码的归档整理。
这么做的好处呢,一方面可以由前端来进行项目构建发布的权限管控,让发布权限可以进行收拢,另一方面也可以解放了项目发布者,让开发者可以更方便的进行代码上线,而又开放了项目的发布。
云长可以对外输出一些构建更新的能力,也就让第三方插件接入构建流程成为了可能,我们贴心的为开发者提供了 VsCode 插件,让你在开发过程中可以进行自由的代码更新,省去打开网页进行构建的时间,足不出户,在编辑器中进行代码的构建更新,常用环境更是提供了一键更新的快捷方式,进一步省去中间这些操作时间,这个时候多写两行代码不是更开心吗。
我们的 VsCode 插件不仅仅提供了云长的一些构建能力,还有小程序构建,路由查找,等等功能,期待这个插件分享的话,请期待我们后续的文章哦。
上面讲过云长的构建流程,云长是依赖于 K8S 提供的一个部署镜像的能力,云长的客户端与服务端都是跑在 Docker 中的服务,所以云长是采用了Docker In Docker 的设计方案,也就是由 Docker 中的服务来进行一个 Docker 镜像的打包。
针对代码的构建,云长服务端部分引入了进程池的处理,每个在云长中构建的项目都是进程池中的一个独立的实例,都有独立的打包进程,而打包过程的进度跟进则是靠 Redis 的定时任务查询来进行,也就实现了云长多实例并行构建的架构。
云长客户端与服务端的接口通信则是正常的 HTTP 请求和 Websocket 请求,客户端发起请求后,服务端则通过 MySQL 数据存储一些应用,用户,构建信息等数据。
外部的资源交互则是,构建的过程中也会上传一些静态资源还有打包的镜像到 cdn 和镜像仓库,最后则是会调用 K8S 的部署接口进行项目的部署操作。
上面看过了“云长”的一些功能介绍,以及“云长”的架构设计,相信很多朋友也想自己做一个类似于“云长”的前端构建发布平台,那需要怎么做呢,随我来看看前端构建平台主要模块的设计思路吧。
前端构建平台的主要核心模块肯定是构建打包,构建部署流程可以分为以下几个步骤:
pipeline
的流程来进行,流程可以参考以下例子// 构建的流程
async run() {
const app = this.app;
const processData = {};
const pipeline = [{
handler: context => app.fetchUpdate(context), // Git 更新代码
name: 'codeUpdate',
progress: 10 // 这里是当前构建的进度
}, {
handler: context => app.installDependency(context), // npm install 安装依赖
name: 'dependency',
progress: 30
}, {
handler: context => app.check(context), // 构建的前置校验(非必须):代码检测,eslint,package.json 版本等
name: 'check',
progress: 40
}, {
handler: context => app.pack(context), // npm run build 的打包逻辑,如果有其他的项目类型,例如 gulp 之类,也可以在这一步进行处理
name: 'pack',
progress: 70
}, {
handler: context => app.injectScript(context), // 构建的后置步骤(非必须):打包后的资源注入
name: 'injectRes',
progress: 80
}, { // docker image build
handler: context => app.buildImage(context), // 生成 docker 镜像文件,镜像上传仓库,以及之后调用 K8S 能力进行部署
name: 'buildImage',
progress: 90
}];
// 循环执行每一步构建流程
for (let i = 0; i < pipeline.length; i++) {
const task = pipeline[i];
const [ err, response ] = await to(this.execProcess({
...task,
step: i
}));
if (response) {
processData[task.name] = response;
}
}
return Promise.resolve(processData);
}
// 执行构建中的 handler 操作
async execProcess(task) {
this.step(task.name, { status: 'start' });
const result = await task.handler(this.buildContext);
this.progress(task.progress);
this.step(task.name, { status: 'end', taskMeta: result });
return result;
}
node
的child_process
模块执行 shell 脚本,下面是代码的一些示例:import { spawn } from 'child_process';
// git clone
execCmd(`git clone ${url} ${dir}`, {
cwd: this.root,
verbose: this.verbose
});
// npm run build
const cmd = ['npm run build', cmdOption].filter(Boolean).join(' ');
execCmd(cmd, options);
// 执行 shell 命令
function execCmd(cmd: string, options:any = {}): Promise<any> {
const [ shell, ...args ] = cmd.split(' ').filter(Boolean);
const { verbose, ...others } = options;
return new Promise((resolve, reject) => {
let child: any = spawn(shell, args, others);
let stdout = '';
let stderr = '';
child.stdout && child.stdout.on('data', (buf: Buffer) => {
stdout = `${stdout}${buf}`;
if (verbose) {
logger.info(`${buf}`);
}
});
child.stderr && child.stderr.on('data', (buf: Buffer) => {
stderr = `${stderr}${buf}`;
if (verbose) {
logger.error(`${buf}`);
}
});
child.on('exit', (code: number) => {
if (code !== 0) {
const reason = stderr || 'some unknown error';
reject(`exited with code ${code} due to ${reason}`);
} else {
resolve({stdout, stderr});
}
child.kill();
child = null;
});
child.on('error', err => {
reject(err.message);
child.kill();
child = null;
});
});
};
Eslint
校验操作,也可以在构建流程中加入,也就可以在线上构建的环节中加入拦截型的校验,控制上线构建代码质量。import { CLIEngine } from 'eslint';
export function lintOnFiles(context) {
const { root } = context;
const [ err ] = createPluginSymLink(root);
if (err) {
return [ err ];
}
const linter = new CLIEngine({
envs: [ 'browser' ],
useEslintrc: true,
cwd: root,
configFile: path.join(__dirname, 'LintConfig.js'),
ignorePattern: ['**/router-config.js']
});
let report = linter.executeOnFiles(['src']);
const errorReport = CLIEngine.getErrorResults(report.results);
const errorList = errorReport.map(item => {
const file = path.relative(root, item.filePath);
return {
file,
errorCount: item.errorCount,
warningCount: item.warningCount,
messages: item.messages
};
});
const result = {
errorList,
errorCount: report.errorCount,
warningCount: report.warningCount
}
return [ null, result ];
};
Docker
镜像,上传镜像仓库后,也需要信息记录,方便后期可用之前构建的镜像再次进行更新或者回滚操作,所以需要添加一张镜像表,下面为 Docker
镜像生成的一些实例代码。import Docker = require('dockerode');
// 保证服务端中有一个基本的 dockerfile 镜像文件
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
const image = '镜像打包名称'
let buildStream;
[ err, buildStream ] = await to(
docker.buildImage({
context: outputDir
}, { t: image })
);
let pushStream;
// authconfig 镜像仓库的一些验证信息
const authconfig = {
serveraddress: "镜像仓库地址"
};
// 向远端私有仓库推送镜像
const dockerImage = docker.getImage(image);
[ err, pushStream ] = await to(dockerImage.push({
authconfig,
tag
}));
// 3s 打印一次进度信息
const progressLog = _.throttle((msg) => logger.info(msg), 3000);
const pushPromise = new Promise((resolve, reject) => {
docker.modem.followProgress(pushStream, (err, res) => {
err ? reject(err) : resolve(res);
}, e => {
if (e.error) {
reject(e.error);
} else {
const { id, status, progressDetail } = e;
if (progressDetail && !_.isEmpty(progressDetail)) {
const { current, total } = progressDetail;
const percent = Math.floor(current / total * 100);
progressLog(`${id} : pushing progress ${percent}%`);
if (percent === 100) { // 进度完成
progressLog.flush();
}
} else if (id && status) {
logger.info(`${id} : ${status}`);
}
}
});
});
await to(pushPromise);
到这里一个项目的构建流程就已经成功跑通了,但一个构建平台肯定不能每次只能构建更新一个项目啊,所以这时候可以引入一个进程池,让你的构建平台可以同时构建多个项目。
Node
是单线程模型,当需要执行多个独立且耗时任务的时候,只能通过 child_process
来分发任务,提高处理速度,所以也需要实现一个进程池,用来控制多构建进程运行的问题,进程池思路是主进程创建任务队列,控制子进程数量,当子进程完成任务后,通过进程的任务队列,来继续添加新的子进程,以此来控制并发进程的运行,流程实现如下。
ProcessPool.ts 以下是进程池的部分代码,主要展示思路。
import * as child_process from 'child_process';
import { cpus } from 'os';
import { EventEmitter } from 'events';
import TaskQueue from './TaskQueue';
import TaskMap from './TaskMap';
import { to } from '../util/tool';
export default class ProcessPool extends EventEmitter {
private jobQueue: TaskQueue;
private depth: number;
private processorFile: string;
private workerPath: string;
private runningJobMap: TaskMap;
private idlePool: Array<number>;
private workPool: Map<any, any>;
constructor(options: any = {}) {
super();
this.jobQueue = new TaskQueue('fap_pack_task_queue');
this.runningJobMap = new TaskMap('fap_running_pack_task');
this.depth = options.depth || cpus().length; // 最大的实例进程数量
this.workerPath = options.workerPath;
this.idlePool = []; // 工作进程 pid 数组
this.workPool = new Map(); // 工作实例进程池
this.init();
}
/**
* @func init 初始化进程,
*/
init() {
while (this.workPool.size < this.depth) {
this.forkProcess();
}
}
/**
* @func forkProcess fork 子进程,创建任务实例
*/
forkProcess() {
let worker: any = child_process.fork(this.workerPath);
const pid = worker.pid;
this.workPool.set(pid, worker);
worker.on('message', async (data) => {
const { cmd } = data;
// 根据 cmd 状态 返回日志状态或者结束后清理掉任务队列
if (cmd === 'log') {
}
if (cmd === 'finish' || cmd === 'fail') {
this.killProcess();//结束后清除任务
}
});
worker.on('exit', () => {
// 结束后,清理实例队列,开启下一个任务
this.workPool.delete(pid);
worker = null;
this.forkProcess();
this.startNextJob();
});
return worker;
}
// 根据任务队列,获取下一个要进行的实例,开始任务
async startNextJob() {
this.run();
}
/**
* @func add 添加构建任务
* @param task 运行的构建程序
*/
async add(task) {
const inJobQueue = await this.jobQueue.isInQueue(task.appId); // 任务队列
const isRunningTask = await this.runningJobMap.has(task.appId); // 正在运行的任务
const existed = inJobQueue || isRunningTask;
if (!existed) {
const len = await this.jobQueue.enqueue(task, task.appId);
// 执行任务
const [err] = await to(this.run());
if (err) {
return Promise.reject(err);
}
} else {
return Promise.reject(new Error('DuplicateTask'));
}
}
/**
* @func initChild 开始构建任务
* @param child 子进程引用
* @param processFile 运行的构建程序文件
*/
initChild(child, processFile) {
return new Promise(resolve => {
child.send({ cmd: 'init', value: processFile }, resolve);
});
}
/**
* @func startChild 开始构建任务
* @param child 子进程引用
* @param task 构建任务
*/
startChild(child, task) {
child.send({ cmd: 'start', task });
}
/**
* @func run 开始队列任务运行
*/
async run() {
const jobQueue = this.jobQueue;
const isEmpty = await jobQueue.isEmpty();
// 有空闲资源并且任务队列不为空
if (this.idlePool.length > 0 && !isEmpty) {
// 获取空闲构建子进程实例
const taskProcess = this.getFreeProcess();
await this.initChild(taskProcess, this.processorFile);
const task = await jobQueue.dequeue();
if (task) {
await this.runningJobMap.set(task.appId, task);
this.startChild(taskProcess, task);
return task;
}
} else {
return Promise.reject(new Error('NoIdleResource'));
}
}
/**
* @func getFreeProcess 获取空闲构建子进程
*/
getFreeProcess() {
if (this.idlePool.length) {
const pid = this.idlePool.shift();
return this.workPool.get(pid);
}
return null;
}
/**
* @func killProcess 杀死某个子进程,原因:释放构建运行时占用的内存
* @param pid 进程 pid
*/
killProcess(pid) {
let child = this.workPool.get(pid);
child.disconnect();
child && child.kill();
this.workPool.delete(pid);
child = null;
}
}
Build.ts
import ProcessPool from './ProcessPool';
import TaskMap from './TaskMap';
import * as path from 'path';
// 日志存储
const runningPackTaskLog = new TaskMap('fap_running_pack_task_log');
//初始化进程池
const packQueue = new ProcessPool({
workerPath: path.join(__dirname, '../../task/func/worker'),
depth: 3
});
// 初始化构建文件
packQueue.process(path.join(__dirname, '../../task/func/server-build'));
let key: string;
packQueue.on('message', async data => {
// 根据项目 id,部署记录 id,以及用户 id 来设定 redis 缓存的 key 值,之后进行日志存储
key = `${appId}_${deployId}_${deployer.userId}`;
const { cmd, value } = data;
if(cmd === 'log') { // 构建任务日志
runningPackTaskLog.set(key,value);
} else if (cmd === 'finish') { // 构建完成
runningPackTaskLog.delete(key);
// 后续日志可以进行数据库存储
} else if (cmd === 'fail') { // 构建失败
runningPackTaskLog.delete(key);
// 后续日志可以进行数据库存储
}
// 可以通过 websocket 将进度同步给前台展示
});
//添加新的构建任务
let [ err ] = await to(packQueue.add({
...appAttrs, // 构建所需信息
}));
有了进程池处理了多进程构建之后,如何记录每个进程构建进度呢,我这边选择用了 Redis 数据库进行构建进度状态的缓存,同时通过Websocket 同步前台的进度展示,在构建完成后,进行日志的本地存储。上面代码简单介绍了进程池的实现以及使用,当然具体的应用还要看自己设计思路了,有了进程池的帮助下,剩下的思路其实就是具体代码实现了。
最后来聊聊我们对于前端构建未来的一些想法吧,首先前端构建必须保证的是更加稳定的构建,在稳定的前提下,来达到更快的构建,对于 CI/CD 方向,比如更加完整的构建流程,在更新完生成线上环境以后,自动处理代码的归档,归档后最新的 Master 代码重新合入各个开发分支,再更新全部的测试环境等等。
而对于服务端性能方面,我们考虑过能不能将云端构建的能力来靠每台开发的电脑来完成,实现本地构建,云端部署的离岸云端构建,将服务器压力分散到各自的电脑上,这样也能减轻服务端构建的压力,服务端只做最后的部署服务即可。
还有比如我们的开发同学很想要项目按组的维度进行打包发布的功能,一次发布的版本中,选定好要一起更新发布的项目以及版本分支,统一发布更新。
所以有了自己的构建发布平台,自己想要的功能都可以自己操作起来,可以做前端自己想要的各类功能,岂不是美滋滋。我猜很多同学可能会对我们做的 VsCode 插件感兴趣吧,除了构建项目,当然还有一些其他的功能,比如公司测试账号的管理,小程序的快速构建等等辅助开发的功能,是不是想进一步了解这个插件的功能呢,请期待我们之后的分享吧。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/RpIDgjpm6cpe9pL6lG2zHg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。