Node企业级应用实战 笔记

685 阅读13分钟

一 概述

1.1 单线程

  • nodeJS只用了一个主线程来接收请求,但它接收请求后并没有直接做处理,而是放到了事件队列中,然后又去接收其他请求了空闲时,再通过事件循环来处理这些事件,从而实现了异步效果。
  • 可简单理解为,NodeJS本身是一个多线程平台,它对JS层面的任务处理是单线程的。
  • 对于I/O任务,NodeJS把任务交给线程池来异步处理,因此NodeJS适于处理I/O密集型任务
  • 对于CPU密集型任务,NodeJS会亲自处理,一个一个地计算,导致后面的任务被阻塞。因此NodeJS不适合CPU密集型任务

1.2 支持微服务

  • 首先:其本身提供了跨平台的能力,可运行在自己的进程中
    其次:其易于构建web服务,并支持HTTP的通信
    最后,其支持从前端到后端再到数据库全栈开发能力

二 模块化

2.1 理解模块化机制

  • 在NodeJS中有两种定义模块的格式
    • commonJS 规范
    • ES6模块
  • NodeJS中,模块父两类
    • NodeJS自身提供的模块,称为核心模块
    • 用户编写的模块,称为文件模块
      • 核心模块在源码的编译过程中,编译进了二进制执行文件。在NodeJS启动时自动加载进内存,所以加载速度最快
        • 文件模块在运行时动态加载,需完整的路径分析、文件定位、编译执行过程、加载速度慢(Node引入了缓存机制,2次加载时会命中缓存 )
  • 核心模块
    • buffer 用于二进制数据的处理
    • events 用于事件处理
    • fs 用于与文件系统交互
    • http 用于提供HTTP服务器和客户端
    • net 提供异步网络API,用于创建基于流的TCP或IPC服务器和客户端
    • path 用于处理文件和目录的路径
    • timers 提供定时器功能
    • tls 提供了基于 OpenSSL构建的传输层安全性(TLS)和安全套接字层(SSL) 协议的实现
    • dgram 提供了UDP数据报套接字的实现

三 测试

3.1 NodeJS内嵌了测试套件 - assert模块

  • 严格模式
    const assert = require(‘assert’).strict
    遗留模块
    const assert = require(‘assert’)

3.2 三方测试工具

  • Nodeunit
  • Mocha
  • Vows

四 buffer

4.1 了解TypedArray

  • ES6定义了一个TypedArray,期望提供一种更加高效的机制来访问和处理二进制数据。基于TypedArray,Buffer类将以更优化和适合NodeJS的方式来实现Unit8Array API
  • Buffer类是基于Uint8Array的,因此其值为0~255的整数数组

4.2 创建缓冲区

  •   Buffer.from()   //  返回一个新的 Buffer  
      Buffer.allocUnsafe(size)   // 返回指定大小的新初始化buffer  
      Buffer.alloc(size) // 比上面慢,但保证新创建的Buffer实例不会有敏感旧实据  
    
  •   // 以UTF-8编码初始化缓冲区数据  
      const buf = Buffer.from('Hello World!你好,世界!', 'utf8');  
        
      // 转为十六进制字符  
      console.log(buf.toString('hex'));  
      // 输出:48656c6c6f20576f726c6421e4bda0e5a5bdefbc8ce4b896e7958cefbc81  
        
      // 转为Base64编码  
      console.log(buf.toString('base64'));  
      // 输出:SGVsbG8gV29ybGQh5L2g5aW977yM5LiW55WM77yB  
    
  • 切分

    buf.slice(srart, end)  
    
  • 连接

    Buffer.concat([list], totalLength)  
    
    • list 待连接的Buffer实例
    • totalLength 连接完成后list中的Buffer实例长度
  • 比较

    Buffer.compare(buf1, buf2)  
    

    通常用于对Buffer实例的数组进行排序

  • 解码

    const buf = Buffer.from([-1, 5]);  
      
    console.log(buf.readInt8(0));  
    // 输出: -1  
      
    console.log(buf.readInt8(1));  
    // 输出: 5```  
    
    
  • 编码

    const buf = Buffer.allocUnsafe(2);  
    buf.writeInt8(2, 0);  
    buf.writeInt8(4, 1);  
      
    console.log(buf);  
      
    // 输出: <Buffer 02 04>  
    

