摘要:大家都说学 Node.js 学 Node.js,到底是学它的什么呢?是学 JavaScript 这门语言,还是学 Node.js 的 API?还是学 Node.js 各种三方库?

 

作者/分享人:死月,大搜车无线架构组中间件团队 Leader,负责包括流量网关等多个项目的架构。开源爱好者,Toshihiko 作者、阿里云 ONS SDK 作者,Node.js 核心贡献者之一。目前在撰写《Node.js:来一打 C++ 扩展》一书。

在写文章的最开头,我想引用一段我已经引用烂了的一段前同事的话,也是对于 Node.js 圈中近几年开始浮躁的风气的一种质疑。

大家都说学 Node.js 学 Node.js,到底是学它的什么呢?是学 JavaScript 这门语言,还是学 Node.js 的 API?还是学 Node.js 各种三方库?

正如问题所说,Node.js 其实就三块内容,一块是 JavaScript,也就是老生常谈的 ECMAScript;另一部分是也就是它的 API;最后一部分也就是最不需要学的,也就是生态圈中的各种库。

表象之学

学 Node.js 的人大抵是两个方向过来的,一是前端同学,二是后端同学。对于前端同学来学 Node.js 来说,对于他们最大的优势是 Node.js 使用了 ECMAScript,与他们的老本行一样一样的;而对于后端同学来说,优势在于他们有后端的一整套体系思想,而对于 Node.js 的 API 看起来会更加好理解。

ECMAScript

ECMAScript 是 Node.js 的根基,也就是大家所熟知的 JavaScript。目前市面上使用地最多的其实还是 ES5,即使是前端同学在写 ES6,在经打包之后,通常编译成的还是 ES5 的代码——出于兼容考虑。

而对于 Node.js 来说,基本上的 ES6 语法都已支持,但是还遗留了一些小坑,如 let 的效率实际上没有 var 高等。举个例子,在 Node.js 的一些源码中,很多的提交都是将 var 批量替换成 const,而少见批量将 var 替换成 const,比如这里。不过由于新版的 V8 中开启了 TurboFan,这效率就不一定了。

不过,哪怕是前端小伙伴们,也不一定能把 ECMAScript 给啃全咯。除了书中自有黄金屋之外,我在这里更推荐的是学习如何看 ECMAScript 规范

ECMAScript 要入门,很简单,无非是 C-Style。稍微精髓点的就是原型与原型链那一套了。不过现在 ES6 出来,大家习惯了 class 之后,兴许就把原型链给淡忘了。希望大家还是不忘本,这东西还是得拾起来。

但是一到比较偏僻的地方,就需要网上提问了。比如这些问题:

  1. 为什么 1 + undefined 结果是 NaN,而 1 + null 结果却是 1?——点此查看原题

  2. 关于两个 {} 的比较(如 ==!=<=>= 等)结果的真假值问题。——点此查看原题

就结果来看,很多已经理解的人就不必吱声了,大多其他小伙伴要么是靠死记结果,要么就是上百度、知乎等地儿搜,搜出来还大多是你抄我我抄你的答案,剩下的小缕同学就干脆不知道结果了。

其实去 StackOverflow 等地查效果应该会更好,然而答案最直接的地方还数ECMAScript 规范了。

其实在一看到这两个问题的时候,我也是不知道原因的,也信不过网上的大多答案解释。然后就上 ECMAScript 规范去追根溯源了。

首先第一个问题,它就是关于操作符 + 的一些定义,我们稍微检索一下,就能找到它的出处在 ES5 的(再早的版本我们就不追溯了)11.6.1 节中。

这一节的内容讲述的就是加号操作符的计算方法。将两个值先执行一个 ToPrimitive 操作,而对于这个操作,ECMAScript 规范中也原原本本给出了一张对照表。

[wximg]http://mmbiz.qpic.cn/mmbiz_jpg/TCHicQEF6XKCKrp19vLd0ib1VsvibqUZxLj19HJrLcB7Hv9DSSdPzfdHeojJzY7iaJrGDaLcZCJvo5Q48mibP08vtYQ/0?wx_fmt=jpeg[/wximg]

1undefined  null 三个值经过 ToPrimitive 操作后一点都没变;接下去判断左右值是否有字符串,若没有,则将两个值再进行 ToNumber 操作进行相加。然后对于 ToNumber 操作,规范也原原本本给出了一张对照表。

[wximg]http://mmbiz.qpic.cn/mmbiz_jpg/TCHicQEF6XKCKrp19vLd0ib1VsvibqUZxLjZNgN6JoGGOC2DuKjGRGrdHOz0XtJVoTKrawVvHxjTcuWOGkYicPAsWQ/0?wx_fmt=jpeg[/wximg]

