缘起

Node.js最初,是Ryan Dahl在09年写的一个基于V8的Web服务器,同时提供一套库。此人是C/C++工程师,本来是要写一个高性能Web服务器的,而且此人早年的工作经历也是搞Web服务器,故而积累了很多丰富的经验,其中,最重要的两个点是:事件驱动、非阻塞IO。而这也是日后Node.js成为最流行的Web应用基础平台后,其最重要的特性。

释名

为什么要叫Node?
因为作者本身就是要面向web。而Web就是由一个一个节点Node构成的。

用武

因为非阻塞IO,所以适合IO密集型,不适合CPU密集型。

架构

关键词:模块、多线程、libuv、事件队列、跨平台
(回头详细分析)

Node.js赋能API

模块系统

实现了CommonJS中定义的模块化规范,简单来讲:

  • 引用模块用require函数;
  • 导出模块用exports;
  • exports是当前文件上下文的一个对象,用来对应挂上某个文件的导出内容,所以可以exports.xxx = ‘xxx’。这里还需要说明的一点是,每一个文件都存在一个module对象,用来指代文件自身。而上面说的exports本质是module对象的一个属性。
  • 输出的东西是值的拷贝,所以,当这个值被输出之后,就算模块内的值发生了变化,被输出的值也不会更新;
  • 多次引入同一个模块,第一引入之后会被缓存,后面的引入都会先在缓存中读取;
  • 同步按顺序加载多个模块;

    EventEmitter

    events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是:事件触发与事件监听器功能的封装。 ```javascript

const events = require(‘events’); const { EventEmitter } = events; var emitter = new EventEmitter();

// 监听事件 emitter.on(‘someEvent’, function(arg1, arg2) { console.log(‘listener1’, arg1, arg2); }); emitter.on(‘someEvent’, function(arg1, arg2) { console.log(‘listener2’, arg1, arg2); });

// 分发事件 emitter.emit(‘someEvent’, ‘arg1 参数’, ‘arg2 参数’);

  1. 通常我们使用的时候都是用我们自己的类去扩展eventEmitter的:
  2. ```javascript
  3. class MyCSG extends EventEmitter {
  4. haveNothingToDo(){
  5. this.emit('call_chicken')
  6. }
  7. }
  8. const cjj = new MyCSG();
  9. cjj.on('call_chicken', () => {
  10. // ....
  11. })
  12. //
  13. cjj.haveNothingToDo();

Buffer

Buffer类的定义是为了处理二进制数据类型,用来创建一个专门存放二进制数据的缓存区。
当需要在 Node.js 中处理I/O操作中移动的数据时,就有可能使用 Buffer 库。原始数据存储在 Buffer 类的实例中。一个 Buffer 类似于一个整数数组,但它是 V8 堆内存之外的一块原始内存。
(值得注意的是在处理二进制的Buffer在ES6中也有规范:ArrayBufferSharedArrayBuffer,这里面牵扯的就多了,后面仔细研究)

Buffer构建

常用这几种方式

  1. // 1. 二进制数组
  2. const buf = new Buffer([0x48, 0x55])
  3. // 2. 通过字符串
  4. const buf2 = new Buffer('csg')
  5. // 3. 最常用
  6. const buf3 = Buffer.from('csg');

new Buffer这种方法不是推荐的用法,这是老方法,被标记为过时,说是可能有安全问题。Buffer.from是当前推荐的用法。

toString和编码

需要将buffer中的数据转换成字符串,需要:

  1. buf.toString(编码格式,开始位置,结束位置);

buffer支持的格式有:

  • ascii - 仅支持 7 位 ASCII 数据。如果设置去掉高位的话,这种编码是非常快的。
  • utf8 - 多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8 。
  • utf16le - 2 或 4 个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。
  • ucs2 - utf16le 的别名。
  • base64 - Base64 编码。
  • latin1 - 一种把 Buffer 编码成一字节编码的字符串的方式。
  • binary - latin1 的别名。
  • hex - 将每个字节编码为两个十六进制字符。

    读、写、拼接

    读写目前可能用到的机会不大,真用到了参考下API
    写:buf.write(string[, offset[, length]][, encoding]);
    读:buf.toString([encoding[, start[, end]]])
    这里说一个拼接的问题,这个问题在《深入浅出》里面讨论了,是toString的一个隐式转换的问题:
  1. // 读取文件
  2. const rs = require('fs').createReadStream('a.txt', { highWaterMark: 10 })
  3. let data = ''
  4. rs.on('data', chunk => {
  5. data += chunk;
  6. });
  7. rs.on('end', () => {
  8. console.log(data)
  9. })

