使用node直接部署到服务器测试环境中

一、第一步,我们需要下载相应的插件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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值