前端自动部署

方案一 配置 gitLab 的 commit 钩子函数

项目是 gitLab 做 git 服务器,可以配置 commit 的钩子函数,实现自动部署和线上发布,就相当服务器监听你的提交,在你 commit 之后,服务器自动执行了一下 npm run build,放到对应的测试服务器目录,配置文件在根目录下有 .gitlab-ci.yml 文件,起作用的是下边一段代码,script 是 linux 脚本,拷贝文件到指定的静态资源 CDN 目录和web服务器目录,这块知识点是 gitlab-ci 持续集成,可以关注一下。

配置说明:

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
before_script:
- echo '=================start build=================='
after_script:
- echo '=================build finish=================='
stages:
# 可将需要执行的脚本分为多个步骤,
# 注意,因A步骤生成的文件无法被B步骤使用,可以使用artifacts缓存
- A
- B

# 执行步骤A
A:
stage: A
only:
#在哪个分支变动时自动执行该步骤
- tags
# - master
script:
# 该步骤的脚本
- echo "=========A start=========="
- mvn install
- echo "=========A finish=========="
artifacts:
# 写在这里的文件可以进入缓存以被其他stage调用,有大小限制(100M)
paths:
- target/

# 执行步骤B
B:
stage: B
only:
- tags
# - master
script:
- echo "===========B start========"
# $CI_PROJECT_DIR 为当前项目的根目录
- cd $CI_PROJECT_DIR/target
- echo " ===========B finish========"

配置案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
npm-build-test:
image: cdn路径
stage: build
cache:
untracked: true
paths:
- node_modules/
before_script:
- export BI_ENV="test"
script:
- "npm install --registry=http://代理地址 --sass_binary_site=https://npm.taobao.org/mirrors/node-sass/"
- "npm run build"
- "rsync -auvz dist/index.html ip::服务器开发分支目录/trunk/resources/views/index/"
- "rsync -auvz dist/* 静态资源cdn目录/trunk/bi/"
only:
- master 分支名称

如果团队有用 Jenkins 可以配置 gitlab commit 后自动触发 Jenkins 构建,构建脚本在 Jenkins 中配置,我们项目组就是这样设置。网上教程很多,这里就不说明了。

方案二 利用scp2自动化部署到静态文件服务器(Nginx)

scp2 是一个基于 ssh2 增强实现,纯粹使用 JavaScript 编写。而 ssh2 就是一个使用 nodejs 对于 SSH2 的模拟实现。scp,是 secure copy 的缩写, scp 是 Linux 系统下基于 SSH 登陆进行安全的远程文件拷贝命令。这里我们就用这个功能,在 Vue 编译构建成功之后,将项目推送至测试/生产环境,以方便测试,提高效率。

安装

1
npm install scp2 --save-dev

配置测试/生产环境 服务器SSH远程登陆账号信息

1.在项目根目录下, 创建 .env.dev 文件 (测试环境变量)

VUE_APP_SERVER_ID 变量表示 当前需部署的测试服务器ID为0

1
2
// .env.dev文件中
VUE_APP_SERVER_ID=0

2.在项目根目录下, 创建 .env.prod 文件 (生产环境变量)

VUE_APP_SERVER_ID变量表示 当前需部署的生产服务器ID为1

1
2
// .env.prod文件中
VUE_APP_SERVER_ID=1

3.在项目根目录下, 创建 deploy/products.js 文件

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

/*
*读取env环境变量
*/
const fs = require('fs');
const path = require('path');
// env 文件 判断打包环境指定对应的服务器id
const envfile = process.env.NODE_ENV === 'prod' ? '../.env.prod' : '../.env.dev';
// env环境变量的路径
const envPath = path.resolve(__dirname, envfile);
// env对象
const envObj = parse(fs.readFileSync(envPath, 'utf8'));
const SERVER_ID = parseInt(envObj['VUE_APP_SERVER_ID']);

