背景目的

自从139邮箱移动端酷版邮箱构建工具由原来的 ant 转移到 gulp 之后,构建速度大大的提升,就拿本人的机子(本人工作机子是32位6G内存)来测试,之前构建全量包要花费将近 20 分钟,如今只需要 4 分钟不到;另外 gulp 是基于 node.js 的,对前端开发来说是相当友好的。但是,还是有点美中不足:第一,比如像我这样的前端小白,我对命令行不是很敏感,甚至我不太喜欢敲一串串的命令行去执行一个任务,我希望能有一个图形化界面工具,能点点按钮什么的就可以完成一个构建任务,那该多爽。第二,目前的 gulp 构建还不支持自定义构建,这个自定义构建其实就是,比如说,我想打包某个目标文件(这个文件可能由好几个文件合并压缩而成),只要选择了这个文件的文件名,然后点击按钮,就可以帮你执行的任务,帮你构建好这个文件,甚至可以帮你部署到资源服务器上。

简单的说就是:

  • 需要一个构建工具图形化界面
  • 需要自定义构建功能

基于以上两个目的,所以就做了一个这样的图形化前端构建小工具。

效果展示

  • 全量构建

全量构建效果图

  • JavaScript 增量构建

全量构建效果图

  • CSS 增量构建

全量构建效果图

  • HTML 增量构建

全量构建效果图

运用技术

  • 基于现有的 gulp 构建

基于现有的 gulp 构建

目前的 gulp 构建通过命令行可执行独立任务任务有

1
2
3
4
5
gulp # 构建全量包
gulp deploy # 构建全量包并自动部署到资源服务器
gulp uploadStaticFiles # 部署静态资源到服务器
gulp restartNodeServer # 部署并重启测试线的 node 服务
gulp watch # 实时代码监听

说明:以上的命令除了部署并重启测试线的 node 服务之外,其他的任务都将加到本次的小工具上。

  • Electron

最重要的东西要来了,它就是 Electron。在这之前,大家应该都有听说过 Node-Webkit (后期改名 NW.js)。NW.js 允许您直接从 DOM 层调用所有 Node.js 模块,并允许使用所有Web技术编写 PC 端的应用程序。而 Electron 也差不多类似这样的一种工具或者说框架。

Electron 是允许使用 JavaScript,HTML 和 CSS 等 Web 技术创建 PC 应用程序的框架。 它负责跟系统打交道,使得开发者可以更加专注于应用本身。Electron 官网 http://electron.atom.io/

Electron 的主要特点有:

  1. Web 技术。 Electron 是基于 Chromium 和 Node.js,因此您可以使用HTML,CSS和JavaScript构建应用程序。

  2. 开源。 Electron是由GitHub和活跃的贡献者社区维护的开源项目。

  3. 跨平台。 兼容 Mac,Windows 和 Linux 系统,Electron 应用程序可在三个平台上构建和运行。

Electron 快速入手:

1
2
3
4
5
6
7
8
# Clone the Quick Start repository
$ git clone https://github.com/electron/electron-quick-start
# Go into the repository
$ cd electron-quick-start
# Install the dependencies and run
$ npm install && npm start

我为什么要用 Electron 而不用 NW.js ?

  1. 好奇心的我,想接触一下新事物;

  2. 剩下的理由主要是受了知乎一些吐槽的影响。《用Nodejs开发桌面应用。NW.js 和 Electron 各有什么优缺点,你选择哪个?

有哪些公司或者 App 在用 Electron?

其实,Electron 已经被微软,Facebook,Slack 和 Docker 等公司用来创建应用程序。成功案例有很多,比较有代表性的有如下这些:

  1. Atom 编辑器

  2. Slack(那个独角兽公司)

  3. Visual Studio Code

  4. WordPress 桌面版

  • Bootstrap

UI 界面用的是 Bootstrap,简洁、直观、强悍的前端开发框架,让web开发更迅速、简单。

  • Q.js

