Nodejs实战 —— 欢迎进入Nodejs的世界
读 《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 库就好了。例如
db.insert(query, err => {});
然后,Node 就会帮你完成那些经过高度优化的非阻塞网络操作。
访问硬盘也差不多,但又不完全一样。在生成了回执邮件并从硬盘中读取邮件模板时,libuv 借助线程池模拟出了一种使用非阻塞调用的假象。管理线程池是个苦差事,想较而言,
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 对象来创建类似类的结构:
function User() {
// 构造器
}
User.prototype.method = function () {
// 方法
};
Node6 和 ES2015,写成
class User() {
constructor() {}
method() {}
}
Node 还支持了子类、超类和静态方法。
const 和 let 是从 Node 4 开始支持的。Node 还有原生的 promise 和 生成器。
[1, 2, 3].map(n => n * 2).filter(n => n > 3);
生成器能把异步 I/O 变成同步编程分隔。Koa Web 应用库中用到了生成器。Koa 使用 promise 和其他生成器就可以抛开层层嵌套的回调,在值上 yield。
ES2105 的模板字符串在 Node 中也非常好用。
this.body = `
<div>
<h1>Hello</h1>
<p>Welcome,${user.name}!</p>
</div>
`;
箭头函数,在 Node 中,一般需要两个参数,因为回调的第一个参数通常是错误对象,这时候需要用括号把参数括起来
const fs = require('fs');
fs.readFile('package.json', (err, text) => console.log('Length:', text.length));
在 ES5 及之前的版本的语言中,在函数中定义函数会把 this 引用变成全局对象。因为这个问题,下面按 ES5 写的类容易出错
function User(id) {
// 构造器
this.id = id;
}
User.prototype.load = function() {
var self = this;
var query = 'SELECT * FROM users WHERE id = ?';
sql.query(query, this, id, function(err, users) {
self.name = users[0].name;
});
}
给 self.name 赋值那行代码不能写成 this.name 。因为这个函数的 this 是全局变量。常用的解决办法就是在函数的入口将 this 赋值给一个变量,但箭头函数的绑定没有这个问题。
class User {
constructor(id) {
this.id = id;
}
load() {
const query = 'SELECT * FROM users WHERE id = ?';
sql.query(query, this.id, (err, users) => {
this.name = users[0].name;
});
}
}
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 特性稳定性较差,需要具体的特性参数来开启。
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。在命令行中输入下面这些命令:
mkdir example-project
cd example-project
npm init -y
打开 package.json,你会看到简单的 JSON 格式的项目描述信息。如果你现在用带有参数 --save
的 npm 命令从 npm 网站上安装一个包,它会自动更新你的 package.json 文件。试着输入 npm install
,或简写为 npm i
:
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)连接起来传输数据。
const fs = require('fs');
const zlib = require('zlib');
const gzip = zlib.createGzip();
const outStream = fs.createWriteStream('output.js.gz');
fs.createReadStream('./node-stream.js').pipe(gzip).pipe(outStream);
网络
在 Node 中搭一个服务器只需要加载 http 模块,然后给它一个函数。这个函数有两个参数,即请求和响应。
const http = require('http');
const port = 8080;
const server = http.createServer((req, res) => {
res.end('Hello world');
});
server.listen(port, () => {
console.log('Server listening on http://localhost:%s', port);
});
将上面的代码保存到 hellow.js 文件中,用 node hello.js 运行它,就可以在 http://locahost:8080 看到这段消息。
调试器
Node 自带调试器支持单步执行 REPL(读取-计算-输出-循环)。这个调试器在工作时会用一个网络协议跟你的程序对话。带着 debug 参数运行城西,就可以对这个程序开启调试器。比如要调试上面的代码
node debug hello.js
然后可以看到下面的输出
< Debugger listening on ws://127.0.0.1:9229/55258211-4e9c-444c-90eb-9f84c28fb532
< For help see https://nodejs.org/en/docs/inspector
> 1 (function (exports, require, module, __filename, __dirname) { const http = r
equire('http');
2 const port = 8080;
3 const server = http.createServer((req,res) =>{
我们可以在代码中的任何地方添加 debugger 语句来设置断点。遇到 debugger 语句后,调试器就会把程序停住,然后你可以输入命令。比如说,你写了一个 REST API 来为新用户创建账号,但发现代码貌似没有把新用户密码的散列值写到数据库里。你可以在 User 类的 save 方法那里加一个 debugger ,然后单步执行每一条指令,看看发生了什么。
交互式调试
Node 支持 Chrome 调试协议。如果要用 Chrome 的开发者工具调试一段脚本,可以在运行程序时加上 --inspect 参数:
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 应用程序
mkdir hello_express
cd hello_express
npm intt -y
npm i express --S
把代码存到 server.js 中
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000, () => {
console.log('Express web app on localhost:3000');
});
接着输入
npm start
启动这个监听端口 3000 的 Node Web 服务器。在浏览器打开就可以看到 res.send 里面的文本了。
命令行工具和后台程序
Node 可以用来编写命令行工具,比如 JavaScript 开发人员所用的进程管理器和转义器。它可以作为一种方便的方式来编写其他操作的命令行工具,比如图片转换、控制媒体文件的播放的脚本等。
下面的例子
// cli.js
const [nodePath, scriptPath, name] = process.argv;
console.log('Hello', name, nodePath, scriptPath);
然后运行
node cli.js yourName
会打印出来
Hello yourName
这里用了解构,从 process.argv 中拉取第三个参数。所有 Node 程序都可以访问 process 对象,这是用户向程序中传递参数的基础。
Node 命令行还可以做其他事情。如果在程序的开头加上 #!
, 并赋予其执行许可( chmod +x cli.js ),shell 就可以在调用程序时使用 Node。也就是说可以像运行其他 shell 脚本那样运行 Node 程序。在类 Unix 系统中用下面这样的代码:
#!/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 应用程序、命令行工具、甚至桌面程序。'