他创造NodeJS的目的是为了实现高性能Web服务器,他首先看重的是事件机制和异步IO模型的优越性,而不是JS。但是他需要选择一种编程语言实现他的想法,这种编程语言不能自带IO功能,并且需要能良好支持事件机制。
- NodeJS是一个JS脚本解析器,任何操作系统下安装NodeJS本质上做的事情都是把NodeJS执行程序复制到一个目录,然后保证这个目录在系统PATH环境变量下,以便终端下可以使用node命令。
- 终端下直接输入node命令可进入命令交互模式,很适合用来测试一些JS代码片段,比如正则表达式。
- NodeJS使用CMD模块系统,主模块作为程序入口点,所有模块在执行过程中只初始化一次。
- 除非JS模块不能满足需求,否则不要轻易使用二进制模块,否则你的用户会叫苦连天。
require exports module
模块路径解析规则
内置模块 node_modules目录 NODE_PATH环境变量
包(package)
index.js
package.json
命令行程序
使用NodeJS编写的东西,要么是一个包,要么是一个命令行程序,而前者最终也会用于开发后者。因此我们在部署代码时需要一些技巧,让用户觉得自己是在使用一个命令行程序。
工程目录
了解了以上知识后,现在我们可以来完整地规划一个工程目录了。以编写一个命令行程序为例,一般我们会同时提供命令行模式和API模式两种使用方式,并且我们会借助三方包来编写代码。除了代码外,一个完整的程序也应该有自己的文档和测试用例。因此,一个标准的工程目录都看起来像下边这样。
- /home/user/workspace/node-echo/ # 工程目录
- bin/ # 存放命令行相关代码
node-echo
+ doc/ # 存放文档
- lib/ # 存放API相关代码
echo.js
- node_modules/ # 存放三方包
+ argv/
+ tests/ # 存放测试用例
package.json # 元数据文件
README.md # 说明文件
其中部分文件内容如下:
/* bin/node-echo */
var argv = require('argv'),
echo = require('../lib/echo');
console.log(echo(argv.join(' ')));
/* lib/echo.js */
module.exports = function (message) {
return message;
};
/* package.json */
{
"name": "node-echo",
"main": "./lib/echo.js"
}
以上例子中分类存放了不同类型的文件,并通过node_moudles目录直接使用三方包名加载模块。此外,定义了package.json之后,node-echo目录也可被当作一个包来使用。
NPM建立了一个NodeJS生态圈,NodeJS开发者和用户可以在里边互通有无。
NPM对package.json的字段做了扩展,允许在其中申明三方包依赖。因此,上边例子中的package.json可以改写如下:
{
"name": "node-echo",
"main": "./lib/echo.js",
"dependencies": {
"argv": "0.0.2"
}
}
- 编写代码前先规划好目录结构,才能做到有条不紊。
- 稍大些的程序可以将代码拆分为多个模块管理,更大些的程序可以使用包来组织模块。
- 合理使用node_modules和NODE_PATH来解耦包的使用方式和物理路径。
- 使用NPM加入NodeJS生态圈互通有无。
- 想到了心仪的包名时请提前在NPM上抢注。
文件操作
让前端觉得如获神器的不是NodeJS能做网络编程,而是NodeJS能够操作文件。小至文件查找,大至代码编译,几乎没有一个前端工具不操作文件。换个角度讲,几乎也只需要一些数据处理逻辑,再加上一些文件操作,就能够编写出大多数前端工具。本章将介绍与之相关的NodeJS内置模块
大文件拷贝
上边的程序拷贝一些小文件没啥问题,但这种一次性把所有文件内容都读取到内存中后再一次性写入磁盘的方式不适合拷贝大文件,内存会爆仓。对于大文件,我们只能读一点写一点,直到完成拷贝。因此上边的程序需要改造如下。
var fs = require('fs');
function copy(src, dst) {
fs.createReadStream(src).pipe(fs.createWriteStream(dst));
}
function main(argv) {
copy(argv[0], argv[1]);
}
main(process.argv.slice(2));
以上程序使用fs.createReadStream创建了一个源文件的只读数据流,并使用fs.createWriteStream创建了一个目标文件的只写数据流,并且用pipe方法把两个数据流连接了起来。连接起来后发生的事情,说得抽象点的话,水顺着水管从一个桶流到了另一个桶。
Buffer(数据块)
Buffer将JS的数据处理能力从字符串扩展到了任意二进制数据。
Stream(数据流)
遍历目录
遍历目录是操作文件时的一个常见需求。比如写一个程序,需要找到并处理指定目录下的所有JS文件时,就需要遍历整个目录。
递归算法
遍历目录时一般使用递归算法,否则就难以编写出简洁的代码。递归算法与数学归纳法类似,通过不断缩小问题的规模来解决问题。以下示例说明了这种方法。
function factorial(n) {
if (n === 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
上边的函数用于计算N的阶乘(N!)。可以看到,当N大于1时,问题简化为计算N乘以N-1的阶乘。当N等于1时,问题达到最小规模,不需要再简化,因此直接返回1。
陷阱: 使用递归算法编写的代码虽然简洁,但由于每递归一次就产生一次函数调用,在需要优先考虑性能时,需要把递归算法转换为循环算法,以减少函数调用次数。
遍历算法
目录是一个树状结构,在遍历时一般使用深度优先+先序遍历算法。深度优先,意味着到达一个节点后,首先接着遍历子节点而不是邻居节点。先序遍历,意味着首次到达了某节点就算遍历完成,而不是最后一次返回某节点才算数。因此使用这种遍历方式时,下边这棵树的遍历顺序是A > B > D > E > C > F。
A
/
B C
/
D E F
异步遍历
如果读取目录或读取文件状态时使用的是异步API,目录遍历函数实现起来会有些复杂,但原理完全相同。travel函数的异步版本如下。
文本编码
使用NodeJS编写前端工具时,操作得最多的是文本文件,因此也就涉及到了文件编码的处理问题。
BOM的移除 GBK转UTF8 单字节编码
- 学好文件操作,编写各种程序都不怕。
- 如果不是很在意性能,fs模块的同步API能让生活更加美好。
- 需要对文件读写做到字节级别的精细控制时,请使用fs模块的文件底层操作API。
- 不要使用拼接字符串的方式来处理路径,使用path模块。
- 掌握好目录遍历和文件编码处理技巧,很实用。
网络操作
不了解网络编程的程序员不是好前端,而NodeJS恰好提供了一扇了解网络编程的窗口。通过NodeJS,除了可以编写一些服务端程序来协助前端开发和测试外,还能够学习一些HTTP协议与Socket协议的相关知识,这些知识在优化前端性能和排查前端故障时说不定能派上用场
HTTP
官方文档:http://nodejs.org/api/http.html
'http'模块提供两种使用方式:
- 作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应。
- 作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应
URL
官方文档:http://nodejs.org/api/url.html
处理HTTP请求时url模块使用率超高,因为该模块允许解析URL、生成URL,以及拼接URL。首先我们来看看一个完整的URL的各组成部分。
href
-----------------------------------------------------------------
host path
--------------- ----------------------------
http: // user:pass @ host.com : 8080 /p/a/t/h ?query=string #hash
----- --------- -------- ---- -------- ------------- -----
protocol auth hostname port pathname search hash
------------
query
- http和https模块支持服务端模式和客户端模式两种使用方式。
- request和response对象除了用于读写头数据外,都可以当作数据流来操作。
- url.parse方法加上request.url属性是处理HTTP请求时的固定搭配。
- 使用zlib模块可以减少使用HTTP协议时的数据传输量。
- 通过net模块的Socket服务器与客户端可对HTTP协议做底层操作。
进程管理
NodeJS可以感知和控制自身进程的运行环境和状态,也可以创建子进程并与其协同工作,这使得NodeJS可以把多个程序组合在一起共同完成某项工作,并在其中充当胶水和调度器的作用
- 使用process对象管理自身。
- 使用child_process模块创建和管理子进程。
异步编程
NodeJS最大的卖点——事件机制和异步IO,对开发者并不是透明的。本身是单线程运行的,不可能在一段代码还未结束运行时去运行别的代码,因此也就不存在异步执行的概念。JS本身是单线程的,无法异步执行,因此我们可以认为setTimeout这类JS规范之外的由运行环境提供的特殊函数做的事情是创建一个平行线程后立即返回,让JS主进程可以接着执行后续代码,并在收到平行进程的通知后再执行回调函数。除了setTimeout、setInterval这些常见的,这类函数还包括NodeJS提供的诸如fs.readFile之类的异步API。
我们仍然回到JS是单线程运行的这个事实上,这决定了JS在执行完一段代码之前无法执行包括回调函数在内的别的代码。也就是说,即使平行线程完成工作了,通知JS主线程执行回调函数了,回调函数也要等到JS主线程空闲时才能开始执行。以下就是这么一个例子。
function heavyCompute(n) {
var count = 0,
i, j;
for (i = n; i > 0; --i) {
for (j = n; j > 0; --j) {
count += 1;
}
}
}
var t = new Date();
setTimeout(function () {
console.log(new Date() - t);
}, 1000);
heavyCompute(50000);
-- Console ------------------------------
8520
可以看到,本来应该在1秒后被调用的回调函数因为JS主线程忙于运行其它代码,实际执行时间被大幅延迟。
异常处理
JS自身提供的异常捕获和处理机制——try..catch..,只能用于同步执行的代码。以下是一个例子。
function sync(fn) {
return fn();
}
try {
sync(null);
// Do something.
} catch (err) {
console.log('Error: %s', err.message);
}
-- Console ------------------------------
Error: object is not a function
域(Domain)
官方文档:http://nodejs.org/api/domain.html
NodeJS提供了domain模块,可以简化异步代码的异常处理。在介绍该模块之前,我们需要首先理解“域”的概念。简单的讲,一个域就是一个JS运行环境,在一个运行环境中,如果一个异常没有被捕获,将作为一个全局异常被抛出。NodeJS通过process对象提供了捕获全局异常的方法
因此,使用uncaughtException或domain捕获异常,代码执行路径里涉及到了C/C++部分的代码时,如果不能确定是否会导致内存泄漏等问题,最好在处理完异常后重启程序比较妥当。而使用try语句捕获异常时一般捕获到的都是JS本身的异常,不用担心上诉问题。
- 不掌握异步编程就不算学会NodeJS。
- 异步编程依托于回调来实现,而使用回调不一定就是异步编程。
- 异步编程下的函数间数据传递、数组遍历和异常处理与同步编程有很大差别。
- 使用domain模块简化异步代码的异常处理,并小心陷阱。
以下是对新诞生的NodeJSer的一些建议。
- 要熟悉官方API文档。并不是说要熟悉到能记住每个API的名称和用法,而是要熟悉NodeJS提供了哪些功能,一旦需要时知道查询API文档的哪块地方。
- 要先设计再实现。在开发一个程序前首先要有一个全局的设计,不一定要很周全,但要足够能写出一些代码。
- 要实现后再设计。在写了一些代码,有了一些具体的东西后,一定会发现一些之前忽略掉的细节。这时再反过来改进之前的设计,为第二轮迭代做准备。
- 要充分利用三方包。NodeJS有一个庞大的生态圈,在写代码之前先看看有没有现成的三方包能节省不少时间。
- 不要迷信三方包。任何事情做过头了就不好了,三方包也是一样。三方包是一个黑盒,每多使用一个三方包,就为程序增加了一份潜在风险。并且三方包很难恰好只提供程序需要的功能,每多使用一个三方包,就让程序更加臃肿一些。因此在决定使用某个三方包之前,最好三思而后行。