前言:
一般来说,我们前端是不需要关心部署的事情的,只需要把打包后的文件直接丢给后台去部署就可以了。但是呢,如果频繁修改一点东西就要叫后台进行部署,这样后台会很烦(毕竟人家还有其他工作嘛),我们也会很不好意思。
或许有些公司会给前端配置可视化操作服务器文件的软件(FTP客户端),这时我们就可以打包后自己到服务器上部署了,如果不同环境需要部署到不同服务器,此时我们又需要区分打包再手动上传到服务器上。
这时我们就会想,有没有直接一句命令就能自动化部署到不同服务器上,根本不需要打开软件来手动上传的???
答案:必须有啊,接下来看看如何进行操作,一劳永逸~~

一、webpack + Nodejs实现前端自动部署 ——完整版(法一)

这种方式就是完全由我们前端工程师来实现的啦,通过写nodejs实现服务器操作,结合webpack打包完成自动部署。

1、首先我们用nodejs来封装一个能操作远程服务器的工具库

文件命名为:serverLib.js

/**
 * 该文件封装了对远程服务器的操作
 */
 const util = require('util');
 const events = require('events');
 const { Client } = require('ssh2'); // ssh2模块需要使用npm安装
 const fs = require('fs');
 const path = require('path');

 /**
 * 描述:连接远程电脑
 * 参数:server 远程电脑凭证;
         then 回调函数
 * 回调:then(conn) 连接远程的client对象
 */
 function Connect(server, then) {
     const conn = new Client();
     conn.on('ready', () => {
         then(conn);
     }).on('error', (err) => {
         // console.log("connect error!");
     }).on('end', () => {
         // console.log("connect end!");
     }).on('close', (had_error) => {
         // console.log("connect close");
     })
         .connect(server);
 }

 /**
 * 描述:运行shell命令
 * 参数:server 远程电脑凭证;
         cmd 执行的命令;
         then 回调函数
 * 回调:then(err, data) : data 运行命令之后的返回数据信息
 */
 function Shell(server, cmd, then) {
     Connect(server, (conn) => {
         conn.shell((err, stream) => {
             if (err) {
                 then(err);
             } else { // end of if
                 let buf = '';
                 stream.on('close', () => {
                     conn.end();
                     then(err, buf);
                 }).on('data', (data) => {
                     buf += data;
                 }).stderr.on('data', (data) => {
                     console.log(`stderr: ${data}`);
                 });
                 stream.end(cmd);
             }
         });
     });
 }

 /**
 * 描述:上传文件
 * 参数:server 远程电脑凭证;
         localPath 本地路径;
         remotePath 远程路径;
         then 回调函数
 * 回调:then(err, result)
 */
 function UploadFile(server, localPath, remotePath, then) {
     Connect(server, (conn) => {
         conn.sftp((err, sftp) => {
             if (err) {
                 then(err);
             } else {
                 sftp.fastPut(localPath, remotePath, (err, result) => {
                     conn.end();
                     then(err, result);
                 });
             }
         });
     });
 }

 /**
 * 描述:下载文件
 * 参数:server 远程电脑凭证;
         remotePath 远程路径;
         localPath 本地路径;
         then 回调函数
 * 回调:then(err, result)
 */
 function DownloadFile(server, remotePath, localPath, then) {
     Connect(server, (conn) => {
         conn.sftp((err, sftp) => {
             if (err) {
                 then(err);
             } else {
                 sftp.fastGet(remotePath, localPath, (err, result) => {
                     if (err) {
                         then(err);
                     } else {
                         conn.end();
                         then(err, result);
                     }
                 });
             }
         });
     });
 }

 /**
 * 描述:获取远程文件路径下文件列表信息
 * 参数:server 远程电脑凭证;
 *       remotePath 远程路径;
 *       isFile 是否是获取文件,true获取文件信息,false获取目录信息;
 *       then 回调函数
 * 回调:then(err, dirs) : dir, 获取的列表信息
 */
 function GetFileOrDirList(server, remotePath, isFile, then) {
     const cmd = `find ${remotePath} -type ${isFile == true ? 'f' : 'd'}\r\nexit\r\n`;
     Shell(server, cmd, (err, data) => {
         let arr = [];
         const remoteFile = [];
         arr = data.split('\r\n');
         arr.forEach((dir) => {
             if (dir.indexOf(remotePath) == 0) {
                 remoteFile.push(dir);
             }
         });
         then(err, remoteFile);
     });
 }

 /**
 * 描述:控制上传或者下载一个一个的执行
 */
 function Control() {
     events.EventEmitter.call(this);
 }
 util.inherits(Control, events.EventEmitter); // 使这个类继承EventEmitter

 const control = new Control();

 control.on('donext', (todos, then) => {
     if (todos.length > 0) {
         const func = todos.shift();
         func((err, result) => {
             if (err) {
                 throw err;
                 then(err);
             } else {
                 control.emit('donext', todos, then);
             }
         });
     } else {
         then(null);
     }
 });

 /**
 * 描述:下载目录到本地
 * 参数:server 远程电脑凭证;
 *       remotePath 远程路径;
 *       localDir 本地路径,
 *       then 回调函数
 * 回调:then(err)
 */
 function DownloadDir(server, remoteDir, localDir, then) {
     GetFileOrDirList(server, remoteDir, false, (err, dirs) => {
         if (err) {
             throw err;
         } else {
             GetFileOrDirList(server, remoteDir, true, (err, files) => {
                 if (err) {
                     throw err;
                 } else {
                     dirs.shift();
                     dirs.forEach((dir) => {
                         const tmpDir = path.join(localDir, dir.slice(remoteDir.length + 1)).replace(/[//]\g/, '\\');
                         // 创建目录
                         fs.mkdirSync(tmpDir);
                     });
                     const todoFiles = [];
                     files.forEach((file) => {
                         const tmpPath = path.join(localDir, file.slice(remoteDir.length + 1)).replace(/[//]\g/, '\\');
                         todoFiles.push((done) => {
                             DownloadFile(server, file, tmpPath, done);
                             console.log(`downloading the ${file}`);
                         });// end of todoFiles.push
                     });
                     control.emit('donext', todoFiles, then);
                 }
             });
         }
     });
 }

 /**
 * 描述:获取windows上的文件目录以及文件列表信息
 * 参数:destDir 本地路径,
 *       dirs 目录列表
 *       files 文件列表
 */
 function GetFileAndDirList(localDir, dirs, files) {
     const dir = fs.readdirSync(localDir);
     for (let i = 0; i < dir.length; i++) {
         const p = path.join(localDir, dir[i]);
         const stat = fs.statSync(p);
         if (stat.isDirectory()) {
             dirs.push(p);
             GetFileAndDirList(p, dirs, files);
         } else {
             files.push(p);
         }
     }
 }

 /**
 * 描述:上传文件夹到远程目录
 * 参数:server 远程电脑凭证;
 *       localDir 本地路径,
 *       remoteDir 远程路径;
 *       then 回调函数
 * 回调:then(err)
 */
 function UploadDir(server, localDir, remoteDir, then) {
     const dirs = [];
     const files = [];
     GetFileAndDirList(localDir, dirs, files);

     // 删除远程指定目录下的所有文件
     const deleteDir = [(done) => {
         const cmd = `rm -rf ${remoteDir}* \r\nexit\r\n`;
         console.log(cmd);
         Shell(server, cmd, done);
     }];

     // 创建远程目录
     const todoDir = [];
     dirs.forEach((dir) => {
         todoDir.push((done) => {
             const to = path.join(remoteDir, dir.slice(localDir.length)).replace(/[\\]/g, '/');
             const cmd = `mkdir -p ${to}\r\nexit\r\n`;
             console.log(cmd);
             Shell(server, cmd, done);
         });// end of push
     });

     // 上传文件
     const todoFile = [];
     files.forEach((file) => {
         todoFile.push((done) => {
             const to = path.join(remoteDir, file.slice(localDir.length)).replace(/[\\]/g, '/');
             console.log(`upload ${to}`);
             UploadFile(server, file, to, done);
         });
     });

     control.emit('donext', deleteDir, (err) => {
         if (err) {
             throw err;
         } else {
             control.emit('donext', todoDir, (err) => {
                 if (err) {
                     throw err;
                 } else {
                     control.emit('donext', todoFile, then);
                 }
             });
         }
     });
 }

 exports.Shell = Shell;
 exports.UploadFile = UploadFile;
 exports.DownloadFile = DownloadFile;
 exports.GetFileOrDirList = GetFileOrDirList;
 exports.DownloadDir = DownloadDir;
 exports.UploadDir = UploadDir;

2、封装一个webpack插件

该插件实现webpack打包后将打包目录文件上传到服务器上。
文件命名为:uploadFileWebPackPlugin.js

/**
 * 上传打包后的文件到服务器上的webpack插件
 */
 const { spawn } = require('child_process');
 const uploadDir = require('./serverLib').UploadDir;

 class UploadFileWebPackPlugin {
     constructor(options) {
         this.options = options;
     }

     apply(compiler) {
     // 定义在打包后执行这个webpack插件
     // 需要用到对应的钩子函数
         compiler.hooks.done.tap('upload-file-plugin', async (status) => {
             // console.log('this.options: ', this.options);
             this.deploy();
         });
     }

     deploy() {
         const chmod = spawn('chmod', ['-R', '777', this.options.buildFolder]);
         chmod.on('exit', (code, signal) => {
             console.log('\n服务器授权成功,开始自动化部署~~\n');
             uploadDir(
                 this.options.serverConfig,
                 this.options.buildFolder,
                 this.options.servePath,
                 (err) => {
                     if (err) throw err;
                     console.log('\n自动化部署成功~\n');
                 },
             );
         });
     }
 }
 module.exports = UploadFileWebPackPlugin;

至于webpack插件如何编写,语法是什么?下面推荐几篇文章大家参考下。
怎样编写一个简单的webpack插件
Webpack原理-编写Plugin
webpack官网-编写自定义插件

3、在webpack配置文件的plugins配置项中引入上面自定义的插件

这里我们以vue-cli脚手架来举例,其他项目的引入方式雷同。
这里需要根据我们设定的运行命令参数,和远程服务器的信息进行对应修改即可。
在这里插入图片描述对上面的截图进行代码更正,可根据实际测试结果进行修改。

// vue.config.js文件
/**
 * 自动化部署代码引入  start
 */
// 引入自定义的上传文件webpack插件
const UploadFileWebPackPlugin = require('./webpack-plugin/uploadFileWebPackPlugin');

// 获取运行命令的参数
const deployArgv = process.argv.pop();
// 通过参数判断是否要执行上传插件
let isNeedUpload = false;
let uploadServerConfig = {};
// 根据参数设置不同服务器信息
if (deployArgv === '-95') {
  isNeedUpload = true;
  uploadServerConfig = {
    host: 'xxx.xxx.xxx.95', // 服务器ip地址
    port: 55314, // 服务器端口号
    username: 'xxxxx', // 登录服务器的用户名
    password: 'xxxxxxx', // 登录服务器的密码
  };
} else if (deployArgv === '-114') {
  isNeedUpload = true;
  uploadServerConfig = {
    host: 'xxx.xxx.xxx.114',
    port: 55314,
    username: 'xxxxx',
    password: 'xxxxxxxxx',
  };
}
/**
 * 自动化部署代码引入  end
 */
const webpackConfig = {
  configureWebpack: {
    // plugin配置项
    plugins: [
      // // 在npm run build的时候才执行这个插件(自动化部署插件)
      // ---- 尝试过这个方法使用插件,但是在不加参数的时候就会报错说webpack插件定义不正确的情况
      // (process.env.NODE_ENV === 'production' && isNeedUpload)
      //   && new UploadFileWebPackPlugin({
      //       // 服务器的配置信息
      //       serverConfig: uploadServerConfig,
      //       // 本地打包输出的文件夹路径
      //       buildFolder: 'dist/',
      //       // 上传到服务器上的路径
      //       servePath: '/home/sendi/fe/winne-test/',
      //   }),
    ],
  },
  // 暂时关闭eslint校验, 方便测试
  devServer: {
    overlay: {
      warining: true,
      errors: true,
    },
  },
  lintOnSave: false,
  // 配置部署应用包时的基本 URL
  publicPath: process.env.NODE_ENV === 'production'
    ? '/winne-test/'
    : '/',
};

// webpack插件根据环境判断来使用改为这个方式(在加参数或者不加参数的情况都能正确运行)
if ((process.env.NODE_ENV === 'production' && isNeedUpload)) {
  webpackConfig.configureWebpack.plugins.push(
    new UploadFileWebPackPlugin({
      // 服务器的配置信息
      serverConfig: uploadServerConfig,
      // 本地打包输出的文件夹路径
      buildFolder: 'dist/',
      // 上传到服务器上的路径
      servePath: '/home/sendi/fe/winne-test/',
  }),
  );
}

module.exports = webpackConfig;

4、运行打包命令,实现前端项目的自动化部署

1)、没用到自动化部署时,我们这样打包项目
使用npm打包:npm run build
使用yarn打包:yarn build

2)、需要自动化部署时,我们这样打包项目(打包命令后面加参数,识别不同参数部署到不同服务器)
使用npm打包:npm run build -- -95 或者 npm run build -- -114 (注意在参数前有两个中划线)
使用yarn打包:yarn build -95 或者 yarn build -114

在这里插入图片描述

最后

如果要更方便的使用,可以把自动部署功能代码直接封装成npm包发布到npm上,这样以后用到时可以直接使用npm下载,就可以使用啦。

二、jenkins实现前端自动部署(法二)

这个方法一般来说都是后端来配置的,此处不展开,感兴趣的伙伴们,自行百度解决,下面推荐几篇文章。

使用jenkins进行前端项目自动部署
一套基础自动化部署搭建过程(vue实战防坑版)
学会使用 Jenkins 自动部署你的项目(实战)

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