五 事件处理

5.1 理解事件和回调

  • 理解事件和回调
    • 在NodeJS的事件机制中主要有三类角色:事件Event、事件发射器Event Emitter、事件监听器Event Listener。

      所有能触发事件的对象在Node中都是EventEmitter类的实例。这些对象有一个eventEmitter.on()函数,用于将一个或多个函数绑定到命名事件上。

      当EventEmitter 对象触发一个事件时,所有绑定在该事件上的函数都会被同步地调用

    •   const EventEmitter = require('events');  
        class MyEmitter extends EventEmitter {}  
        const myEmitter = new MyEmitter();  
          
        myEmitter.on('event', function(a, b) {  
          console.log(a, b, this, this === myEmitter);  
        });  
          
        myEmitter.emit('event', 'a', 'b');  
      
      •     // 输出:  
            // a b MyEmitter {  
            //   _events: [Object: null prototype] { event: [Function] },  
            //   _eventsCount: 1,  
            //   _maxListeners: undefined  
            // } true  
        
  • 事件循环
    • Node是单进程单线程应用程序,但是因为V8提供了异步回调接口,可处理大量并发请求,所以性能非常高。
      • node 几乎每个API都支持回调函数
        • node 基本所有的事件机制都是用观察者模式实现
        • node 单线程类似进入一个while(ture)的事件循环,直到没有事件观察者退出
  • 事件驱动

5.2 事件发射器

  • 获取EventEmitter类的方式

    • const EventEmitter = require(‘evnets’);
  • lambda

    •   const EventEmitter = require('events');  
        class MyEmitter extends EventEmitter { }  
        const myEmitter = new MyEmitter();  
          
        myEmitter.on('event', (a, b) => {  
            console.log(a, b, this);  
            // 输出: a b {}  
        });  
          
        myEmitter.emit('event', 'a', 'b');  
      
  • 异步与同步
    EventEmitter会执照监听器注册的顺序同步地调用所有监听器。所以必须确保事件的排序正确,且避免竟态条件。可使用setImmediate() 或 process.nextTick()切换到异步模式

    •   const EventEmitter = require('events');  
        class MyEmitter extends EventEmitter { }  
        const myEmitter = new MyEmitter();  
          
        myEmitter.on('event', (a, b) => {  
            setImmediate(() => {  
                console.log('异步进行');  
            });  
        });  
        myEmitter.emit('event', 'a', 'b');  
      
  • 仅处理事件一次

    •   const EventEmitter = require('events');  
        class MyEmitter extends EventEmitter { }  
        const myEmitter = new MyEmitter();  
        let m = 0;  
          
        myEmitter.once('event', () => {  
            console.log(++m);  
        });  
          
        myEmitter.emit('event');  
        // 打印: 1  
          
        myEmitter.emit('event');  
        // 不触发  
      

5.3 事件类型

  • 事件类型可定义为任意的字符串,但node有内置的事件类型
  • newListener
    • 当EventEmitter类实例新增监听器时
  • removeListener
    • 当移除已存在的监听器时
  • error
    • 当EventEmitter出错时
    • 如果错误没有做进一步的处理,极易导致nodeJS进程崩溃。
      • 为error事件注册监听器
      •   const EventEmitter = require('events');  
          class MyEmitter extends EventEmitter { }  
          const myEmitter = new MyEmitter();  
            
          // 为error事件注册监听器  
          myEmitter.on('error', (err) => {  
              console.error('错误信息');  
          });  
            
            
          // 模拟触发error事件  
          myEmitter.emit('error', new Error('错误信息'));  
          // 抛出错误  
        

