使用NodeJS开发前端打包程序
每种语言都有自己的优势,互相结合各取所长使程序执行起来效率更高或者哪种实现方式较简单就用哪个,NodeJS是利用子进程来调用系统命令或者文件,文档见http://nodejs.org/api/child_process.html ,NodeJS子进程提供了与系统交互的重要接口,其主要API有: 标准输入、标准输出及标准错误输出的接口。
NodeJS 子进程提供了与系统交互的重要接口,其主要 API 有:
标准输入、标准输出及标准错误输出的接口
child.stdin 获取标准输入
child.stdout 获取标准输出
child.stderr 获取标准错误输出
获取子进程的PID:child.pid
提供生成子进程的方法:child_process.spawn(cmd, args=[], [options])
提供直接执行系统命令的方法:child_process.exec(cmd, [options], callback)
提供调用脚本文件的方法:child_process.execFile(file, [args], [options], [callback])
提供杀死进程的方法:child.kill(signal='SIGTERM'
1、利用子进程调用系统命令(获取系统内存使用情况)
新建nodejs文件,名为cmd_memory.js,代码如下:
var spawn = require('child_process').spawn; free = spawn('free', ['-m']); // 捕获标准输出并将其打印到控制台 free.stdout.on('data', function (data) { console.log('standard output:\n' + data); }); // 捕获标准错误输出并将其打印到控制台 free.stderr.on('data', function (data) { console.log('standard error output:\n' + data); }); // 注册子进程关闭事件 free.on('exit', function (code, signal) { console.log('child process eixt ,exit:' + code); });
注意:运行该脚本和直接运行命令'free -m'的结果是一样的
2、利用NodeJS开发前端打包程序
我们在做前端开发的时候经常会在部署上线的时候做程序的打包和合并,我们接下来就会对如何使用 node.js 开发前端打包程序做非常深入的讲解,希望能够帮到有需要的同学。
我们现在做前端开发更多的是多人共同协作开发,每个人负责不同的模块,便于开发和调试。这样就导致我们最后部署上线的时候需要把所有人开发的模块进行合并,生成单个或多个文件上线。如果手动合并的话肯定是费时又费力,而且非常容易出错,所以我们一般都是通过一些工具来实现自动合并的功能。
打包程序的原理非常简单,入口文件->寻找依赖关系->替换依赖关系->生成文件,其中中间的两个步骤是递归执行的。
我们先来看一下使用 node.js 如何完成一个简单的文件合并功能:
01 // 打包文件内容 |
02 |
var contentList = []; |
03 |
// 排重列表 |
04 |
var loadedFileList = {}; |
05 |
06 |
// 打包主程序 |
07 |
function combine(filePath){ |
08 |
// 这里获取入口文件的内容 |
09 |
var fileContent = fs.readFileSync(filePath); |
10 |
// 遍历文件内容 |
11 |
fileContent.forEach(function(value){ |
12 |
// 这里的findImport是需要你来实现的方法,用正则来匹配依赖关系 |
13 |
var matchFile = findImport(value); |
14 |
if(matchFile){ |
15 |
//如果匹配到依赖关系 |
16 |
If(!loadedFileList[matchFile]){ |
17 |
//如果依赖关系不在排重列表中,递归调用combine |
18 |
combine(matchFile); |
19 |
contentList.push(‘n’); |
20 |
} |
21 |
}else{ |
22 |
contentList.push(value); |
23 |
} |
24 |
}); |
25 |
} |
最后只要根据 contentList 里面的内容来生成文件就可以了,怎么样,是不是很简单呢?下面我们就要介绍另外一种方式,使用流来完成我们的打包程序。
在 node.js 中,流(Stream)是一个由不同对象实现的抽象接口。流可以是可读的、可写的、或者既可读又可写的。所有的流都是 EventEmitter 的实例。我们可以通过继承接口来构造我们自己所需要的流。在我们的打包程序里面需要两个流,一个负责按行输出文件内容,另外一个负责处理依赖关系。所有的文件内容都在这两个流里面循环流动,当所有的依赖关系都处理完毕之后就结束流动并生成对应的文件,这样就达到我们的目的了。
让我们先来看一下负责按行输出文件内容的流是怎么样的:
01 |
var Stream = require('stream').Stream, |
02 |
util = require('util'), |
03 |
path = require('path'), |
04 |
fs = require('fs'); |
05 |
06 |
// 构造函数 |
07 |
function LineStream() { |
08 |
this.writable = true; |
09 |
this.readable = true; |
10 |
this.buffer = ''; |
11 |
} |
12 |
13 |
module.exports = LineStream; |
14 |
// 继承流接口 |
15 |
util.inherits(LineStream, Stream); |
16 |
17 |
// 重写write方法,所有pipe过来的数据都会调用此方法 |
18 |
LineStream.prototype.write = function(data, encoding) { |
19 |
var that = this; |
20 |
// 把buffer转换为string类型 |
21 |
if (Buffer.isBuffer(data)) { |
22 |
data = data.toString(encoding || 'utf8'); |
23 |
} |
24 |
25 |
var parts = data.split(/n/g); |
26 |
27 |
// 如果有上一次的buffer存在就添加到最前面 |
28 |
if (this.buffer.length > 0) { |
29 |
parts[0] = this.buffer + parts[0]; |
30 |
} |
31 |
32 |
// 遍历并发送数据 |
33 |
for (var i = 0; i < parts.length - 1; i++) { |
34 |
this.emit('data', parts[i]); |
35 |
} |
36 |
// 把最后一行数据保存到buffer,使传递过来的数据保持连续和完整。 |
37 |
this.buffer = parts[parts.length - 1]; |
38 |
}; |
39 |
// end方法,在流结束时调用 |
40 |
LineStream.prototype.end = function() { |
41 |
// 如果还有buffer,发送出去 |
42 |
if(this.buffer.length > 0){ |
43 |
this.emit('data',this.buffer); |
44 |
this.buffer = ''; |
45 |
} |
46 |
this.emit('end'); |
47 |
}; |
这样我们的 lineStream 就完成了,我们看到在 write 方法里面就做了一件事,分解传递过来的数据并按行发送出去,然后我们看下处理依赖关系的流 DepsStream。
01 |
var stream = require('stream').Stream; |
02 |
var util = require('util'); |
03 |
var fs = require('fs'); |
04 |
var path = require('path'); |
05 |
06 |
module.exports = DepsStream; |
07 |
util.inherits(DepsStream,stream); |
08 |
09 |
function DepsStream(){ |
10 |
this.writable = true; |
11 |
this.readable = true; |
12 |
this.buffer = ''; |
13 |
this.depsList = []; |
14 |
}; |
15 |
16 |
// 这里的write方法只发送数据,不对数据做任何的处理 |
17 |
DepsStream.prototype.write = function(data){ |
18 |
this.emit('data',data); |
19 |
}; |
20 |
21 |
// 我们在这里重新pipe方法,使其能够处理依赖关系和生成最终文件 |
22 |
DepsStream.prototype.pipe = function(dest,opt){ |
23 |
var that = this; |
24 |
function ondata(chunk){ |
25 |
var matches = findImport(chunk); |
26 |
if(matches){ |
27 |
if(this.depsList.indexOf(matches) >= 0){ |
28 |
// 我们在这里把处理过后的数据pipe回lineStream |
29 |
dest.write('n'); |
30 |
}else{ |
31 |
this.depsList.push(matches); |
32 |
var code = getFileContent(matches); |
33 |
// 我们在这里把处理过后的数据pipe回lineStream |
34 |
dest.write('n' + code); |
35 |
} |
36 |
}else{ |
37 |
this.buffer += chunk + 'n'; |
38 |
} |
39 |
} |
40 |
function onend(){ |
41 |
// 生成最终文件 |
42 |
var code = this.buffer; |
43 |
fs.writeFileSync(filePublishUrl,code); |
44 |
console.log(filePublishUrl + ' combine done.'); |
45 |
} |
46 |
// 监听end事件 |
47 |
that.on('end',onend); |
48 |
// 监听data事件 |
49 |
that.on('data',ondata); |
50 |
}; |
51 |
52 |
// end方法 |
53 |
DepsStream.prototype.end = function(){ |
54 |
this.emit('end'); |
55 |
}; |
我们看到上面的程序里面我们在 pipe 方法里面监听了 end 事件和 data 事件,ondata 方法主要用来对数据进行处理,发现有依赖关系的话就获取对应依赖关系的文件并重新发回给 LineStream 进行处理。onend 方法用来生成最终的文件,我们来看一下最终的调用方法:
1 |
var fileStream = fs.createReadStream(filepath); |
2 |
var lineStream = new LineStream(); |
3 |
var depsStream = new DepsStream(); |
4 |
5 |
fileStream.pipe(lineStream); |
6 |
lineStream.pipe(depsStream); |
7 |
depsStream.pipe(lineStream); |