由于本图形构建工具功能比较简单,所以想做一个单页面就好,但是又不想用像 vue 这样的框架,网上找了一下,发现 Q.js 这个路由框架。

Q.js 是一个炒鸡轻量的前端单页路由框架。官网地址是 http://mouto.org/#!54092,Github 地址是 https://github.com/itorr/q.js

Q.js 特点是轻量、快速、极简。为了更好的利用缓存以及更少的后端支援,Q.js放弃了 HTML5 State,通过#!格式的 url hach 重现了 url 路由功能。

  1. 无 JavaScript 库依托,可随意搭配使用;

  2. 源代码不及百行压缩后 834byte ;

  3. 支持 IE6+ Chrome Safari FF (其实 Electron 算是很新的浏览器内核,已经没必要考虑这一点);

  4. 未做情况判定,使用 Q.js 必然会注册 window.Q 。

来一段简单的 Hello, World 来简单演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!doctype>
<html>
<head>
<meta charset="UTF-8" >
<title>演示</title>
<script src="a.js"></script>
</head>
<body>
<div id="m"></div>
<script>
Q.reg('home', function () {
document.getElementById('m').innerHTML = 'Hello, World!';
});
Q.init({
index: 'home' /* 首页地址 */
});
</script>
</body>
</html>

打开例子后,浏览器会从 http://simple.com/ 跳转到 http://simple.com/#!home ,并且在页面显示 Hello World。

技术实现

首先,我们整体看一下,整个应用的目录结构:

项目结构

在执行 gulp 各个任务,主要用了 node.js 的 child_process 进程模块的 spawn 方法。spawn 使用如下:

1
2
const spawn = require('child_process').spawn;
const gulpTask = spawn('gulp', [ 'default' ]);

对自定义构建,主要通过命令行传参的方式指定的 gulp 构建的 json 配置,从而实现文件的自定义构建,代码如下:

1
2
3
4
5
// 前端获取选择的配置文件的文件名并以`,`号分隔
// 如 --fileConf=/Users/zdl/Documents/Projects/Mail139_iPad_F2010/trunk/src/buildNew/config/jsFiles/concat/calendar_birthday.html.pack.js.json,/Users/zdl/Documents/Projects/Mail139_iPad_F2010/trunk/src/buildNew/config/jsFiles/concat/calendar_calendar.html.pack.js.json
let yargv = require('yargs').argv;
var fileConf = yargv.fileConf;
  • 全量构建

全量构建里面包含“构建全量包”、“构建并部署”、“构建并监听”三个任务。实现代码如下:

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
//-- renders.js --
/**
* 构建全量包
* @description gulp default
*/
$E('btnBuildGlobal').onclick = function () {
executeBuild({
processingBarId: 'btnBuildGlobalProgress',
processingName: '构建全量包',
command: 'gulp'
});
};
/**
* 构建并部署
* @deacription gulp deploy
*/
$E('btnBuildDeploy').onclick = function () {
executeBuild({
processingBarId: 'btnBuildDeployProgress',
processingName: '构建并部署',
command: 'gulp',
args: [ 'deploy' ]
});
};
/**
* 构建并监听
* @description gulp watch
*/
$E('btnBuildWatch').onclick = function () {
var _this = this;
if (_this.dataset.watching === 'off') {
if (processingObject.processingGulp) return;
// 关闭其他线程的提示框
closeTips();
processingObject.processingGulp = spawn('gulp', [ 'watch' ], {
cwd: config[$E('btnSwitchRDlines').dataset['switch']]
});
processingObject.processingName = '构建并监听';
processingObject.processingBarId = 'btnBuildWatchProgress';
_this.innerHTML = '点击不监听';
_this.dataset.watching = 'on';
$E('btnBuildWatchProgress').style.width = '100%';
processingObject.processingGulp.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
processingObject.processingGulp.on('close', (data) => {
console.log(`stdout close: ${data}`);
if (data === null) {
_this.innerHTML = '构建并监听';
_this.dataset.watching = 'off';
}
});
} else if (_this.dataset.watching === 'on') {
_this.innerHTML = '构建并监听';
_this.dataset.watching = 'off';
$E('btnBuildWatchProgress').style.width = '0%';
showTips('代码监听已取消!', 'alert-danger');
killCurrentProcessing();
}
};
  • JavaScript 自定义构建

