node编写一款适合自己的部署插件

823 阅读3分钟

有一段时间工作重心集中在公众号的开发上,虽然团队有类似jenkins的自动部署,但有时为了解决兼容问题需要反复进行发布测试,类似基于仓库的部署工具就会造成许多没必要的提交信息,故有了这篇文章。

理想的部署工具需要具备的功能

  1. 基于目前团队的开发,这款部署工具平常需要发布经打包后的webpack以及类express的整库上传
  2. 自动对需要发布的文件进行压缩
  3. 对服务器账号增删查
  4. 对存储在本地的服务器关键信息进行加密

使用插件

  • ssh: 用于处理登录
  • commander: 声明指令
  • compressing: 打包压缩
  • inquirer: 终端交互
  • fs: 文件读取
  • path:存储路径
  • crypto-js: 密码加密

服务器地址存储

插件需实现服务器的增删查就需要对服务器进行存储,故在本地创建data.json进行数据存储。数据格式:

{
  "data": [
      {
          "name": "test",
          "config": {
              "ip": "*****",
              "pw": "*****.",
              "dir": "*****",
              "user": "*****"
          }
      }
  ]
}

增删查服务器

使用commander对指令进行定于,-a为添加操作,-l为列表操作,-rm为删除操作。

program
 .option('-a, --add', 'add service')
 .option('-rm, --remove', 'remove service')
 .option('-l, --list', 'list service');
program.parse(process.argv);

switch (true) {
 case program.add:
     add()
     break
 case program.remove:
     remove()
     break
 case program.list:
     list()
     break
 default:
     push()
}

fs将通过inquirer获取参数对data.json进行增删查。注:nodejs的写入文件只认识字符串或者二进制数,所以把json对象转换成字符串再写入json文件中。

文件压缩

实现了压缩发布dist以及全库压缩上传两种方式,默认情况下发布当前目录下的dist文件夹,当选择是全库上传时,需要多调用copy2dist函数将当前目录下文件打包成dist压缩包,注:忽略node模块文件夹。


var fs = require('fs');
var path = require("path");

function writeFile(p, text) {
  fs.writeFile(p, text, function (err) {
      if (!err)
          console.log("写入成功!")
  })
}

//递归创建目录 同步方法  
function mkdirsSync(dirname) {
  if (fs.existsSync(dirname)) {
      return true;
  } else {
      if (mkdirsSync(path.dirname(dirname))) {
          console.log("mkdirsSync = " + dirname);
          fs.mkdirSync(dirname);
          return true;
      }
  }
}

function _copy(src, dist) {
  var paths = fs.readdirSync(src)
  paths.forEach(function (p) {
      var _src = src + '/' + p;
      var _dist = dist + '/' + p;
      var stat = fs.statSync(_src)
      if (stat.isFile()) { // 判断是文件还是目录
          fs.writeFileSync(_dist, fs.readFileSync(_src));
      } else if (stat.isDirectory() && p != 'node_modules' && p != 'dist') {
          copyDir(_src, _dist) // 当是目录是,递归
      }
  })
}

/*
* 复制目录、子目录,及其中的文件
* @param src {String} 要复制的目录
* @param dist {String} 复制到目标目录
*/
function copyDir(src, dist) {
  var b = fs.existsSync(dist)
  if (!b) {
      mkdirsSync(dist); //创建目录
  }
  _copy(src, dist);
}

function createDocs(src, dist, callback) {
  console.log("createDocs...")
  copyDir(src, dist);
  console.log("copyDir finish exec callback")
  if (callback) {
      callback();
  }
}

module.exports = createDocs

加密

存储服务器信息的data是存储在本地的,所以应对服务器信息的密码进行必要的加密。这里的加密选择了crypto-js,可逆的对称加密方案。
const CryptoJS = require('crypto-js'); //引用AES源码js


function getAesString(data, key, iv) { //加密
   var key = CryptoJS.enc.Utf8.parse(key);
   var iv = CryptoJS.enc.Utf8.parse(iv);
   var encrypted = CryptoJS.AES.encrypt(data, key, {
       iv: iv,
       mode: CryptoJS.mode.CBC,
       padding: CryptoJS.pad.Pkcs7
   });
   return encrypted.toString(); //返回的是base64格式的密文
}

function getDAesString(encrypted, key, iv) { //解密
   var key = CryptoJS.enc.Utf8.parse(key);
   var iv = CryptoJS.enc.Utf8.parse(iv);
   var decrypted = CryptoJS.AES.decrypt(encrypted, key, {
       iv: iv,
       mode: CryptoJS.mode.CBC,
       padding: CryptoJS.pad.Pkcs7
   });
   return decrypted.toString(CryptoJS.enc.Utf8);
}

function getAES(data) { //加密
   var key = '****************'; //密钥
   var iv = 'ABCDEF1234123412';
   var encrypted = getAesString(data, key, iv); //密文
   var encrypted1 = CryptoJS.enc.Utf8.parse(encrypted);
   return encrypted;
}

function getDAes(data) { //解密
   var key = '****************'; //密钥
   var iv = 'ABCDEF1234123412';
   var decryptedStr = getDAesString(data, key, iv);
   return decryptedStr;
}

module.exports = {
   getAES,
   getDAes
}

注:密钥key需为16位。

发布

建立连接将dist压缩包发布至服务器指定路径中并执行设定好的comment指令集。

// 连接函数
function conFun() {
   console.log('开始连接...');
   connection = new ssh2();
   connection.connect({
       "host": ip,
       "username": user,
       "password": getDAes(password)
   });
   connection.on('error', function (err) {
       connection.end()
       console.log('连接失败');
   });
   connection.on('ready', function () {
       console.log('开始上传');
       upLoadFile(connection, params)
   });
}

// 上传函数
function upLoadFile(conn, params) {
   const file = params.file
   const target = params.target
   if (!conn) {
       return
   }
   conn.sftp(function (err, sftp) {
       if (err) {
           throw err
       }
       console.log(file, target);
       sftp.fastPut(file, target, {}, function (err, result) {
           if (err) {
               throw err
           }
           Shell(conn)
       })
   })
}
// 执行脚本函数
function Shell(conn) {
   conn.shell(function (err, stream) {
       if (err) throw err;
       stream.on('close', function () {
           console.log('发布完成!!');
           // 删除压缩包
           fs.unlinkSync('./dist.zip')
           conn.end();
       }).on('data', function (data) {
           console.log('STDOUT: ' + data)
       });
       stream.end(comment.join('\n'));
   });
}
comment = [
     `cd ${servicePath}`,
     'unzip -o dist.zip',
     'rm -f dist.zip',
     'exit',
     'close'
 ]

小结

写这款部署插件的初衷是由于真机调试公众号端时需要频繁的发布,总的来说并不建议使用于正式版本的发布。