5.4 事件的操作

  • 1 设置最大监听器
    • 默认情况下,每个事件可注册最多10个监听器。可使用emitter.setMaxListeners(n)方法改变单个EventEmitter实例的限制,也可使用EventEmitter.defaultMaxListeners属性来改变所有EventEmitter实例的默认值
  • 2 获取已注册的事件名称
    • 可通过emitter.eventNames()方法来返回已注册监听器的事件名数组
    •   const EventEmitter = require('events');  
        class MyEmitter extends EventEmitter { }  
        const myEmitter = new MyEmitter();  
          
        myEmitter.on('foo', () => {});  
        myEmitter.on('bar', () => {});  
          
        const sym = Symbol('symbol');  
        myEmitter.on(sym, () => {});  
          
        console.log(myEmitter.eventNames());  
        // 输出: [ 'foo', 'bar', Symbol(symbol) ]  
      
  • 3 获取监听器数组的副本
    • emitterllisteners(eventName)
    •   const EventEmitter = require('events');  
        class MyEmitter extends EventEmitter { }  
        const myEmitter = new MyEmitter();  
          
        myEmitter.on('foo', () => {});  
          
        console.log(myEmitter.listeners('foo'));  
        // 输出: [ [Function] ]  
      
  • 4 将事件监听器添加到监听器数组的开头
    • emitter.on(eventName, listener)方法,监听器会被添加到末尾
      emitter.prependListener() 将事件监听器添加到开头

    •   const EventEmitter = require('events');  
        class MyEmitter extends EventEmitter { }  
        const myEmitter = new MyEmitter();  
          
        myEmitter.on('foo', () => console.log('a'));  
          
        myEmitter.prependListener('foo', () => console.log('b'));  
        myEmitter.emit('foo');  
          
        // 输出:  
        //   b  
        //   a  
      
  • 5 移除监听器
    •   const EventEmitter = require('events');  
        class MyEmitter extends EventEmitter { }  
        const myEmitter = new MyEmitter();  
          
        let listener1 = function () {  
            console.log('监听器listener1');  
        }  
          
        // 获取监听器的各数  
        let getListenerCount = function () {  
            let count = myEmitter.listenerCount('foo');  
            console.log("监听器监听各数为:" + count);  
        }  
          
        myEmitter.on('foo', listener1);  
        getListenerCount();  
        myEmitter.emit('foo');  
          
        // 移除监听器  
        myEmitter.removeListener('foo', listener1);  
        getListenerCount();  
          
        //////  
          
        // 添加多个监听器  
        myEmitter.on('foo', listener1);  
        myEmitter.on('foo', listener1);  
        myEmitter.on('foo', listener1);  
        getListenerCount();  
          
        // 移除所有监听器  
        myEmitter.removeAllListeners(['foo']);  
        getListenerCount();  
      

六 定时处理

6.1 定时处理常用类

  • timer模块公开了一个全局API,用于调度在将来某个时间段调用的函数。是全局变量,不需要require(‘timers’)

6.2 定时调度

  • setImmediate(callback[, ...args])方法,用于设定定时器为立即执行定时器。callback在当前回合事件循环结束后调用,args指当前传入callback的参数

  •   const util = require(‘util’);  
      const setImmediatePromise = util.promisify(setImmediate);  
        
      async function timerExample(){  
        console.log(‘在I/O回调之前’)  
        await setImmediatePromise()  
        console.log(‘在I/O回调之后’)  
      }  
      timerExample();  
    
  • setInterval 用于设定执行周期

  • setTimeout 用于在上一次定时器执行的deleay毫秒后设定定时器执行时机

七 文件处理