JavaScript 自定义构建包含“打包”、“打包并部署”两个任务,实现代码如下:

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
//-- renders.js --
/**
* 自定义构建JS
* @description gulp customBuildJs --fileConf
*
*/
$E('btnBuildJS').onclick = function () {
var oCheckeds = $E('jsBuildContainerContent').querySelectorAll('input[type="checkbox"]:checked');
console.log('oCheckeds =>', oCheckeds);
if (!oCheckeds.length) {
alert('请选择要打包的 js 文件!');
return;
}
let fileConf = [];
for (var i = 0, len = oCheckeds.length; i < len; ++i) {
fileConf.push(oCheckeds[i].value);
}
console.log('fileConf =>', fileConf.join(','));
executeBuild({
processingBarId: 'btnBuildProgressJS',
processingName: 'JS文件自定义构建',
command: 'gulp',
args: [ 'customBuildJs', '--fileConf=' + fileConf.join(',') ]
});
};
/**
* 自定义构建并自动部署JS
* @description gulp customBuildAndDeployJs --fileConf
*
*/
$E('btnBuildDeployJS').onclick = function () {
var oCheckeds = $E('jsBuildContainerContent').querySelectorAll('input[type="checkbox"]:checked');
console.log('oCheckeds =>', oCheckeds);
if (!oCheckeds.length) {
alert('请选择要打包部署的 js 文件');
return;
}
let fileConf = [];
for (var i = 0, len = oCheckeds.length; i < len; ++i) {
fileConf.push(oCheckeds[i].value);
}
console.log('fileConf =>', fileConf.join(','));
executeBuild({
processingBarId: 'btnBuildProgressJS',
processingName: 'JS文件自定义构建并自动部署',
command: 'gulp',
args: [ 'customBuildAndDeployJs', '--fileConf=' + fileConf.join(',') ]
});
};
//-- gulpfile.js --
// 自定义构建打包 js
// 参考:task uglifyJs
gulp.task('customBuildJs', [ 'clean', 'compileTsFiles', 'updateJsConcatConfig' ], function () {
console.log(`yargv.fileConf => ${yargv.fileConf}`);
var fileConf = yargv.fileConf;
return pump([
concatJsFiles({
concatConfig: fileConf.split(',')
}),
debug({title: "concating --> "}),
// sourcemaps.init(),
uglify(),
debug({title: "uglifying --> "}),
// sourcemaps.write("./"),
gulp.dest( path.join(destDir))
]);
});
// 自定义构建打包并部署 js
gulp.task('customBuildAndDeployJs', [ 'customBuildJs' ], function () {
let conn = ftp.create( {
host: '此处是host ip',
user: 'root',
password: '此处是密码',
parallel: 10,
log: gutil.log
// debug: gutil.log
} );
gutil.log("----------**********-----------");
gutil.log(gutil.colors.magenta("---------上传静态资源文件--------"));
gutil.log("----------**********-----------");
// gulp-ssh插件在上传文件数量过多时会报错,所以使用vinyl-ftp替代,效率更高
// turn off buffering in gulp.src for best performance
return gulp.src( path.join( resourceDir, "**/*"), { buffer: false } )
.pipe( conn.dest('/home/richmail/nginx/htdocs/html5') );
});

对于 JavaScript 自定义构建,在 gulpfile.js 里面新加了 customBuildJs、customBuildAndDeployJs 两个 task。

  • CSS 自定义构建

