一、第一步,我们需要下载相应的插件inquirer、compressing、ssh2-sftp-client、ssh2。
提示:下载这些插件,前提是,你的项目还有你自己本地已经与服务端建立远程连接。我这里是用xshell之前就建立起过连接。
package.json中
{
"devDependencies": {
"ssh2": "^1.11.0",
"ssh2-sftp-client": "^9.1.0",
"compressing": "^1.7.0",
"inquirer": "^9.1.4"
}
}
下面介绍一下这几个插件的用法
inquirer
inquirer是一个基于Node.js的命令行交互工具,它可以方便地创建交互式命令行界面。
inquirer官网:[https://www.npmjs.com/package/inquirer](https://www.npmjs.com/package/inquirer)
安装方式
cnpm install -D inquirer
引入使用
import inquirer from 'inquirer';
inquirer
.prompt([
{
type: 'input', // 类型
name: 'yourName', // 字段名称,在then里可以打印出来
message: 'your name:' // 提示信息
},
])
.then(answers => {
console.log('answers', answers.yourName) // 与prompt的name字段对应
})
.catch(error => {
if(error.isTtyError) {
// Prompt couldn't be rendered in the current environment
} else {
// Something else went wrong
}
});
compressing
compressing压缩文件的一个工具,提供一些用于文件压缩的api,当前支持tar、gzip、tgz、zip格式。
一般情况下都是配合node中fs(文件读写库)还有path一起使用。
path提供了一些用于处理文件路径的api,最常用的就是path.join(),可将多个字符串连接成路径。该方法的主要用途在于,会正确使用当前系统的路径分隔符,Unix系统是"/“,Windows系统是”"。
官网地址:https://www.npmjs.com/package/compressing
安装方式
npm install compressing
使用方式在下列使用例子中展示。
ssh2-sftp-client
ssh2-sftp-client是一个基于Node.js编写的轻量级、高效的SFTP(Secure File Transfer Protocol)客户端库,能够在Node.js环境中便捷地实现文件的上传和下载功能。
安装方式
npm install ssh2-sftp-client -D
使用方式在下列使用例子中展示。
ssh2
SSH2的主要功能包括密钥交换、身份认证、加密通信和数据传输。
SSH2的工作原理大致可以分为三个阶段:
1、认证阶段:客户端向服务器发送连接请求,服务器响应并提供自己的公钥。客户端使用服务器的公钥加密一个随机生成的会话密钥,并发送给服务器。服务器使用自己的私钥解密,从而获得会话密钥。
2、密钥交换阶段:客户端和服务器使用会话密钥进行加密通信,确保后续的数据传输安全。
3、数据传输阶段:客户端和服务器使用加密的会话密钥进行数据传输,所有传输的数据都经过加密处理,保证了数据的安全性。
安装方式
npm install ssh2 -D
第二步 在项目中使用过程
在几个配置文件中的一些步骤
package.json
{
// 项目的基本信息
"name": "vue-admin-template", // 项目的名称
"version": "4.4.0", // 项目的版本号
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", // 项目描述
"author": "", // 项目作者
"type": "module", // 指定项目使用ES模块
// 脚本命令集合
"scripts": {
"start": "node deployment/guide.js", // 启动项目的引导脚本
"dev": "vite --force --open --port 8888 --host 0.0.0.0", // 开发模式启动Vite服务
"devMember": "vite serve --mode developmentMember", // 开发模式启动Vite服务,使用developmentMember环境
"build": "vite build", // 构建生产环境
"buildMember": "vite build --mode productionMember", // 构建生产环境,使用productionMember环境
"buildTest": "vite build --mode staging", // 构建测试环境
"buildTestMember": "vite build --mode stagingMember", // 构建测试环境,使用stagingMember环境
"deploy:test": "node node_modules/vite/bin/vite.js build --mode staging && node deployment/compression.js -INTRANETCENTOS --prefix=fjdProductManage", // 部署到测试环境
"deploy:testMember": "node node_modules/vite/bin/vite.js build --mode stagingMember && node deployment/compression.js -INTRANETCENTOS --prefix=productManageMbr", // 部署到测试环境,使用stagingMember环境
"preview": "node build/index.js --preview", // 预览构建结果
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", // 优化SVG图标
"lint": "eslint --ext .js,.vue src", // 运行ESLint检查
"test:unit": "jest --clearCache && vue-cli-service test:unit", // 运行单元测试
"test:ci": "npm run lint && npm run test:unit" // 运行CI流程,先检查代码风格再运行单元测试
},
// 项目依赖
"dependencies": {
"axios": "0.18.1",
"clipboard": "^2.0.10",
"core-js": "^3.27.1",
"echarts": "^5.2.0",
"el-cascader-multi": "^1.1.8",
"ele-multi-cascader": "^2.2.5",
"element-ui": "2.13.2",
"file-saver": "^2.0.2",
"jquery": "^3.6.1",
"js-cookie": "2.2.0",
"less-loader": "^5.0.0",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"pptxgenjs": "^3.12.0",
"qrcodejs2-fix": "^0.0.1",
"qs": "^6.12.1",
"sortablejs": "^1.14.0",
"vue": "^2.7.16",
"vue-amap": "^0.5.10",
"vue-router": "3.0.6",
"vuedraggable": "^2.24.3",
"vuex": "3.1.0",
"xlsx": "^0.16.6"
},
// 开发依赖
"devDependencies": {
"ssh2": "^1.11.0",
"ssh2-sftp-client": "^9.1.0",
"compressing": "^1.7.0",
"@vitejs/plugin-vue2": "^2.3.1",
"@vitejs/plugin-vue2-jsx": "^1.1.1",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "9.5.1",
"babel-eslint": "10.1.0",
"babel-jest": "23.6.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"chalk": "2.4.2",
"connect": "3.6.6",
"eslint": "6.7.2",
"eslint-plugin-vue": "6.2.2",
"html-webpack-plugin": "3.2.0",
"less": "^4.1.2",
"mockjs": "1.0.1-beta3",
"rollup-plugin-visualizer": "^5.12.0",
"runjs": "4.3.2",
"sass": "1.26.8",
"sass-loader": "8.0.2",
"script-ext-html-webpack-plugin": "2.1.3",
"serve-static": "1.13.2",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.2",
"unplugin-auto-import": "^0.17.5",
"unplugin-vue-setup-extend-plus": "^1.0.1",
"vite": "^4.0.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-svg-icons": "^2.0.1",
"vue-cropper": "^0.5.8",
"vue-template-compiler": "2.6.10",
"inquirer": "^9.1.4"
},
// 浏览器兼容性列表
"browserslist": [
"> 1%", // 支持使用率大于1%的浏览器
"last 2 versions" // 支持最近两个版本的主要浏览器
],
// 指定项目运行所需的Node.js和npm版本
"engines": {
"node": ">=8.9", // Node.js版本要求
"npm": ">= 3.0.0" // npm版本要求
},
// 项目许可类型
"license": "MIT" // MIT许可
}
在package.json文件中也可以看到命令,我单独在项目中建立一个文件夹deployment,里面有四个文件分别是compression.js、consoleTemplate.js、guide.js、uploader.js
// 在compression.js中
// 导入path模块,用于处理文件路径
import path from 'path';
// 导入url模块,用于转换文件URL到路径
import { fileURLToPath } from 'url';
// 导入fs模块的同步方法,用于文件系统操作
import {
existsSync, readdirSync, statSync, unlinkSync, rmdirSync,
} from 'fs';
// 导入compressing模块,用于文件压缩
import { zip } from 'compressing';
// 导入Uploader类,用于文件上传
import Uploader from './uploader.js';
// 导入自定义的日志打印模板
import { success, info, err } from './consoleTemplate.js';
// 函数用于解析命令行参数
function getArgs() {
const params = {}; // 初始化参数对象
// 遍历命令行参数,从第二个开始(跳过node命令本身和脚本路径)
//process.argv是一个数组,包含了启动Node.js进程时的命令行参数。
process.argv.slice(2, process.argv.length).forEach((arg) => {
if (arg.slice(0, 2) === '--') { // 长选项参数
const longArg = arg.split('='); // 分割选项和值
// 解构赋值,将长选项和值存入params对象
[, params[longArg[0].slice(2, longArg[0].length)], , params[longArg[2]]] = longArg;
} else if (arg[0] === '-') { // 短选项参数
const flag = arg.slice(1, arg.length); // 获取短选项标志
params.serverType = flag; // 将标志存入params对象
}
});
return params; // 返回解析后的参数对象
}
// 函数用于删除文件夹及其内容
function deleteDir(folderPath) {
if (existsSync(folderPath)) { // 检查文件夹是否存在
// 遍历文件夹内的所有文件和子文件夹
readdirSync(folderPath).forEach((file) => {
const curPath = `${folderPath}/${file}`; // 当前文件或子文件夹的完整路径
if (statSync(curPath).isDirectory()) { // 如果是子文件夹
// 递归调用deleteDir删除子文件夹
deleteDir(curPath);
} else { // 如果是文件
// 删除文件
unlinkSync(curPath);
}
});
// 删除空文件夹
rmdirSync(folderPath);
}
}
// 获取命令行参数
const args = getArgs();
// 获取当前脚本的父目录路径
const parent = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
// 从package.json获取项目名,如果args中有prefix则使用prefix
const outputDir = args.prefix ? args.prefix : '';
// 清除控制台输出
console.clear();
// 部署服务器配置
const serverConfig = {
// 部署到测试服务器的配置
INTRANETCENTOS: {
projectName: outputDir,
hostname: '47.117.88.91',
port: 22,
username: 'root',
password: 'AHFtdzKJ2021',
path: '/web_program',
},
};
// 输出压缩文件的信息
info('正在压缩', '请稍后');
// 压缩文件夹
zip.compressDir(`${parent}/${outputDir}`, `${outputDir}.zip`)
.then(() => {
// 删除原文件夹
deleteDir(`${parent}/${outputDir}`);
// 输出打包成功的消息
success('打包成功', `已经压缩到文件:${outputDir}.zip`);
// 如果指定了服务器类型,则上传到服务器
if (args.serverType) {
new Uploader(serverConfig[args.serverType]).start();
}
})
.catch((e) => {
// 输出打包失败的消息
err('打包失败', e);
});
// 在consoleTemplate.js中
// 定义一个获取当前时间的函数,返回格式化的中文时间字符串
function now() {
return new Date().toLocaleString('chinese', {
hour12: false, // 不使用12小时制
hour: 'numeric', // 小时数字显示
minute: 'numeric', // 分钟数字显示
second: 'numeric', // 秒数数字显示
});
}
// 定义一个成功信息的打印函数,带颜色输出
function success(a, b) {
// 使用ANSI转义序列设置背景色和前景色,打印带有时间戳的成功信息
console.log(`\x1B[42m \x1B[37m${now()} ${a}\x1B[39m \x1B[49m ${b}`);
}
// 定义一个信息提示的打印函数,带颜色输出
function info(a, b) {
// 使用ANSI转义序列设置背景色,打印带有时间戳的信息提示
console.log(`\x1B[44m ${now()} ${a} \x1B[49m ${b}`);
}
// 定义一个错误信息的打印函数,带颜色输出
function err(a, b) {
// 使用ANSI转义序列设置背景色和前景色,打印带有时间戳的错误信息
console.log(`\x1B[41m \x1B[37m${now()} ${a}\x1B[39m \x1B[49m \x1B[31m${b}\x1B[39m`);
}
// 导出success, info, err函数,供其他模块使用
export { success, info, err };
//在guide.js中
// 导入inquirer模块,用于创建命令行界面
import inquirer from 'inquirer';
// 导入execSync函数,用于执行shell命令
import { execSync as exec } from 'child_process';
// 定义命令类型数组,包含启动和部署两种类型
const commandType = [
{
name: '启动项目', // 命令名称
value: 'run', // 命令对应的值
},
{
name: '部署项目',
value: 'deploy',
}
];
// 定义启动命令数组,包含非会员版和会员版的启动命令
const commandRun = [
{
name: '非会员版',
value: 'cnpm run dev', // npm命令
envFile: '.env.development', // 环境变量文件
},
{
name: '会员版',
value: 'cnpm run devMember',
envFile: '.env.developmentMember',
},
];
// 定义部署命令数组,包含非会员版和会员版的部署命令
const commandDeploy = [
{
name: '非会员版系统部署到测试',
value: 'npm run deploy:test',
envFile: '.env.staging',
},
{
name: '会员版系统部署到测试',
value: 'npm run deploy:testMember',
envFile: '.env.stagingMember',
},
];
// 使用inquirer创建一个交互式菜单
inquirer
.prompt([ // 显示第一个菜单,询问用户是要启动还是部署项目
{
type: 'list', // 菜单类型为列表
message: '现在要启动项目还是部署项目?', // 提示信息
name: 'commandLev1', // 回答的属性名
choices: commandType, // 可选择的选项
},
])
.then(({ commandLev1 }) => { // 处理用户的选择
let commandList; // 存储下一步的命令列表
let message; // 存储下一步的提示信息
let type; // 存储下一步的菜单类型
// 函数用于格式化命令列表的显示
const commandListFilter = ({ name, value, envFile }, i) => ({
name: `${i + 1}、${name}(${value})[${envFile}]`, // 格式化的命令名称
value, // 命令的实际值
});
// 根据用户的第一步选择,决定下一步的操作
switch (commandLev1) {
// 如果用户选择了启动项目
case 'run':
commandList = commandRun.map(commandListFilter); // 格式化启动命令列表
message = '请选择要启动的项目:'; // 设置下一步的提示信息
type = 'list'; // 设置下一步的菜单类型为列表
break;
// 如果用户选择了部署项目
case 'deploy':
commandList = commandDeploy.map(commandListFilter); // 格式化部署命令列表
message = '请选择要部署的项目:'; // 设置下一步的提示信息
type = 'checkbox'; // 设置下一步的菜单类型为复选框
break;
default:
break; // 如果用户的选择不在预设范围内,不做任何操作
}
// 显示下一步菜单,让用户选择具体的命令
inquirer
.prompt([
{
type,
message,
name: 'commandLev2',
choices: commandList,
},
])
.then(({ commandLev2 }) => { // 处理用户的第二次选择
// 根据菜单类型执行相应的命令
switch (type) {
case 'checkbox':
commandLev2.forEach((cmd) => { // 如果是复选框,遍历所有选择的命令并执行
exec(cmd, { stdio: 'inherit' }); // 执行命令,继承标准输入输出
});
break;
case 'list':
exec(commandLev2, { stdio: 'inherit' }); // 如果是列表,执行单个选择的命令
break;
default:
break; // 如果菜单类型不在预设范围内,不做任何操作
}
});
});
// 在uploader.js中
// 导入fs模块,用于文件系统操作
import fs from 'fs';
// 导入path模块,用于路径操作
import path from 'path';
// 导入SftpClient类,用于SFTP操作
import SftpClient from 'ssh2-sftp-client';
// 导入Client类,用于SSH连接
import { Client } from 'ssh2';
// 导入自定义的日志打印模板
import { success, info, err } from './consoleTemplate.js';
// 创建Uploader类,用于上传和部署项目
export default class Uploader {
// 构造函数,初始化Uploader实例
constructor(args) {
// 从参数对象中提取配置信息
this.hostname = args.hostname; // 服务器主机名
this.port = args.port; // 服务器端口
this.username = args.username; // 用户名
this.password = args.password; // 密码
this.path = args.path; // 服务器上的目标路径
this.projectName = args.projectName; // 项目名称
// 设置本地压缩文件的完整路径
this.fileName = path.resolve(`./${this.projectName}.zip`);
}
// 开始上传和部署方法
async start() {
// 创建SFTP客户端实例
const sftpClient = new SftpClient();
// SFTP连接配置
const config = {
host: this.hostname,
port: this.port,
username: this.username,
password: this.password,
};
// 读取本地压缩文件内容
const fileStream = fs.readFileSync(this.fileName);
try {
// 连接SFTP服务器
await sftpClient.connect(config);
// 将文件上传到服务器指定路径
await sftpClient.put(fileStream, `${this.path}/${this.projectName}.zip`);
// 断开SFTP连接
await sftpClient.end();
// 成功上传后打印信息
success('上传成功');
} catch (e) {
// 如果上传过程中出现错误,断开连接并打印错误信息
sftpClient.end();
err('上传出错', `原因:${e.message}`);
return;
}
// 开始部署前的准备信息打印
info('开始部署', '正在连接服务器,执行文件解压……');
// 创建SSH客户端实例
const conn = new Client();
// 监听SSH连接的ready事件
conn
.on('ready', () => {
// 执行远程命令,解压并清理文件
conn.exec(`cd ${this.path} && rm -rf ${this.projectName} && unzip -o -q ${this.projectName}.zip && rm -rf ${this.projectName}.zip`, (e, stream) => {
if (e) throw e; // 如果有错误抛出异常
// 监听stream的close事件,处理命令执行完成后的逻辑
stream
.on('close', (code, signal) => {
if (code !== 0) {
// 如果命令执行失败,打印错误信息
err(`断开连接,状态码:${code}`);
} else {
// 如果命令执行成功,打印成功信息
success('部署成功', `部署时间:${new Date()}`);
}
// 断开SSH连接
conn.end();
})
.on('data', (data) => {
// 打印服务器响应的数据
success('服务器响应', data);
})
.stderr.on('data', (data) => {
// 打印服务器响应的警告信息
info(`服务器响应警告(不影响部署): ${data}`);
});
});
})
// 连接SSH服务器
.connect({
host: this.hostname,
port: this.port,
username: this.username,
password: this.password,
});
}
}
不懂也可参考https://blog.csdn.net/qq_63358859/article/details/133880096