上述代码可能看上去没啥问题,但是有乱码的风险。为什么呢?因为highWaterMark,表示每次读取的chunk的大小,那么我们每次读取都会data += chunk,这里面包含了隐式转换:data += chunk.toString(); 这就是风险的所在,比如,当highWaterMark: 10,(10个字节)我们读取汉字文本,每一个汉字是3个字节,那么这就意味着,第一次只能正确读出3个汉字,而第四个汉字会被截断,那不就成乱码了吗?
解决的办法就是先push:

  1. const rs = require('fs').createReadStream('a.txt', { highWaterMark: 10 })
  2. let data = [];
  3. rs.on('data', chunk => {
  4. data.push(chunk);
  5. });
  6. rs.on('end', () => {
  7. const tmpBuf = Buffer.cancat(data);
  8. console.log(tmpBuf.toString());
  9. })

Buffer.concat(list[, totalLength]), 这个api是用于缓冲区合并。
list - 用于合并的 Buffer 对象数组列表。
totalLength - 指定合并后Buffer对象的总长度。

文件系统

require(“fs”)导入内置的文件系统。fs模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()。
异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。一般不用同步方法。
文件系统的API有很多,这里只说几个最可能用的吧:

  • readFile:异步读文件的内容,但是会一次性把文件内容放入内存中,这就是说当文件比较大的时候,肯定readFile是不合适的,这时候最好用流来处理;
  • writeFile:writeFile 直接打开文件默认是 w 模式,所以如果文件存在,该方法写入的内容会覆盖旧的文件内容;

此外还有非常多的API

Stream

Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)。(看到这里不禁想说一句,JDK中的IO流的是装饰器模式的典型设计案例,设计了非常庞大的一系列类来处理各种各样的流)。
Node.js,Stream 有四种流类型:

  • Readable - 可读操作。
  • Writable - 可写操作。
  • Duplex - 可读可写操作.
  • Transform - 操作被写入数据,然后读出结果。

所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:

  • data - 当有数据可读时触发。
  • end - 没有更多的数据可读时触发。
  • error - 在接收和写入过程中发生错误时触发。
  • finish - 所有数据已被写入到底层系统时触发。

    基本读写

    读: ```javascript var fs = require(“fs”); var data = ‘’;

// 创建可读流 var readerStream = fs.createReadStream(‘input.txt’);

// 设置编码为 utf8。 readerStream.setEncoding(‘UTF8’);

// 处理流事件 —> data, end, and error readerStream.on(‘data’, function(chunk) { data += chunk; });

readerStream.on(‘end’,function(){ console.log(data); });

readerStream.on(‘error’, function(err){ console.log(err.stack); });

  1. 写:
  2. ```javascript
  3. var fs = require("fs");
  4. var data = 'CSG GOGOGO~';
  5. // 创建一个可以写入的流,写入到文件 output.txt 中
  6. var writerStream = fs.createWriteStream('output.txt');
  7. // 使用 utf8 编码写入数据
  8. writerStream.write(data, 'UTF8');
  9. // 标记文件末尾
  10. writerStream.end();
  11. // 处理流事件 --> finish、error
  12. writerStream.on('finish', function() {
  13. console.log("写入完成。");
  14. });
  15. writerStream.on('error', function(err){
  16. console.log(err.stack);
  17. });

pipe管道

所谓管道就相当打通读写之间,一条管道,架设在读写之间, 这样可读数据就自然而然的流入可写数据:

  1. var fs = require("fs");
  2. // 创建一个可读流
  3. var readerStream = fs.createReadStream('input.txt');
  4. // 创建一个可写流
  5. var writerStream = fs.createWriteStream('output.txt');
  6. // 管道读写操作
  7. // 读取 input.txt 文件内容,并将内容写入到 output.txt 文件中
  8. readerStream.pipe(writerStream);
  9. console.log("done!");

稍微复杂点的例子,简单的静态服务器:

  1. const stream = require('stream');
  2. const http = require('http');
  3. const fs = require('fs');
  4. const server = http.createServer((req, rsp) => {
  5. if(req.url === '/') {
  6. const fileList = fs.readdirSync('./')
  7. rsp.writeHead(200, {
  8. contentType: 'text/plain',
  9. });
  10. res.end(fileList.toString());
  11. } else {
  12. const path = `.${req.url}`;
  13. fs.createReadStream(path)
  14. .pipe(rsp);
  15. }
  16. });
  17. server.listen(12580);

全局对象

JavaScript 中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可以在程序的任何地方访问,即全局变量。
在浏览器中,通常 window 是全局对象, 而 Node.js 中的全局对象是 global,所有全局变量(除了 global 本身以外)都是 global 对象的属性。
不过这里需要再说下ES2020中的globalThis,Node.js 12+版本也已经实现了。MDN:globalThis

The globalThis property provides a standard way of accessing the global this value (and hence the global object itself) across environments. Unlike similar properties such as window and self, it’s guaranteed to work in window and non-window contexts. In this way, you can access the global object in a consistent manner without having to know which environment the code is being run in. To help you remember the name, just remember that in global scope the this value is globalThis. (以前比如说要判断浏览器环境:typeof self == “object” && self.self === self && self)