CSS 自定义构建包含“打包”、“打包并部署”两个任务,实现代码如下:

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
//-- renders.js --
/**
* 自定义构建CSS
* @description gulp customBuildCss --fileConf
*
*/
$E('btnBuildCSS').onclick = function () {
var oCheckeds = $E('cssBuildContainerContent').querySelectorAll('input[type="checkbox"]:checked');
console.log('oCheckeds =>', oCheckeds);
if (!oCheckeds.length) {
alert('请选择要打包的 css 文件配置');
return;
}
let fileConf = [];
for (var i = 0, len = oCheckeds.length; i < len; ++i) {
fileConf.push(oCheckeds[i].value);
}
console.log('fileConf =>', fileConf.join(','));
executeBuild({
processingBarId: 'btnBuildProgressCSS',
processingName: 'CSS文件自定义构建',
command: 'gulp',
args: [ 'customBuildCss', '--fileConf=' + fileConf.join(',') ]
});
};
/**
* 自定义构建并自动部署CSS
* @description gulp customBuildAndDeployCss --fileConf
*
*/
$E('btnBuildDeployCSS').onclick = function () {
var oCheckeds = $E('cssBuildContainerContent').querySelectorAll('input[type="checkbox"]:checked');
console.log('oCheckeds =>', oCheckeds);
if (!oCheckeds.length) {
alert('请选择要打包的 css 文件配置');
return;
}
let fileConf = [];
for (var i = 0, len = oCheckeds.length; i < len; ++i) {
fileConf.push(oCheckeds[i].value);
}
console.log('fileConf =>', fileConf.join(','));
executeBuild({
processingBarId: 'btnBuildProgressCSS',
processingName: 'CSS文件自定义构建并自动部署',
command: 'gulp',
args: [ 'customBuildAndDeployCss', '--fileConf=' + fileConf.join(',') ]
});
};
//-- gulpfile.js --
// 自定义构建 css
// 参考 task compressCss
gulp.task('customBuildCss', [ 'clean' ], function () {
console.log(`yargv.fileConf => ${yargv.fileConf}`);
var fileConf = yargv.fileConf;
// 压缩css文件
// return concatCssFiles({concatConfig: './config/cssFiles/concatcss.json'})
return concatCssFiles({ concatConfig: fileConf })
.pipe(debug({title: 'concating css file --> '}))
.pipe(replaceImageVersion({
rootDir: html5Dir,
images: path.join(html5Dir, '/**/*.{png,gif,jpg,ico}')
}))
.pipe(debug({title: 'img url reversion file --> '}))
.pipe(minifyCss({processImport: false}))
.pipe(debug({title: 'compress css file --> '}))
.pipe(gulp.dest(path.join(resourceDir, 'css')));
});
// 自定义构建并部署 css
gulp.task('customBuildAndDeployCss', [ 'customBuildCss' ], function () {
let conn = ftp.create( {
host: '此处是host ip',
user: 'root',
password: '此处是密码',
parallel: 10,
log: gutil.log
// debug: gutil.log
} );
gutil.log("----------**********-----------");
gutil.log(gutil.colors.magenta("---------上传静态资源文件--------"));
gutil.log("----------**********-----------");
// gulp-ssh插件在上传文件数量过多时会报错,所以使用vinyl-ftp替代,效率更高
// turn off buffering in gulp.src for best performance
return gulp.src( path.join( resourceDir, "**/*"), { buffer: false } )
.pipe( conn.dest('/home/richmail/nginx/htdocs/html5') );
});

