vue+node+webpack前端自动化部署

本文最后更新于:1 年前

vue+node+webpack前端自动化部署是前端工程师所使用的一种自动化部署方式,它可以让我们的项目在服务器上运行,并且可以自动更新。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// filename 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打包后将打包目录文件上传到服务器上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// filename 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;

3. webpack配置文件的plugins配置项引入自定义插件

引入方式同Vue-cli,这里需要设定运行参数,与远程服务器对应即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// filename webpack.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下载,就可以使用啦。