读 《node.js实战2.0》,进行学习记录总结。
欢迎进入Nodejs的世界
一个典型的Node Web 应用程序
大体上来说,Node和JavaScript的优势之一是它们的单线程编程模型。在为浏览器编写代码时,我们写的指令序列一次执行一条,代码不是并行执行。然后对于用户界面来说,这样是不合理的:没有哪个用户想在浏览器执行网络访问或文件获取这样的低速操作时干等着。为了解决这个问题,浏览器引入了事件机制:在你点击按钮时,就有一个事件被触发,还有一个之前定义的函数会跑起来。这种机制可以规避一些线程编程中经常出现的问题,比如资源死锁和竞态条件。
非阻塞I/O
在服务端编程中,访问磁盘和网络这样的I/O请求会比较慢。Node用三种技术来解决这个问题:时间、异步API、非阻塞I/O。非阻塞I/O是底层术语,你的程序可以在做其他事件时发起一个请求来获取网络资源,然后当网络操作完成时,将会运行一个回调函数来处理这个操作的结果。
上图展示了一个典型的 Node Web 应用程序,它用Web 应用库Express来处理商店的订单流程。为了购买产品,浏览器发起了一个请求,然后应用程序检查库存,为用户创建一个账号,发回执邮件,并返回一个JSON HTTP 响应给浏览器。同时在做的其他事件有:发送了一封回执邮件,更新了数据库来保存用户的详细消息和订单。运行平台是并发操作的,因为它用了非阻塞I/O。
数据库是通过网络访问的。Node中的网络访问也是非阻塞的。用了 libuv 库来访问操作系统的非阻塞网络操作。在Linux/macOS/Windows 中的实现时不同的,但是我们只需要会操作数据库的 JavaScript 库就好了。例如
1 | db.insert(query,err => {}); |
然后,Node 就会帮你完成那些经过高度优化的非阻塞网络操作。
访问硬盘也差不多,但又不完全一样。在生成了回执邮件并从硬盘中读取邮件模板时,libuv借助线程池模拟出了一种使用非阻塞调用的假象。管理线程池是个苦差事,想较而言,
1 | email.send('template.ejs',(err,html) => {}); |
上面这样的代码就要容易理解得多了。
在进行速度较慢的处理时让Node能够做其他事情,是使用带非阻塞I/O的异步API真正的好处,即使你只有一个单线程、单线程的Node Web 应用,它也可以同时处理上千个网络访客发起的连接。要知道Node是怎么做到的,得先研究一下事件轮询。
事件轮询
仔细研究上图的“响应浏览器的请求”的那部分。在这个应用程序中,Node内置了HTTP服务器库,即核心模块 http.server ,负责用流、事件、Node的HTTP请求解析器的组合来处理请求,是本地代码。可以使用Express Web 应用库添加的回调函数,也是由它触发的。这个回调函数又会触发数据查询语句,最终应用程序会用 HTTP 发送 JSON 作为响应。整个过程用了三个非阻塞网络调用:一个用于请求,一个用于数据库,还有一个用于响应。Node 是如何调用这些网络操作的呢?由事件轮询(event loop)。下图展示了如何用事件轮询完成这三个网络操作。
事件轮询是单向运行的先入先出队列,它要经过几个阶段,轮询中每个迭代都要运行的重要阶段在上图展示出来了。首先是计时器开始执行,这些计时器都是用 JavaScript 函数 setTimeout 和 setInterval 安排好的。接下来是运行 I/O 回调,即触发你的回调函数。轮询阶段会去获取新的I/O事件,最后是用 setImmediate 安排回调。这是一个特例,因为它允许你将回调安排在当前队列中的I/O回调完成之后立即执行。
ES2105、Node和V8
从Node6开始可以使用默认函数参数、剩余参数、spread操作符、for…of循环、模块字符串、结构、生成器等很多新特征。点击这里 查看Node支持的ES2015特性。
在ES5及之前,我们使用 prototype 对象来创建类似类的结构:
1 | function User(){ |
Node6和ES2015,写成
1 | class User(){ |
Node还支持了子类、超类和静态方法。
const 和 let 是从Node 4 开始支持的。Node 还有原生的 promise 和 生成器。
1 | [1,2,3].map(n => n*2).filter(n => n > 3) |
生成器能把异步I/O变成同步编程分隔。Koa Web 应用库中用到了生成器。Koa使用promise 和其他生成器就可以抛开层层嵌套的回调,在值上 yield。
ES2105的模板字符串在Node中也非常好用。
1 | this.body = ` |
箭头函数,在Node中,一般需要两个参数,因为回调的第一个参数通常是错误对象,这时候需要用括号把参数括起来
1 | const fs = require('fs'); |
在ES5及之前的版本的语言中,在函数中定义函数会把 this 引用变成全局对象。因为这个问题,下面按ES5写的类容易出错
1 | function User(id){ |
给 self.name 赋值那行代码不能写成 this.name 。因为这个函数的 this 是全局变量。常用的解决办法就是在函数的入口将 this 赋值给一个变量,但箭头函数的绑定没有这个问题。
1 | class User { |
Node 与 V8
Node 的动力源自 V8 JavaScript 引擎,是由服务于 Google Chrome 的 Chromiun 项目组开发的。V8 的一个值得称道的特性是它会被 JavaScript 直接编译成机器码,另外它还有一些代码优化特性,所以Node 才会这么快。
Node的另一个本地部件 libuv ,是负责处理I/O。V8负责 JavaScript 代码的解释和执行。用C++绑定层可将 libuv 和 V8 结合起来。
使用特性组
Node 包含了V8提供的ES2015特性。这些特性分为shipping、staged 和 in progress 三组。shipping 组件的特性是默认开启的,staged 和 in progress 组的特性则需要用命令行参数开启。如果想使用 staged 特性,可以在运行 Node 时加上参数 –harmony。in progress 特性稳定性较差,需要具体的特性参数来开启。
1 | node --v8-option | grep "in progress" |
上面指令可以来查询当前可用的 ”in progress“ 特性。不同版本执行后的结果也是不一样的。
了解 Node 的发布计划
Node的发行版分为长期支持版(LTS)、当前版和每日构建版三组。有些人可能喜欢更新不那么频繁的 LTS,对于那些难以管理频繁更新的大公司来说,这个版本可能更好。但如果你想跟上性能和功能的改进,当前版更合适。
安装 Node
你可以直接上官网下载对用的操作系统的版本。这里我个人用的是 nvm 版本管理,自由切换版本。有兴趣可以点击了解
Node 自带的工具
Node 自带了一个包管理器,以及从文件和网络 I/O 到 zlib 压缩等无所不包的核心 JavaScript模块,还有一个调试器。npm 包管理器是这个基础设施中的重要组成部分
npm
命令行工具 npm是用 npm 调用的。你可以用它来安装 npm注册中心里的包,也可以用它来查找和分享你自己的项目,开源的和闭源的都行。注册中心里的每个 npm 包都会有个页面显示它的自述文件、作者和下载统计信息。
另外,npm 还是一家提供 npm 服务的公司的名字。这家公司为企业提供商业服务,包括托管私有的 npm包。你可以按月支付服务费,把公司的源码托管给他们,这样你的 JavaScript开发人员就可以用 npm轻松安装你的私有包了。
npm要求 Node项目所在的目录下有一个 package.json文件。创建 package.json文件的最简单方法是使用 npm。在命令行中输入下面这些命令:
1 | mkdir example-project |
打开 package.json,你会看到简单的 JSON 格式的项目描述信息。如果你现在用带有参数--save
的 npm命令从 npm 网站上安装一个包,它会自动更新你的 package.json文件。试着输入 npm install
,或简写为 npm i
:
1 | npm i --save express |
打开 package.json,应该会看到 dependencies 属性下面新增加的 express 。另外,看一下node_modules 文件夹,你会看到新创建的 express目录。里面是刚安装的那个版本的 Express。你也可以用 –global 参数做全局安装。应尽可能地将包安装在项目里,但对于用在 Node JavaScript代码之外的命令行工具,全局安装更合适。比如用 npm 安装命令行工具 ESLint 时,我们采用全局安装。
Node还自带了很多非常实用的库,统称为核心模块。
核心模块
Node的核心模块相当于其他语言的标准库,它们是编写服务器端 JavaScript 所需要的工具。JavaScript 标准本身没有任何处理网络的东西,甚至连处理文件I/O的东西都没有。Node以最少的代码给它加上了文件和TCP/IP网络功能,使其成为了一个可用的服务器端编程语言。
文件系统
Node 不仅有文件系统(fs、path)、TCP客户端和服务端库(net)、HTTP库(http和https)和域名解析库(dns),还有一些经常用来写判断的断言库(assert),以及一个用来查询平台消息的操作系统库(os)
Node 还有一些独有库。事件模块是一个处理事件的小型库,Node的大多数API都是以它为基础来做的。比如说,流模块用事件模块提供了一个处理流数据的抽象接口。因为Node 中的所有数据流用的都是同样的API,所以可以轻松组装出来软件组件。如果有一个文件流读取器,就可以很方便地把它跟压缩数据的 zlib 连接在一起,然后这个 zlib 再连接一个文件流写入器,从而形成一个文件流处理管道。
下面代码中,用Node 的 fs 模块创建了读和写流,然后把它们通过另外一个流(gzip)连接起来传输数据。
1 | const fs = require('fs'); |
网络
在Node 中搭一个服务器只需要加载 http 模块,然后给它一个函数。这个函数有两个参数,即请求和响应。
1 | const http = require('http'); |
将上面的代码保存到hellow.js文件中,用 node hello.js 运行它,就可以在 http://locahost:8080 看到这段消息。
调试器
Node 自带调试器支持单步执行 REPL(读取-计算-输出-循环)。这个调试器在工作时会用一个网络协议跟你的程序对话。带着 debug 参数运行城西,就可以对这个程序开启调试器。比如要调试上面的代码
1 | node debug hello.js |
然后可以看到下面的输出
1 | < Debugger listening on ws://127.0.0.1:9229/55258211-4e9c-444c-90eb-9f84c28fb532 |
我们可以在代码中的任何地方添加 debugger 语句来设置断点。遇到 debugger 语句后,调试器就会把程序停住,然后你可以输入命令。比如说,你写了一个 REST API来为新用户创建账号,但发现代码貌似没有把新用户密码的散列值写到数据库里。你可以在 User 类的 save 方法那里加一个 debugger ,然后单步执行每一条指令,看看发生了什么。
交互式调试
Node支持 Chrome调试协议。如果要用 Chrome的开发者工具调试一段脚本,可以在运行程序时加上 –inspect 参数:
1 | node --inspect --debug-brk |
这样 Node 就会启动调试器,并停在第一行。它会输出一个 URL 到控制台,你可以在Chrome 中打开这个 URL,然后用 Chrome的调试器进行调试。Chrome的调试器可以一行行地执行代码,还能显示每个变量和对象的值。这要比在代码里敲 console.log 好得多。
三种主流的Node程序
Node 程序主要分成三种类型:Web 应用程序、命令行工具和后台程序、桌面程序。提供单页应用的简单程序、REST微服务已经全栈的Web应用都属于Web应用程序。npm/gulp和webpack 都属于Node写的命令行工具。后台程序就是后台服务,比如PM2进程管理器。桌面程序一般是用Electron框架写的软件,Electron用Node作为基于Web 的桌面应用的后台。Atom 和 Studio Code 文本编辑器都属于这一类。
Web 应用程序
Node 是服务端JavaScript平台,所以用它搭建Web应用程序是理所当然的事件。既然客户端和服务端用的都是 JavaScript ,代码难免会有在这两种环境里重用的机会。Node Web 应用一般是用 Express 这样的框架写的。创建一个新目录,安装Express 模板,来快速创建一个Express Web 应用程序
1 | mkdir hello_express |
把代码存到 server.js 中
1 | const express = require('express'); |
接着输入
1 | npm start |
启动这个监听端口3000的 Node Web 服务器。在浏览器打开就可以看到 res.send 里面的文本了。
命令行工具和后台程序
Node 可以用来编写命令行工具,比如 JavaScript 开发人员所用的进程管理器和转义器。它可以作为一种方便的方式来编写其他操作的命令行工具,比如图片转换、控制媒体文件的播放的脚本等。
下面的例子
1 | // cli.js |
然后运行
1 | node cli.js yourName |
会打印出来
1 | Hello yourName |
这里用了解构,从 process.argv 中拉取第三个参数。所有 Node 程序都可以访问 process 对象,这是用户向程序中传递参数的基础。
Node 命令行还可以做其他事情。如果在程序的开头加上 #!
,并赋予其执行许可( chmod +x cli.js ),shell 就可以在调用程序时使用 Node。也就是说可以像运行其他 shell 脚本那样运行 Node程序。在类 Unix系统中用下面这样的代码:
1 | !/usr/bin/env node |
这样你就可以用 Node代替 shell脚本。也就是说 Node可以跟其他任何命令行工具配合,包括后台程序。Node程序可以由 cron调用,也可以作为后台程序运行。
桌面程序
如果你用过 Atom或 Visual Studio Code文本编辑器,那就用过 Node。Electron框架用 Node做后台,所以只要需要访问硬盘或网络,Electron 就会用到 Node。Electron 还用 Node 来管理依赖项,也就是说你可以用 npm往 Electron项目里添加包。
适合 Node 的应用程序
我们已经看过一些能用 Node搭建的应用程序了,但 Node擅长的领域不止于此。Node一般用来创建实时的 Web 应用,这几乎无所不包,从直接面对用户的聊天服务器到采集分析数据的后台程序都属于此类。在 JavaScript中,函数是一等对象,Node又有内建的事件模型,所以用它来写异步实时程序比用其他脚本语言更自然。
如果你要搭建传统的模型视图控制器(MVC)Web 应用,用 Node 也很适合。Ghost 等一些流行的博客引擎就是用 Node 搭建的。在搭建这几种类型的 Web 应用程序方面,Node 是一个经过实践检验的平台。虽然开发风格跟用 PHP的 WordPress不同,但 Ghost支持的功能是类似的,包括模板和多用户管理区。
Node还能做一些用其他语言很难做到的事情。它是基于 JavaScript的,所以在 Node中能运行浏览器中的 JavaScript。复杂的客户端应用可以经过改造在 Node服务器上运行,让服务器进行预渲染,从而加快页面在浏览器中的渲染速度,也有利于搜索引擎进行索引。
最后,如果你想要搭建一个桌面端或移动端应用,建议试一下 Electron,它也是由 Node 支撑起来的。现在 Web用户界面的体验跟桌面端应用一样丰富,Electron桌面端应用足以抗衡本地Web应用,还能缩短开发时间。Electron支持三种主流操作系统,所以你可以在 Windows、Linux和 macOS上重用这些代码。
总结
- Node 是用来搭建 JavaScript 应用程序的平台,基于事件和非阻塞的特性
- V8 被用作 JavaScript 运行
- libuv 是提供快速、跨平台、非阻塞I/O的本地库
- 被称为核心模块的Node 标准库很精巧,为 JavaScript 添加了磁盘I/O
- Node 自带了一个调试器和依赖管理器(npm)
- Node 可以用于搭建 Web应用程序、命令行工具、甚至桌面程序。