vue3.0 + vite 项目打包后通过ssh发布到服务器

605 阅读5分钟

一、使用npm ssh2-sftp-client 插件

1、安装

npm install ssh2-sftp-client

2、使用

let Client = require('ssh2-sftp-client')
let sftp = new Client()

二、配置链接服务器信息 配置链接服务器信息之前,需要把自己的公钥放到服务器,如没有密钥对,则需要生成密钥对 不同服务器的链接配置

let serverConfig = {
  dev: {
    不同服务器的前端包放在服务器的相对路径
    path: '/data/code/',
    linkConfig: {
        host: 'xxx.xxx.xxx.xx',
        username: 'xxx',
        privateKey: fs.readFileSync('C:/xxx/.ssh/id_rsa'),
        port: xxx
    }
  },
  test: {
    path: '/data/code/',
    linkConfig: {
        host: 'xxx.xxx.xxx.xx',
        username: 'xxx',
        privateKey: fs.readFileSync('C:/xxx/.ssh/id_rsa'),
        port: xxx
    }
  },
  pre: {
    path: '/data/code/',
    linkConfig: {
        host: 'xxx.xxx.xxx.xx',
        username: 'xxx',
        privateKey: fs.readFileSync('C:/xxx/.ssh/id_rsa'),
        port: xxx
    }
  },
  prod: {
    path: '/data/code/',
    linkConfig: {
        host: 'xxx.xxx.xxx.xx',
        username: 'xxx',
        privateKey: fs.readFileSync('C:/xxx/.ssh/id_rsa'),
        port: xxx
    }
  },
}
let config = {
  // 前端包放在服务器的相对路径,如'/data/code'
  path: '/data/code/',
  // 服务器配置信息
  serverConfig: {
    host: 'xxx.xxx.xxx.xx',
    username: 'xxx',
    privateKey: fs.readFileSync('C:/xxx/.ssh/id_rsa'),
    port: xxx
  },
  // 发送到服务器的名称,例如有3个开发服,dev1, dev2, dev3
  serverName: ''
}
// 获取当前命令所在文件夹名称
const dirName = path.basename(__dirname)

三、根据命令初始化配置信息 从命令中获取部署环境,运行的命令 需要带参数 --serverName=xxx

const getEnv = () => {
  const commandList = process.argv
  const commandEnv = commandList.filter((item) => {
    return item.indexOf('--serverName') > -1
  })
  if (commandEnv.length) {
    return commandEnv[0].substring(commandEnv[0].indexOf('=') + 1) 
  } else {
    console.log('命令需带上 打包后的服务器位置, --serverName=xxx')
    return ''
  }
}

不同的项目打包后可能放在不同的文件夹

const packageStorageDirectory = () => {
  let path = '/html'
  switch (dirName) {
      case 'admin':
        path += '/manage'
        break
      case 'pc':
        path += '/www'
        break
      case 'm':
        path += '/www/m'
        break
   }
  return path
}

根据命令重新配置全局变量

const getConfigFromCommand = () => {
  // 命令打包上传服务器名称
  let serverName = getEnv()
  if (!serverName) return
  // 服务器地址后的文件路径
  let serverFilePath = packageStorageDirectory()
  config.serverName = serverName
  config.serverConfig = serverConfig[serverName].linkConfig
  config.path = serverConfig[serverName].path + serverFilePath
}

四、链接服务器

await sftp.connect(config.serverConfig)

五、进入对应的服务器,并删除远程文件,创建远程文件夹

判断文件夹存不存在

// 判断文件夹存不存在
const exists = async (originPath) => {
  return await sftp.exists(originPath)
}
// 判断父级目录是否是文件夹
const parentIsDir = async (originPath) => {
  const parentDir = originPath.substring(0, originPath.lastIndexOf('/'))
  let info = await sftp.stat(parentDir)
  return info.isDirectory
}

// 判断父级目录是否存在
const parentExists = async (originPath) => {
  const parentDir = originPath.substring(0, originPath.lastIndexOf('/'))
  return await exists(parentDir)
}

特殊处理文件夹