7.1 打开文件

  •   onst fs = require('fs');  
      fs.open('data.txt', 'r', (err, fd) => {  
          if (err) {  
              throw err;  
          }  
        
          fs.fstat(fd, (err, stat) => {  
              if (err) {  
                  throw err;  
              }  
        
              // 始终关闭文件描述符!  
              fs.close(fd, (err) => {  
                  if (err) {  
                      throw err;  
                  }  
              });  
          });  
      });  
    

7.2 读取文件

  • fs.read用于异步地读取文件
    •   const fs = require('fs');  
        fs.open('data.txt', 'r', (err, fd) => {  
            if (err) {  
                throw err;  
            }  
            var buffer = Buffer.alloc(255);  
          
            // 读取文件  
            fs.read(fd, buffer, 0, 255, 0, (err, bytesRead, buffer) => {  
                if (err) {  
                    throw err;  
                }  
                // 打印出buffer中存入的数据  
                console.log(bytesRead, buffer.slice(0, bytesRead).toString());  
          
                // 始终关闭文件描述符!  
                fs.close(fd, (err) => {  
                    if (err) {  
                        throw err;  
                    }  
                });  
            });  
        });  
      
  • fs.readdir用户异步读取目录内容
    •   const fs = require("fs");  
        console.log("查看当前目录下所有的文件");  
          
        fs.readdir(".", (err, files) => {  
            if (err) {  
                throw err;  
            }  
          
            // 列出文件名称  
            files.forEach(function (file) {  
                console.log(file);  
            });  
        });  
      
  • fs.readFile用于异步地读取文件的全部内容
    •   const fs = require('fs');  
        fs.readFile('data.txt', (err, data) => {  
            if (err) {  
                throw err;  
            }  
            console.log(data);  
        });  
          
        // 指定为UTF-8  
        fs.readFile('data.txt', 'utf8', (err, data) => {  
            if (err) {  
                throw err;  
            }  
            console.log(data);  
        });  
      
  • fs.write 用于写入文件
    • 将Buffer写入文件
      •   const fs = require('fs');  
          // 打开文件用于写入。如果文件不存在则创建文件  
          fs.open('write-data.txt', 'w', (err, fd) => {  
              if (err) {  
                  throw err;  
              }  
              let buffer = Buffer.from("《Node.js企业级应用开发实战》");  
            
              // 写入文件  
              fs.write(fd, buffer, 0, buffer.length, 0, (err, bytesWritten, buffer) => {  
                  if (err) {  
                      throw err;  
                  }  
            
                  // 打印出buffer中存入的数据  
                  console.log(bytesWritten, buffer.slice(0, bytesWritten).toString());  
                  // 始终关闭文件描述符!  
                  fs.close(fd, (err) => {  
                      if (err) {  
                          throw err;  
                      }  
                  });  
              });  
          });  
        
    • 将字符串写入文件
      •   const fs = require('fs');  
          // 打开文件用于写入。如果文件不存在则创建文件  
          fs.open('write-data.txt', 'w', (err, fd) => {  
              if (err) {  
                 throw err;  
              }  
              let string = "《Node.js企业级应用开发实战》";  
            
              // 写入文件  
              fs.write(fd, string, 0, 'utf8', (err, written, buffer) => {  
                  if (err) {  
                      throw err;  
                  }  
                  // 打印出存入的字节数  
                  console.log(written);  
                  // 始终关闭文件描述符!  
                  fs.close(fd, (err) => {  
                      if (err) {  
                          throw err;  
                      }  
                  });  
              });  
          });  
        
    • 将数据写入文件
      •   const fs = require('fs');  
          let data = "《Node.js企业级应用开发实战》";  
          // 将数据写入文件。如果文件不存在则创建文件  
          fs.writeFile('write-data.txt', data, 'utf-8', (err) => {  
              if (err) {  
                  throw err;  
              }  
          });  
        

八 进程

nodeJS是被设计用来高效处理I/O操作的,在CPU密集型任务中,可能会阻塞事件循环。一个替代方式是,将CPU密集型任务分配给另外一个线程进行处理,这样不但能释放事件循环,同时也能利用多核的计算优势。