function parse(src) {
// 解析KEY=VAL的文件
const res = {};
src.split('\n').forEach(line => {
// matching "KEY' and 'VAL' in 'KEY=VAL'
// eslint-disable-next-line no-useless-escape
const keyValueArr = line.match(/^\s*([\w\.\-]+)\s*=\s*(.*)?\s*$/);
// matched?
if (keyValueArr != null) {
const key = keyValueArr[1];
let value = keyValueArr[2] || '';

// expand newlines in quoted values
const len = value ? value.length : 0;
if (len > 0 && value.charAt(0) === '"' && value.charAt(len - 1) === '"') {
value = value.replace(/\\n/gm, '\n');
}

// remove any surrounding quotes and extra spaces
value = value.replace(/(^['"]|['"]$)/g, '').trim();

res[key] = value;
}
});
return res;
}

/*
*定义多个服务器账号 及 根据 SERVER_ID 导出当前环境服务器账号
*/
const SERVER_LIST = [
{
id: 0,
name: 'A-生产环境',
domain: 'www.prod.com',// 域名
host: '46.106.38.24',// ip
port: 22,// 端口
username: 'root', // 登录服务器的账号
password: 'root',// 登录服务器的账号
path: '/mdm/nginx/dist'// 发布至静态服务器的项目路径
},
{
id: 1,
name: 'B-测试环境',
domain: 'test.xxx.com',
host: 'XX.XX.XX.XX',
port: 22,
username: 'root',
password: 'xxxxxxx',
path: '/usr/local/www/xxx_program_test/'
}
];

module.exports = SERVER_LIST[SERVER_ID];

使用scp2库,创建自动化部署脚本

在项目根目录下, 创建 deploy/index.js 文件

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
const scpClient = require('scp2');
const ora = require('ora');
const chalk = require('chalk');
const server = require('./products');
const spinner = ora('正在发布到' + (process.env.NODE_ENV === 'prod' ? '生产' : '测试') + '服务器...');
spinner.start();
scpClient.scp(
'dist/',
{
host: server.host,
port: server.port,
username: server.username,
password: server.password,
path: server.path
},
function (err) {
spinner.stop();
if (err) {
console.log(chalk.red('发布失败.\n'));
throw err;
} else {
console.log(chalk.green('Success! 成功发布到' + (process.env.NODE_ENV === 'prod' ? '生产' : '测试') + '服务器! \n'));
}
}
);

添加 package.json 中的 scripts 命令, 自定义名称为 “deploy”

发布到测试环境命令为 npm run deploy:dev,生产环境为 npm run deploy:prod

1
2
3
4
5
6
"scripts": {
"serve": "vue-cli-service serve --mode dev",
"build": "vue-cli-service build --mode prod",
"deploy:dev": "npm run build && cross-env NODE_ENV=dev node ./deploy",
"deploy:prod": "npm run build && cross-env NODE_ENV=prod node ./deploy",
},

ps 这里用到了cross_env 得安装 npm i –save-dev cross-env cross-env能跨平台地设置及使用环境变量,这里用来设置是生产环境还是测试环境。

删除 dist 文件

打包每次hash值不同,dist里面文件岂不是越来越多,可以用 ssh2 先把dist文件删除,删除后重启nginx再上传至服务器。

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
//  deploy/index.js里面
const scpClient = require('scp2');
const ora = require('ora');
const chalk = require('chalk');
const server = require('./products');
const spinner = ora(
'正在发布到' +
(process.env.NODE_ENV === 'prod' ? '生产' : '测试') +
'服务器...'
);

var Client = require('ssh2').Client;

var conn = new Client();
conn
.on('ready', function() {
// rm 删除dist文件,\n 是换行 换行执行 重启nginx命令 我这里是用docker重启nginx
conn.exec('rm -rf /mdm/nginx/dist\ndocker restart nginx', function(
err,
stream
) {
if (err) throw err;
stream
.on('close', function(code, signal) {
// 在执行shell命令后,把开始上传部署项目代码放到这里面
spinner.start();
scpClient.scp(
'./dist',
{
host: server.host,
port: server.port,
username: server.username,
password: server.password,
path: server.path
},
function(err) {
spinner.stop();
if (err) {
console.log(chalk.red('发布失败.\n'));
throw err;
} else {
console.log(
chalk.green(
'Success! 成功发布到' +
(process.env.NODE_ENV === 'prod'
? '生产'
: '测试') +
'服务器! \n'
)
);
}
}
);

conn.end();
})
.on('data', function(data) {
console.log('STDOUT: ' + data);
})
.stderr.on('data', function(data) {
console.log('STDERR: ' + data);
});
});
})
.connect({
host: server.host,
port: server.port,
username: server.username,
password: server.password
//privateKey: require('fs').readFileSync('/home/admin/.ssh/id_dsa')
});

方案二内容来自:

Vue-CLI 3.x 自动部署项目至服务器

方案三 github webhook

自动部署的关键就在于webhook ,主流的代码托管平台都有这个功能,包括coding , github等,在仓库里可以设置,以github为例:

在 setting 中点击 Webhooks 即可,在 Webhooks 中 可以填写一个自己服务器的接口地址和一个 seret 用于验证 , 填写完了仓库会监听 push 操作 , 一旦有 push 操作,webhook 就会发出一个 post 请求到你设置的接口,然后在服务器端执行脚本进行 git pull 操作,把最新的代码拉下来,就完成了部署。

其实简单的 shell 脚本写起来非常简单,例如:

部署node服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
echo "开始更新 服务1 服务器端"
cd /root/wechat/node-server
echo "正在更新代码..."
git pull
echo "正在重启服务..."
node app.js &
echo "服务1 服务端启动成功"

echo "开始更新 服务2 服务器端"
cd /root/wechat/node-admin-server
echo "正在更新代码..."
git pull
echo "正在重启服务..."
node app.js &
echo "服务2 服务端启动成功"

部署前端服务

1
2
3
4
5
6
7
8
9
10
11
echo "开始更新前端文件"
cd /root/local/app
echo "正在更新代码..."
git pull
echo "正在安装依赖和打包代码..."
cnpm i
npm run build
echo "正在删除旧文件和部署新文件..."
rm -rf ~/upload/app
mv dist ~/upload/app
echo "前端代码重新部署成功"

以上都是只用到了比较简单的 shell 语法。

方案四 利用 webpack 插件

其实就是在 Webpack 即将退出时再附加一些额外的操作,例如在 Webpack 成功编译和输出了文件后执行发布操作把输出的文件上传到服务器。

要实现该插件,需要借助两个事件:

done:在成功构建并且输出了文件后,Webpack 即将退出时发生;
failed:在构建出现异常导致构建失败,Webpack 即将退出时发生;
实现该插件非常简单,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class EndWebpackPlugin {

constructor(doneCallback, failCallback) {
// 存下在构造函数中传入的回调函数
this.doneCallback = doneCallback;
this.failCallback = failCallback;
}

apply(compiler) {
compiler.plugin('done', (stats) => {
// 在 done 事件中回调 doneCallback
this.doneCallback(stats);
});
compiler.plugin('failed', (err) => {
// 在 failed 事件中回调 failCallback
this.failCallback(err);
});
}
}
// 导出插件
module.exports = EndWebpackPlugin;

使用该插件时方法如下:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
plugins:[
// 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
new EndWebpackPlugin(() => {
// Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
}, (err) => {
// Webpack 构建失败,err 是导致错误的原因
console.error(err);
})
]
}

案例:webpack打包后自动发布插件UploadPlugin(以上传到七牛网为例),代码来自网上

UploadPlugin.js

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
class UploadPlugin {
constructor(options) {
let {bucket = '', domain = '', accessKey = '', secretKey = ''} = options;
let mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
var putPolicy = new qiniu.rs.PutPolicy({scope: bucket});
this.uploadToken=putPolicy.uploadToken(mac);
let config = new qiniu.conf.Config();
this.formUploader = new qiniu.form_up.FormUploader(config);
this.putExtra = new qiniu.form_up.PutExtra();
}

apply(compiler) {
compiler.hooks.afteremit.tapPromise('UploadPlugin', (compilation) => {
let assets = compilation.assets;
let promises = [];
Object.keys(assets).forEach(filename => {
promises.push(this.upload(filname));
});
return Promise.all();
})
}

upload(filename) {
return new Promise((resolve, reject) => {
let localFile = path.resolve(__dirname, '../dist', filename);
this.formUploader.putFile(this.uploadToken, filename, localFile, this.putExtra, (respErr, respBody, respInfo) => {
if (respErr) {
reject(respErr);
}
if (respInfo.statusCode == 200) {
sesolve(respBody);
}
});
})
}
}

module.exports = UploadPlugin;
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
let webpack = require('webpack');
let path = require('path');
let DonePlugin = require('./plugins/DonePlugin');
let AsyncPlugin = require('./plugins/AsyncPlugin');
let HtmlWebpackPlugin = require('html-webpack-plugin');
let FileListPlugin = require('./plugins/FileListPlugin');
let MiniCssExtractPlugin = require('mini-css-extract-plugin');
let InlineSourcePlugin = require('./plugins/InlineSourcePlugin');
let UploadPlugin = require('./plugins/UploadPlugin');

module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
// publicPath: "", 资源
},
mode: 'development',
plugins: [
new MiniCssExtractPlugin({
filename: 'main.css'
}),
new HtmlWebpackPlugin({
template: "./src/index.html"
}),
new FileListPlugin({
filename: 'list.md',
}),
// new InlineSourcePlugin({
// match: /\.(js|css)$/,
// })
// 上传到七牛
new UploadPlugin({
bucket: '', // 桶
domain: "", // 域名
accessKey: '', //
secretKey: '', //
})
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
}
};