对于 CSS 自定义构建,在 gulpfile.js 里面新加了 customBuildCss、customBuildAndDeployCss 两个 task。

  • HTML 自定义构建
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
//-- renders.js --
/**
* 自定义构建HTML
* @description gulp customBuildHtml --fileConf
*
*/
$E('btnBuildHTML').onclick = function () {
var oCheckeds = $E('htmlBuildContainerContent').querySelectorAll('input[type="checkbox"]:checked');
console.log('oCheckeds =>', oCheckeds);
if (!oCheckeds.length) {
alert('请选择要打包的 html 文件');
return;
}
let fileConf = [];
for (var i = 0, len = oCheckeds.length; i < len; ++i) {
fileConf.push(oCheckeds[i].value);
}
console.log('fileConf =>', fileConf.join(','));
executeBuild({
processingBarId: 'btnBuildProgressHTML',
processingName: 'HTML文件自定义构建',
command: 'gulp',
args: [ 'customBuildHtml', '--fileConf=' + fileConf.join(',') ]
});
};
/**
* 自定义构建并自动部署HTML
* @description gulp customBuildAndDeployHtml --fileConf
*
*/
$E('btnBuildDeployHTML').onclick = function () {
var oCheckeds = $E('htmlBuildContainerContent').querySelectorAll('input[type="checkbox"]:checked');
console.log('oCheckeds =>', oCheckeds);
if (!oCheckeds.length) {
alert('请选择要打包的 html 文件');
return;
}
let fileConf = [];
for (var i = 0, len = oCheckeds.length; i < len; ++i) {
fileConf.push(oCheckeds[i].value);
}
console.log('fileConf =>', fileConf.join(','));
executeBuild({
processingBarId: 'btnBuildProgressHTML',
processingName: 'HTML文件自定义构建并自动部署',
command: 'gulp',
args: [ 'customBuildAndDeployHtml', '--fileConf=' + fileConf.join(',') ]
});
};
//-- gulpfile.js --
// 自定义构建 html
// 参考 task htmlminFiles
gulp.task('customBuildHtml', [ 'clean' ], function () {
console.log(`yargv.fileConf => ${yargv.fileConf}`);
var fileConf = yargv.fileConf;
// fileConf = fileConf.replace(/(\/mpost_operation\/)|(\/mpost_topic\/)|(\/mpost_v2\/)|(\/operation\/)|(\/umcupgrade\/)|(\/upPackage\/)|(\/weixin\/)/, '/**/');
// 因为 html 目录下还包含一些二级目录的 html 文件
fileConf = fileConf.replace(/\/html\/\w+\//g, '/html/**/');
return gulp.src(fileConf.split(','))
.pipe(debug({title: 'htmlmin file --> '}))
.pipe(htmlmin())
.pipe(debug({title: 'minify-inline file --> '}))
.pipe(minifyInline())
.pipe(gulp.dest(resourceDir));
});
// 自定义构建 html
gulp.task('customBuildAndDeployHtml', [ 'customBuildHtml' ], function () {
let conn = ftp.create( {
host: '此处是 host ip',
user: 'root',
password: '此处是密码',
parallel: 10,
log: gutil.log
// debug: gutil.log
} );
gutil.log("----------**********-----------");
gutil.log(gutil.colors.magenta("---------上传静态资源文件--------"));
gutil.log("----------**********-----------");
// gulp-ssh插件在上传文件数量过多时会报错,所以使用vinyl-ftp替代,效率更高
// turn off buffering in gulp.src for best performance
return gulp.src( path.join( resourceDir, "**/*"), { buffer: false } )
.pipe( conn.dest('/home/richmail/nginx/htdocs/html5') );
});

对于 HTML 自定义构建,在 gulpfile.js 里面新加了 customBuildHtml、customBuildAndDeployHtml 两个 task。

总结

  • 工具可以随意切换139邮箱代码当前工作目录,全网(release)、灰度(beta)、测试线(trunk),甚至可以手动输入分支(branch)目录;

  • 工具执行任务是单线程,当前执行任务最多只有 1 个;即,如果当前正在构建时,点击其他按钮是无效的,应该等待当前任务执行完毕之后,才去点击执行其他任务。当然,你也可以右键终止当前任务;

  • 由于工具是基于 Electron 的,所以包比较大,这个你懂的。

参考

  1. Electron 官网:http://electron.atom.io/
  2. gulp 中文网:http://www.gulpjs.com.cn/
  3. Q.js Github项目地址:https://github.com/itorr/q.js