nodeJS提供了child_process模块,来管理子进程

8.1 执行外部命令

  • 当需要执行外部的shell命令或可执行文件时,可使用child_process模块的 spawn() / exec() / execFile() 方法来实现

九 流

9.1 了解流

  • 流是可读、可写或可读可写的。所有的流都是EventEmitter的实例
  • 可读流:如 fs.createWriteStream()
    可写流:如 fs.createReadStream()
    双工流:可读可写的流 如 net.Socket
    转换流:在读写过程中可修改或转换数据的Duplex流 如 zlib.createDefilate()

9.2 可读流

    • close 在流或其底层资源被关闭时触发。表明不会再触发其他事件,也不会再发生操作
      • data 在流将数据块传送给“消费者”后触发。
        • 当调用readable.pipe() / readable.resume()或绑定监听器到data事件时,流会转换到流动模式。
        • 当调用readable.read()且有数据块返回时,也会触发data事件
      • end 在流中没有数据可消费时触发
      • error 当流遇底层内部出错而不能产生数据,或推送无效数据块时触发
      • pause 调用stream.pause()并且readsFlowing不为false时,会发出pause事件
      • readable 在当流中有数据可供读取时触发
      • resume 调用stream.resume() 且 readsFlowing 不为true是,将发出resume事件
  •   const fs = require('fs');  
      const rr = fs.createReadStream('data.txt');  
        
      rr.on('readable', () => {  
        console.log(`读取的数据: ${rr.read()}`);  
      });  
        
      rr.on('end', () => {  
        console.log('结束');  
      });  
    
  • pipe

    const fs = require('fs');  
    const zlib = require('zlib');  
    const readable = fs.createReadStream('data.txt');  
    const writable = fs.createWriteStream('write-data.txt');  
      
    // readable的所有数据都推送到'write-data.txt'  
    readable.pipe(writable);  
      
    /////  
      
    const gzip = zlib.createGzip();  
    const writable2 = fs.createWriteStream('write-data.txt.gz');  
      
    // 在单个可读流上绑定多个可写流  
    readable.pipe(gzip).pipe(writable2);  
    readable.on('readable', () => {  
      console.log(`读取的数据: ${readable.read()}`);  
    });  
      
    readable.on('end', () => {  
      console.log('结束');  
    });  
    

十 HTTP

10.1 创建HTTP服务器

  •   const http = require('http');  
      const hostname = '127.0.0.1';  
      const port = 8080;  
        
      const server = http.createServer((req, res) => {  
        res.statusCode = 200;  
        res.setHeader('Content-Type', 'text/plain');  
        res.end('Hello World\n');  
      });  
        
      server.listen(port, hostname, () => {  
        console.log(`服务器运行在 http://${hostname}:${port}/`);  
      });  
    
  • http.server事件

    • checkContinue
      • 每次收到 HTTP Expect:100-continue 的请求时触发。如未监听,服务器自动响应 100 Continue
    • checkExpectation
      • 每次收到带有 HTTP Expect 请求头的请求时触发,如未监听,自动响应 417 Expectation Failed
    • clientError
      • 如客户端连接发出error事件,则会在此处转发。此事件的监听器负责关闭或销毁底层套接字
    • close
      • 服务器关闭时触发
    • connect
      • 每次客户端请求HTTP CONNECT方法时触发。如果未监听此事件,中请求的客户端将关闭其连接
    • connection
      • 建立新的TCP流时会发出此事件
    • request
      • 每次有请求是都会发出此事件
    • upgrade
      • 每次客户端请求HTTP等级时都会发出此事件

