读书笔记《Node.JS开发指南》
0. 小工具 npm: node第三方库管理工具,用来获取第三方包、升级、删除或发布编写的包。 nvm: node的多版本管理工具。 supervisor:监视你对代码的改动,重启当前执行的js文件,一般用在网站、web开发以实时修改并观察结果便于调试。 1. 事件 events内置模块,可注册事件监听器、触发指定的事件信号和相应的事件处理。 事实上,很多操作,如读写文件、http服务器、数据库查询等均内部使用events模块的EventEmitter实现的。 2. 事件循环 Node执行过程中有个事件循环,还有一个事件队列,依次取出每个事件以及相应的事件回调函数。 Node的事件循环对外不可见,内部底层由libuv库实现支持。 3. 模块和包 模块以把功能拆分、封装,然后组合起来,供其他模块使用,基本上认为一个文件就是一个模块(js文件、JSON、C/C++实现的扩展等,且Node会以.js、.json、.node的次序补足扩展名,依次尝试分析加载)。 Node的模块和包的实现机制,大多数是参照CommonJS的标准实现的。 模块和包的区别,可以认为包是一个更大的模块功能的集合体(一个目录,在模块基础上的更高层次抽象,提供外部的一些相对固定的接口的函数库),主要用于发布、维护、更新等, 而对使用者来说,模块和包的区别是透明的,因此经常不作区分。如果package.json或main字段不存在,会尝试寻找index.js作为包的接口。 多次require同一个模块,不会多次重复加载该模块,第一次加载时已缓存在内存,后面加载同一个模块时直接从内存缓存读取,无论内置模块或文件模块均会缓存。 4. 调试 调试方式:console.log等或者观察-> FireBug、Chrome 开发者工具 -> V8支持的调试。 Node的命令行调试:node debug xxx.js,可开启调试,进入debug调试后,可通过其他的调试参数进行调试。 V8提供的远程调试:基于TCP协议。 远程:node --debug-brk xxx.js或 node --debug debug.js, 开启调试服务器,可指定端口(=port)参数。 本地:node debug ip:port ip为远程IP地址,port为远程端口,默认为5858,若指定了端口,则用远程指定的端口连接。 PS: 命令行的调试采用的在本地执行了一个远程和本地的两条命令实现的,即node --debug xxx.js + node debug 127.0.0.1:5858 = node debug xxx.js。 此外还可以通过某些IDE可方便的提供调试操作。 (新版本Node,建议使用node inspect替代) 5. 全局对象 node中全局对象:global对象,而浏览器一般为windows对象;同样的,除了全局对象外,其他的全局变量可认为均是全局对象的属性。 一般情况下:在最外层定义的变量(node中无法定义最外层变量)、全局对象的属性、隐式定义的变量(没有使用var声明,直接赋值的)均为全局变量。 注意:尽量不要使用全局变量,尤其是没有用var声明定义的变量,造成命名空间污染,可能引入BUG。 process:一个全局变量,global对象的属性。它用于描述当前Node.js进程状态的对象,提供了一个与操作系统的简单接口。 console: 控制台输出对象,console.log,console.error,console.warn,console.info,console.trace打印调用堆栈。 6. 常用的工具内置模块util util.inherits(Sub, Base),提供从Base类中继承原型链,而不是继承Base类(属性和方法)的工具函数(Sub仅继承Base的原型链)(建议使用此方式实现继承,避免多重继承或深层次的继承树)。 util.inspect(object,[showHidden],[depth],[colors]),是一个将任意对象转换为字符串的方法,通常用于调试和错误输出(不是通过调用对象的toString方法)。 此外还有很多如:util.isArray,util.isDate,util.isRegExp,util.isError等类型测试工具,util.format格式化等工具。 7. 最重要的内置模块之一events事件模块 该模块不仅用于用用户代码与Node下层事件循环交互,而且也是其他几乎所有模块的依赖实现的基础,以支持事件响应的核心模块均是events.EventEmitter的子类。 events.EventEmitter对象:事件发射器,封装了事件发射、若干事件监听功能的封装。 EventEmitter.on(event, listener)为指定事件注册一个监听器,接受一个字符串event和一个回调函数listener。 EventEmitter.emit(event, [arg1], [arg2], [...])发射event事件,传递若干可选参数到事件监听器的参数列表。 EventEmitter.once(event, listener)为指定事件注册一个单次监听器,即监听器最多只会触发一次,触发后立刻解除该监听器。 EventEmitter.removeListener(event, listener)移除指定事件的某个监听器,listener必须是该事件已经注册过的监听器。 EventEmitter.removeAllListeners([event])移除所有事件的所有监听器,如果指定event,则移除指定事件的所有监听器。 EventEmitter对象提供一特殊事件error,遇到异常时发射error事件,当error被发射时EventEmitter 规定如果没有响应的监听器, Node.js会把它当作异常,退出程序并打印调用栈。可为error事件设定监听器,以避免被抛出异常未被捕获而导致进程退出。 8. 文件系统内置模块fs fs模块提供文件操作的封装,文件读取、写入、更名、删除、目录遍历、链接等posix文件系统操作,且提供了同步和异步两个版本的操作接口。 如:fs.readFile(filename,[encoding],[callback(err,data)]),若指定了encoding编码,则data值为转化后的字符串,否则为Buffer对象实例的二进制数据。 当读取文件出现错误时,err将会是Error对象实例。 同步版本:fs.readFileSync(filename, [encoding]),读取到的文件内容会以函数返回值的形式返回。如果有错误发生,fs将会抛出异常, 应使用try和catch捕捉并处理异常(与同步I/O函数不同,node的很多异步操作接口是没有返回值的)。 其他的如:fs.open,fs.read,fs.close,fs.readFile,fs.writeFile,fs.fsync等。 9. HTTP服务器与客户端 http内置模块,比较底层,其封装了一个高效的HTTP服务器和一个简易的HTTP客户端。事实上http的目标不是仅仅作web服务器,还可以做其他的, 如可作为网站、社交应用、代理服务器等,如手动实现HTPP服务器则需要实现更多的操作,故而一般使用封装好了的框架,以提供高级接口和提高开发效率, 如egg,mean、Express、koa等。 http.Server是一个基于事件的HTTP服务器,它的核心由Node.js下层C++部分实现,而接口由 JavaScript 封装,兼顾了高性能与简易性。 http.request是一个HTTP客户端工具,用于向HTTP服务器发起请求。 同样的http.Server继承于EventEmitter的,基于事件,其中有几个比较重要的事件。
request事件:当客户端请求到来时,该事件被触发,提供两个参数req和res,分别是http.ServerRequest和http.ServerResponse的实例,表示请求和响应信息。 connection事件:当TCP连接建立时,该事件被触发,提供一个参数socket,为net.Socket的实例。connection事件的粒度要大于request事件, 因为客户端在Keep-Alive模式下可能会在同一个连接内发送多次请求。 close事件:当服务器关闭时,该事件被触发。注意不是在用户连接断开时。 此外还有checkContinue、upgrade、clientError事件。其中request事件可在CreateServer时指定,也可以在创建server后,调用on("request", listener)也可。 http.ServerRequest是HTTP请求的信息,是后端开发者最关注的内容;一般在request事件的回调函数中。 http.ServerRequest提供了几个事件用于控制请求体传输。如data、end、close事件,此外还有其他的属性可获取到更多的信息。 获取GET请求内容(URL中?后面的内容):Node.js的url模块中的parse函数提供解析url的功能,可从解析内容对象中的query属性来获取到Get请求数据。 获取POST请求内容:GET请求把所有的内容编码到访问路径中(URL),POST请求的内容全部都在请求体中,Node.js默认是不会解析请求体的, 当需要的时候,需手动来获取请求内容,如通过querystring.parse模块解析获取的数据内容以获取到真正的POST请求格式。 http.ServerResponse是返回给客户端的信息,决定了用户最终能看到的结果。它也是由http.Server的request事件回调作为第二个参数传递的。 此外其有三个函数: response.writeHead:向请求的客户端发送响应头,一次请求最多一次,若没有调用则会默认生成响应头给客户端。 response.write:向请求的客户端发送响应内容,若内容为字符串,则需指定编码方式,若为Buffer对象则不需要;在调用response.end前可多次调用。 response.end:结束响应,告知客户端所有发送已经完成。当所有要返回的内容发送完毕的时候,该函数必须被调用一次。 它接受两个可选参数,意义和response.write相同。如果不调用该函数,客户端将永远处于等待状态。 http模块提供了两个函数http.request和http.get,功能是作为客户端向HTTP服务器发起请求。 http.request(options, callback):options为关联数组,可指定多次参数内容,callback回调参数,参数内容为http.ClientResponse的实例。 此外该函数http.request返回一个http.ClientRequest的实例,可向服务端发送数据内容,write以及end函数可调用。 http.get(options, callback) http 模块还提供了一个更加简便的方法用于处理GET请求:http.get。 它是http.request的简化版,唯一的区别在于http.get自动将请求方法设为了GET请求,同时不需要手动调用req.end();同样的返回一个http.ClientRequest的实例。 http.ClientRequest实例对象,有response响应事件,也提供了write和end函数,用于向服务器发送请求体,通常用于POST、PUT等操作。 http.ClientResponse实例对象,也提供了三个事件data、end和close,分别在数据到达、传输结束和连接结束时触发,此外还提供了setEncoding、pause、resume方法, 分别为指定响应data事件的数据解码方式、暂停接收数据和发送事件、从暂停的状态中恢复。 10. 模块加载 node加载模块通过require加载,可加载内置核心模块和外部文件(文件夹)模块(依次以js/json/node文件),内置核心模块具有最高优先权,当外部模块和内置模块命名冲突时,其将加载核心模块。 文件模块的加载有两种方式,一种是按路径加载模块,一种是查找当前路径下node_modules文件夹下对应命名的模块。 如果require参数不以“ / ”、“ ./ ”或“ ../ ”开头,而该模块又不是核心模块,那么就要通过查找node_modules加载模块,此时如require('express')便可代替require('./node_modules/express')。 此时在当前目录下的 node_modules 目录中来查找是不是有这样一个模块。如果没有找到,则会在当前目录的上一层中的 node_modules 目录中继续查找,反复执行这一过程,直到遇到根目录为止。 之所以采取这个查找流程,主要是因为依赖模块可能依赖于其他被依赖的模块,便于查找公共依赖模块,以上溯查找到。 Node.js模块不会被重复加载,这是因为Node.js通过文件名缓存所有加载过的文件模块,所以以后再访问到时就不会重新加载了。 require(some_module) 时的加载顺序: 1. 若some_module是一个核心模块,直接加载,结束。 2. 若some_module以“ / ”、“ ./ ”或“ ../ ”开头,按路径加载some_module,结束。 3. 若当前目录为current_dir,按路径加载current_dir/node_modules/some_module。如果加载成功,结束,如果加载失败,令current_dir为其父目录,回溯,重复这一过程, 直到遇到根目录,抛出异常,结束。 对于不同的文件扩展名,加载处理方式不同,js文件则是fs模块加载同步读取文件后编译执行,node文件(C/C++扩展)则用process.dlopen等加载扩展, json文件则是fs模块加载同步读取文件后用JSON.parse解析返回结果,其余文件扩展名的则按照js文件的处理方式一样。 JSON文件在用作项目的配置文件时比较有用,通过require引入即可而不需要单独用fs模块来读取,此外还有模块缓存的方便。 内置核心模块分为:C/C++实现的模块位于Node项目源码的src目录,JavaScript实现的模块则放置于lib目录下。 11. 控制流 Node的异步操作的事件式编程容易将程序的逻辑变得混乱。 异步机制是由事件和回调函数实现的,其容易造成回调循环陷阱(主要是js的作用域对象使用方式错误),以及深层的回调函数嵌套,难以弄清回调函数之间的关系。 有一些手段来降低回调的复杂度、耦合度以实现可读的代码,如:async控制流解耦模块、streamlinejs和jscex以同步的方式编写代码,而在执行时异步实现、 或者eventproxy,其实现了对事件发射器的深度封装,采用一种完全基于事件松散耦合的方式来实现控制流的梳理。不过这些方法也带来了一些复杂度。 12. Node不适合的场景 1. CPU计算密集型的任务。Node单线程基于事件,CPU占用过多则会影响其他请求响应,对于计算密集型任务可交给其他服务器的其他进程或专门进程处理,然后返还给服务器 并在适时发给客户端。 2. 单用户多任务型应用。Node单线程,需要利用多核资源时只能通过多进程的方式通信处理任务,而多进程相互协作时比较麻烦。 3. 逻辑十分复杂的事务。Node基于事件、异步回调机制,其实现方式并不是线性的,对于复杂的逻辑处理会比较困难,难以实现和维护, Node.js更善于处理那些逻辑简单但访问频繁的任务。 4. Node.js不支持完整的Unicode,很多字符无法用string表示。这不是Node.js的缺陷,而是JavaScript标准的问题,在使用中很多时候把所有的字符当作二进制的Buffer 数据来处理。对于中文支持,则需要:1. 保证JS文件是以UTF-8格式保存。2. JS文件中的writeHead方法中加入内容类型"charset=utf-8"编码。