Node.js是一个运行javascript的环境,NodeJS的作者创造NodeJS的目的是为了实现高性能Web服务器。
Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js’ package ecosystem, npm, is the largest ecosystem of open source libraries in the world.
—— Node.js官网
这篇文章主要摘录一些个人认为的重点,希望加深对Node.js的熟悉。
一、Node.js基础
exports 对象
exports对象是当前模块的导出对象,用于导出模块公有方法和属性。别的模块通过require函数使用当前模块时得到的就是当前模块的exports对象。以下例子中导出了一个公有方法。
1 | exports.hello = function () { |
exports默认是{},是module.exports的一个引用。
模块初始化
一个模块中的JS代码仅在模块第一次被使用时执行一次,并在执行过程中初始化模块的导出对象。之后,缓存起来的导出对象被重复利用。
主模块
通过命令行参数传递给NodeJS以启动程序的模块被称为主模块。主模块负责调度组成整个程序的其它模块完成工作。例如通过以下命令启动程序时,main.js就是主模块。
1 | $ node main.js |
说明多次
require不会导致执行多次模块内的程序内容。
- NodeJS是一个JS脚本解析器,任何操作系统下安装NodeJS本质上做的事情都是把NodeJS执行程序复制到一个目录,然后保证这个目录在系统PATH环境变量下,以便终端下可以使用
node命令。 - 终端下直接输入
node命令可进入命令交互模式,很适合用来测试一些JS代码片段,比如正则表达式。 - NodeJS使用CMD模块系统,主模块作为程序入口点,所有模块在执行过程中只初始化一次。
- 除非JS模块不能满足需求,否则不要轻易使用二进制模块,否则你的用户会叫苦连天。
Node.js使用CMD规范,Sea.js也是遵循CMD规范。
二、代码的组织和部署
模块路径解析规则
- 内置模块
- node_modules目录
- NODE_PATH环境变量
包(package)
当模块的文件名是index.js,加载模块时可以使用模块所在目录的路径代替模块文件路径,因此接着上例,以下两条语句等价。
1 | var cat = require('/home/user/lib/cat'); |
这样处理后,就只需要把包目录路径传递给require函数,感觉上整个目录被当作单个模块使用,更有整体感。
所以就是
require一个文件夹就等同于require里面的index.js模块,这个很方便。
package.json
1 | { |
name是包名称,main是入口模块(主模块),dependencies是依赖模块。
版本号
语义版本号分为X.Y.Z三位,分别代表主版本号、次版本号和补丁版本号。当代码变更时,版本号按以下原则更新。
1 | 如果只是修复bug,需要更新Z位。 |
NPM
- 使用
npm help可查看某条命令的详细帮助,例如npm help install。 - 在
package.json所在目录下使用npm install . -g可先在本地安装当前命令行程序,可用于发布前的本地测试。 - 使用
npm update可以把当前目录下node_modules子目录里边的对应模块更新至最新版本。 - 使用
npm update -g可以把全局安装的对应命令行程序更新至最新版。 - 使用
npm cache clear可以清空NPM本地缓存,用于对付使用相同版本号发布新版本代码的人。
使用NodeJS编写代码前需要做的准备工作
- 编写代码前先规划好目录结构,才能做到有条不紊。
- 稍大些的程序可以将代码拆分为多个模块管理,更大些的程序可以使用包来组织模块。
- 合理使用
node_modules和NODE_PATH来解耦包的使用方式和物理路径。
三、文件操作
小文件拷贝
代码片段
1 | fs.writeFileSync(dst, fs.readFileSync(src)); // dst:目标路径,src:源路径 |
大文件拷贝
代码片段
1 | fs.createReadStream(src).pipe(fs.createWriteStream(dst)); |
以上程序使用fs.createReadStream创建了一个源文件的只读数据流,并使用fs.createWriteStream创建了一个目标文件的只写数据流,并且用pipe方法把两个数据流连接了起来。连接起来后发生的事情,说得抽象点的话,水顺着水管从一个桶流到了另一个桶。
Buffer(数据块)
Stream(数据流)
File System(文件系统)
fs模块的所有异步API都有对应的同步版本。
1 | // 以文件内容读取为例 |
Path(路径)
遍历目录
目录是一个树状结构,在遍历时一般使用深度优先+先序遍历算法。使用这种遍历方式时,下边这棵树的遍历顺序是A > B > D > E > C > F。
1 | A |
同步遍历、异步遍历
思路:某个目录作为遍历的起点。遇到一个子目录时,就先接着遍历子目录。遇到一个文件时,就把文件的绝对路径传给回调函数。回调函数拿到文件路径后,就可以做各种判断和处理。
遍历的思路值得去好好琢磨。
文件编码
常用的文本编码有UTF8和GBK,UTF8文件还可能带有BOM。在读取不同编码的文本文件时,需要将文件内容转换为JS使用的UTF8编码字符串后才能正常处理。
BOM移除
在不同的Unicode编码下,BOM字符对应的二进制字节如下:
1 | Bytes Encoding |
移除UTF8 BOM
1 | function readText(pathname) { |
GBK转UTF8(借助iconv-lite)
1 | var iconv = require('iconv-lite'); |
单字节编码
1 | function replace(pathname) { |
小结
- 学好文件操作
- 掌握目录遍历和文件编码
四、网络操作
HTTP
http模块提供两种使用方式:
- 作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应。
- 作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应。
1 | // 创建HTTP服务器 |
request和response对象除了用于读写头数据外,都可以当作数据流来操作。
HTTPS
URL
URL的各组成部分
1 | href |
常用方法
parseformatresolve等
Query String
querystring.parse('foo=bar&baz=qux')
querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' })
Zlib
zlib模块提供了数据压缩和解压的功能。
Net
net模块可用于创建Socket服务器或Socket客户端。
通过
net模块的Socket服务器与客户端可对HTTP协议做底层操作。
五、进程管理
Process
process不是内置模块,而是一个全局对象。
应用:获取命令行参数、退出程序、控制输入输出、进程间通讯
Child Process
使用child_process模块可以创建和控制子进程。该模块提供的API中最核心的是.spawn,其余API都是针对特定使用场景对它的进一步封装,算是一种语法糖。
Cluster
cluster模块是对child_process模块的进一步封装,专用于解决单进程NodeJS Web服务器无法充分利用多核CPU的问题。使用该模块可以简化多进程服务器程序的开发,让每个核上运行一个工作进程,并统一通过主进程监听端口和分发请求。
六、异步编程
我们仍然回到JS是单线程运行的这个事实上,这决定了JS在执行完一段代码之前无法执行包括回调函数在内的别的代码。也就是说,即使平行线程完成工作了,通知JS主线程执行回调函数了,回调函数也要等到JS主线程空闲时才能开始执行。以下就是这么一个例子。
1 | function heavyCompute(n) { |
可以看到,本来应该在1秒后被调用的回调函数因为JS主线程忙于运行其它代码,实际执行时间被大幅延迟。
代码设计模式
遍历数组
同步
1 | var len = arr.length, |
异步(数组成员串行处理)
1 | (function next(i, len, callback) { |
异步(数组成员并行处理)
1 | (function (i, len, count, callback) { |
异常处理
1 | function async(fn, callback) { |
在NodeJS中,几乎所有异步API都按照以上方式设计,回调函数中第一个参数都是err。因此我们在编写自己的异步函数时,也可以按照这种方式来处理异常,与NodeJS的设计风格保持一致。
域(Domain)
Node.js通过process对象提供了捕获全局异常的方法
1 | process.on('uncaughtException', function (err) { |
使用domain模块创建一个子域(JS子运行环境)
1 | function async(request, callback) { |
子域可以独立处理抛出的异常,通过
.run方法进入子域中运行代码
按照官方文档的说法,发生异常后的程序处于一个不确定的运行状态,如果不立即退出的话,程序可能会发生严重内存泄漏,也可能表现得很奇怪。
小结
- 不掌握异步编程就不算学会NodeJS。
- 异步编程依托于回调来实现,而使用回调不一定就是异步编程。
- 异步编程下的函数间数据传递、数组遍历和异常处理与同步编程有很大差别。
- 使用
domain模块简化异步代码的异常处理,并小心陷阱