10.2 REST

  • REST应具备的条件
      • 不应该依赖于任何通信协议
        • 不应该包含对通信协议的任何改动
        • 决不应该定义一个固定的资源名或层次结构
        • 永远不应该有那些会影响客户端的 “类型化” 资源
        • 不应该要求有先验知识
  • RESTful基本的设计原则
      • 通过URI来标识资源。系统中们每一个对象或资源都可通过唯一的URI来进行寻址
        • 统一接口。
        • 若要创建资源,应用POST方法
        • 若要更新或添加资源,应用PUT方法
        • 若要删除某个资源,应用DELETE方法
        • 资源多重表述
        • 无状态
  • REST实现等级
    • 第0级:使用HTTP作为传输方式
    • 第1级:引入资源概念
    • 第2级:根据语义使用HTTP动词
    • 第3级:使用HATEOAS
  •   const http = require('http');  
      const hostname = '127.0.0.1';  
      const port = 8080;  
      let users = new Array();  
      let user;  
        
      const server = http.createServer((req, res) => {  
        req.setEncoding('utf8');  
        req.on('data', function (chunk) {  
          user = chunk;  
          console.log(req.method + user);  
        
          // 判断不同的方法类型  
          switch (req.method) {  
            case 'POST':  
              users.push(user);  
              console.log(users);  
              break;  
            case 'PUT':  
              for (let i = 0; i < users.length; i++) {  
                if (user == users[i]) {  
                  users.splice(i, 1, user);  
                  break;  
                }  
              }  
              console.log(users);  
              break;  
            case 'DELETE':  
              for (let i = 0; i < users.length; i++) {  
                if (user == users[i]) {  
                  users.splice(i, 1);  
                  break;  
                }  
              }  
              console.log(users);  
              break;  
          }  
          res.statusCode = 200;  
          res.setHeader('Content-Type', 'text/plain');  
          res.end(JSON.stringify(users));  
        });  
      });  
        
      server.listen(port, hostname, () => {  
        console.log(`服务器运行在 http://${hostname}:${port}/`);  
        
      });  
    

十一 WebSocket

11.1 web投送技术总结

  • HTTP一个不足之处是每次请求响应完成后,服务器与客户端的就断开了,如果客户端想继续获取服务器消息,必须再次发起请求。
    为了改善HTTP的不足,出现了WebSocket等

  • 轮询

    • 重复发送新的请求到服务端
    • 缺点:
      间隔过长:会导致用户不能及时接收到更新的数据。
      间隔过短:会导致查询请求过多
  • 长轮询

    • 客户端发送一个请求到服务端,如果服务端没有新的数据,就保持这个连接直到有数据。
      一旦有了数据,就发送给客户端并关闭

    • 缺点:
      需第三方库支持,实现复杂,且每次连接只能发送一个数据,多个数据发送时耗费服务器性能

  • SSE 服务器推送事件

    • 客户端发送一个请求,服务端保持这个连接直到一个新的消息准备好,返回给客户端,同时仍然保持这个连接是打开的。
    • 基于HTML5标准,实现较简单,同时一个连接可发送多个数据。
      缺点:IE不支持,服务器只能单向推送到客户端
  • WebSocket

    • 提供一个真正的全双工连接。发起者是客户端,发送一个带特殊HTTP头的请求到服务端。

    • WebSocket连接上的交互不再是基于HTTP协议了。其可用于需要快速在两个方向上交换小块数据的在线游戏或任何其他应用程序

    • 属于HTML5标准,真正的全双工,性能好。
      缺点:实现复杂,需要对ws协议专门处理

    • nodeJS有原生并未提供WebSocket支持,需安装三方包

      • npm i ws

十二 TLS/SSL

12.1 了解TLS/SSL

  • HTTP协议是明文的,存在很多缺点,如传输内容会被偷窥篡改。为了加强网络安全,出现了TLS/SSL协议
  • 安全通道
    • SSL (Secure Sockets Layer) 是在网络上应用最广泛的加密协议实现,其使用结合加密过程来提供网络的安全通信
  • HTTPS
    • 是基于SSL安全连接的HTTP协议

12.2 NodeJS中的TLS/SSL

  • const tls = require(‘tls’)