从这张对照表看出来,undefined 的结果是 NaN  null 的结果是 +0,自然两个相加结果一个是 NaN 一个是 1 了。

第二个问题的结果也是类似,只要顺藤摸瓜,自然能在规范中找到相应的答案。其实什么原因不原因的,最后追溯到结果都是人(也就是委员会)制定出来的结果。

这里略微提醒一下,等值操作和大小比较操作分别在 ES5 的 11.9.3 节 11.8.5 节中。

所以本节中,我们说的就是要学好 JavaScript,在初步入门之后,借助 ECMAScript 规范加深自己对其的理解

Node.js API

讲完 ECMAScript,我们接下来再要讲讲的就是 Node.js 内置的 API 了。大家都知道,Node.js 就是靠着 V8 来作为 JavaScript 的运行引擎,libuv 作为事件循环的框架,然后上面像胶水一样黏上一堆易用的 API 就成了。

所以这些 API 就是大家入门 Node.js 的时候最先遇到的必修课了。

相信很多人跟我一样,第一次接触 Node.js 的时候都是这样的几行代码:

var http = require("http");var server = http.createServer(function (req, res) {
  res.writeHeader(200, {"Content-Type": "text/plain"});
  res.end("Hello Node.jsn");})server.listen(8000);console.log("httpd start @8000");

一开始学的人可能会觉得很神奇,其实现在看来无非就是 Node.js 内置的 http 模块中的一些 API,如 http.createServer(callback) 等。

要说这些 API 去哪学,其实用不着看什么网上的教程啊什么东西,所有的这些原原本本都在 Node.js 的文档之内。

如 6.x 的 Node.js 文档就可以点击这里,里面无非三十几个模块,而且内容都十分简单且实用。

例如 http 和 https 模块就是我们最常用的用于创建我们 HTTP(S) 服务器以及用于 HTTP(S) 客户端请求的模块,虽然现在大家用于线上开发大多都用了类似 Express、Koa 等框架或者 Request 这类库,但是实际上他们的最底层用的还是 Node.js 的这些模块这些 API。

API 没多少要学的,就算真的从头到尾看一遍,甚至也花不了一天(前提是对其中的各种前置知识了解)。大不了在一开始学的时候先过一遍,拣常见的学熟络了,然后在日常开发中再积攒肌肉记忆,就差不多了。

为什么这么说呢,因为哪怕是你真的死记硬背背下来了,下个版本说不定得变。举个例子来说,dns.setServer() 这个函数,自 Node.js 8.2.0 起,就支持了自定义 DNS 服务器端口,如:

dns.setServers([ "103.238.225.181:666" ]);

这个函数的新特性自 Issue#7903 提出并在 PR#13723 中被实现并且在 8.2.0 版本中正式发布。

除此之外,在 Node.js 的弃置 API 中也列了一堆在各种版本中被弃用的 API,就我以前用最多的就是 fs.exists() 函数,但是在 1.0.0 中就被标记为弃用了,只是目前来说还没有下架而已——指不定哪天就下架了呢。

这些被弃用的 API 也在 Node.js 的文档中被列了出来。

总之在 Node.js 的快速迭代中,各种事情都是有可能的。在维护者和贡献者的提交中,分为 Patch 提交、SEMVER-MINOR 和 SEMVER-MAJOR 三类提交,其中 SEMVER-MINOR 是中间的版本号更新,即非 Break 但是有功能改动、更新的提交,而 SEMVER-MAJOR 指的是那类 SEMVER-MAJOR 提交,如下架 API 就是一类 SEMVER-MAJOR 的提交。

由此可见,学 Node.js 的 API 时,我们大多只需要记住并熟练使用常用的一些即可,其余的就仰仗实时翻阅 Node.js 文档,以及关注 Node.js 新版本发布时候的 Changelog,尤其是那些 Notable changes

再下一步就可以有事没事去看看 Node.js 源码仓库中的 PR 了,参与讨论讨论也是不错的选择,还能参与到 Node.js 的贡献当中来。

Node.js 生态圈

个人认为,这是 Node.js 中最不需要学的一块内容了。

怎么说呢,Node.js 的生态圈非常庞大,其生态圈中的库质量参差不齐。有有着很大用户群的 Express、Koa 等,也有着闹着玩的 five 这类库。

无论是什么,总之根源还是在于检索加上看文档以及造轮子

首先第一步检索,无论是谷歌、NPM 自带的搜索引擎,还是自己脑海里的人肉搜索库,都是检索的根基所在。