// pc文件夹下面,不能删除的文件夹
const pcCantRemoveList = ['m', 'xxx']
// 删除除参数外的文件
const removeFilesAndFolders = async (excludeList = pcCantRemoveList) => {
  const files = await sftp.list(config.path)
  const fileNames = files.map(file => file.name)

  for (const fileName of fileNames) {
    if (!excludeList.includes(fileName)) {
      let filePath = config.path + '/' + fileName
      let fileInfo = await sftp.stat(filePath)

      if (fileInfo && fileInfo.isDirectory) {
        const removeReulst = await sftp.rmdir(filePath, true)
      } else {
        const removeFileReulst = await sftp.delete(filePath)
      }
    }
  }
}

链接服务器成功后

/**
 * 
 * @param {*} originPath 远程文件目录,
 * @param {*} targetPath 本地文件目录
 */
const serverOperate = async (originPath, targetPath) => {
  try {

    // 检查父级目录是否存在
    const parentFlag = await parentExists(originPath)
    if (!parentFlag) {
      console.log('父级目录不存在')
      return
    }
    // 检查父级目录是否存在
    const parentIsDirFlag = await parentIsDir(originPath)
    if (!parentIsDirFlag) {
      console.log('父级不是一个文件夹')
      return
    }

    // 如果文件存在
    if (dirName == 'pc') {
      // 如果pc端,那么不能全部删除,因为pc端里面有移动端文件夹 m
      await removeFilesAndFolders()
    } else {
      const flag = await exists(originPath)
      if (flag) {
        // 删除当前目录下所有文件和子文件夹
        await sftp.rmdir(originPath, true)
      }
      // 创建文件夹
      await sftp.mkdir(originPath)
    }
    // 更新代码
    await sftp.uploadDir(targetPath, originPath);
    console.log('文件夹替换成功!');
  } catch (err) {
    console.error(err.message);
  }

}

六、完整流程

// 连接服务器
const linkedServer = async () => {
  try {
    // 初始配置
    getConfigFromCommand()

    // 链接服务器
    await sftp.connect(config.serverConfig)

    // 获取路径目录
    // await listRemoteDirectory(config.path)

    // 本地目录
    const targetPath = path.join(__dirname, './dist')
    await goServer(config.path, targetPath)

  } catch (err) {
    console.error('部署失败')
  } finally {
    sftp.end()
  }
}
linkedServer()

七、package.json配置命令

"scripts": {
    "build:devServer1": "vue-cli-service build --mode development & node deploy.js --serverName=dev1",
    "build:devServer2": "vue-cli-service build --mode development & node deploy.js --serverName=dev2",
    "build:testServer": "vue-cli-service build --mode test & node deploy.js --serverName=test",
}

八、deploy.js文件全部内容

import path from 'path'
import fs from 'fs'
import Client from 'ssh2-sftp-client'
import { fileURLToPath } from 'url'

// 获取当前命令所在文件夹名称
const __filename = fileURLToPath(import.meta.url)
let dirPath = path.dirname(__filename)
const dirName = path.basename(dirPath)

let serverConfig = {
  dev: {
    // 不同服务器的前端包放在服务器的相对路径
    path: '/data/code/',
    linkConfig: {
        host: 'xxx.xxx.xxx.xx',
        username: 'xxx',
        privateKey: fs.readFileSync('C:/xxx/.ssh/id_rsa'),
        port: xxx
    }
  },
  test: {
    path: '/data/code/',
    linkConfig: {
        host: 'xxx.xxx.xxx.xx',
        username: 'xxx',
        privateKey: fs.readFileSync('C:/xxx/.ssh/id_rsa'),
        port: xxx
    }
  },
  prod: {
    path: '/data/code/',
    linkConfig: {
        host: 'xxx.xxx.xxx.xx',
        username: 'xxx',
        privateKey: fs.readFileSync('C:/xxx/.ssh/id_rsa'),
        port: xxx
    }
  },
}

let config = {
  // 前端包放在服务器的相对路径,如'/data/code'
  path: '/data/code/',
  // 服务器配置信息
  serverConfig: {
    host: 'xxx.xxx.xxx.xx',
    username: 'xxx',
    privateKey: fs.readFileSync('C:/xxx/.ssh/id_rsa'),
    port: xxx
  },
  // 发送到服务器的名称,例如有3个开发服,dev1, dev2, dev3
  serverName: ''
}


