背景
由于公司需要对公司内部的软件需要对外部署,对于前端的部署,需要一套部署的方案。尝试了写了一个适配多模块可配置的部署脚本,本文对实现的过程进行一个记录。
目的
脚本采用的是node,前端同学的首选。脚本的目的就是实现k8s中的自动化部署中的部分功能
熟悉k8s的同学或许了解在k8s的自动构建过程中,大概实现了以下几个步骤
- 从指定的代码仓库中获取代码
- 执行npm i或者npm ci(如果项目依赖内网npm还需要在这之前设置一下内网镜像)
- 执行npm build
- 将生成的静态文件放在指定的文件内
我们的node脚本就需要实现以上功能即可。
实现过程
第一步,从指定的代码仓库中获取代码
从指定仓库获取代码,需要怎么指定。
我们平时克隆代码的时候执行的是git clone xxx
,xxx
就是指定的代码地址,如果我们模仿一下node index.js xxx
,这样的话如果需要克隆多个后面就会很长,不太美观。
这里我选择通过写入配置文件的方式进行clone。并且提供两种clone方式。
1.给一个全量的仓库配置文件,通过手动选择其中的仓库进行clone。
2.创建一个默认的配置,对里面所有的git地址进行clone。
创建一个config.js
和一个customeConfig.js
文件
// config.js 全量仓库
export default [{url: 'https://github.com/yourProject1.git', // 代码地址modulesName: 'yourProject1', // 项目简称或者标识desc: '项目一' // 项目描述
},{url: 'https://github.com/yourProject2.git',modulesName: 'yourProject2', desc: '项目二'
},{url: 'https://github.com/yourProject3.git',modulesName: 'yourProject3', desc: '项目三'
},{url: 'https://github.com/yourProject4.git',modulesName: 'yourProject4', desc: '项目四'
}]
// customConfig.js 不在全量仓库里面的地址,全部下载
export default [{url: 'https://github.com/yourProject1.git', // 代码地址modulesName: 'yourProject1', // 项目简称或者标识desc: '项目一' // 项目描述
},{url: 'https://github.com/yourProject2.git',modulesName: 'yourProject2', desc: '项目二'
}]
使用者如果需要clone的仓库不在config.js里面,可以直接修改customConfig.js里面的文件。
我们还是分步骤实现
flowchart TD
A[选择clone方式 chooseDownLoadType] --> B{选择项目clone?}
B --是 选择config中的clone --> C[chooseGit]
C --选择需要clone的仓库地址 --> E[downLoadGit]
B --否 根据customConfig中的clone ----> E[downLoadGit]
根据上面流程图,我们需要实现三个方法
- chooseDownLoadType(); // 选择克隆方式
- chooseGit(); // 选择克隆仓库
- downLoadGit(); // 实现git clone
Talking is cheap show me your code 话不多说直接实现代码吧
这里通过inquirer
实现控制台的交互效果,具体使用方法可以查看npm官网的使用方法 www.npmjs.com/package/inq…
- 实现chooseDownLoadType()
function chooseDownLoadType () {inquirer.prompt([{type: "list",name: "preset",message: "请选择克隆的方式",choices: ["选择项目clone", "根据customConfig配置clone"]}]).then(choice => {if(choice.preset === '选择项目clone') { // 按照config文件clonechooseGit(); // 选择git地址}else { // 按照customConfig文件配置downLoadGits = customConfig.map(res => res.url); // 收集地址downLoadGit(); // 下载git}})
}
- 实现chooseGit()
function chooseGit () {inquirer.prompt([{type: "checkbox",name: "gits",message: "请选择需要克隆的项目地址:",choices: config.map(res => {return {name: `${res.modulesName}_(${res.desc})`,...res}}),default: [] // 默认选中的git}]).then(choice => {// 收集地址choice.gits.forEach(git => {const moduleName = git.split("_")[0]; // 获取nameif(config.map(conf => conf.modulesName).includes(moduleName)) {downLoadGits.push(config.find(gitConf => gitConf.modulesName === moduleName).url);}})downLoadGit();})
}
详细代码如下:
import { createRequire } from "module";
const require = createRequire(import.meta.url);
import inquirer from "inquirer";
import config from "./config.js";
import customConfig from "./customConfig.js";
let downLoadGits = [] // 收集需要clone的地址
function chooseDownLoadType () {inquirer.prompt([{type: "list",name: "preset",message: "请选择克隆的方式",choices: ["选择项目clone", "根据customConfig配置clone"]}]).then(choice => {if(choice.preset === '选择项目clone') { // 按照config文件clonechooseGit(); // 选择git地址}else { // 按照customConfig文件配置downLoadGits = customConfig.map(res => res.url); // 收集地址downLoadGit(); // 下载git}})
}
function chooseGit () {inquirer.prompt([{type: "checkbox",name: "gits",message: "请选择需要克隆的项目地址:",choices: config.map(res => {return {name: `${res.modulesName}_(${res.desc})`,...res}}),default: [] // 默认选中的git}]).then(choice => {// 收集地址choice.gits.forEach(git => {const moduleName = git.split("_")[0]; // 获取nameif(config.map(conf => conf.modulesName).includes(moduleName)) {downLoadGits.push(config.find(gitConf => gitConf.modulesName === moduleName).url);}})downLoadGit();})
}
function downLoadGit () {console.log('需要下载的git', downLoadGits)
}
function main () {chooseDownLoadType();
}
main()
测试一下,运行
node index.js
直接回车选择第一种方式
代码运行到
chooseGit()
这个方法了,这时根据提示通过空格()选择需要clone的项目地址,通过键盘的上下移动。这里我们选择前面两个然后回车下一步。
这时代码就到
downLoadGit()
这里了。需要下载的地址已经打印出来了
再测试第二种,运行
node index.js
这里就直接把customConfig里面所有的仓库地址都打印出来了。
怎么指定已经实现了,后面就是根据地址下载了,我们再实现downLoadGit()
这个比较重要的一步
execa
是一个可以通过js实现shell脚本的npm依赖包,后面的npm install和npm build都会通过这个实现。具体使用方法可以查看npm官网的使用方法www.npmjs.com/package/exe…。
- 实现downLoadGit()
async function downLoadGit () {console.log('需要下载的git', downLoadGits);if(fs.existsSync(PATH)) { // 如果目录存在则删除clearFolder(PATH)}await fs.mkdirSync(PATH);downLoadGits.forEach(async gitUrl => {let childProcess = execa("git", ["clone", gitUrl, "--progress"], {cwd: `${PATH}` // 执行的目录也就是git clone需要运行的目录})childProcess.stderr.pipe(process.stderr); // 将clone进度在控制台输出try {const result = await childProcess;if(result.exitCode === 0) {console.log('克隆完成')}} catch (error) {console.log('克隆失败', error)}})
}
这里需要fs模块创建和删除目录,PATH为代码存放的目录
import { createRequire } from "module";
const require = createRequire(import.meta.url);
import inquirer from "inquirer";
import config from "./config.js";
import customConfig from "./customConfig.js";
const fs = require('fs');
let downLoadGits = [] // 收集需要clone的地址
let PATH = 'gits' // clone的代码存放的目录
删除代码的目录的实现
function clearFolder (path) {let files = [];if (fs.existsSync(path)) {// 是否存在目录files = fs.readdirSync(path); // 读取目录下的目录和文件files.forEach(file => {let curPath = `${path}/${file}`; // 拼接路径if(fs.statSync(curPath).isDirectory()) { // 如果是文件夹就递归遍历clearFolder(curPath)}else {fs.unlinkSync(curPath); // 不是文件夹是文件直接删除}})fs.rmdirSync(path) // 清空文件夹}
}
到这里,基本上实现了git clone代码的功能,执行node index.js
测试图一下,经过一波选择。
index.js
完整代码
import { createRequire } from "module";
const require = createRequire(import.meta.url);
import inquirer from "inquirer";
import config from "./config.js";
import customConfig from "./customConfig.js";
import { execa } from 'execa';
const fs = require('fs');
let downLoadGits = [] // 收集需要clone的地址
let PATH = 'gits' // clone的代码存放的目录
function chooseDownLoadType () { inquirer.prompt([{ type: "list", name: "preset", message: "请选择克隆的方式", choices: ["选择项目clone", "根据customConfig配置clone"] }]).then(choice => { if(choice.preset === '选择项目clone') { // 按照config文件clone chooseGit(); // 选择git地址 }else { // 按照customConfig文件配置 downLoadGits = customConfig.map(res => res.url); // 收集地址 downLoadGit(); // 下载git } })
}
function chooseGit () { inquirer.prompt([{ type: "checkbox", name: "gits", message: "请选择需要克隆的项目地址:", choices: config.map(res => { return { name: `${res.modulesName}_(${res.desc})`, ...res } }), default: [] // 默认选中的git }]).then(choice => { // 收集地址 choice.gits.forEach(git => { const moduleName = git.split("_")[0]; // 获取name if(config.map(conf => conf.modulesName).includes(moduleName)) { downLoadGits.push(config.find(gitConf => gitConf.modulesName === moduleName).url); } }) downLoadGit(); })
}
function clearFolder (path) { let files = []; if (fs.existsSync(path)) { files = fs.readdirSync(path); files.forEach(file => { let curPath = `${path}/${file}` if(fs.statSync(curPath).isDirectory()) { clearFolder(curPath) }else { fs.unlinkSync(curPath); } }) fs.rmdirSync(path) }
}
async function downLoadGit () { console.log('需要下载的git', downLoadGits); if(fs.existsSync(PATH)) { // 如果目录存在则删除 clearFolder(PATH) } await fs.mkdirSync(PATH); downLoadGits.forEach(async gitUrl => { let childProcess = execa("git", ["clone", gitUrl, "--progress"], { cwd: `${PATH}` // shell执行的目录 }) childProcess.stderr.pipe(process.stderr); // 将clone进度在控制台输出 try { const result = await childProcess; if(result.exitCode === 0) { console.log('克隆完成') } } catch (error) { console.log('克隆失败', error) } })
}
function main () { chooseDownLoadType();
}
main()
需要注意的一些问题
1.clone的地址必须得有权限。
2.有些仓库设置了SSL验证,clone的时候需要git config --global http.sslVerify false
这样设置一下。
3.为什么clone的时候需要先删除一下目录,因为如果存在相同目录clone会失败,删除再clone这种操作类似build打包的时候对dist文件夹的处理。
第二步,实现执行npm install
第一步的工作已经把主要的原理实现了,接下来的工作相信大部分同学都知道怎么去实现了。
把大象装冰箱的步骤:打开冰箱门,把大象放进去。这里也分两步
- 找出执行
npm install
的路径 - 执行
npm install
实现思路:
新建一个build.js文件,定义一个数组installBuildPaths
来存放路径,一般执行npm命令行的文件路径需要满足该路径下存在package.json
这个文件。
let installBuildPaths = []
实现一个方法findPackagePath()
去递归刚才存放代码的路径PATH
下面的所有文件和目录,查找存在package.json
这个文件的路径并保存。为什么需要递归是考虑了多模块的情况。
function findPackagePaths (files, paths) {if(files.length === 0) return;files.filter(file => {return fs.statSync(joinPath([...paths, file])).isDirectory() // 返回目录路径}).forEach(file => {const path = joinPath([...paths, file]);const packageJsonPath = `${path}/package.json`;let existPackageJson = fs.existsSync(packageJsonPath);if(existPackageJson) {installBuildPaths.push(path);}else {let childrenFiles = fs.readdirSync(path);findPackagePaths(childrenFiles, [...paths, file]);}})
}
// 拼接路径
function joinPath (pathArr) {return `${pathArr.join('/')}`;
}
遍历installBuildPaths
里面的路径去执行npm install
,最好按顺序执行npm install。
function npmInstall () {run(installBuildPaths.shift());
}async function run (path) {if(path) {let childInstallProcess = execa("npm", ["install", "--loglevel", "silly"], {cwd: `${path}/`})childInstallProcess.stderr.pipe(process.stderr);try {const result = await childInstallProcess;if(result.exitCode === 0) {console.log(`${result.stdout}`)console.log(`install 成功`)}} catch (error) {console.log('install 失败', error)}}
}
这里执行的是
npm install --loglevel silly
目的是为了打印出更详细的信息,避免控制台出现await等待的焦虑。
执行脚本的main()
function main () {findPackagePaths(files, [PATH]);npmInstall();
}
main();
执行node build.js
最终安装完成就是这样,和平常执行npm install
最后的输出是一样的。
第三步,实现执行npm build
这里我们选择直接install完就build
npmBuild()实现
async function run (path) {if(path) {let childInstallProcess = execa("npm", ["install", "--loglevel", "silly"], {cwd: `${path}/`})childInstallProcess.stderr.pipe(process.stderr);try {const result = await childInstallProcess;if(result.exitCode === 0) {console.log(`${result.stdout}`)console.log(`install 成功`);console.log(`--------开始执行npm run build-------`);npmBuild(path);}} catch (error) {console.log('install 失败', error)}}
}async function npmBuild (path) {let childBuildProcess = execa("npm", ["run", "build"], {cwd: `${path}/`})childBuildProcess.stderr.pipe(process.stderr);try {const result = await childBuildProcess;if(result.exitCode === 0) {console.log(`${result.stdout}`)console.log(`build 成功`);}} catch (error) {console.log('build 失败', error)}
}
我们执行一下node build.js
看看,如果控制台能看到打包出来的文件那就说明成功了。
第四步,copy文件到指定目录
现在静态文件有了,就只剩最后一步了,将打包出来的文件放入指定的目录当中,在k8s里面这一步就是将静态文件放在镜像里面。
这里面我们需要获取项目打包的输出静态文件的目录,需要读取项目里面的配置文件,一般默认是在dist目录,有些项目可能设置了输出路径就需要获取打包文件的配置,例如我的项目是react,就需要获取build.confing.js这个配置文件。
实现putDistToAimFolder()
,STATIC
是存放静态文件的根目录,PATH
是存放代码的根目录,这里需要借助fs-extra
模块复制文件夹。
function putDistToAimFolder (path) {const aimFolder = joinPath([STATIC, path.split(`${PATH}/`)[1]]);let config = {};// 这里需要找到项目中的打包配置文件找到打包后的静态文件,我这里是react项目// 如果没找到默认dist目录// 其他项目可以视情况而定 这里需要完善一下if(fs.existsSync(`./${path}/build.config.js`)) {config = require(`./${path}/build.config.js`);config.outputDir = config.outputDir || `dist`;}else {config.outputDir = `dist`;} try {fs.copy(`${path}/${config.outputDir}`, aimFolder);console.log('静态文件已存放到指定目录');} catch (error) {console.log('文件复制失败', error);}
}
再执行完npmBuild()
后执行putDistToAimFolder()
;
async function npmBuild (path) {let childBuildProcess = execa("npm", ["run", "build"], {cwd: `${path}/`})childBuildProcess.stderr.pipe(process.stderr);try {const result = await childBuildProcess;if(result.exitCode === 0) {console.log(`${result.stdout}`);console.log(`build 成功`);console.log(`-----------copy文件到指定目录-------------`);putDistToAimFolder(path);}} catch (error) {console.log('build 失败', error)}
}
执行一下node build.js
看下效果
这样我们完整的一个项目的构建流程就走完了,然后继续下一个项目,然后再继续执行npmInstall()
这个方法即可。
打完收工贴一下完整的代码
import { createRequire } from "module";
const require = createRequire(import.meta.url);
import chalk from 'chalk';
import { execa } from 'execa';
var fs = require("fs-extra");
const PATH = 'gits';
const STATIC = 'static';
let files = fs.readdirSync(PATH);
let installBuildPaths = [];
function findPackagePaths (files, paths) {if(files.length === 0) return;files.filter(file => {return fs.statSync(joinPath([...paths, file])).isDirectory() // 返回目录路径}).forEach(file => {const path = joinPath([...paths, file]);const packageJsonPath = `${path}/package.json`;let existPackageJson = fs.existsSync(packageJsonPath);if(existPackageJson) {installBuildPaths.push(path);}else {let childrenFiles = fs.readdirSync(path);findPackagePaths(childrenFiles, [...paths, file]);}})
}
function joinPath (pathArr) {return `${pathArr.join('/')}`;
}
async function run (path) {if(path) {let childInstallProcess = execa("npm", ["install", "--loglevel", "silly"], {cwd: `${path}/`})childInstallProcess.stderr.pipe(process.stderr);try {const result = await childInstallProcess;if(result.exitCode === 0) {console.log(`${result.stdout}`)console.log(`install 成功`);console.log(`--------开始执行npm run build-------`);npmBuild(path);}} catch (error) {console.log('install 失败', error)}}
}
function npmInstall () {run(installBuildPaths.shift());
}
async function npmBuild (path) {let childBuildProcess = execa("npm", ["run", "build"], {cwd: `${path}/`})childBuildProcess.stderr.pipe(process.stderr);try {const result = await childBuildProcess;if(result.exitCode === 0) {console.log(`${result.stdout}`);console.log(`build 成功`);console.log(`-----------copy文件到指定目录-------------`);putDistToAimFolder(path);}} catch (error) {console.log('build 失败', error)}
}
function putDistToAimFolder (path) {const aimFolder = joinPath([STATIC, path.split(`${PATH}/`)[1]]);let config = {};// 这里需要找到项目中的打包配置文件找到打包后的静态文件,我这里是react项目// 如果没找到默认dist目录// 其他项目可以视情况而定 这里需要完善一下if(fs.existsSync(`./${path}/build.config.js`)) {config = require(`./${path}/build.config.js`);config.outputDir = config.outputDir || `dist`;}else {config.outputDir = `dist`} try {fs.copy(`${path}/${config.outputDir}`, aimFolder);console.log('静态文件已存放到指定目录');console.log(`--------${path}构建完成继续执行下一个--------`);npmInstall();} catch (error) {console.log('文件复制失败', error);}
}
function main () {findPackagePaths(files, [PATH]);npmInstall();
}
main();
需要优化的不足之处
以上代码只是简单的实现了一个自动化构建的过程,如果将这段代码放在服务器上运行,理论上是可以实现简单的自动部署的功能。
当然这里面还有许多需要优化的地方
- 脚本构建的指令可能需要统一处理,这里没有考虑带参数的情况
- 对脚本执行失败后的处理,比如
npm install
安装失败的处理,若安装失败删除node_modules
防止下次执行脚本安装失败。 - 对脚本执行目录的收集,虽然考虑到多模块的构建,但是有些情况还是会漏掉,比如嵌套模块的识别就做不到。
- 对文件静态文件输出目录的识别还得需要从具体的情况考虑,比如老项目手动构建的webpack配置就需要特殊处理了。
总结
在编写这个脚本的过程中学习了很多npm依赖库的使用,比如inpuirer
,脚手架必备工具,有兴趣的可以自己捣鼓一下,还有execa
,可以通过javascript
执行shell
脚本,简直YYDS。
对了,还有个chalk
,可以改变控制台输出的文字的颜色,我演示的时候没有用,但是也很简单。
虽然原理很简单,自己实现的过程当中也遇到了不少问题,
- 有些模块最新版本是支持import导入,有些还是只支持require导入。require怎么和import混用靠的就是以下两行代码,前提是你得设置脚本的
package.json
文件的"type": "module",
// 兼容CommonJS
import { createRequire } from "module";
const require = createRequire(import.meta.url);
package.json
{"name": "k8s","version": "1.0.0","description": "","main": "index.js","type": "module","scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"author": "andy","license": "ISC","dependencies": {"chalk": "^5.0.1","execa": "^6.1.0","fs-extra": "^10.1.0","inquirer": "^9.1.1"}
}
- 控制台怎么显示脚本运行的过程,这个
execa
官方使用说明中有提到过
childInstallProcess.stderr.pipe(process.stderr);
脚本后续还需不需要优化看具体使用情况吧。
最后
最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。
有需要的小伙伴,可以点击下方卡片领取,无偿分享