例如我们如果要写一个 Web 应用,首先就是想到要一个 Web 框架,然后脑子里应该首先冒出 Express、Koa 等既是框架又算不上框架的 Web 库,或者再小众点就是 hapi、egg 等框架;如果觉得自己想另辟蹊径,找找看有没有什么更小众能体现自己逼格的东西,就可以上搜索引擎搜了,比如 Node.js Web Framework 等关键字;最后,如果觉得还不过瘾,那就自己造哇,比如我们团队就用的是自己造的 Akyuu.js 框架(框架还在沉淀中,文档还未来得及写)。

再比如,我们在写 Web 应用的时候,要用 md5 给密码进行简易加密。其中一个办法是使用 Node.js 的 crypto 模块,但是写起来也略冗长,还有个办法就是去 NPM 上搜搜看有什么好的包可以用咯。

输入 md5 关键字,我们能看到好多可以用的包。可以拣一两个看着顺眼的用,如:

  • md5

  • blueimp-md5

  • md5.js

  • apache-md5

至于用嘛,到自己拣好的包里面看看文档就好用了。所以这就是我所说的,Node.js 生态圈中的包,看文档就够了。大不了看官方出的教程,例如 Express 的官网。看这些东西要远好于自己去百度搜索抄来抄去的教程。

还有一类库,是通过其它的库被包装而成的,那在看文档之前,可能要先去了解一下原库的一些内容。

举个例子来说,gm 这个包是 Node.js 中用于图像处理的包,其原理是通过执行GraphicsMagick 命令的形式加上管道来进行处理的。所以要用 gm 的话,还需要事先去了解 GraphicsMagick 的一些用法,尤其是一些参数等等。还有就是像tensorflow2 这种包,相信大家都想到了,用它之前还得先去了解 TensorFlow

最后就是,当大家找不到想要的包的时候,就轮到自己造轮子,反哺 Node.js 生态圈了。

比如我就曾经实现了一套 Node.js 中使用的 ORM 库,唤作 Toshihiko,与市面上常见的 ORM 如 Sequelize 等大相径庭,主张简约和性能,并目前在我厂广泛应用。其它更多的我参与或者我发起的一些 Node.js 生态圈的贡献,可以查看我的me 仓库。

所以本节中我们所说的,学习 Node.js 生态圈,无非就是学习如何检索生态圈的内容、好好过滤并去其糟普取其精华,然后还是很重要一点就是学习如何查看文档,在有前置知识的库面前要先学习它的前置知识

包与模块机制

Node.js 中的包与模块机制都是借鉴了 CommonJS 的模块规范和包规范。关于这一块我曾经开了一场知乎 Live 详细讲解——足足讲了三四个小时还是虎头蛇尾。

这么长的一块内容还是值得一学的。如果大家偷个懒,也可以看看我的知乎 Live——《深入了解 Node.js 包与模块机制》

异步之旅

很多从后端转过来学 Node.js 的同学中,一开始入门最大的障碍就是它的异步非常令人烦恼,尤其是回调地狱。

就连我当年一开始学习 Node.js 的时候,也非常不适应。在什么也不懂的时候硬生生搞了个自己习惯的框架,把里面的异步操作通过 fibers 硬生生转成了同步的写法。现在回过头来想想,还是太年轻了。

习惯 Node.js 中的异步,个人认为是非常有必要的。

Callback

回调函数是 Node.js 中的基础所在,大量的内置 API 都是通过回调函数来触发事件返回结果的,如 fs、http 中的一堆堆函数,以及目前市面上大部分的三方库。

不过这货最让大家烦恼的就是传说中的回调地狱了。

但是我个人认为这都不是事儿,通过 async 这个包就能比较大程度地化解了,而且它还有非常多地流程控制的函数,形如 eachLimitwaterfallparallelauto 等等,在 async 中就能非常简单地实现了一堆的流程。

可以说 Callback 正是 Node.js 已至 JavaScript 的精髓所在,连响马大大都在微博中挺 Callback 了。

callback 虽然慢,但是并不太慢。而为了解决 callback hell 引入的流程框架,一个更比一个慢。导致使用这些框架的应用,性能直线下滑 50-100 倍。【没错,说的就是 async(指的是 async 语法)】

把 callback 玩到这么精致,也就是 JavaScript 了。

以及在交流过程中说的话:

如果没得选(指 fibjs),肯定是用 callback。

这玩意儿,主要就是要熟练,和思维转换。当大家熟悉了 Node.js 中事件循环的原理后,再回过头来看 Callback,其实也没那么难了。当再配合上 async 这个包,照样能写出非常优雅的代码。