设计
Node.js 就是 javascript, 所以你所了解的 JavaScript 知识都可以应用到 Node.js 应用中你在前端中编写代码的模式在编写服务器端应用逻辑时同样适用 Node.js 没有使用 JavaScript 语法扩展或者任何修改以适应 JavaScript 服务端的开发
但是, Node.js 中仍然有一些模式能够帮助你设计应用
事件发射器模式
下面讲到第一个模式是事件发射器模式, 该模式可以让实现者发射事件, 然后让消费者订阅其感兴趣的事件你可以把其当做传递回调给异步函数并在完成时执行回调模式的一种扩展通常情况下基于回调的异步方案是不够充分的, 因为可能存在不只一个调用者对该事件感兴趣
比如, 一个调用者可能会请求一个远程服务器的文件列表你或许想要在每检测到一个文件时就将结果传递回去, 即对每个文件都调用一遍回调函数事件发射器模式允许你每检测到一个文件就发射一个'file'事件, 然后在操作结束后发射'end'事件
使用事件发射器时, 你只需要发射事件及其对应负载即可
- const EventEmitter = require('events').EventEmitter;
- class MyClass extends EventEmitter {
- constructor() {
- super();
- setTimeout(() => {
- this.emit('myEvent', 'hello world', 42);
- }, 1000);
- }
- }
MyClass 的构造器创建了一个 1 秒后执行的定时器, 当定时器触发时会发射一个带有字符串'hello world'和数字 42 的事件 myEvent 你可以使用 on()来订阅该事件, on 方法会通过继承 EventEmitter 附加在 MyClass 上:
- const myObj = new MyClass();
- const start = Date.now();
- myObj.on('myEvent', (str, num) => {
- console.log('myEvent triggered', str, num, Date.now() - start);
- });
你需要注意到, 你所订阅的事件是异步事件, 但是监听函数是在事件触发时是同步执行的所以如果 myEvent 有 10 个订阅, 那么这 10 个订阅会立即顺序执行, 而不会等到下一个事件循环记住这个同步特性, 对事件发射者来说, 延迟发射事件就显得非常重要, 这样订阅者就可以在同一个事件循环中订阅多个事件, 而且不会错过在未来某个时间发射的事件
如果 EventEmitter 子类发射了'error'时间, 且无人订阅该事件, 那么 EventEmitter 会抛出一个异常, 进而导致 process 对象触发 uncaughtException 事件
verror
verror 组件是 Error 的子类, 允许你通过使用 printf 的形式自定义错误信息你的应用逻辑通常是异步方法的组合, 在添加错误处理函数时, 你经常会想要将错误信息冒泡至上级 verror 组件包含两个类 - VError 和 WError, 允许你将调用链上的错误累计起来可以看到合并的错误信息 (VError,unix 风格) 或者最终的错误信息, 但是可以访问上一级的错误信息(WError)
joyent/node-verror:https://github.com/joyent/node-verror
流模式
流是另一个在 Node.js 中广泛应用的基础模式除了大量实现事件发射器模式的核心模块之外, 还有模块很多实现了诸如 Readable, Writable 或者兼有 (Duplex) 的接口
流是一个抽象接口, 提供了常用事件比如 readablewritabledraindataend 和 close 这些事件本身就具有高互动性, 但是流最强大的部分是允许你将不同的流整合到一个管道 (pipeline) 中
管道可以帮助你的代码降低复杂性易于理解并提高可读性你可以通过使用. pipe()模式启动 Node.js 通过管道来传递反压 (back-pressure) 这个反压意思是你只能读取你可以写的或者你只能写你可以读的东西因此, 在一定时间内, 你只能在内存中保持你能够完成的数量的工作
假设你想要将 stdin 的数据发送至本地文件和远程服务器
- const fs = require('fs');
- const net = require('net');
- const localFile = fs.createWriteStream('localFile.tmp');
- const client = net.connect(80, '98.139.183.24', () => {
- process.stdin.pipe(client);
- process.stdin.pipe(localFile);
- });
client 和 localFile 都会从 stdin 读取数据, 但是读取速度由最慢的消费者决定
pipe()返回的是目标流, 所以如果目标是一个 Duplex, 或者是一个特殊的 Duplex, 比如 Transform, 那么你就可以使用链式调用管道
继续上一个例子, 这次我们只能将内容发送至本地文件, 而且在保存前对内容使用 gunzip 进行压缩
- const fs = require('fs');
- const zlib = require('zlib');
- process.stdin.pipe(zlib.createGunzip()).pipe(fs.createWriteStream('localFile.tar'));
更多关于流的信息, 可以参考最新版 Node.js 的文档
控制流程
因为 JavaScript 中函数是头等对象和还有闭包的概念, 所以很容易在需要的地方定义回调函数这在对解决方案进行快速原型时会很方便, 因为你可以直接在你需要的地方实现你的逻辑但是, 这样的话会导致一堆臃肿的内嵌函数, 又称为圣诞树或者毁灭金字塔
比如你想要顺序读取一系列文件, 然后在这些文件内容上做一些通用操作:
- fs.readFile('firstFile', 'utf8', (err, firstFile) => {
- doSomething(firstFile);
- fs.readFile('secondFile', 'utf8', (err, secondFile) => {
- doSomething(secondFile);
- fs.readFile('thirdFile', 'utf8', (err, thirdFile) => {
- doSomething(thirdFile);
- });
- });
- });
现在看起来还不算太糟, 但是这个模式有一些缺点:
---- 如果这些代码的逻辑太复杂的话, 那么理解程序流向和操作顺序就会变得非常困难
---- 没有错误处理逻辑, 在第三层代码执行时, 已经忽略了可能出现的两次错误
---- 第一个文件读取的结果在第三个操作完成之前一直是 GC 不可回收的状态闭包内存泄露在 JS 应用中非常常见, 而且多数都很难被诊断和发现
---- 如果你需要在一个输入集上做一系列异步操作的话, 最好找一个控制流程函数库来帮助你简化流程我们使用 vasync, 因为它还让调试器探查管道变得容易
vasync
vasync 是一个控制流程函数库, 受异步组件模式的启发而成然而 vasync 设计目的是使消费者可以查看指定任务的执行进度在获取这些任务在错误发生前执行了多久时, 这些信息非常重要
代码风格
为你的函数命名
考虑给所有的函数命名, 即使是那些你想不到的小闭包尽管 V8 使出浑身解数去通过脚本名和错误来源中的函数位置去识别函数, 想要一眼区分这些函数还是非常困难你不会想要在调试的时候浪费时间去修正这个错误的函数
避免使用闭包
同样地, 尽量不要在其他函数内部定义函数这会将你的思维方式由基于闭包变为基于栈这个逻辑上小小的改变可以帮助消除许多由于使用闭包无意引入的内存泄露
更多更小的函数
尽管 V8 JIT 是一个强大引擎, 可以进行许多代码优化, 但如果你的函数越小定义越清晰, 那么 JIT 就越有可能内联缓存这些函数从好的方面说, 如果你的函数都很小(代码不超过 100 行), 那么你的代码可读性和易理解性都有可能得到提升, 同时也降低了应用的维护成本
启用代码风格检查
使用一致的代码风格, 安装一个检查工具并启用它我们使用的是 jsstyle, 对 js 来说是一个不常见的风格提示工具, 但是至少有一个了另一个流行的提示工具是 eslint 风格检查工具的错误看起来就像是字符错误或者单元测试失败一样: 构建失败需要立即修复
易于测试
保证所有的关键函数都是可以在外部执行的简单地说, 就是这些函数可以用 export 导出或者可以通过 class 的方法访问按照这个方式, 保证这些函数被测试用例覆盖, 通过运行代码覆盖率报告者, 比如 lab
代码提示
代码提示工具可以对代码进行静态分析 (无需运行) 以识别出潜在的 bug 或者危险代码, 比如使用了未声明的变量或者 switch 中的 case 没有写 break 好点的代码提示工具应该略微激进一些(比如, 有的时候就是需要 case 中没有 break), 而你可以通过单行操作来忽略提示这些操作不应该被用来仅仅是无视提示工具, 而是因为代码这样书写比按照提示的书写更加清晰通过忽略避免了某些让人困惑的代码
如果你要使用代码提示, 那么就请严肃对待这件事将提示检查作为构筑的一部分, 就像单元测试一样, 并且拒绝掉没有通过所有见的代码(记住, 你可以按照需要忽略若干单一的提示, 但这么做的目的是因为写代码的人已经看到了这个提示并且做出了正确的决定)
代码提示同风格检查不是一个东西风格检查同样有用(见上), 但是代码提示通常是指客观上存在危险的模式, 而不是随意的风格选择(必须承认, 这里存在有中间地带, 比如有的时候使用 "==" 而不是 "===" 是 ok 的)
有很多不同的代码提示工具按需取我们使用 javascriptlint 因为它有很多好的检查策略支持基于项目的配置(每个项目可以设置不同的检查策略), 同时支持对单个提示点的忽略(以便于编写者手动指定忽略哪些校验点)
同样地, 强烈推荐在代码中使用'use strict'开始严格模式, 这样可以帮助你的代码在 js 解析器识别出全局泄露或者其他相似行为时尽快失效
日志
当设计和构建你的应用时, 确定未来的计划特别是考虑到你在调试时需要的工具一个优秀且明显的措施就是给你的应用添加合适的日志记录确保你选择了一个日志函数库能够支持你需要的特性一些常见的考虑: 是否支持你关注的需求? 是否是你想要的格式? 记录日志的 API 不会同 Node.js 和你的应用风格差异过大等
在应用运行时识别出对调试和分析应用有用的信息非常重要但是要记住, 在日志中包含过量的信息会对性能和存储产生不良影响确认你只在必要的地方包含必要的信息, 以免拖慢应用的速度 bunyan 有一个很不错的功能是可以按照需要在生产环境读取 debug 级别的日志, 而不需启用也无需重启进程
bunyan
bunyan 是一款专为 Node.js 应用设计的日志函数库 Bunyan 的输出是按行分隔的 JSON 数据, 非常易于在 unix 的命令行函数处理, 比如 grep 和 sed, 同样在你自己的 CLI 函数或者 json CLI 函数也适用
bunyan 内置支持 DTrace, 其允许你为现有目标保留现有日志级别(e.g. INFO), 但是可以在运行时启用更详细的级别(e.g. Trace), 结果会记录至这些日志文件所在的用户空间中, 即使文件不存在也会输出至已有的文件中, 存在填满硬盘的可能 DTrace 用在运行时是绝对安全的, 所以如果启用高级别的日志记录会对你的系统造成负面影响时, DTrace 会在影响发生之前退出
也就是说, 你已经在你的应用中配置好想要记录的日志等级, 但是你的应用运行不正常, 而且你想要在不重新启动服务或者增加日志存储量的情况下获取更多信息依靠 bunyan 和 DTrace 即可在运行时从进程中获取你感兴趣的等级的日志
Read more about using bunyan to do runtime log snooping.
客户端 服务端
在扩展至分布式系统时, 使用这种方式设计你的应用会有很大优势使用 REST 比如 HTTP 上的 API 或者甚至是 TCP 上的 JSON 来描述这一类接口都很自然这种方式使得一个人可以将 Node 擅长的异步网络环境和流的使用整合至强大的分布式和弹性系统
具体软件
bunyan
详情见上
fast
fast 是一个用于在 TCP 中高效处理 JSON 消息的轻量级函数库基础用法是为了实现基于消息的 RPC, 即使用指定的指令后将一系列相关的对象被发送至客户端 fast 设计时兼顾了可观测性, 故其同样支持 DTrace, 允许你快速获取服务端和客户端的性能数据
restify
restify 是一个用于创建和消费 REST 的终端转为了增加应用的可观性和易调试性而设计, restify 对 Bunyan 支持 Bunyan 和 DTrace 有了 Bunyan 和 DTrace 的支持, 你就获取了查看日志或运行时路由和请求延迟的能力
vasync
来源: https://sdk.cn/news/8112