1.明确需求
进行开发前需要首先明确需求,根据常见的前端部署流程总结为以下流程: 根据部署流程明确自动化部署的需求:
2.开发前准备
2.1 导入依赖模块
由于需要实现文件压缩、模拟form表单提交、loading效果、友好提示效果、文件读写、文件上传,因此至少需要以下模块:
- compressing 模块(支持压缩文件夹,支持zip压缩)
- child_process 模块 (可以用来创建一个子进程,在js里面调用shell命令)
- fs 模块 (可用于与文件系统进行交互)
- form-data 模块 (将form表单元素的name与value进行组合,实现表单数据的序列化,从而减少表单元素的拼接,提高工作效率)
- Ora 模块 (命令行环境的loading效果及显示各种状态的图标)
- Chalk 模块(修改控制台中字符串的样式,包括字体样式、字体颜色、背影颜色)
- http 模块(创建http服务器、客户端)
2.2 如何实现规范
为实现需求中的解耦合理与逻辑清晰/灵活,需要关注整体程序逻辑,这里选择封装相关功能实现,并在主程序中自由调度(可灵活调用、关闭、修改相关功能),并对于当前所执行的功能给与提示,以保证功能实现的完整性和异常提示。
到这里就完成了对程序功能构建的梳理工作,下面进入项目实现。
3.功能实现
3.1 打包构建
使用child_process模块调用shell命令,进行本地打包构建,其中env代表不同的构建环境,可从不同的执行脚本中获取到。
由于package.json scripts下面配置如下: "upload:server": "node build/deploy.js --sit",可以根据自己需要灵活配置。
const env = process.argv[process.argv.length - 1].replace('--', ''); // 用于获取构建脚本环境参数
childProcess.exec(`npm run build:${env}`, function(error, stdout, stderr) {
if (error) {
console.log('exec error: ' + error);
}
compress();
});
3.2 本地压缩文件、功能提示
项目打包构建完成后,执行功能提示、压缩操作,此处使用compressing、ora模块,用于对产物文件夹进行zip压缩及功能性提示
const spinner = ora('开始打包……').start();
spinner.text = '打包完成,开始压缩';
// 压缩命令
// 此处第一个参数为要打包的目录, 第二个参数是打包后的文件名
compressing.zip
.compressDir('web/', 'web.tar.gz')
.then(() => {
setTimeout(() => {
spinner.text = '压缩完成,开始上传';
upload();
}, 300);
})
.catch(handleError);
3.3 上传压缩后的文件到文件服务器
文件服务器搭建方式:
如何构建简易版的文件服务器
使用说明:
- Form-data表单模拟文件上传
- 发送post请求上传文件到服务器
- 获取接口返回code,用于下载上传到文件服务器的文件
<form action="/ishare/profile" method="post" enctype="multipart/form-data">
<input type="file" name="avatar" />
<button type="submit">submit</button>
</form>
- form表单中entype属性可以用来控制对表单数据发送前如何进行编码
- multipart/form-data不对字符编码,用于发送二进制的文件,其他两种类型(text/plain、application/x-www-form-urlencoded)不能用于发送文件;
- type=file 用于文件上传
- name 用于获取文件名
大致实现过程如下:
var FormData = require('form-data');
var formData = new FormData();
formData.append(
'avatar',
fs.createReadStream(path.resolve(__dirname, '../web.tar.gz'))
); //'file'是服务器接受的key
var headers = formData.getHeaders(); //这个不能少
var request = http.request(
{
method: 'post',
host: config.remotePath,
path: '/ishare/profile',
headers: headers
},
function(res) {
var str = '';
res.on('data', function(buffer) {
str += buffer; //用字符串拼接
});
res.on('end', () => {
if (str) {
const result = JSON.parse(str);
const uploaded = result.uploaded;
const fileCode = uploaded.substring(uploaded.indexOf('/') + 1);
spinner.text = '上传完成,开始从服务器获取文件';
getFile(fileCode);
}
});
}
);
formData.pipe(request);
3.4 在远程服务器上获取上传的文件
压缩文件上传到文件服务器后,通过接口返回的fileCode,调用远程服务器部署好的接口服务。
接口服务主要做以下几件事:
- 避免连接无端服务器操作,减少开墙、不稳定因素
- 下载上传成功的文件到服务器指定位置
- 配置化配置服务器和目标文件目录地址
- 不重复备份原始文件
- 删除静态资源文件目录
- 解压新的压缩文件
3.5 获取fileCode和sessionTick(避免用户恶意调用接口)并下载文件到服务器指定位置
app.get('/getFile', function(req, res) {
const fileCode = req.query.code;
const sessionTick = req.query.sessionTick;
getFileByCode({fileCode: fileCode,sessionTick: sessionTick}, res);
});
async function getFileByCode(obj, res) {
if (obj.sessionTick && obj.sessionTick !== '522314cc-f038-4abf-bb36-adc912e506e2') {
res.msg = 'sessionTick missed or error';
res.code = '0';
res.status = 200;
res.json(res);
return
}
console.log('sessionTick missed');
await dirExists(`./${config.filePath}`);
let httpStream = request({
method: 'GET',
url: `http://${config.remotePath}/ishare/${obj.fileCode}`
});
let writeStream = fs.createWriteStream(`./${config.filePath}/${config.fileName}`);
// 联接Readable和Writable
httpStream.pipe(writeStream);
let totalLength = 0;
// 当获取到第一个HTTP请求的响应获取
httpStream.on('response', response => {
console.log('response headers is: ', response.headers);
});
httpStream.on('data', chunk => {
totalLength += chunk.length;
console.log('recevied data size: ' + totalLength + 'KB');
});
// 下载完成
writeStream.on('close', () => {
console.log('download finished');
executeShell(res);
});
}
其中dirExists用于判断指定路径是否存在,不存在就创建
/**
* 路径是否存在,不存在则创建
* @param {string} dir 路径
*/
async function dirExists(dir) {
let isExists = await getStat(dir)
//如果该路径且不是文件,返回true
if (isExists && isExists.isDirectory()) {
return true
} else if (isExists) {
//如果该路径存在但是文件,返回false
return false
}
//如果该路径不存在
let tempDir = path.parse(dir).dir //拿到上级路径
//递归判断,如果上级目录也不存在,则会代码会在此处继续循环执行,直到目录存在
let status = await dirExists(tempDir)
let mkdirStatus
if (status) {
mkdirStatus = await mkdir(dir)
}
return mkdirStatus
}
/**
* 读取路径信息
* @param {string} path 路径
*/
function getStat(path) {
return new Promise((resolve, reject) => {
fs.stat(path, (err, stats) => {
if (err) {
resolve(false);
} else {
resolve(stats);
}
});
});
}
/**
* 创建路径
* @param {string} dir 路径
*/
function mkdir(dir) {
return new Promise((resolve, reject) => {
fs.mkdir(dir, err => {
if (err) {
resolve(false);
} else {
resolve(true);
}
});
});
}
3.6 执行deploy.sh并备份服务器前端资源
其中executeShell函数主要用于执行shell命令sh deploy.sh ,通过传递的目标文件目录备份原始文件
async function executeShell(res) {
await dirExists(`${config.projectPath}/${config.projectName}/${config.filePath}`);
childprocess.exec(`sh deploy.sh ${config.projectName} ${config.filePath} ${config.fileName}`, function(error, stdout, stderr) {
if (error) {
result.msg = `执行脚本deploy.sh报错,错误信息为: ${error}`;
result.code = '1';
result.status = 500;
res.json(result);
console.log('exec error: ' + error);
} else {
console.log('执行脚本deploy.sh完成');
copy(res);
}
});
}
deploy.sh代码实现如下:
#!/bin/sh
nowtime=$(date "+%Y%m%d")
echo $nowtime
cd /home/bankdplyop/${1}
rm -rf ${3}
tar -cf ${2}$nowtime.tar.gz ${2}
其中1、{2}、3指执行shell命令时传递的参数,此处执行的命令为:shdeploy.sh{config.projectName} config.filePath{config.fileName}
config文件用于自由化配置目标文件地址、项目源文件地址及一些远程服务器相关参数
module.exports = {
sit: {
remotePath: '29.2.221.176', //上传的远程服务器的目录
filePath: 'web',
fileName: 'web.tar.gz',
projectName: 'iris_front',
projectPath: '/home/bankdplyop',
host: '0.0.0.0', //远程主机
port: 3000 //服务器端口号
}
};
3.7 删除静态资源文件目录并解压下载的文件
- 拷贝下载完成的文件到待部署的项目所在地
- 执行replace.sh脚本
- 提示用户发布完成,请测试
async function copy(res) {
const destFilePath = path.resolve(__dirname, `${config.projectPath}/${config.projectName}/${config.fileName}`);
const filePath = path.resolve(__dirname, `./${config.filePath}/${config.fileName}`);
await dirExists(`${config.projectPath}/${config.projectName}`);
fs.writeFile(destFilePath, fs.readFileSync(filePath), function(err) {
if (err) {
result.msg = '文件拷贝失败';
result.code = '1';
result.status = 500;
res.json(result);
};
executeReplaceShell(res);
});
}
其中executeReplaceShell(res),当文件拷贝完成时,执行删除和解压操作
async function executeReplaceShell(res) {
console.log(
'replace shell start: ' +
`${config.projectName}/${config.filePath}/${config.fileName}`
)
try {
childprocess.exec(
`sh replace.sh ${config.projectName} ${config.filePath} ${config.fileName} `,
function (error, stdout, stderr) {
if (error) {
result.msg = `替换脚本replace.sh报错,错误信息为: ${error}`
result.code = '1'
result.status = 500
res.json(result)
} else {
result.msg = '发布完成,请测试'
result.code = '0'
result.status = 200
res.json(result)
}
}
)
} catch (e) {
console.log(e)
}
}
replace.sh代码实现如下:
cd /home/bankdplyop/${1}
ls ${2}
rm -rf ${2}
unzip ${3}
其中1、{2}、3指执行shell命令时传递的参数,此处执行的命令为:shreplace.sh{config.projectName} config.filePath{config.fileName}
至此完成了整个自动化发布流程。
使用
项目根目录直接运行即可
npm run upload:server
总结
前端自动化部署是一件非常有意义的功能,记得最开始代码上传服务器,都需要手动进行操作,真得是好不麻烦,希望感兴趣的同学,如果公司内部没有一套完整的自动化部署流程,可以自己玩玩看。里面还有很多可以完善的点,希望大家多多交流,提一些宝贵意见。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!