留给自己参考的Node.js学习提纲
目录:
- HelloWorld
- 回调函数
- 事件循环
- EventEmitter
- 模块系统
- web 模块
- express 框架
- Restful API
- MySQL
- Buffer/Stream
- 全局对象
- fs模块
- util模块
- 其余常用模块
HelloWorld
过程
新建一个 server.js 文件,输入以下代码:
1 | var http = require('http'); |
然后在命令行使用 node 命令执行以上代码:
1 | node server.js |
之后访问本地的 8888 端口,就可以看到我们的 HelloWorld 消息了。
1 | http://localhost:8888/ |
代码分析
从上面的代码中,我们可以看到,首先代码 require 了一个名为 http 的模块,用来提供 http 服务
之后使用 http.createServer 方法,传入了一个 function 作为回调方法,用来处理监听到的请求,之后在 8888 端口启动服务,监听该端口的请求。
在回调方法中可以看到,我们在响应中定义了响应头,其中响应的 http 状态码是200,内容类型是 text/plain,之后在响应中使用 end 方法写入数据并返回
回调函数
Node.js 异步编程的直接体现就是回调。
异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了。
回调函数在完成任务后就会被调用,Node 使用了大量的回调函数,Node 所有 API 都支持回调函数。
在之前学过的 ES6 Promise 中,大家一定深有体会。
回调函数一般作为函数的最后一个参数出现。
阻塞代码(同步)
1 | var fs = require("fs"); |
执行结果:
1 | $ node main.js |
可见程序是强顺序执行的,不到读取完毕文件,不会继续执行。
非阻塞代码(异步)
1 | var fs = require("fs"); |
执行结果:
1 | $ node main.js |
可见程序是先执行完当前任务,再在读取文件完成的时候打印文件内容的。
以上两个实例我们了解了阻塞与非阻塞调用的不同。
第一个实例在文件读取完后才执行程序, 第二个实例我们不需要等待文件读取完,这样就可以在读取文件时同时执行接下来的代码,大大提高了程序的性能。
因此,阻塞是按顺序执行的,而非阻塞是不需要按顺序的,所以如果需要处理回调函数的参数,我们就需要写在回调函数内。
为什么会先执行当前任务呢?我们将在下节介绍。
事件循环
众所周知,JavaScript 是单线程的,Node.js 也是单进程单线程应用程序,那么他是怎么提高运行效率的呢?
答案就是 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以效率相比普通的单线程大大提高。
事件驱动程序
Node.js 使用事件驱动模型,每当 web server 接收到一个新的请求,触发了对应的事件,就会将请求放入事件队列中,然后继续按队列顺序处理。
这个模型非常高效可扩展性非常强,因为 web server 一直接受请求而不等待任何读写操作。(这也称之为非阻塞式IO或者事件驱动IO)
事件发生流程一般如下:
- 将回调函数绑定到事件上
- 触发某个事件
- 将回调函数放入事件队列中
- 按队列顺序执行
我们可以通过内置模块 events 来模拟这个事件的流程。
1 | // 引入 events 模块 |
以下程序绑定事件处理程序:
1 | // 绑定事件及事件的处理程序 |
我们可以通过程序触发事件:
1 | // 触发事件 |
关于 events 模块,将在下一节详细介绍。
EventEmitter
Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。
Node.js 里面的许多对象都会分发事件:一个 net.Server 对象会在每次有新连接时触发一个事件, 一个 fs.readStream 对象会在文件被打开的时候触发一个事件。 所有这些产生事件的对象都是 events.EventEmitter 的实例。
EventEmitter类
Node.js 内置模块中含有一个名为 events 的模块,该模块向外 exports 了一个 events 对象,该对象包含一个名为 EventEmitter 的类。我们可以通过 require 命令来引入它。
1 | // 引入 events 模块 |
接下来我们来解析一下上篇提到的代码。
1 | // 引入 events 模块 |
执行结果:
1 | $ node main.js |
在上述代码中,我们创建了一个事件发生器,并通过 on 命令指定了两个事件及其回调,然后通过 emit 命令触发指定事件。
我们观察到,尽管先访问到了 setTimeout,但是其结果却在最后被打印,说明 eventEmitter 的执行逻辑与 setTimeout 并不相同。通过阅读源码,可知 eventEmitter 的执行方式类似于方法调用,此处暂略。
on 和 emit
上文代码中我们用到了 on 和 emit 两个命令。现在来看看他们的方法原型。
1 | on(event,listener) |
1 | emit(event,[data]) |
这是 EventEmitter 两个最为重要的方法。
其余方法如下:
1 | addListener(event, listener) |
1 | once(event, listener) |
1 | removeListener(event, listener) |
1 | removeAllListeners([event]) |
1 | setMaxListeners(n) |
1 | listeners(event) |
1 | listenerCount(event) |
以上是 EventEmitter 的方法,可以设置自定义事件。
EventEmitter 还有内置的事件。
1 | newListener |
1 | removeListener |
1 | error |
同步调用
上文说到,eventEmitter 的执行逻辑与 setTimeout 并不相同。那么具体是如何执行的呢?我们需要阅读 eventEmitter 的源码,或者自己手写一个。此处选择手写一个作为示例。
1 | class EventEmitter { |
可见,eventEmitter 是以类似方法调用的方式传递触发信号的,然后又以数组顺序为基础来进行顺序执行,所以整体上是同步执行的,不是 setTimeout 一样的异步执行。
比起一般的方法调用, eventEmitter 胜在一个入口可以调用多个(同一数组内)的方法。
模块系统
模块是Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件可能是JavaScript 代码、JSON 或者编译过的C/C++ 扩展。在模块系统的帮助下,Node.js的文件可以相互调用。
模块系统有两个重要动作:导入(require)和导出(exports)。
require
我们可以通过 require 命令来引入一个模块,例如:
1 | var hello = require('./hello'); |
在 require 之后,可以对这个模块做出哪些操作,则取决于这个模块 exports 了什么东西。
exports
对于任何一个模块,都可以通过操作内置对象 exports 来导出内容。
例如导出一个 function world,可以通过两种方案来实现。
exports.world = function (){}
module.exports = function (){}
第一种方案,在 require 之后,主模块引入了一个来自从模块的对象,该对象包含一个名为 world 的方法。
第二种方案,在 require 之后,主模块只引入了一个方法,该方法的名字取决于主模块中所起的变量名。
一般不建议两种方案同时使用,因为阅读 require 源码可以发现,在 require 一个模块的时候,会先执行 exports.xxx 的赋值,最后才执行 module.exports 的赋值,这使得之前所做的赋值被完全覆盖了,只有最后的赋值才有效。
模块分类
之前的章节中,我们使用了 http 模块,event 模块等。事实上,JavaScript 有 4 种模块,分为原生模块和 3 种文件模块,通过在 require 命令中指定的字符串不同而导入。
- http、fs、path等,原生模块。
- ./mod或../mod,相对路径的文件模块。
- /pathtomodule/mod,绝对路径的文件模块。
- mod,非原生模块的文件模块。
由于有多种模块,所以导入模块也有对应的优先级,如图。
web 模块
使用 Node.js 创建 web 客户端需要引入 http 模块。
我们可以用 Node.js 搭建一个 web 服务器与一个客户端。
服务器代码
1 | var http = require('http'); |
需要在服务器文件目录下创建一个 index.html 文件来配合。
1 |
|
客户端代码
1 | var http = require('http'); |
Express 框架
Express 是一个基于 Node.js 的 web 应用框架。
Express 框架的核心特性如下:
- 可以设置中间件来响应 HTTP 请求。
- 定义了路由表用于执行不同的 HTTP 请求动作。
- 可以通过向模板传递参数来动态渲染 HTML 页面。
express 可以通过 require(‘express’) 引入,暴露的是一个方法。使用例如下:
1 | var express = require('express'); |
从上述代码中,可以看到 app 有两个关键方法:get 和 listen
- get(url, func(request, response)),表示使用 http get 请求访问,第一个参数是访问的路径(可以是一个正则表达式,或是 Rest 风格的 url 字符串,详见下章),第二个参数是这个事件发生时的回调。其中回调含有 2 个参数:request 和 response,分别表示这个 http 请求的请求包体和响应包体。
- listen(port, func),返回一个 app 实例,监听来自端口 port 的所有请求,并在服务成功启动后调用指定的回调函数。
在 request 对象和 response 对象上,各自还有自己的对象方法。
Request 对象 - request 对象表示 HTTP 请求,包含了请求查询字符串,参数,内容,HTTP 头部等属性。常见属性有:
- req.app:当callback为外部文件时,用req.app访问express的实例
- req.baseUrl:获取路由当前安装的URL路径
- req.body / req.cookies:获得「请求主体」/ Cookies
- req.fresh / req.stale:判断请求是否还「新鲜」
- req.hostname / req.ip:获取主机名和IP地址
- req.originalUrl:获取原始请求URL
- req.params:获取路由的parameters
- req.path:获取请求路径
- req.protocol:获取协议类型
- req.query:获取URL的查询参数串
- req.route:获取当前匹配的路由
- req.subdomains:获取子域名
- req.accepts():检查可接受的请求的文档类型
- req.acceptsCharsets / req.acceptsEncodings / req.acceptsLanguages:返回指定字符集的第一个可接受字符编码
- req.get():获取指定的HTTP请求头
- req.is():判断请求头Content-Type的MIME类型
Response 对象 - response 对象表示 HTTP 响应,即在接收到请求时向客户端发送的 HTTP 响应数据。常见属性有:
- res.app:同req.app一样
- res.append():追加指定HTTP头
- res.set()在res.append()后将重置之前设置的头
- res.cookie(name,value [,option]):设置Cookie
- opition: domain / expires / httpOnly / maxAge / path / secure / signed
- res.clearCookie():清除Cookie
- res.download():传送指定路径的文件
- res.get():返回指定的HTTP头
- res.json():传送JSON响应
- res.jsonp():传送JSONP响应
- res.location():只设置响应的Location HTTP头,不设置状态码或者close response
- res.redirect():设置响应的Location HTTP头,并且设置状态码302
- res.render(view,[locals],callback):渲染一个view,同时向callback传递渲染后的字符串,如果在渲染过程中有错误发生next(err)将会被自动调用。callback将会被传入一个可能发生的错误以及渲染后的页面,这样就不会自动输出了。
- res.send():传送HTTP响应
- res.sendFile(path [,options] [,fn]):传送指定路径的文件 -会自动根据文件extension设定Content-Type
- res.set():设置HTTP头,传入object可以一次设置多个头
- res.status():设置HTTP状态码
- res.type():设置Content-Type的MIME类型
静态文件
可以使用形如 app.use('/public', express.static('public'));
的代码来设置静态文件路径。
该代码使用了 use 方法,该方法需要两个参数。
- url,表示 http 请求需要访问的路径
- express.static(localpath),其中 localpath 表示存放静态文件的文件夹的本地路径
比如此时我在 public 文件夹下存放了一张图片,名为 hello.png,则我访问 http://localhost:port/public/hello.png 时,页面上就会返回这张图片。
Restful API
Restful API 是用 http 动词描述动作,用 URL 定位资源的 API 设计风格。在 Express 框架中,我们可以使用 Restful 默认的 4 个动词来定义 app 监听。
- app.get(url, func(request, response))
- app.post(url, func(request, response))
- app.put(url, func(request, response))
- app.delete(url, func(request, response))
其中,绑定 url 时,要绑定指定的 id,可以使用形如 /user/:id
的 url,来访问路径上的动态参数。该写法在前端类似于 vue 的 router 绑定,在后端类似于 spring 的 @PathVariable
MySQL
Node.js 既然可以写后端,自然也可以连接 MySQL。容易想到,我们可以通过 require(‘mysql’) 来引入 mysql模块,不过需要先 npm install mysql
连接数据库
1 | var mysql = require('mysql'); |
通过向 createConnection 传入一个对象作为参数,来配置连接的必要参数。
之后可以通过 query 方法,来提交 sql 请求。query 方法包含 3 个参数。
query(sqlString, sqlParams, func(err, result))
在 sqlString 中,可以留若干个?作为占位符,?会按顺序匹配 sqlParams 中的参数。
sql 请求成功返回后,使用 func 作为回调函数。该回调函数含有 2 个参数,第一个是报错信息,第二个是 sql 执行结果。若没有出错,则 err 为空。
sql 请求的返回值与 spring mapper 类似,select、insert 返回具体数据,update、delete 返回的 result 对象中只包含一个 affectedRows 字段,表示影响的行数。
Buffer/Stream
Buffer
JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。
但在处理像 TCP 流或文件流时,必须使用到二进制数据。因此在 Node.js 中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。
Buffer 在使用时需要指定字符编码。支持的编码如下:
- ascii - 仅支持 7 位 ASCII 数据。如果设置去掉高位的话,这种编码是非常快的。
- utf8 - 多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8 。
- utf16le - 2 或 4 个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。
- ucs2 - utf16le 的别名。
- base64 - Base64 编码。
- latin1 - 一种把 Buffer 编码成一字节编码的字符串的方式。
- binary - latin1 的别名。
- hex - 将每个字节编码为两个十六进制字符。
Buffer 具有类似数组的特性。重要的 API 如下。
创建 Buffer
Buffer.alloc(size[, fill[, encoding]])
返回一个指定大小的 Buffer 实例,如果没有设置 fill,则默认填满 0
Buffer.allocUnsafe(size)
返回一个指定大小的 Buffer 实例,但是它不会被初始化,所以它可能包含敏感的数据
Buffer.allocUnsafeSlow(size)
Buffer.from(array)
返回一个被 array 的值初始化的新的 Buffer 实例(传入的 array 的元素只能是数字,不然就会自动被 0 覆盖)
Buffer.from(arrayBuffer[, byteOffset[, length]])
返回一个新建的与给定的 ArrayBuffer 共享同一内存的 Buffer。
Buffer.from(buffer)
复制传入的 Buffer 实例的数据,并返回一个新的 Buffer 实例
Buffer.from(string[, encoding])
返回一个被 string 的值初始化的新的 Buffer 实例
写入 Buffer
1 | buf.write(string[, offset[, length]][, encoding]) |
参数
参数描述如下:
- string - 写入缓冲区的字符串。
- offset - 缓冲区开始写入的索引值,默认为 0 。
- length - 写入的字节数,默认为 buffer.length
- encoding - 使用的编码。默认为 ‘utf8’ 。
根据 encoding 的字符编码写入 string 到 buf 中的 offset 位置。 length 参数是写入的字节数。 如果 buf 没有足够的空间保存整个字符串,则只会写入 string 的一部分。 只部分解码的字符不会被写入。
返回值
返回实际写入的大小。如果 buffer 空间不足, 则只会写入部分字符串。
实例
1 | buf = Buffer.alloc(256); |
执行以上代码,输出结果为:
1 | $node main.js |
其余与数组方法基本相同,详见 Node.js Buffer(缓冲区) | 菜鸟教程
Stream
Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)。
Node.js,Stream 有四种流类型:
- Readable - 可读操作。
- Writable - 可写操作。
- Duplex - 可读可写操作.
- Transform - 操作被写入数据,然后读出结果。
所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:
- data - 当有数据可读时触发。
- end - 没有更多的数据可读时触发。
- error - 在接收和写入过程中发生错误时触发。
- finish - 所有数据已被写入到底层系统时触发。
主要有三种流:
- 写入流
- 管道流
- 链式流
11-14 章 待写
待续……