filename / dirname

  • __filename 表示当前正在执行的脚本的文件名。
  • __dirname 表示当前执行脚本所在的目录。

    定时器、console

    setTimeout、setInterval、console…
    不赘述了。

    process

    process 是一个全局变量,即 global 对象的属性, 用于描述当前Node.js 进程状态的对象,提供了一个与操作系统的简单接口

  • process可以监听事件:

    1. process.on('exit', function(code) {
    2. // 以下代码永远不会执行
    3. setTimeout(function() {
    4. console.log("该代码不会执行");
    5. }, 0);
    6. console.log('退出码为:', code);
    7. });
    8. console.log("done!");
  • 可以从操作系统中活的进程信息: ```javascript // 通过参数读取 process.argv.forEach(function(val, index, array) { console.log(index + ‘: ‘ + val); }); // pid console.log(process.pid); // 获取执行路径 console.log(process.execPath);

// 平台信息 console.log(process.platform);

  1. <a name="16df2816"></a>
  2. ### Web服务器
  3. 之前说过,建立高性能的web server是Node.js的初衷。
  4. <a name="8aded850"></a>
  5. #### http server
  6. 通过http模块的createServer,我们可以构造一个简单的http服务器,非常简单,直接从菜鸟教程中拉过来了例子:
  7. ```javascript
  8. var http = require('http');
  9. var fs = require('fs');
  10. var url = require('url');
  11. // 创建服务器
  12. http.createServer( function (request, response) {
  13. // 解析请求,包括文件名
  14. var pathname = url.parse(request.url).pathname;
  15. // 输出请求的文件名
  16. console.log("Request for " + pathname + " received.");
  17. // 从文件系统中读取请求的文件内容
  18. fs.readFile(pathname.substr(1), function (err, data) {
  19. if (err) {
  20. console.log(err);
  21. // HTTP 状态码: 404 : NOT FOUND
  22. // Content Type: text/html
  23. response.writeHead(404, {'Content-Type': 'text/html'});
  24. }else{
  25. // HTTP 状态码: 200 : OK
  26. // Content Type: text/html
  27. response.writeHead(200, {'Content-Type': 'text/html'});
  28. // 响应文件内容
  29. response.write(data.toString());
  30. }
  31. // 发送响应数据
  32. response.end();
  33. });
  34. }).listen(8080);

虽然注释写的很详尽,但这里面有一些点需要注意下:

  • Node用stream来处理Http的请求体;
  • response对象也是一个writableStream实例,所以直接调用write方法写入,或者在上面的stream例子中,我们用pipe将文件流写入response;
  • response.end表示请求结束,而且可以在end方法中写入最后的数据,这些数据会在浏览器页面中显示出来,通常用了write就不需要额外写,或者直接不用write,在end中写也可以。

    https

    https也是node中内置的模块。
    我们使用openSSL先创建好公钥私钥之后,我们可以设置https.createServer(option, function)中的option,就可以完成对https服务的创建。
    https模块参考文档

    tcp

    net模块用来处理TCP协议,但可能不是特别重点。
    Socket:Socket,是编程接口,在接口上定义了一些基础方法,比如:listen、accept、write等。如果一种语言实现了socket接口,那么就能通过这些方法来解析使用TCP协议传输的数据流。(Socket最初并不是针对TCP设计的,虽然现在说起socket好像特质TCP协议的编程接口。) ```javascript const net = require(‘net’); const server = net.creatrServer(c => { console.log(‘connected…’); c.on(‘end’, () => { console.log(‘disconnected…’); }) c.write(‘hey, jude~’); c.pipe(c) });

server.linsten(8888, () => { console.log(‘server listen’) })

  1. TCP的服务器具你用浏览器自然无法通信,这是基于HTTP的,但是可以在shell中用telnet
  2. ```shell
  3. $ telnet localhost 8888

Yo~See What Hanppend~

websocket

WebSocket是弄啥的这里就不多说了。
如果Http的请求头中有Upgrade:websocket,以及connection:Upgrade,这就表示客户端希望用websocket进行链接。
WebSocket在Node.js中没有原生API或者模块,不过常见的第三方模块比如:ws、socket.IO都比较常用:
浏览器端:

  1. <script>
  2. const ws = new WebSocket('ws://localhost:9090');
  3. ws.onopen = () => {
  4. ws.send('hi');
  5. }
  6. ws.onmessage = msg => {
  7. console.log('recieve:', msg);
  8. }
  9. </script>

Server端

  1. const WSServer = require('ws').Server;
  2. const wss = new WSServer({ port: 9090 });
  3. wss.on('connection', ws => {
  4. ws.on('message', message => {
  5. console.log('server recieved:', message)
  6. });
  7. ws.send('Hi there~')
  8. })

多进程

这里需要细致一点,见这篇文章