博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Node.js Design Patterns--1.Node.js Design Fundamentals
阅读量:6357 次
发布时间:2019-06-23

本文共 13858 字,大约阅读时间需要 46 分钟。

  hot3.png

Node.js哲学

小核心(small core)

Node.js的内核设计基于一些小原则, 其中之一就是由小的函数集合组成, 其余部分为由模块组成的生态圈, 而核心则起到连接不同模块进行自由的组合.

小模块(small modules)

Node.js中, 尽量设计小的模块, 这是基于Unix的哲学:

1. 小即美.

2. 使每个模块只做一件事.

Node.js使用npm解决了"依赖地狱"的问题, 保证每个安装的包具有完整可分离的依赖包, 而不会产生依赖冲突.

具有复用性的模块应该具有以下特性(符合DRY原则: Don't Repeat Yourself):

1. 易于理解和使用.

2. 简单测试和管理.

3. 浏览器中可共享.

小接触层(small surface area)

Node.js中, 定义模块的通用原则是导出一小块主要功能, 其它的实现细节应该隐藏起来. 这样方便调用者分清模块的主次, 并且更易于进行模块的组合使用.

模块是用来使用的, 而非用来扩展, 这样它可易于复用, 实现简单, 操作便利, 以及增加它的可用性.

简单和实用(simplicity and pragmatism)

遵循KISS(keep it simple, stupid)原则.

介绍Node.js 6和ES2015

let: 所定义的变量具有块级作用域.

const: 定义一个常量.

"=>"函数: 简化函数的编写. 如果函数只有一行, 可忽略return.

const numbers = [2, 5, 6, 7, 9];const even = numbers.filter(function(x) {	return x % 2 === 0;});

使用"=>"函数可简化为:

const numbers = [2, 5, 6, 7, 9];const even = numbers.filter(x => x % 2 === 0);

"=>"函数同时保留着它的词法作用域, 所以this指针会指向此函数的对象. 

function DelayedGreeter(name) {	this.name = name;}DelayedGreeter.prototype.greet = function() {	// Hello undefined	setTimeout(function cb() {		console.log('Hello ' + this.name);	}, 500);	// Hello world!	setTimeout(() => console.log('Hello ' + this.name), 1000);};const greeter = new DelayedGreeter('world!');greeter.greet();

class的引入则简化了继承.

function Person(name, surname, age) {	this.name = name;	this.surname = surname;	this.age = age;}Person.prototype.getFullName = function() {	return this.name + ' ' + this.surname;};Person.older = function(person1, person2) {	return (person1.age >= person2.age) ? person1 : person2;};

而使用class属性则简化如下:

class Person {	constructor(name, surname, age) {		this.name = name;		this.surname = surname;		this.age = age;	}	getFullName() {		return this.name + ' ' + this.surname;	}	static older(person1, person2) {		return (person1.age >= person2.age) ? person1 : person2;	}}

而class甚至可以使用extend进行继承:

class PersonWithMiddlename extends Person {	constructor(name, middlename, surname, age) {		super(name, surname, age);		this.middlename = middlename;	}	getFullName() {		return this.name + ' ' + this.middlename + ' ' + this.surname;	}}

对象字面量的赋值功能被增强:

const x = 22;const y = 17;const obj = {x, y};// {x: 22, y: 17}console.log(obj);

之前我们导出一个module, 需要如此编写代码:

module.exports = {  square: function(x) {    return x * x;  },  cube: function(x) {    return x * x * x;  }};

而现在可简化为:

module.exports = {  square(x) {    return x * x;  },  cube(x) {    return x * x * x;  }};

Map数据结构的出现, 简化了"字典"型数据结构的编写:

const m = new Map();m.set('a', 1);m.set('b', 2);//2console.log(m.size);// trueconsole.log(m.has('a'));// 1console.log(m.get('a'));m.delete('b');// undefinedconsole.log(m.get('b'));// a:1for (const entry of m) {	console.log(entry[0] + ':' + entry[1]);}

Set简化了集合的操作:

const s = new Set([0, 1, 2, 3]);s.add(3);// 4console.log(s.size);s.delete(0);// falseconsole.log(s.has(0));// 1 2 3for (const entry of s) {	console.log(entry);}

模板字符串

我们可以使用"``"来实现模板字符串, 模板字符串中使用${expression}来求解表达式.

 

reactor模式

I/O是缓慢的

I/O操作是缓慢的, 接触RAM的速度为纳秒级别, 接触磁盘或者网络的速度为微秒级别.

阻塞I/O

在传统的I/O操作中, 在当前线程中I/O将阻塞直到操作完成.

// 线程阻塞直到数据有效data = socket.read();// 数据有效print(data);

如果一个Web服务器是基于阻塞I/O的, 那它在单线程情况下不支持多个连接. 传统的解决方案就是新建一个连接, 则fork出一个子线程来处理.

095610_c5Xt_1017135.png

线程耗费的资源是昂贵的, 并且每个线程的空闲时间也是及其浪费的(例如数据库操作时候的阻塞). 所以在Web中使用多线程来处理多连接, 不是完美的解决方案.

非阻塞I/O

非阻塞模式下, 系统不需要等待数据读写完毕而是马上返回. 调用的函数会立即返回一个变量, 可表示暂时没有数据, 正在读写, 或者读写完毕.

一种非阻塞I/O模式是将执行一次循环, 等待循环中数据全部操作完毕, 而非顺序等待每个元素操作执行完毕:

resources = [socketA, socketB, pipeA];while (!resources.isEmpty()) {  for (i = 0; i < resources.length; i++) {    resource = resources[i];    var data = resource.read();    if (data === NO_DATA_AVAILABLE) {      // 当前没有任何的有效数据      continue;    }    if (data === RESOURCE_CLOSED) {      // 数据读取完毕, 将resource从列表中删除      resource.remove(i);    } else {      // 读取到一些数据, 处理它      consumeData(data);    }  }}

考虑一个实际的Node.js例子, 我们使用net模块创建一个服务器, 客户端连接到服务器后, 输入数据, 服务器原样返回. 客户端全部关闭后, 服务器也关闭.

服务器代码test.js:

const net = require('net');const server = net.createServer((c) => {  console.log('client connected');  c.on('end', () => {    console.log('client disconnected');    server.unref();  });  c.write('hello\r\n');  c.pipe(c);});server.on('error', (err) => {  throw err;});server.listen(8000, () => {  console.log('server bound');});

服务器运行:

leicj@leicj:~/test$ node test.jsserver boundclient connectedclient connectedclient disconnectedclient disconnectedleicj@leicj:~/test$

客户端1:

leicj@leicj:~/test$ telnet localhost 8000Trying 127.0.0.1...Connected to localhost.Escape character is '^]'.hellonihaonihao^]telnet> quitConnection closed.leicj@leicj:~/test$

客户端2:

leicj@leicj:~/test$ telnet localhost 8000Trying 127.0.0.1...Connected to localhost.Escape character is '^]'.hellowhatwhat^]telnet> quitConnection closed.leicj@leicj:~/test$

事件多路(event demultiplexing)

同步事件多路操作: 将所有的I/O操作排列好, 循环监测事件直到新的事件可操作时候进行阻塞, 直到操作完成后继续循环监测.

socketA, pipeB;watchedList.add(socketA, FOR_READ);    // [1]watchedList.add(pipeB, FOR_READ);while (events = dumultplexer.watch(watchedList)) {    // [2]  // event loop  foreach(event in events) {    // [3]    // read将永远不会阻塞并且总是返回数据    data = event.resource.read();    if (data === RESOURCE_CLOSED)      demultiplexer.unwatch(event.resource);    else      consumeData(data);  }}

1. resource添加到具体的数据结构中, 关联一个特定的操作, 如read

2. 监测resources直到具体的操作有效(例如可读)后进行阻塞操作, 直到操作完毕后继续监测.

3. 针对每个resource操作完毕后, 重新阻塞.

这种情况下, 可在单线程下执行多个I/O操作。

105550_ZQCO_1017135.png

 

reactor模式

reactor模式是特殊版的event demultiplexing, 它主要观点在于: 每个I/O操作都有一个关联的handler, 在event loop中会被产生和调用.

091811_P2ya_1017135.png

1. Application通过提交给Event Demultiplexer生成一个新的I/O操作.

2. 当一系列的I/O操作完成以后, Event Demultiplexer将新的事件push到Event Queue中.

3. Event Loop会顺序循环Event Queue, 确定哪个Event将被触发.

4. 针对每个Event, 相应的handler会被调用.

5. 当handler执行完毕后, 会将控制权返回给Event Loop中(5a). 然而, 也可能handler中会产生新的异步操作, 则执行(5b).

6. 当所有的Event Queue都执行完毕后, loop会被阻塞, Event Demultiplexer会执行下一次的循环.

回调模式

持续传递类型(The continuation-passing style)

在JavaScript中, 将一个函数作为参数进行传递, 在另一个函数执行完毕后进行调用, 则称为回调函数.

同步性回调

考虑以下函数:

function add(a, b) {  return a + b;}

假设将return当做一个函数(实际上return为一个操作符), 则可修改成:

function (a, b, cb) {  cb(a + b);}

测试代码如下:

function add(a, b, cb) {    cb(a + b);}console.log('before');add(1, 2, result => console.log('Result: ' + result));console.log('after');

输出:

leicj@leicj:~/test$ node test.jsbeforeResult: 3after

异步性回调

我们使用setTimeout来模拟异步性回调:

function addAsync(a, b, cb) {    setTimeout(() => cb(a + b), 100);}console.log('before');addAsync(1, 2, result => console.log('Result: ' + result));console.log('after');

输出:

leicj@leicj:~/test$ node test.jsbeforeafterResult: 3

095015_HwiA_1017135.png

类似异步, 实为同步的回调

例如, map函数:

> [1, 5, 7].map((item) => item - 1);[ 0, 4, 6 ]

 

同步还是异步?

针对一个封装的函数, 最危险的一个行为是在某些条件下为同步, 而在某些条件下为异步:

var fs = require('fs');var cache = {};function inconsistentRead(filename, cb) {  if (cache[filename]) {    cb(cache[filename]);  } else {    fs.readFile(filename, 'utf8', (err, data) => {      cache[filename] = data;      cb(data);    });  }}

这种情况下, 会造成一些意外的情况, 考虑如下的代码:

const fs = require('fs');let cache = {};function inconsistentRead(filename, cb) {    if (cache[filename]) cb(cache[filename]);    else {        fs.readFile(filename, 'utf8', (err, data) => {            cache[filename] = data;            cb(data);        });    }}function createFileReader(filename) {    let listeners = [];    inconsistentRead(filename, value => {        listeners.forEach(listener => listener(value));    });    return {        onDataReady: listener => listeners.push(listener)    };}let reader1 = createFileReader('data.txt');reader1.onDataReady(data => {    console.log('First call data: ' + data);    let reader2 = createFileReader('data.txt');    reader2.onDataReady(data => {        console.log('Second call data: ' + data);    });});

如果data.txt的内容为: some data, 则实际输出为:

leicj@leicj:~/test$ node test.jsFirst call data: some data

我们认真查看createFileReader函数, 里面inconsistentRead函数只有可能其为异步调用情况下, 回调函数才会被调用. 而第一次读取data.txt时候为异步, 第二次读取data.txt时候为同步,  则inconsistentRead函数在第二次永远不会被调用.

一种改进的方法为: 将函数修改为同步API:

function inconsistentRead(filename, cb) {    if (cache[filename]) cb(cache[filename]);    else cache[filename] = fs.readFileSync(filename, 'utf8');}

或者修改为异步API:

function inconsistentRead(filename, cb) {    if (cache[filename]) {        process.nextTick(() => {            cb(cache[filename]);        });    } else {        fs.readFile(filename, 'utf8', (err, data) => {            cache[filename] = data;            cb(data);        });    }}

 

Node.js回调规则

回调函数作为最后一个参数

fs.readFile(filename, [options], callback)

回调函数中, Error作为第一个参数

fs.readFile('foo.txt', 'utf8', function(err, data) {  if (err) handleError(err);  else processData(data);});

传播错误

一个典型的错误处理方式如下:

var fs = require('fs');function readJSON(filename, cb) {  fs.readFile(filename, 'utf8', (err, data) => {    let parsed;    if (err) return cb(err);    try {      parsed = JSON.parse(data);    } catch (err) {      return cb(err);    }    cb(null, parsed);  });}

正常情况下, 我们不能使用try/catch来捕获异步的异常. 在Node.js中, 一种使用on('error')来捕获, 一种使用process.on('uncaughtException')来捕获.

模块系统和模式

The revealing module pattern

var module = (() => {  const privateFoo = () => {...};  const privateVar = [];  const export = {    publicFoo: () => {...},    publicBar: () => {...}  };  return export;})();

 

Node.js模块

以下代码用于模拟Node.js的模块调用:

function loadModule(filename, module, require) {  var wrappedSrc =    '(function(module, exports, require) {' +      fs.readFileSync(filename, 'utf8') +    '})(module, module.exports, require);';  eval(wrappedSrc);}var require = function(moduleName) {  console.log('Require invoked for module:' + moduleName);  var id = require.resolve(moduleName);  // [1]  if (require.cache[id]) {  // [2]    return require.cache[id].exports;  }  // module metadata  var module = {  // [3]    exports: {},    id: id  };  // Update the cache  require.cache[id] = module;  // [4]  // load the module  loadModule(id, module, require);  // [5]  // return exported variables  return module.exports;  // [6]};require.cache = {};require.resolve = function(moduleName) {  /* resolve a full module id from the moduleName */}

1. 通过模块名称, 使用require.resolve来解析, 得到模块的完整路径.

2. 判断id是否已经存在于缓存之中(即是否已加载).

3. 如果未加载, 则定义一个module包含exports和id.

4. 将此变量缓存起来.

5. 读取文件id的内容, 并与之前的module关联起来.

6. 导出其相应的模块.

备注: 这仅仅只是一个例子, [5]的操作会将文件的加载和module关联起来。

定义一个模块

// load another dependencyvar dependency = require('./anotherModule');// a private functionfunction log() {  console.log('Well done ' + dependency.username);}// the API to be exported for public usemodule.exports.run = function() {  log();};

模块内的所有变量都是私有的, 除非调用module.exports将变量暴露出去. 而使用require进行模块的调用.

虽然模块内的变量都是私有的, 但Node.js支持特殊的变量global, 任何赋值给global的都是全局变量.

exports和module.exports的区别

exports和module.exports本质上并没有什么区别, 如下的exports代码:

exports.hello = function() {  console.log('hello');}exports.world = function() {  console.log('world');}

等价于:

module.exports = {  hello: function() {    console.log('hello');  },  world: function() {    console.log('world');  }};

require是同步的

加载一个模块是同步操作, 以下代码是错误的:

setTimeout(function() {  module.exports = function() {...};}, 100);

resolving算法

File modules: 如果模块名称以"/"开头, 则为绝对路径. 如果以"./"开头, 则为相对路径.

Core modules: 如果模块不以"/"和"./"开头, 则从Node.js的核心模块进行寻找.

Package modules: 如果核心模块没有找到, 则从当前目录的node_modules进行查找, 找不到则向父目录继续查找...

针对目录或者文件, 则算法优先匹配:

<moduleName>.js

<moduleName>/index.js

<moduleName>/package.json中指定的文件.

一个具体的例子, 考虑如下的目录结构:

144050_4FFM_1017135.png

1. 在/myApp/foo.js中require('depA'), 则加载/myApp/node_modules/depA/index.js

2. 在/myApp/node_modules/depB/bar中require('depA'), 则加载/myApp/node_modules/depB/node_modules/depA/index.js

模块缓存

每个模块在第一次require时候会被加载和计算, 之后多次的加载均从缓存中读取. 

缓存的作用有两个:1是避免模块加载中的循环依赖. 2是保证同一个模块被多次加载, 则输出一样.

Module a.js:

exports.loaded = false;var b = require('./b');module.exports = {  bWasLoaded: b.loaded,  loaded: true};

Module b.js:

exports.loaded = false;var a = require('./a');module.exports = {  aWasLoaded: a.loaded,  loaded: true};

test.js:

var a = require('./a');var b = require('./b');console.log(a);console.log(b);

输出:

leicj@leicj:~/test$ node test.js{ bWasLoaded: true, loaded: true }{ aWasLoaded: false, loaded: true }

 

观察者模型

观察者模型: 定义一个观察对象, 用于监测一个对象集合.

观察者模型不同于回调模型的一点在于: 它可以观察多个对象, 而每个对象被触发后所执行的动作可能不同, 而回调函数只有一个。

EventEmitter

var EventEmitter = require('events').EventEmitter;class MyEmitter extends EventEmitter {}var myEmitter = new MyEmitter();myEmitter.on('ok', () => {  console.log('ok');});myEmitter.on('error', (err) => {  console.log(err.message);});myEmitter.emit('ok');myEmitter.emit('error', new Error('err.....'));

一个典型的EventEmitter的实现如上, 可视化如下:

230503_0Yad_1017135.png

EventEmitter所给予的基本操作如下:

on(event, listener): 给特定一个事件注册一个监听处理函数.

once(event, listener): 给特定一个事件注册一个监听处理函数, 当此事件触发后移除处理函数.

emit(event, [arg1], [...]): 触发一个事件, 并传递特定的参数.

removeListener(event, listener): 移除特定时间的监听处理函数.

 

对于错误来说, 一般都是注册一个error事件, 然后emit('error', new Error());

 

如果我们想继承EventEmitter, 则可以如下编写代码:

const EventEmitter = require('events').EventEmitter;const fs = require('fs');class FindPattern extends EventEmitter {  constructor(regex) {    super();    this.regex = regex;    this.files = [];  }  addFile(file) {    this.files.push(file);    return this;  }  find() {    this.files.forEach(file => {      fs.readFile(file, 'utf8', (err, content) => {        if (err) return this.emit('error', err);        this.emit('fileread', file);        let match = null;        if (match = content.match(this.regex)) {          match.forEach(elem => this.emit('found', file, elem));        }      });    });    return this;  }}const findPatternObject = new FindPattern(/hello \w+/);findPatternObject.addFile('fileA.txt')  .addFile('fileB.txt')  .find()  .on('found', (file, match) => console.log(`Matched "${match} in file ${file}`))  .on('error', err => console.log(`Error emitted ${err.message}`));

 

而对于EventEmitter来说, 它是同步注册的, 即遵循: 先注册, 后触发原则. 所以下例代码并无输出:

const EventEmitter = require('events').EventEmitter;class MyEmitter extends EventEmitter {}const myEmitter = new MyEmitter();myEmitter.emit('ok');myEmitter.on('ok', () => {    console.log('....');});

 

而使用EventEmitter还是callbacks的一个原则是: EventEmitter用于处理"当某件事发生时候要做什么", 而callbacks用于处理:"当某件事处理完毕时候一定要做什么".

转载于:https://my.oschina.net/voler/blog/824928

你可能感兴趣的文章
SKF密码设备研究
查看>>
数据对象映射模式(通过工厂模式和注册树模式)v2
查看>>
4939 欧拉函数[一中数论随堂练]
查看>>
MySQL笔记(一)
查看>>
spring boot 包jar运行
查看>>
18年秋季学习总结
查看>>
Effective前端1:能使用html/css解决的问题就不要使用JS
查看>>
网络攻防 实验一
查看>>
由莫名其妙的错误开始---浅谈jquery的dom节点创建
查看>>
磨刀-CodeWarrior11生成的Makefile解析
查看>>
String StringBuffer StringBuilder对比
查看>>
bootstrap随笔点击增加
查看>>
oracle 中proc和oci操作对缓存不同处理
查看>>
[LeetCode] Spiral Matrix 解题报告
查看>>
60906磁悬浮动力系统应用研究与模型搭建
查看>>
指纹获取 Fingerprint2
查看>>
面试题目3:智能指针
查看>>
flask ORM: Flask-SQLAlchemy【单表】增删改查
查看>>
vim 常用指令
查看>>
nodejs 获取自己的ip
查看>>