// 获取命令走哪一个环境
const getEnv = () => {
  const commandList = process.argv
  const commandEnv = commandList.filter((item) => {
    return item.indexOf('--serverName') > -1
  })
  if (commandEnv.length) {
    return commandEnv[0].substring(commandEnv[0].indexOf('=') + 1) 
  } else {
    console.log('命令需带上 打包后的服务器位置, --serverName=xxx')
    return ''
  }
}

// 不同的项目打包后可能放在不同的文件夹
const packageStorageDirectory = () => {
  let path = '/html'
  switch (dirName) {
      case 'admin':
        path += '/manage'
        break
      case 'pc':
        path += '/www'
        break
      case 'm':
        path += '/www/m'
        break
   }
  return path
}

// 根据命令重新配置全局变量
const getConfigFromCommand = () => {
  // 命令打包上传服务器名称
  let serverName = getEnv()
  if (!serverName) return
  // 服务器地址后的文件路径
  let serverFilePath = packageStorageDirectory()
  config.serverName = serverName
  config.serverConfig = serverConfig[serverName].linkConfig
  config.path = serverConfig[serverName].path + serverFilePath
}

// 判断文件夹存不存在
const exists = async (originPath) => {
  return await sftp.exists(originPath)
}
// 判断父级目录是否是文件夹
const parentIsDir = async (originPath) => {
  const parentDir = originPath.substring(0, originPath.lastIndexOf('/'))
  let info = await sftp.stat(parentDir)
  return info.isDirectory
}

// 判断父级目录是否存在
const parentExists = async (originPath) => {
  const parentDir = originPath.substring(0, originPath.lastIndexOf('/'))
  return await exists(parentDir)
}

// pc文件夹下面,不能删除的文件夹
const pcCantRemoveList = ['m', 'xxx']
// 删除除参数外的文件
const removeFilesAndFolders = async (excludeList = pcCantRemoveList) => {
  const files = await sftp.list(config.path)
  const fileNames = files.map(file => file.name)

  for (const fileName of fileNames) {
    if (!excludeList.includes(fileName)) {
      let filePath = config.path + '/' + fileName
      let fileInfo = await sftp.stat(filePath)

      if (fileInfo && fileInfo.isDirectory) {
        const removeReulst = await sftp.rmdir(filePath, true)
      } else {
        const removeFileReulst = await sftp.delete(filePath)
      }
    }
  }
}

/**
 * 
 * @param {*} originPath 远程文件目录,
 * @param {*} targetPath 本地文件目录
 */
const serverOperattion = async (originPath, targetPath) => {
  try {

    // 检查父级目录是否存在
    const parentFlag = await parentExists(originPath)
    if (!parentFlag) {
      console.log('父级目录不存在')
      return
    }
    // 检查父级目录是否存在
    const parentIsDirFlag = await parentIsDir(originPath)
    if (!parentIsDirFlag) {
      console.log('父级不是一个文件夹')
      return
    }

    // 如果文件存在
    if (dirName == 'pc') {
      // 如果pc端,那么不能全部删除,因为pc端里面有移动端文件夹 m
      await removeFilesAndFolders()
    } else {
      const flag = await exists(originPath)
      if (flag) {
        // 删除当前目录下所有文件和子文件夹
        await sftp.rmdir(originPath, true)
      }
      // 创建文件夹
      await sftp.mkdir(originPath)
    }
    // 更新代码
    await sftp.uploadDir(targetPath, originPath);
    console.log('文件夹替换成功!');
  } catch (err) {
    console.error(err.message);
  }

}

// 连接服务器
const linkedServer = async () => {
  try {
    // 初始配置
    getConfigFromCommand()

    // 链接服务器
    await sftp.connect(config.serverConfig)

    // 获取路径目录
    // await listRemoteDirectory(config.path)

    // 本地目录
    const targetPath = path.join(__dirname, './dist')
    await serverOperattion(config.path, targetPath)

  } catch (err) {
    console.error('部署失败')
  } finally {
    sftp.end()
  }
}

linkedServer()