发布于 

nodejs笔记二

Node.js基础

一、Node.js是什么

Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine.

1、特性

Node.js 可以解析JS代码(没有浏览器安全级别的限制)提供很多系统级别的API,如:

  • 文件的读写 (File System)
  • 进程的管理 (Process)
  • 网络通信 (HTTP/HTTPS)
  • ……

2、举例

2.1 浏览器安全级别的限制

Ajax测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>browser-safe-sandbox</title>
</head>
<body>
<div>browser-safe-sandbox</div>
<script>
const xhr = new XMLHttpRequest()
xhr.open('get', 'https://m.maoyan.com/ajax/moreClassicList?sortId=1&showType=3&limit=10&offset=30&optimus_uuid=A5518FF0AFEC11EAAB158D7AB0D05BBBD74C9789D9F649898982E6542C7DD479&optimus_risk_level=71&optimus_code=10', false)
xhr.send()
</script>
</body>
</html>

浏览器预览

1
browser-sync start --server --files **/* --directory
2.2 文件的读写 (File System)
1
2
3
4
5
const fs = require('fs')

fs.readFile('./ajax.png', 'utf-8', (err, content) => {
console.log(content)
})
2.3 进程的管理(Process)
1
2
3
4
5
function main(argv) {
console.log(argv)
}

main(process.argv.slice(2))

运行

1
node 2.3-process.js argv1 argv2
2.4 网络通信(HTTP/HTTPS)
1
2
3
4
5
6
7
8
9
const http = require("http")

http.createServer((req,res) => {
res.writeHead(200, {
"content-type": "text/plain"
})
res.write("hello nodejs")
res.end()
}).listen(3000)

二、Node 相关工具

1、NVM: Node Version Manager

1.1 Mac 安装 nvm

1
https://github.com/nvm-sh/nvm/blob/master/README.md

1.2 Windows 安装 nvm

1
2
nvm-windows
nodist

2、NPM: Node Package Manager

2.1 全局安装package
1
2
3
4
$ npm install forever --global (-g)
$ forever
$ npm uninstall forever --global
$ forever

全局安装包的目录

  • Mac

    1
    /Users/felix/.nvm/versions/node/nvm各个版本/bin/
  • Windows

    1
    C:\Users\你的用户名\AppData\Roaming\npm\node_modules
2.2 本地安装package
1
2
3
4
5
$ cd ~/desktop
$ mkdir gp-project
$ cd gp-project
$ npm install underscore
$ npm list (ls)
2.3 package.json初始化
1
2
3
4
$ pwd
$ npm init -y
$ ls
$ cat package.json
2.4 使用package.json
1
2
3
4
5
6
7
8
9
10
$ npm install underscore --save
$ cat package.json
$ npm install lodash --save-dev
$ cat package.json
$ rm -rf node_modules
$ ls
$ npm install
$ npm uninstall underscore --save
$ npm list | grep underscore
$ cat package.json
2.5 安装指定版本的包
1
2
3
4
5
6
7
8
$ pwd
$ npm list
$ npm info underscore
$ npm view underscore versions
$ npm install underscore@1.8.0
$ npm list
$ npm uninstall underscore
$ npm list
2.6 更新本地安装的包
1
2
3
4
5
6
7
$ npm info underscore
$ npm view underscore versions
$ npm install underscore@1.4.4 --save-dev
$ npm list | grep gulp
$ npm outdated //~2.0.0表示patch, ^2.0.0表示minor * 表示xx最新版本
$ npm list | grep gulp
$ npm update
2.7 清除缓存
1
npm cache clean --force
2.8 上传自己的包
2.8.1 编写模块

保存为index.js

1
2
3
exports.sayHello = function(){ 
return 'Hello World';
}
2.8.2 初始化包描述文件

$ npm init package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{ 
"name": "gp19-npm",
"version": "1.0.1",
"description": "gp19 self module",
"main": "index.js",
"scripts": {
"test": "make test"
},
"repository": {
"type": "Git",
"url": "git+https://github.com/lurongtao/gp19-npm.git"
},
"keywords": [
"demo"
],
"author": "Felixlu",
"license": "ISC",
"bugs": {
"url": "https://github.com/lurongtao/gp19-npm/issues"
},
"homepage": "https://github.com/lurongtao/gp19-npm#readme",
}
2.8.3 注册npm仓库账号
1
2
3
https://www.npmjs.com 上面的账号
felix_lurt/qqmko09ijn
$ npm adduser
2.8.4 上传包
1
$ npm publish

坑:403 Forbidden

1
2
3
查看npm源:npm config get registry
切换npm源方法一:npm config set registry http://registry.npmjs.org
切换npm源方法二:nrm use npm
2.8.5 安装包
1
$ npm install gp19-npm
2.8.6 卸载包
1
2
3
4
查看当前项目引用了哪些包 :
npm ls
卸载包:
npm unpublish --force
2.8.7 使用引入包
1
2
var hello = require('gp19-npm')
hello.sayHello()
2.9 npm 脚本

Node 开发离不开 npm,而脚本功能是 npm 最强大、最常用的功能之一。

一、什么是 npm 脚本?

npm 允许在 package.json 文件里面,使用 scripts 字段定义脚本命令。

1
2
3
4
5
6
{
// ...
"scripts": {
"build": "node build.js"
}
}

二、执行顺序

如果 npm 脚本里面需要执行多个任务,那么需要明确它们的执行顺序。

script1.js

1
2
var x = 0
console.log(x)

script2.js

1
2
3
4
5
6
var y = 0
console.log(y)
"scripts": {
"script1": "node script1.js",
"script2": "node script2.js"
}

如果是并行执行(即同时的平行执行),可以使用 & 符号。

1
$ npm run script1 & npm run script2

如果是继发执行(即只有前一个任务成功,才执行下一个任务),可以使用 && 符号。

1
$ npm run script1 && npm run script2

三、简写形式

常用的 npm 脚本简写形式。

1
npm start 是 npm run start

四、变量

npm 脚本有一个非常强大的功能,就是可以使用 npm 的内部变量。

首先,通过 npm_package_ 前缀,npm 脚本可以拿到 package.json 里面的字段。比如,下面是一个 package.json。

注意:一定要在 npm 脚本中运行(如:npm run view)才可以,直接在命令行中运行JS(如:node view.js)是拿不到值的

1
2
3
4
5
6
7
{
"name": "foo",
"version": "1.2.5",
"scripts": {
"view": "node view.js"
}
}

那么,变量 npm_package_name 返回 foo,变量 npm_package_version 返回 1.2.5。

1
2
3
// view.js
console.log(process.env.npm_package_name); // foo
console.log(process.env.npm_package_version); // 1.2.5

上面代码中,我们通过环境变量 process.env 对象,拿到 package.json 的字段值。如果是 Bash 脚本,可以用$npm_package_name 和 $npm_package_version 取到这两个值。

npmpackage前缀也支持嵌套的package.json字段。

1
2
3
4
5
6
7
"repository": {
"type": "git",
"url": "xxx"
},
scripts: {
"view": "echo $npm_package_repository_type"
}

上面代码中,repository 字段的 type 属性,可以通过 npm_package_repository_type 取到。

下面是另外一个例子。

1
2
3
"scripts": {
"install": "foo.js"
}

上面代码中,npm_package_scripts_install 变量的值等于 foo.js。

然后,npm 脚本还可以通过 npmconfig 前缀,拿到 npm 的配置变量,即 npm config get xxx 命令返回的值。比如,当前模块的发行标签,可以通过 npm_config_tag 取到。

1
"view": "echo $npm_config_tag",

注意,package.json 里面的 config 对象,可以被环境变量覆盖。

1
2
3
4
5
{ 
"name" : "foo",
"config" : { "port" : "8080" },
"scripts" : { "start" : "node server.js" }
}

上面代码中,npm_package_config_port 变量返回的是 8080。这个值可以用下面的方法覆盖。

1
$ npm config set foo:port 80

最后,env命令可以列出所有环境变量。

“env”: “env”

2.10 npm 安装 git 上发布的包
1
2
3
4
5
# 这样适合安装公司内部的git服务器上的项目
npm install git+https://git@github.com:lurongtao/gp-project.git

# 或者以ssh的方式
npm install git+ssh://git@github.com:lurongtao/gp-project.git
2.11 cross-env 使用
2.11.1 cross-env是什么

运行跨平台设置和使用环境变量的脚本

2.11.2 出现原因

当您使用 NODE_ENV=production, 来设置环境变量时,大多数 Windows 命令提示将会阻塞(报错)。(异常是Windows上的Bash,它使用本机Bash。)换言之,Windows 不支持 NODE_ENV=production 的设置方式。

2.11.3 解决

cross-env 使得您可以使用单个命令,而不必担心为平台正确设置或使用环境变量。这个迷你的包(cross-env)能够提供一个设置环境变量的 scripts,让你能够以 Unix 方式设置环境变量,然后在 Windows 上也能兼容运行。

2.11.4 安装

npm install –save-dev cross-env

2.11.5 使用
1
2
3
4
5
{
"scripts": {
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"
}
}

NODE_ENV环境变量将由 cross-env 设置 打印 process.env.NODE_ENV === ‘production’

3、NRM: npm registry manager

3.1 手工切换源
3.1.1 查看当前源
1
npm config get registry
3.1.2 切换淘宝源
1
npm config set registry https://registry.npm.taobao.org
3.2 NRM 管理源

NRM (npm registry manager)是npm的镜像源管理工具,有时候国外资源太慢,使用这个就可以快速地在 npm 源间切换。

3.2.1 安装 nrm

在命令行执行命令,npm install -g nrm,全局安装nrm。

3.2.2 使用 nrm

执行命令 nrm ls 查看可选的源。 其中,带*的是当前使用的源,上面的输出表明当前源是官方源。

3.2.3 切换 nrm

如果要切换到taobao源,执行命令nrm use taobao。

3.2.4 测试速度

你还可以通过 nrm test 测试相应源的响应时间。

1
nrm test

4、NPX: npm package extention

npm 从5.2版开始,增加了 npx 命令。它有很多用处,本文介绍该命令的主要使用场景。

Node 自带 npm 模块,所以可以直接使用 npx 命令。万一不能用,就要手动安装一下。

1
$ npm install -g npx
4.1 调用项目安装的模块

npx 想要解决的主要问题,就是调用项目内部安装的模块。比如,项目内部安装了Mocha。

1
$ npm install -D mocha

一般来说,调用 Mocha ,只能在项目脚本和 package.json 的scripts字段里面,如果想在命令行下调用,必须像下面这样。

1
2
# 项目的根目录下执行
$ node-modules/.bin/mocha --version

npx 就是想解决这个问题,让项目内部安装的模块用起来更方便,只要像下面这样调用就行了。

1
$ npx mocha --version

npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。

由于 npx 会检查环境变量$PATH,所以系统命令也可以调用。

1
2
# 等同于 ls
$ npx ls

注意,Bash 内置的命令不在$PATH里面,所以不能用。比如,cd是 Bash 命令,因此就不能用npx cd。

4.2 避免全局安装模块

除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app 这个模块是全局安装,npx 可以运行它,而且不进行全局安装。

1
$ npx create-react-app my-react-app

上面代码运行时,npx 将 create-react-app 下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载 create-react-app。

注意,只要 npx 后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装http-server模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。

1
$ npx http-server
4.3 –no-install 参数和 –ignore-existing 参数

如果想让 npx 强制使用本地模块,不下载远程模块,可以使用–no-install参数。如果本地不存在该模块,就会报错。

1
$ npx --no-install http-server

反过来,如果忽略本地的同名模块,强制安装使用远程模块,可以使用–ignore-existing参数。比如,本地已经安装了http-server,但还是想使用远程模块,就用这个参数。

1
$ npx --ignore-existing http-server

三、模块/包 与 CommonJS

1、模块/包分类

Node.js 有三类模块,即内置的模块、第三方的模块、自定义的模块。

1.1 内置的模块

Node.js 内置模块又叫核心模块,Node.js安装完成可直接使用。如:

1
2
3
const path = require('path')
var extname = path.extname('index.html')
console.log(extname)
1.2 第三方的Node.js模块

第三方的Node.js模块指的是为了实现某些功能,发布的npmjs.org上的模块,按照一定的开源协议供社群使用。如:

1
2
3
npm install chalk
const chalk = require('chalk')
console.log(chalk.blue('Hello world!'))
1.3 自定义的Node.js模块

自定义的Node.js模块,也叫文件模块,是我们自己写的供自己使用的模块。同时,这类模块发布到npmjs.org上就成了开源的第三方模块。

自定义模块是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程、速度相比核心模块稍微慢一些,但是用的非常多。

1.3.1 模块定义、接口暴露和引用接口

我们可以把公共的功能 抽离成为一个单独的 js 文件 作为一个模块,默认情况下面这个模块里面的方法或者属性,外面是没法访问的。如果要让外部可以访问模块里面的方法或者属性,就必须在模块里面通过 exports 或者 module.exports 暴露属性或者方法。

m1.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const name = 'gp19'

const sayName = () => {
console.log(name)
}

console.log('module 1')

// 接口暴露方法一:
module.exports = {
say: sayName
}

// 接口暴露方法二:
exports.say = sayName

// 错误!
exports = {
say: sayName
}

main.js:

1
2
const m1 = require('./m1')
m1.say()
1.3.2 模块的循环引用

由于 exports 使用方式方式不对,会在两个不同 js 循环引用的情况下,导致其中一个 js 无法获取另外一个 js 的方法,从而导致执行报错。如:

  • a.js
1
2
3
4
5
exports.done = false
const b = require('./b.js')
console.log('in a, b.done = %j', b.done)
exports.done = true
console.log('a done')
  • b.js
1
2
3
4
5
6
console.log('b starting')
exports.done = false
const a = require('./a.js')
console.log('in b, a.done = %j', a.done)
exports.done = true
console.log('b done')
  • main.js
1
2
3
4
console.log('main starting')
const a = require('./a.js')
const b = require('./b.js')
console.log('in main, a.done = %j, b.done = %j', a.done, b.done)

main.js 首先会 load a.js, 此时执行到const b = require(‘./b.js’);的时候,程序会转去loadb.js, 在b.js中执行到const a = require(‘./a.js’); 为了防止无限循环,将a.jsexports的未完成副本返回到b.js模块。然后b.js完成加载,并将其导出对象提供给a.js模块。

我们知道nodeJs的对每个js文件进行了一层包装称为module,module中有一个属性exports,当调用require(‘a.js’)的时候其实返回的是module.exports对象,module.exports初始化为一个{}空的object,所以在上面的例子中,执行到b.js中const a = require(‘./a.js’);时不会load新的a module, 而是将已经load但是还未完成的a module的exports属性返回给b module,所以b.js拿到的是a module的exports对象,即:{done:false}, 虽然在a.js中exports.done被修改成了true,但是由于此时a.js未load完成,所以在b.js输出的a module的属性done为false,而在main.js中输出的a module的属性done为true. Nodejs通过上面这种返回未完成exports对象来解决循环引用的问题。

四、常用内置模块

这里介绍几个常用的内置模块:url, querystring, http, events, fs, stream, readline, crypto, zlib

1、url
1.1 parse

url.parse(urlString[, parseQueryString[, slashesDenoteHost]])

1
2
3
4
const url = require('url')
const urlString = 'https://www.baidu.com:443/ad/index.html?id=8&name=mouse#tag=110'
const parsedStr = url.parse(urlString)
console.log(parsedStr)
1.2 format

url.format(urlObject)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const url = require('url')
const urlObject = {
protocol: 'https:',
slashes: true,
auth: null,
host: 'www.baidu.com:443',
port: '443',
hostname: 'www.baidu.com',
hash: '#tag=110',
search: '?id=8&name=mouse',
query: { id: '8', name: 'mouse' },
pathname: '/ad/index.html',
path: '/ad/index.html?id=8&name=mouse',
href: 'https://www.baidu.com:443/ad/index.html?id=8&name=mouse#tag=110'
}
const parsedObj = url.format(urlObject)
console.log(parsedObj)
1.3 resolve

url.resolve(from, to)

1
2
3
4
5
const url = require('url')
var a = url.resolve('/one/two/three', 'four')
var b = url.resolve('http://example.com/', '/one')
var c = url.resolve('http://example.com/one', '/two')
console.log(a + "," + b + "," + c)

2、querystring

2.1 parse

querystring.parse(str[, sep[, eq[, options]]])

1
2
3
4
const querystring = require('querystring')
var qs = 'x=3&y=4'
var parsed = querystring.parse(qs)
console.log(parsed)
2.2 stringify

querystring.stringify(obj[, sep[, eq[, options]]])

1
2
3
4
5
6
7
const querystring = require('querystring')
var qo = {
x: 3,
y: 4
}
var parsed = querystring.stringify(qo)
console.log(parsed)
2.3 escape/unescape

querystring.escape(str)

1
2
3
4
const querystring = require('querystring')
var str = 'id=3&city=北京&url=https://www.baidu.com'
var escaped = querystring.escape(str)
console.log(escaped)

querystring.unescape(str)

1
2
3
4
const querystring = require('querystring')
var str = 'id%3D3%26city%3D%E5%8C%97%E4%BA%AC%26url%3Dhttps%3A%2F%2Fwww.baidu.com'
var unescaped = querystring.unescape(str)
console.log(unescaped)

3、http/https

3.1 get
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var http = require('http')
var https = require('https')

// 1、接口 2、跨域
const server = http.createServer((request, response) => {
var url = request.url.substr(1)

var data = ''

response.writeHeader(200, {
'content-type': 'application/json;charset=utf-8',
'Access-Control-Allow-Origin': '*'
})

https.get(`https://m.lagou.com/listmore.json${url}`, (res) => {

res.on('data', (chunk) => {
data += chunk
})

res.on('end', () => {
response.end(JSON.stringify({
ret: true,
data
}))
})
})

})

server.listen(8080, () => {
console.log('localhost:8080')
})
3.2 post:服务器提交(攻击)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const https = require('https')
const querystring = require('querystring')

const postData = querystring.stringify({
province: '上海',
city: '上海',
district: '宝山区',
address: '同济支路199号智慧七立方3号楼2-4层',
latitude: 43.0,
longitude: 160.0,
message: '求购一条小鱼',
contact: '13666666',
type: 'sell',
time: 1571217561
})

const options = {
protocol: 'https:',
hostname: 'ik9hkddr.qcloud.la',
method: 'POST',
port: 443,
path: '/index.php/trade/add_item',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData)
}
}

function doPost() {
let data

let req = https.request(options, (res) => {
res.on('data', chunk => data += chunk)
res.on('end', () => {
console.log(data)
})
})

req.write(postData)
req.end()
}

// setInterval(() => {
// doPost()
// }, 1000)
3.3 跨域:jsonp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const http = require('http')
const url = require('url')

const app = http.createServer((req, res) => {
let urlObj = url.parse(req.url, true)

switch (urlObj.pathname) {
case '/api/user':
res.end(`${urlObj.query.cb}({"name": "gp145"})`)
break
default:
res.end('404.')
break
}
})

app.listen(8080, () => {
console.log('localhost:8080')
})
3.4 跨域:CORS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const http = require('http')
const url = require('url')
const querystring = require('querystring')

const app = http.createServer((req, res) => {
let data = ''
let urlObj = url.parse(req.url, true)

res.writeHead(200, {
'content-type': 'application/json;charset=utf-8',
'Access-Control-Allow-Origin': '*'
})

req.on('data', (chunk) => {
data += chunk
})

req.on('end', () => {
responseResult(querystring.parse(data))
})

function responseResult(data) {
switch (urlObj.pathname) {
case '/api/login':
res.end(JSON.stringify({
message: data
}))
break
default:
res.end('404.')
break
}
}
})

app.listen(8080, () => {
console.log('localhost:8080')
})
3.5 跨域:middleware(http-proxy-middware)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const http = require('http')
const proxy = require('http-proxy-middleware')

http.createServer((req, res) => {
let url = req.url

res.writeHead(200, {
'Access-Control-Allow-Origin': '*'
})

if (/^\/api/.test(url)) {
let apiProxy = proxy('/api', {
target: 'https://m.lagou.com',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
})

// http-proy-middleware 在Node.js中使用的方法
apiProxy(req, res)
} else {
switch (url) {
case '/index.html':
res.end('index.html')
break
case '/search.html':
res.end('search.html')
break
default:
res.end('[404]page not found.')
}
}
}).listen(8080)
3.6 爬虫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const https = require('https')
const http = require('http')
const cheerio = require('cheerio')

http.createServer((request, response) => {
response.writeHead(200, {
'content-type': 'application/json;charset=utf-8'
})

const options = {
protocol: 'https:',
hostname: 'maoyan.com',
port: 443,
path: '/',
method: 'GET'
}

const req = https.request(options, (res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk
})

res.on('end', () => {
filterData(data)
})
})

function filterData(data) {
let $ = cheerio.load(data)
let $movieList = $('.movie-item')
let movies = []
$movieList.each((index, value) => {
movies.push({
title: $(value).find('.movie-title').attr('title'),
score: $(value).find('.movie-score i').text(),
})
})

response.end(JSON.stringify(movies))
}

req.end()
}).listen(9000)

4、Events

1
2
3
4
5
6
7
8
9
10
11
12
const EventEmitter = require('events')

class MyEventEmitter extends EventEmitter {}

const event = new MyEventEmitter()

event.on('play', (movie) => {
console.log(movie)
})

event.emit('play', '我和我的祖国')
event.emit('play', '中国机长')

5、File System

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
const fs = require('fs')
const fsP = require('fs').promises

// 创建文件夹
fs.mkdir('./logs', (err) => {
console.log('done.')
})

// 文件夹改名
fs.rename('./logs', './log', () => {
console.log('done')
})

// 删除文件夹
fs.rmdir('./log', () => {
console.log('done.')
})

// 写内容到文件里
fs.writeFile(
'./logs/log1.txt',
'hello',
// 错误优先的回调函数
(err) => {
if (err) {
console.log(err.message)
} else {
console.log('文件创建成功')
}
}
)

// 给文件追加内容
fs.appendFile('./logs/log1.txt', '\nworld', () => {
console.log('done.')
})

// 读取文件内容
fs.readFile('./logs/log1.txt', 'utf-8', (err, data) => {
console.log(data)
})

// 删除文件
fs.unlink('./logs/log1.txt', (err) => {
console.log('done.')
})

// 批量写文件
for (var i = 0; i < 10; i++) {
fs.writeFile(`./logs/log-${i}.txt`, `log-${i}`, (err) => {
console.log('done.')
})
}

// 读取文件/目录信息
fs.readdir('./', (err, data) => {
data.forEach((value, index) => {
fs.stat(`./${value}`, (err, stats) => {
// console.log(value + ':' + stats.size)
console.log(value + ' is ' + (stats.isDirectory() ? 'directory' : 'file'))
})
})
})

// 同步读取文件
try {
const content = fs.readFileSync('./logs/log-1.txt', 'utf-8')
console.log(content)
console.log(0)
} catch (e) {
console.log(e.message)
}

console.log(1)

// 异步读取文件:方法一
fs.readFile('./logs/log-0.txt', 'utf-8', (err, content) => {
console.log(content)
console.log(0)
})
console.log(1)

// 异步读取文件:方法二
fs.readFile('./logs/log-0.txt', 'utf-8').then(result => {
console.log(result)
})

// 异步读取文件:方法三
function getFile() {
return new Promise((resolve) => {
fs.readFile('./logs/log-0.txt', 'utf-8', (err, data) => {
resolve(data)
})
})
}

;(async () => {
console.log(await getFile())
})()

// 异步读取文件:方法四
const fsp = fsP.readFile('./logs/log-1.txt', 'utf-8').then((result) => {
console.log(result)
})

console.log(fsP)

// watch 监测文件变化
fs.watch('./logs/log-0.txt', () => {
console.log(0)
})

6、Stream

1
2
3
4
5
6
const fs = require('fs')

const readstream = fs.createReadStream('./note.txt')
const writestream = fs.createWriteStream('./note2.txt')

writestream.write(readstream)

7、Zlib

1
2
3
4
5
6
7
8
9
10
11
12
13
const fs = require('fs')
const zlib = require('zlib')

const gzip = zlib.createGzip()

const readstream = fs.createReadStream('./note.txt')
const writestream = fs.createWriteStream('./note2.txt')

readstream
.pipe(gzip)
.pipe(writestream)

writestream.write(readstream)

8、ReadLine

1
2
3
4
5
6
7
8
9
10
11
12
13
const readline = require('readline')

const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})

rl.question('What do you think of Node.js? ', (answer) => {
// TODO: Log the answer in a database
console.log(`Thank you for your valuable feedback: ${answer}`)

rl.close()
})

9、Crypto

1
2
3
4
5
6
7
const crypto = require('crypto')

const secret = 'abcdefg'
const hash = crypto.createHmac('sha256', secret)
.update('I love you')
.digest('hex')
console.log(hash)

四、路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var http = require('http')
var fs = require('fs')

http.createServer( function ( req, res ) {

switch ( req.url ) {
case '/home':
res.write('home')
res.end()
break
case '/mine':
res.write('mine')
res.end()
break
case '/login':
fs.readFile( './static/login.html',function ( error , data ) {
if ( error ) throw error
res.write( data )
res.end()
})
break
case '/fulian.jpg':
fs.readFile( './static/fulian.jpg', 'binary', function( error , data ) {
if( error ) throw error
res.write( data, 'binary' )
res.end()
})
break
default:
break
}

}).listen( 8000, 'localhost', function () {
console.log( '服务器运行在: http://localhost:8000' )
})

五、静态资源服务

5.1 readStaticFile

/modules/readStaticFile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 引入依赖的模块
var path = require('path')
var fs = require('fs')
var mime = require('mime')

function readStaticFile(res, filePathname) {

var ext = path.parse(filePathname).ext
var mimeType = mime.getType(ext)

// 判断路径是否有后缀, 有的话则说明客户端要请求的是一个文件
if (ext) {
// 根据传入的目标文件路径来读取对应文件
fs.readFile(filePathname, (err, data) => {
// 错误处理
if (err) {
res.writeHead(404, { "Content-Type": "text/plain" })
res.write("404 - NOT FOUND")
res.end()
} else {
res.writeHead(200, { "Content-Type": mimeType })
res.write(data)
res.end()
}
});
// 返回 true 表示, 客户端想要的 是 静态文件
return true
} else {
// 返回 false 表示, 客户端想要的 不是 静态文件
return false
}
}

// 导出函数
module.exports = readStaticFile

5.2 server

/server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 引入相关模块
var http = require('http');
var url = require('url');
var path = require('path');
var readStaticFile = require('./modules/readStaticFile');

// 搭建 HTTP 服务器
var server = http.createServer(function(req, res) {
var urlObj = url.parse(req.url);
var urlPathname = urlObj.pathname;
var filePathname = path.join(__dirname, "/public", urlPathname);

// 读取静态文件
readStaticFile(res, filePathname);
});

// 在 3000 端口监听请求
server.listen(3000, function() {
console.log("服务器运行中.");
console.log("正在监听 3000 端口:")
})

5.3 最终目录结构

Yarn 入门

Yarn 对你的代码来说是一个包管理器。它可以让你使用并分享全世界开发者的(例如 JavaScript)代码。 Yarn 能够快速、安全、并可靠地完成这些工作,所以你不用有任何担心。

通过Yarn你可以使用其他开发者针对不同问题的解决方案,使自己的开发过程更简单。

代码通过包(package) (或者称为 模块(module)) 的方式来共享。 一个包里包含所有需要共享的代码,以及描述包信息的文件,称为 package.json。

1、安装

yarn 安装请进 传送门

2、Yarn 使用方法

现在 Yarn 已经 安装完毕,可以开始使用了。 以下是一些你需要的最常用的命令:

2.1 初始化一个新项目

1
yarn init

2.2 添加依赖包

1
2
3
yarn add [package]
yarn add [package]@[version]
yarn add [package]@[tag]

2.3 将依赖项添加到不同依赖项类别中

分别添加到 devDependencies、peerDependencies 和 optionalDependencies 类别中:

1
2
3
yarn add [package] --dev
yarn add [package] --peer
yarn add [package] --optional

devDependencies、peerDependencies 和 optionalDependencies区别

在一个Node.js项目中,package.json几乎是一个必须的文件,它的主要作用就是管理项目中所使用到的外部依赖包,同时它也是npm命令的入口文件。

npm 目前支持以下几类依赖包管理:

  • dependencies
  • devDependencies
  • peerDependencies
  • optionalDependencies
  • bundledDependencies / bundleDependencies

dependencies

应用依赖,或者叫做业务依赖,这是我们最常用的依赖包管理对象!它用于指定应用依赖的外部包,这些依赖是应用发布后正常执行时所需要的,但不包含测试时或者本地打包时所使用的包。

devDependencies

开发环境依赖,仅次于dependencies的使用频率!它的对象定义和dependencies一样,只不过它里面的包只用于开发环境,不用于生产环境,这些包通常是单元测试或者打包工具等,例如gulp, grunt, webpack, moca, coffee等。

peerDependencies

同等依赖,或者叫同伴依赖,用于指定当前包(也就是你写的包)兼容的宿主版本。如何理解呢? 试想一下,我们编写一个gulp的插件,而gulp却有多个主版本,我们只想兼容最新的版本,此时就可以用同等依赖(peerDependencies)来指定。

1
2
3
4
5
6
7
{
"name": "gulp-my-plugin",
"version": "0.0.1",
"peerDependencies": {
"gulp": "3.x"
}
}

optionalDependencies

可选依赖,如果有一些依赖包即使安装失败,项目仍然能够运行或者希望npm继续运行,就可以使用optionalDependencies。另外optionalDependencies会覆盖dependencies中的同名依赖包,所以不要在两个地方都写。

bundledDependencies / bundleDependencies

打包依赖,bundledDependencies是一个包含依赖包名的数组对象,在发布时会将这个对象中的包打包到最终的发布包里。

2.4 升级依赖包

1
2
3
yarn upgrade [package]
yarn upgrade [package]@[version]
yarn upgrade [package]@[tag]

2.5 移除依赖包

1
yarn remove [package]

2.6 安装项目的全部依赖

1
yarn

或者

1
yarn install

Express

基于 Node.js 平台,快速、开放、极简的 web 开发框架。

1
$ npm install express --save

一、特色

1、Web 应用

Express 是一个基于 Node.js 平台的极简、灵活的 web 应用开发框架,它提供一系列强大的特性,帮助你创建各种 Web 和移动设备应用。

2、API

丰富的 HTTP 快捷方法和任意排列组合的 Connect 中间件,让你创建健壮、友好的 API 变得既快速又简单。

3、性能

Express 不对 Node.js 已有的特性进行二次抽象,我们只是在它之上扩展了 Web 应用所需的基本功能。

二、安装

首先假定你已经安装了 Node.js,接下来为你的应用创建一个目录,然后进入此目录并将其作为当前工作目录。

1
2
$ mkdir myapp
$ cd myapp

通过 npm init 命令为你的应用创建一个 package.json 文件。 欲了解 package.json 是如何起作用的,请参考 Specifics of npm’s package.json handling。

1
$ npm init

此命令将要求你输入几个参数,例如此应用的名称和版本。 你可以直接按“回车”键接受默认设置即可,下面这个除外:

1
entry point: (index.js)

键入 app.js 或者你所希望的名称,这是当前应用的入口文件。如果你希望采用默认的 index.js 文件名,只需按“回车”键即可。

接下来安装 Express 并将其保存到依赖列表中:

1
$ npm install express --save

如果只是临时安装 Express,不想将它添加到依赖列表中,只需略去 –save 参数即可:

1
$ npm install express

安装 Node 模块时,如果指定了 –save 参数,那么此模块将被添加到 package.json 文件中 dependencies 依赖列表中。 然后通过 npm install 命令即可自动安装依赖列表中所列出的所有模块。

三、Hello world 实例

接下来,我们一起创建一个基本的 Express 应用。

注意:这里所创建是一个最最简单的 Express 应用,并且仅仅只有一个文件 — 和通过 Express 应用生成器 所创建的应用完全不一样,Express 应用生成器所创建的应用框架包含多 JavaScript 文件、Jade 模板和针对不同用途的子目录。

进入 myapp 目录,创建一个名为 app.js 的文件,然后将下列代码复制进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
var express = require('express');
var app = express();

app.get('/', function (req, res) {
res.send('Hello World!');
});

var server = app.listen(3000, function () {
var host = server.address().address;
var port = server.address().port;

console.log('Example app listening at http://%s:%s', host, port);
});

上面的代码启动一个服务并监听从 3000 端口进入的所有连接请求。他将对所有 (/) URL 或 路由 返回 “Hello World!” 字符串。对于其他所有路径全部返回 404 Not Found。

req (请求) 和 res (响应) 与 Node 提供的对象完全一致,因此,你可以调用 req.pipe()、req.on(‘data’, callback) 以及任何 Node 提供的方法。

通过如下命令启动此应用:

1
$ node app.js

然后在浏览器中打开 http://localhost:3000/ 并查看输出结果。

四、路由

路由是指如何定义应用的端点(URIs)以及如何响应客户端的请求。

路由是由一个 URI、HTTP 请求(GET、POST等)和若干个句柄组成,它的结构如下: app.METHOD(path, [callback…], callback), app 是 express 对象的一个实例, METHOD 是一个 HTTP 请求方法, path 是服务器上的路径, callback 是当路由匹配时要执行的函数。

下面是一个基本的路由示例:

1
2
3
4
5
6
7
var express = require('express');
var app = express();

// respond with "hello world" when a GET request is made to the homepage
app.get('/', function(req, res) {
res.send('hello world');
});

1、路由方法

路由方法源于 HTTP 请求方法,和 express 实例相关联。

下面这个例子展示了为应用跟路径定义的 GET 和 POST 请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// GET method route
// 对网站首页的访问返回 "Hello World!" 字样
app.get('/', function (req, res) {
res.send('Hello World!')
})

// 网站首页接受 POST 请求
app.post('/', function (req, res) {
res.send('Got a POST request')
})

// /user 节点接受 PUT 请求
app.put('/user', function (req, res) {
res.send('Got a PUT request at /user')
})

// /user 节点接受 DELETE 请求
app.delete('/user', function (req, res) {
res.send('Got a DELETE request at /user')
})

Express 定义了如下和 HTTP 请求对应的路由方法: get, post, put, head, delete, options, trace, copy, lock, mkcol, move, purge, propfind, proppatch, unlock, report, mkactivity, checkout, merge, m-search, notify, subscribe, unsubscribe, patch, search, 和 connect。

有些路由方法名不是合规的 JavaScript 变量名,此时使用括号记法,比如: app[‘m-search’](‘/‘, function …

app.all() 是一个特殊的路由方法,没有任何 HTTP 方法与其对应,它的作用是对于一个路径上的所有请求加载中间件。

在下面的例子中,来自 “/secret” 的请求,不管使用 GET、POST、PUT、DELETE 或其他任何 http 模块支持的 HTTP 请求,句柄都会得到执行。

1
2
3
4
app.all('/secret', function (req, res, next) {
console.log('Accessing the secret section ...')
next(); // pass control to the next handler
})

2、路由路径

路由路径和请求方法一起定义了请求的端点,它可以是字符串、字符串模式或者正则表达式。

Express 使用 path-to-regexp 匹配路由路径,请参考文档查阅所有定义路由路径的方法。 Express Route Tester 是测试基本 Express 路径的好工具,但不支持模式匹配。

查询字符串不是路由路径的一部分。

使用字符串的路由路径示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 匹配根路径的请求
app.get('/', function (req, res) {
res.send('root');
});

// 匹配 /about 路径的请求
app.get('/about', function (req, res) {
res.send('about');
});

// 匹配 /random.text 路径的请求
app.get('/random.text', function (req, res) {
res.send('random.text');
});

使用字符串模式的路由路径示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 匹配 acd 和 abcd
app.get('/ab?cd', function(req, res) {
res.send('ab?cd');
});

// 匹配 abcd、abbcd、abbbcd等
app.get('/ab+cd', function(req, res) {
res.send('ab+cd');
});

// 匹配 abcd、abxcd、abRABDOMcd、ab123cd等
app.get('/ab*cd', function(req, res) {
res.send('ab*cd');
});

// 匹配 /abe 和 /abcde
app.get('/ab(cd)?e', function(req, res) {
res.send('ab(cd)?e');
});

字符 ?、+、* 和 () 是正则表达式的子集,- 和 . 在基于字符串的路径中按照字面值解释。

使用正则表达式的路由路径示例:

1
2
3
4
5
6
7
8
9
// 匹配任何路径中含有 a 的路径:
app.get(/a/, function(req, res) {
res.send('/a/');
});

// 匹配 butterfly、dragonfly,不匹配 butterflyman、dragonfly man等
app.get(/.*fly$/, function(req, res) {
res.send('/.*fly$/');
});

3、路由句柄

可以为请求处理提供多个回调函数,其行为类似 中间件。唯一的区别是这些回调函数有可能调用 next(‘route’) 方法而略过其他路由回调函数。可以利用该机制为路由定义前提条件,如果在现有路径上继续执行没有意义,则可将控制权交给剩下的路径。

路由句柄有多种形式,可以是一个函数、一个函数数组,或者是两者混合,如下所示.

使用一个回调函数处理路由:

1
2
3
app.get('/example/a', function (req, res) {
res.send('Hello from A!');
});

使用多个回调函数处理路由(记得指定 next 对象):

1
2
3
4
5
6
app.get('/example/b', function (req, res, next) {
console.log('response will be sent by the next function ...');
next();
}, function (req, res) {
res.send('Hello from B!');
});

使用回调函数数组处理路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var cb0 = function (req, res, next) {
console.log('CB0')
next()
}

var cb1 = function (req, res, next) {
console.log('CB1')
next()
}

var cb2 = function (req, res) {
res.send('Hello from C!')
}

app.get('/example/c', [cb0, cb1, cb2])

混合使用函数和函数数组处理路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var cb0 = function (req, res, next) {
console.log('CB0')
next()
}

var cb1 = function (req, res, next) {
console.log('CB1')
next()
}

app.get('/example/d', [cb0, cb1], function (req, res, next) {
console.log('response will be sent by the next function ...')
next()
}, function (req, res) {
res.send('Hello from D!')
})

4、响应方法

下表中响应对象(res)的方法向客户端返回响应,终结请求响应的循环。如果在路由句柄中一个方法也不调用,来自客户端的请求会一直挂起。

方法 描述
res.download() 提示下载文件。
res.end() 终结响应处理流程。
res.json() 发送一个 JSON 格式的响应。
res.jsonp() 发送一个支持 JSONP 的 JSON 格式的响应。
res.redirect() 重定向请求。
res.render() 渲染视图模板。
res.send() 发送各种类型的响应。
res.sendFile 以八位字节流的形式发送文件。
res.sendStatus() 设置响应状态代码,并将其以字符串形式作为响应体的一部分发送。

5、app.route()

可使用 app.route() 创建路由路径的链式路由句柄。由于路径在一个地方指定,这样做有助于创建模块化的路由,而且减少了代码冗余和拼写错误。

下面这个示例程序使用 app.route() 定义了链式路由句柄。

1
2
3
4
5
6
7
8
9
10
app.route('/book')
.get(function(req, res) {
res.send('Get a random book');
})
.post(function(req, res) {
res.send('Add a book');
})
.put(function(req, res) {
res.send('Update the book');
});

6、express.Router

可使用 express.Router 类创建模块化、可挂载的路由句柄。Router 实例是一个完整的中间件和路由系统,因此常称其为一个 “mini-app”。

下面的实例程序创建了一个路由模块,并加载了一个中间件,定义了一些路由,并且将它们挂载至应用的路径上。

在 app 目录下创建名为 birds.js 的文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var express = require('express');
var router = express.Router();

// 该路由使用的中间件
router.use(function timeLog(req, res, next) {
console.log('Time: ', Date.now());
next();
});
// 定义网站主页的路由
router.get('/', function(req, res) {
res.send('Birds home page');
});
// 定义 about 页面的路由
router.get('/about', function(req, res) {
res.send('About birds');
});

module.exports = router;

然后在应用中加载路由模块:

1
2
3
var birds = require('./birds')
...
app.use('/birds', birds)

应用即可处理发自 /birds 和 /birds/about 的请求,并且调用为该路由指定的 timeLog 中间件。

五、利用 Express 托管静态文件

通过 Express 内置的 express.static 可以方便地托管静态文件,例如图片、CSS、JavaScript 文件等。

将静态资源文件所在的目录作为参数传递给 express.static 中间件就可以提供静态资源文件的访问了。例如,假设在 public 目录放置了图片、CSS 和 JavaScript 文件,你就可以:

1
app.use(express.static('public'))

现在,public 目录下面的文件就可以访问了。

1
2
3
4
5
http://localhost:3000/images/kitten.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/images/bg.png
http://localhost:3000/hello.html

所有文件的路径都是相对于存放目录的,因此,存放静态文件的目录名不会出现在 URL 中。

如果你的静态资源存放在多个目录下面,你可以多次调用 express.static 中间件:

1
2
app.use(express.static('public'))
app.use(express.static('files'))

访问静态资源文件时,express.static 中间件会根据目录添加的顺序查找所需的文件。

如果你希望所有通过 express.static 访问的文件都存放在一个“虚拟(virtual)”目录(即目录根本不存在)下面,可以通过为静态资源目录指定一个挂载路径的方式来实现,如下所示:

1
app.use('/static', express.static('public'))

现在,你就可以通过带有 “/static” 前缀的地址来访问 public 目录下面的文件了。

1
2
3
4
5
http://localhost:3000/static/images/kitten.jpg
http://localhost:3000/static/css/style.css
http://localhost:3000/static/js/app.js
http://localhost:3000/static/images/bg.png
http://localhost:3000/static/hello.html

六、使用中间件

Express 是一个自身功能极简,完全是由路由和中间件构成一个的 web 开发框架:从本质上来说,一个 Express 应用就是在调用各种中间件。

中间件(Middleware) 是一个函数,它可以访问请求对象(request object (req)), 响应对象(response object (res)), 和 web 应用中处于请求-响应循环流程中的中间件,一般被命名为 next 的变量。

中间件的功能包括:

  • 执行任何代码。
  • 修改请求和响应对象。
  • 终结请求-响应循环。
  • 调用堆栈中的下一个中间件。

如果当前中间件没有终结请求-响应循环,则必须调用 next() 方法将控制权交给下一个中间件,否则请求就会挂起。

Express 应用可使用如下几种中间件:

  • 应用级中间件
  • 路由级中间件
  • 错误处理中间件
  • 内置中间件
  • 第三方中间件

使用可选则挂载路径,可在应用级别或路由级别装载中间件。另外,你还可以同时装在一系列中间件函数,从而在一个挂载点上创建一个子中间件栈。

1、应用级中间件

应用级中间件绑定到 app 对象 使用 app.use() 和 app.METHOD(), 其中, METHOD 是需要处理的 HTTP 请求的方法,例如 GET, PUT, POST 等等,全部小写。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var app = express()

// 没有挂载路径的中间件,应用的每个请求都会执行该中间件
app.use(function (req, res, next) {
console.log('Time:', Date.now())
next()
})

// 挂载至 /user/:id 的中间件,任何指向 /user/:id 的请求都会执行它
app.use('/user/:id', function (req, res, next) {
console.log('Request Type:', req.method)
next()
})

// 路由和句柄函数(中间件系统),处理指向 /user/:id 的 GET 请求
app.get('/user/:id', function (req, res, next) {
res.send('USER')
})

下面这个例子展示了在一个挂载点装载一组中间件。

1
2
3
4
5
6
7
8
// 一个中间件栈,对任何指向 /user/:id 的 HTTP 请求打印出相关信息
app.use('/user/:id', function(req, res, next) {
console.log('Request URL:', req.originalUrl)
next()
}, function (req, res, next) {
console.log('Request Type:', req.method)
next()
})

作为中间件系统的路由句柄,使得为路径定义多个路由成为可能。在下面的例子中,为指向 /user/:id 的 GET 请求定义了两个路由。第二个路由虽然不会带来任何问题,但却永远不会被调用,因为第一个路由已经终止了请求-响应循环。

1
2
3
4
5
6
7
8
9
10
11
12
// 一个中间件栈,处理指向 /user/:id 的 GET 请求
app.get('/user/:id', function (req, res, next) {
console.log('ID:', req.params.id)
next()
}, function (req, res, next) {
res.send('User Info')
})

// 处理 /user/:id, 打印出用户 id
app.get('/user/:id', function (req, res, next) {
res.end(req.params.id)
})

如果需要在中间件栈中跳过剩余中间件,调用 next(‘route’) 方法将控制权交给下一个路由。 注意: next(‘route’) 只对使用 app.VERB() 或 router.VERB() 加载的中间件有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 一个中间件栈,处理指向 /user/:id 的 GET 请求
app.get('/user/:id', function (req, res, next) {
// 如果 user id 为 0, 跳到下一个路由
if (req.params.id == 0) next('route')
// 否则将控制权交给栈中下一个中间件
else next() //
}, function (req, res, next) {
// 渲染常规页面
res.render('regular')
});

// 处理 /user/:id, 渲染一个特殊页面
app.get('/user/:id', function (req, res, next) {
res.render('special')
})

2、路由级中间件

路由级中间件和应用级中间件一样,只是它绑定的对象为 express.Router()。

1
var router = express.Router()

路由级使用 router.use() 或 router.VERB() 加载。

上述在应用级创建的中间件系统,可通过如下代码改写为路由级:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var app = express()
var router = express.Router()

// 没有挂载路径的中间件,通过该路由的每个请求都会执行该中间件
router.use(function (req, res, next) {
console.log('Time:', Date.now())
next()
})

// 一个中间件栈,显示任何指向 /user/:id 的 HTTP 请求的信息
router.use('/user/:id', function(req, res, next) {
console.log('Request URL:', req.originalUrl)
next()
}, function (req, res, next) {
console.log('Request Type:', req.method)
next()
})

// 一个中间件栈,处理指向 /user/:id 的 GET 请求
router.get('/user/:id', function (req, res, next) {
// 如果 user id 为 0, 跳到下一个路由
if (req.params.id == 0) next('route')
// 负责将控制权交给栈中下一个中间件
else next() //
}, function (req, res, next) {
// 渲染常规页面
res.render('regular')
})

// 处理 /user/:id, 渲染一个特殊页面
router.get('/user/:id', function (req, res, next) {
console.log(req.params.id)
res.render('special')
})

// 将路由挂载至应用
app.use('/', router)

3、错误处理中间件

错误处理中间件有 4 个参数,定义错误处理中间件时必须使用这 4 个参数。即使不需要 next 对象,也必须在签名中声明它,否则中间件会被识别为一个常规中间件,不能处理错误。

错误处理中间件和其他中间件定义类似,只是要使用 4 个参数,而不是 3 个,其签名如下: (err, req, res, next)。

1
2
3
4
app.use(function(err, req, res, next) {
console.error(err.stack)
res.status(500).send('Something broke!')
})

4、内置中间件

从 4.x 版本开始,, Express 已经不再依赖 Connect 了。除了 express.static, Express 以前内置的中间件现在已经全部单独作为模块安装使用了。请参考 中间件列表。

express.static(root, [options])

express.static 是 Express 唯一内置的中间件。它基于 serve-static,负责在 Express 应用中提托管静态资源。

参数 root 指提供静态资源的根目录。

可选的 options 参数拥有如下属性。

属性 描述 类型 缺省值
dotfiles 是否对外输出文件名以点(.)开头的文件。可选值为 “allow”、“deny” 和 “ignore” String “ignore”
etag 是否启用 etag 生成 Boolean true
extensions 设置文件扩展名备份选项 Array []
index 发送目录索引文件,设置为 false 禁用目录索引。 Mixed “index.html”
lastModified 设置 Last-Modified 头为文件在操作系统上的最后修改日期。可能值为 true 或 false。 Boolean true
maxAge 以毫秒或者其字符串格式设置 Cache-Control 头的 max-age 属性。 Number 0
redirect 当路径为目录时,重定向至 “/”。 Boolean true
setHeaders 设置 HTTP 头以提供文件的函数。 Function

下面的例子使用了 express.static 中间件,其中的 options 对象经过了精心的设计。

1
2
3
4
5
6
7
8
9
10
11
12
13
var options = {
dotfiles: 'ignore',
etag: false,
extensions: ['htm', 'html'],
index: false,
maxAge: '1d',
redirect: false,
setHeaders: function (res, path, stat) {
res.set('x-timestamp', Date.now())
}
}

app.use(express.static('public', options))

每个应用可有多个静态目录。

1
2
3
app.use(express.static('public'))
app.use(express.static('uploads'))
app.use(express.static('files'))

5、第三方中间件

通过使用第三方中间件从而为 Express 应用增加更多功能。

安装所需功能的 node 模块,并在应用中加载,可以在应用级加载,也可以在路由级加载。

下面的例子安装并加载了一个解析 cookie 的中间件: cookie-parser

1
2
3
4
5
6
7
$ npm install cookie-parser
var express = require('express')
var app = express()
var cookieParser = require('cookie-parser')

// 加载用于解析 cookie 的中间件
app.use(cookieParser())

七、在 Express 中使用模板引擎

需要在应用中进行如下设置才能让 Express 渲染模板文件:

  • views, 放模板文件的目录,比如: app.set(‘views’, ‘./views’)
  • view engine, 模板引擎,比如: app.set(‘view engine’, ‘ejs’)

art-template

art-template for express 4.x.

1、Install
1
2
npm install --save art-template
npm install --save express-art-template
2、Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var express = require('express')
var app = express()

// view engine setup
app.engine('art', require('express-art-template'))
app.set('view', {
debug: process.env.NODE_ENV !== 'production'
})
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'art')

// routes
app.get('/', function (req, res) {
res.render('index.art', {
user: {
name: 'aui',
tags: ['art', 'template', 'nodejs']
}
})
})

Koa2

一、koa2 快速开始

1、环境准备

因为node.js v7.6.0开始完全支持async/await,不需要加flag,所以node.js环境都要7.6.0以上 node.js环境 版本v7.6以上 npm 版本3.x以上

2、快速开始

2.1 安装koa2
1
2
3
4
5
# 初始化package.json
npm init

# 安装koa2
npm install koa
2.2 hello world 代码
1
2
3
4
5
6
7
8
9
const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
ctx.body = 'hello koa2'
})

app.listen(3000)
console.log('[demo] start-quick is starting at port 3000')
2.3 启动demo

由于koa2是基于async/await操作中间件,目前node.js 7.x的harmony模式下才能使用,所以启动的时的脚本如下:

1
node index.js

二、async/await使用

1、快速上手理解

先复制以下这段代码,在粘贴在chrome的控制台console中,按回车键执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getSyncTime() {
return new Promise((resolve, reject) => {
try {
let startTime = new Date().getTime()
setTimeout(() => {
let endTime = new Date().getTime()
let data = endTime - startTime
resolve( data )
}, 500)
} catch ( err ) {
reject( err )
}
})
}

async function getSyncData() {
let time = await getSyncTime()
let data = `endTime - startTime = ${time}`
return data
}

async function getData() {
let data = await getSyncData()
console.log( data )
}

getData()

2、从上述例子可以看出 async/await 的特点:

  • 可以让异步逻辑用同步写法实现
  • 最底层的await返回需要是Promise对象
  • 可以通过多层 async function 的同步写法代替传统的callback嵌套

三、koa2简析结构

1、源码文件

├── lib │ ├── application.js │ ├── context.js │ ├── request.js │ └── response.js └── package.json

这个就是 GitHub https://github.com/koajs/koa上开源的koa2源码的源文件结构,核心代码就是lib目录下的四个文件

  • application.js 是整个koa2 的入口文件,封装了context,request,response,以及最核心的中间件处理流程。
  • context.js 处理应用上下文,里面直接封装部分request.js和response.js的方法
  • request.js 处理http请求
  • response.js 处理http响应

2、koa2特性

  • 只提供封装好http上下文、请求、响应,以及基于async/await的中间件容器。
  • 利用ES7的async/await的来处理传统回调嵌套问题和代替koa@1的generator,但是需要在node.js 7.x的harmony模式下才能支持async/await。
  • 中间件只支持 async/await 封装的,如果要使用koa@1基于generator中间件,需要通过中间件koa-convert封装一下才能使用。

四、koa中间件开发和使用

  • koa v1和v2中使用到的中间件的开发和使用
  • generator 中间件开发在koa v1和v2中使用
  • async await 中间件开发和只能在koa v2中使用

1、generator中间件开发

1.1 generator中间件开发

generator中间件返回的应该是function * () 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* ./middleware/logger-generator.js */
function log( ctx ) {
console.log( ctx.method, ctx.header.host + ctx.url )
}

module.exports = function () {
return function * ( next ) {

// 执行中间件的操作
log( this )

if ( next ) {
yield next
}
}
}
1.2 generator中间件在koa@1中的使用

generator 中间件在koa v1中可以直接use使用

1
2
3
4
5
6
7
8
9
10
11
12
const koa = require('koa')  // koa v1
const loggerGenerator = require('./middleware/logger-generator')
const app = koa()

app.use(loggerGenerator())

app.use(function *( ) {
this.body = 'hello world!'
})

app.listen(3000)
console.log('the server is starting at port 3000')
1.3 generator中间件在koa@2中的使用

generator 中间件在koa v2中需要用koa-convert封装一下才能使用

1
2
3
4
5
6
7
8
9
10
11
12
13
const Koa = require('koa') // koa v2
const convert = require('koa-convert')
const loggerGenerator = require('./middleware/logger-generator')
const app = new Koa()

app.use(convert(loggerGenerator()))

app.use(( ctx ) => {
ctx.body = 'hello world!'
})

app.listen(3000)
console.log('the server is starting at port 3000')

2、async中间件开发

2.1 async 中间件开发
1
2
3
4
5
6
7
8
9
10
11
12
/* ./middleware/logger-async.js */

function log( ctx ) {
console.log( ctx.method, ctx.header.host + ctx.url )
}

module.exports = function () {
return async function ( ctx, next ) {
log(ctx);
await next()
}
}
2.2 async 中间件在koa@2中使用

async 中间件只能在 koa v2中使用

1
2
3
4
5
6
7
8
9
10
11
12
const Koa = require('koa') // koa v2
const loggerAsync = require('./middleware/logger-async')
const app = new Koa()

app.use(loggerAsync())

app.use(( ctx ) => {
ctx.body = 'hello world!'
})

app.listen(3000)
console.log('the server is starting at port 3000')

Ⅱ、路由

一、koa2 原生路由实现

1、简单例子

1
2
3
4
5
6
7
8
const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
let url = ctx.request.url
ctx.body = url
})
app.listen(3000)

访问 http://localhost:3000/hello/world 页面会输出 /hello/world,也就是说上下文的请求request对象中url之就是当前访问的路径名称,可以根据ctx.request.url 通过一定的判断或者正则匹配就可以定制出所需要的路由。

2、定制化的路由

demo源码

https://github.com/ChenShenhai/koa2-note/tree/master/demo/route-simple

2.1 源码文件目录
1
2
3
4
5
6
7
.
├── index.js
├── package.json
└── view
├── 404.html
├── index.html
└── todo.html
2.2 demo源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()

/**
* 用Promise封装异步读取文件方法
* @param {string} page html文件名称
* @return {promise}
*/
function render( page ) {
return new Promise(( resolve, reject ) => {
let viewUrl = `./view/${page}`
fs.readFile(viewUrl, "binary", ( err, data ) => {
if ( err ) {
reject( err )
} else {
resolve( data )
}
})
})
}

/**
* 根据URL获取HTML内容
* @param {string} url koa2上下文的url,ctx.url
* @return {string} 获取HTML文件内容
*/
async function route( url ) {
let view = '404.html'
switch ( url ) {
case '/':
view = 'index.html'
break
case '/index':
view = 'index.html'
break
case '/todo':
view = 'todo.html'
break
case '/404':
view = '404.html'
break
default:
break
}
let html = await render( view )
return html
}

app.use( async ( ctx ) => {
let url = ctx.request.url
let html = await route( url )
ctx.body = html
})

app.listen(3000)
console.log('[demo] route-simple is starting at port 3000')
2.3 运行demo

执行运行脚本

1
node -harmony index.js

二、koa-router中间件

如果依靠ctx.request.url去手动处理路由,将会写很多处理代码,这时候就需要对应的路由的中间件对路由进行控制,这里介绍一个比较好用的路由中间件koa-router

1、安装koa-router中间件

1
2
# koa2 对应的版本是 7.x
npm install --save koa-router@7

2、快速使用koa-router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()

const Router = require('koa-router')

let home = new Router()

// 子路由1
home.get('/', async ( ctx )=>{
let html = `
<ul>
<li><a href="/page/helloworld">/page/helloworld</a></li>
<li><a href="/page/404">/page/404</a></li>
</ul>
`
ctx.body = html
})

// 子路由2
let page = new Router()
page.get('/404', async ( ctx )=>{
ctx.body = '404 page!'
}).get('/helloworld', async ( ctx )=>{
ctx.body = 'helloworld page!'
})

// 装载所有子路由
let router = new Router()
router.use('/', home.routes(), home.allowedMethods())
router.use('/page', page.routes(), page.allowedMethods())

// 加载路由中间件
app.use(router.routes()).use(router.allowedMethods())

app.listen(3000, () => {
console.log('[demo] route-use-middleware is starting at port 3000')
})

Ⅲ、请求数据获取

一、GET请求数据获取

1、使用方法

在koa中,获取GET请求数据源头是koa中request对象中的query方法或querystring方法,query返回是格式化好的参数对象,querystring返回的是请求字符串,由于ctx对request的API有直接引用的方式,所以获取GET请求数据有两个途径。

  • 是从上下文中直接获取 请求对象ctx.query,返回如 { a:1, b:2 } 请求字符串 ctx.querystring,返回如 a=1&b=2
  • 是从上下文的request对象中获取 请求对象ctx.request.query,返回如 { a:1, b:2 } 请求字符串 ctx.request.querystring,返回如 a=1&b=2

2、举个例子

2.1 例子代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
let url = ctx.url
// 从上下文的request对象中获取
let request = ctx.request
let req_query = request.query
let req_querystring = request.querystring

// 从上下文中直接获取
let ctx_query = ctx.query
let ctx_querystring = ctx.querystring

ctx.body = {
url,
req_query,
req_querystring,
ctx_query,
ctx_querystring
}
})

app.listen(3000, () => {
console.log('[demo] request get is starting at port 3000')
})
2.2 执行程序
1
node get.js

二、POST请求参数获取

1、原理

对于POST请求的处理,koa2没有封装获取参数的方法,需要通过解析上下文context中的原生node.js请求对象req,将POST表单数据解析成query string(例如:a=1&b=2&c=3),再将query string 解析成JSON格式(例如:{“a”:”1”, “b”:”2”, “c”:”3”})

注意:ctx.request是context经过封装的请求对象,ctx.req是context提供的node.js原生HTTP请求对象,同理ctx.response是context经过封装的响应对象,ctx.res是context提供的node.js原生HTTP请求对象。

解析出POST请求上下文中的表单数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 解析上下文里node原生请求的POST参数
function parsePostData( ctx ) {
return new Promise((resolve, reject) => {
try {
let postdata = "";
ctx.req.addListener('data', (data) => {
postdata += data
})
ctx.req.addListener("end",function(){
let parseData = parseQueryStr( postdata )
resolve( parseData )
})
} catch ( err ) {
reject(err)
}
})
}

// 将POST请求参数字符串解析成JSON
function parseQueryStr( queryStr ) {
let queryData = {}
let queryStrList = queryStr.split('&')
console.log( queryStrList )
for ( let [ index, queryStr ] of queryStrList.entries() ) {
let itemList = queryStr.split('=')
queryData[ itemList[0] ] = decodeURIComponent(itemList[1])
}
return queryData
}

2、举个例子

2.1 例子代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {

if ( ctx.url === '/' && ctx.method === 'GET' ) {
// 当GET请求时候返回表单页面
let html = `
<h1>koa2 request post demo</h1>
<form method="POST" action="/">
<p>userName</p>
<input name="userName" /><br/>
<p>nickName</p>
<input name="nickName" /><br/>
<p>email</p>
<input name="email" /><br/>
<button type="submit">submit</button>
</form>
`
ctx.body = html
} else if ( ctx.url === '/' && ctx.method === 'POST' ) {
// 当POST请求的时候,解析POST表单里的数据,并显示出来
let postData = await parsePostData( ctx )
ctx.body = postData
} else {
// 其他请求显示404
ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
}
})

// 解析上下文里node原生请求的POST参数
function parsePostData( ctx ) {
return new Promise((resolve, reject) => {
try {
let postdata = "";
ctx.req.addListener('data', (data) => {
postdata += data
})
ctx.req.addListener("end",function(){
let parseData = parseQueryStr( postdata )
resolve( parseData )
})
} catch ( err ) {
reject(err)
}
})
}

// 将POST请求参数字符串解析成JSON
function parseQueryStr( queryStr ) {
let queryData = {}
let queryStrList = queryStr.split('&')
console.log( queryStrList )
for ( let [ index, queryStr ] of queryStrList.entries() ) {
let itemList = queryStr.split('=')
queryData[ itemList[0] ] = decodeURIComponent(itemList[1])
}
return queryData
}

app.listen(3000, () => {
console.log('[demo] request post is starting at port 3000')
})
2.2 启动例子
1
node post.js

三、koa-bodyparser中间件

1、原理

对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到ctx.request.body中

安装koa2版本的koa-bodyparser@3中间件
1
npm install --save koa-bodyparser@3

2、举个例子

2.1 例子代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const Koa = require('koa')
const app = new Koa()
const bodyParser = require('koa-bodyparser')

// 使用ctx.body解析中间件
app.use(bodyParser())

app.use( async ( ctx ) => {

if ( ctx.url === '/' && ctx.method === 'GET' ) {
// 当GET请求时候返回表单页面
let html = `
<h1>koa2 request post demo</h1>
<form method="POST" action="/">
<p>userName</p>
<input name="userName" /><br/>
<p>nickName</p>
<input name="nickName" /><br/>
<p>email</p>
<input name="email" /><br/>
<button type="submit">submit</button>
</form>
`
ctx.body = html
} else if ( ctx.url === '/' && ctx.method === 'POST' ) {
// 当POST请求的时候,中间件koa-bodyparser解析POST表单里的数据,并显示出来
let postData = ctx.request.body
ctx.body = postData
} else {
// 其他请求显示404
ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
}
})

app.listen(3000, () => {
console.log('[demo] request post is starting at port 3000')
})
2.2 启动例子
1
node post-middleware.js

Ⅳ、静态资源加载

koa-static中间件使用

1、使用例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Koa = require('koa')
const path = require('path')
const static = require('koa-static')

const app = new Koa()

// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'

app.use(static(
path.join( __dirname, staticPath)
))


app.use( async ( ctx ) => {
ctx.body = 'hello world'
})

app.listen(3000, () => {
console.log('[demo] static-use-middleware is starting at port 3000')
})

Ⅴ、模板引擎

koa2加载模板引擎

1、快速开始

1.1 安装模块
1
2
3
4
5
# 安装koa模板使用中间件
npm install --save koa-views

# 安装ejs模板引擎
npm install --save ejs
1.2 使用模板引擎

文件目录

1
2
3
4
├── package.json
├── index.js
└── view
└── index.ejs

./index.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()

// 加载模板引擎
app.use(views(path.join(__dirname, './view'), {
extension: 'ejs'
}))

app.use( async ( ctx ) => {
let title = 'hello koa2'
await ctx.render('index', {
title,
})
})

app.listen(3000)

./view/index.ejs 模板

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1><%= title %></h1>
<p>EJS Welcome to <%= title %></p>
</body>
</html>

MongoDB

一、安装数据库

https://docs.mongodb.com/manual/administration/install-community/

二、启动数据库

1、windows
1
2
mongod --dbpath d:/data/db
mongo
2、mac
1
2
mongod --config /usr/local/etc/mongod.conf
mongo

三、数据库操作

1
2
3
4
5
6
7
8
use gp145
db/db.getName()
show dbs
db.createCollection('movies')
db.stats()
db.version()
db.getMongo() //connection to 127.0.0.1:27017
db.dropDatabase()

四、集合操作

1
2
db.createCollection('users')
db.getCollectionNames()

五、文档的操作

5.1 添加

1
2
3
4
5
6
db.users.insertOne({username: 'yangli', password: 'abc123'})
db.users.insertOne({username: 'haozeliang', email: 'hzl@126.com'})
db.users.insertOne({"username": 1, password: 123})
db.users.insertMany([{username: 'gaojie', password: 'gj', email: 'gj@126.com'}, {username: 'xinyi', password: 123, email: 123}])
db.users.insert([{username: 'yangli'}, {useranme: 'zeliang'}])
db.users.save()

5.2 修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
db.users.update({username: 'yangli'}, {username: 'yl'})

// 1、如果第二个参数是一个对象,后边两个参数无效
// 2、如果第二个参数是通过$set设置的话, 后两个参数才有效
// 3、后两个参数的第一个参数:true/如果数据查询不到,就创建 false/如果数据查询不到,就什么都不做
// 4、后两个参数第第二个参数:true/更新多条,false/更新一条
db.users.update({username: 'gp145'}, {$set: {username: 'yl'}}, true, true)
// 5、如果使用updateMany, 就不需要传递后两个参数第二个了
db.users.updateMany({username: 'yl'}, {$set: {username: 'yangli'}})
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>,
collation: <document>,
arrayFilters: [ <filterdocument1>, ... ],
hint: <document|string> // Available starting in MongoDB 4.2
}
)

参考文档:https://docs.mongodb.com/manual/reference/method/db.collection.update/#db.collection.update

5.3 删除

1
db.users.remove({username: 'xinyi'}, true)

参考文档:https://docs.mongodb.com/manual/reference/method/db.collection.remove/

5.4 查找

1
2
3
4
5
6
7
8
9
10
db.movies.find({}, {nm: 1, _id: 0, rt: 1})
db.movies.find({}, {nm: 1, _id: 0, rt: 1}).sort({rt: -1})
db.movies.find({}, {nm: 1, _id: 0, rt: 1}).limit(10)
db.movies.find({}, {nm: 1, _id: 0, rt: 1}).sort({rt: -1}).limit(10)
db.movies.find({}, {nm: 1, _id: 0, rt: 1}).sort({rt: -1}).limit(3).skip(6)
db.movies.find({rt: {$gte: '2019-10-14'}}, {nm: 1, _id: 0, rt: 1})
db.movies.find({rt: {$gte: '2019-10-14'}}, {nm: 1, _id: 0, rt: 1})
db.movies.find({rt: {$gte: '2019-10-14'}}, {nm: 1, _id: 0, rt: 1}).count()
db.movies.find({rt: {$lte: '2019-10-14'}}, {nm: 1, _id: 0, rt: 1}).count()
db.movies.find({nm: /小/}, {nm: 1, _id: 0, rt: 1}).sort({rt: -1})

六、数据库管理工具

robo 3T

Ⅱ、Mongoose

1、数据库连接

1
2
3
4
5
6
7
8
9
10
11
const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost:27017/lagou-admin', { useUnifiedTopology: true, useNewUrlParser: true })

const Users = mongoose.model('users', {
username: String,
password: String
})

module.exports = {
Users
}

2、Route

1
2
3
4
5
6
7
8
var express = require('express')
var router = express.Router()

const { signup, hasUsername } = require('../controllers/users')

router.post('/signup', hasUsername, signup)

module.exports = router

3、Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { Users } = require('../utils/db')

const save = (data) => {
const users = new Users(data)
return users.save()
}

const findOne = (conditions) => {
return Users.findOne(conditions)
}

module.exports = {
save,
findOne
}

4、View

art-template + express

1
2
3
4
{
"ret": true,
"data": {{data}}
}

5、Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const usersModel = require('../models/users')

const signup = async function(req, res, next) {
res.set('Content-Type', 'application/json; charset=utf-8')

let { username, password } = req.body

let result = await usersModel.save({
username,
password: hash
})

if (result) {
res.render('succ', {
data: JSON.stringify({
message: '用户注册成功.'
})
})
} else {
res.render('fail', {
data: JSON.stringify({
message: '用户注册失败.'
})
})
}
}

const hasUsername = async function(req, res, next) {
res.set('Content-Type', 'application/json; charset=utf-8')
let { username } = req.body
let result = await usersModel.findOne({username})
if (result) {
res.render('fail', {
data: JSON.stringify({
message: '用户名已经存在.'
})
})
} else {
next()
}
}

module.exports = {
signup,
hasUsername
}

Socket 编程

一、基于 Net 模块的 Socket 编程

1.1 ServerSocket.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const net = require('net')

const server = new net.createServer()

let clients = {}
let clientName = 0

server.on('connection', (client) => {
client.name = ++clientName
clients[client.name] = client

client.on('data', (msg) => {
// console.log('客户端传来:' + msg);
broadcast(client, msg.toString())
})

client.on('error', (e) => {
console.log('client error' + e);
client.end()
})

client.on('close', (data) => {
delete clients[client.name]
console.log(client.name + ' 下线了');
})
})

function broadcast(client, msg) {
for (var key in clients) {
clients[key].write(client.name + ' 说:' + msg)
}
}

server.listen(9000, 'localhost')

1.2 ClientSocket.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var net = require('net')
const readline = require('readline')

var port = 9000
var host = '127.0.0.1'

var socket = new net.Socket()

socket.setEncoding = 'UTF-8'

socket.connect(port, host, () => {
socket.write('hello.')
})

socket.on('data', (msg) => {
console.log(msg.toString())
say()
})

socket.on('error', function (err) {
console.log('error' + err);
})

socket.on('close', function () {
console.log('connection closeed');
})

const r1 = readline.createInterface({
input: process.stdin,
output: process.stdout
})

function say() {
r1.question('请输入:\n', (inputMsg) => {
if (inputMsg != 'bye') {
// socket.write(inputMsg + '\n')
} else {
socket.destroy()
r1.close()
}
})
}

二、基于 WebSocket 的 Socket 编程

2.1 WebSocketServer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const WebSocket = require('ws')
const ws = new WebSocket.Server({ port: 8081 })

let clients = {}
let clientName = 0

ws.on('connection', (client) => {

client.name = ++clientName
clients[client.name] = client

client.on('message', (msg) => {
broadcast(client, msg)
})

client.on('close', () => {
delete clients[client.name]
console.log(client.name + ' 离开了~')
})
})

function broadcast(client, msg) {
for (var key in clients) {
clients[key].send(client.name + ' 说:' + msg)
}
}

2.2 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>WebSocket</title>
<script src="WsClient.js" charset="utf-8"></script>
</head>
<body>
<h1>gp 交流区</h1>
<div id="content" name="name" style="overflow-y: scroll; width: 400px; height: 300px; border: solid 1px #000"></div>
<br />
<div>
<input type="text" id="msg" style="width: 200px;">
</div>
<button id="submit">提交</button>
<script>
document.querySelector('#submit')
.addEventListener('click', function () {
var msg2 = msg.value
ws.send(msg2) // 核心代码,将表单里的数据提交给server端
msg.value = ''
}, false)
</script>
</body>
</html>

2.3 WsClient.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const ws = new WebSocket('ws://localhost:8081/')

ws.onopen = () => {
ws.send('大家好!')
}

ws.onmessage = (msg) => {
const content = document.getElementById('content')
content.innerHTML += msg.data + '<br/>'
}

ws.onerror = (err) => {
console.log(err);
}

ws.onclose = () => {
console.log('closed~');
}

三、基于 Socket.io 的 Socket 编程

3.1 SocketIoServer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var express = require('express');
var app = express();
var server = require('http').Server(app);
var io = require('socket.io')(server);

// app.use(express.static(__dirname + '/client'))

io.on('connection', function (socket) {
// setInterval(function () {
// socket.emit('list', 'abc')
// }, 1000)
// socket.broadcast.emit('list', 'test');
// socket.on('backend', (msg) => {
// console.log(msg);
// })

socket.on('receive', (msg) => {
socket.broadcast.emit('message', msg);
})
});

server.listen(8082, '10.9.49.156');

3.2 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>socket.io</title>
<script src="socket.io.js" charset="utf-8"></script>
</head>
<body>
<h1>gp 交流区</h1>
<div id="content" name="name" style="overflow-y: scroll; width: 400px; height: 300px; border: solid 1px #000"></div>
<br />
<div>
<input type="text" id="msg" style="width: 200px;">
</div>
<button id="submit">提交</button>
<script>
var socket = io.connect('http://10.9.49.156:8082');
const content = document.getElementById('content')
document.querySelector('#submit')
.addEventListener('click', function () {
var msg2 = msg.value
socket.emit('receive', msg2) // 核心代码
msg.value = ''
content.innerHTML += msg2 + '<br/>'
}, false)

socket.on('message', function(msg){
content.innerHTML += msg + '<br/>'
})
</script>
</body>
</html>

socket.io.js

1
2
3
# 安装包
npm i socket.io
# 在 node_modules/socket.io-client/dist/ 找到 socket.io.js

Node 中间件机制

理解 Node.js 中间件机制核心代码的实现,加深对中间件机制的理解,有助于更好的使用和编写中间件。

一、中间件概念

Node.js 中,中间件主要是指封装所有 Http 请求细节处理的方法。一次 Http 请求通常包含很多工作,如记录日志、ip 过滤、查询字符串、请求体解析、Cookie 处理、权限验证、参数验证、异常处理等,但对于 Web 应用而言,并不希望接触到这么多细节性的处理,因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。

中间件的行为比较类似 Java 中过滤器的工作原理,就是在进入具体的业务处理之前,先让过滤器处理。它的工作模型下图所示。

二、中间件机制核心实现

中间件是从 Http 请求发起到响应结束过程中的处理方法,通常需要对请求和响应进行处理,因此一个基本的中间件的形式如下:

1
2
3
4
const middleware = (req, res, next) => {
// TODO
next()
}

以下通过两种方式的中间件机制的实现来理解中间件是如何工作的。

1、方式一

如下定义三个简单的中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const middleware1 = (req, res, next) => {
console.log('middleware1 start')
next()
}

const middleware2 = (req, res, next) => {
console.log('middleware2 start')
next()
}

const middleware3 = (req, res, next) => {
console.log('middleware3 start')
next()
}

通过递归的形式,将后续中间件的执行方法传递给当前中间件,在当前中间件执行结束,通过调用 next() 方法执行后续中间件的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 中间件数组
const middlewares = [middleware1, middleware2, middleware3]
function run (req, res) {
const next = () => {
// 获取中间件数组中第一个中间件
const middleware = middlewares.shift()
if (middleware) {
middleware(req, res, next)
}
}
next()
}
run() // 模拟一次请求发起

执行以上代码,可以看到如下结果:

1
2
3
middleware1 start
middleware2 start
middleware3 start

如果中间件中有异步操作,需要在异步操作的流程结束后再调用 next() 方法,否则中间件不能按顺序执行。改写 middleware2 中间件:

1
2
3
4
5
6
7
8
const middleware2 = (req, res, next) => {
console.log('middleware2 start')
new Promise(resolve => {
setTimeout(() => resolve(), 1000)
}).then(() => {
next()
})
}

执行结果与之前一致,不过middleware3会在middleware2异步完成后执行。

2、方式二

有些中间件不止需要在业务处理前执行,还需要在业务处理后执行,比如统计时间的日志中间件。在方式一情况下,无法在 next() 为异步操作时再将当前中间件的其他代码作为回调执行。因此可以将next() 方法的后续操作封装成一个 Promise 对象,中间件内部就可以使用 next.then()形式完成业务处理结束后的回调。改写 run() 方法如下:

1
2
3
4
5
6
7
8
9
10
function run (req, res) {
const next = () => {
const middleware = middlewares.shift()
if (middleware) {
// 将middleware(req, res, next)包装为Promise对象
return Promise.resolve(middleware(req, res, next))
}
}
next()
}

中间件的调用方式需改写为:

1
2
3
4
5
6
7
8
const middleware1 = (req, res, next) => {
console.log('middleware1 start')
// 所有的中间件都应返回一个Promise对象
// Promise.resolve()方法接收中间件返回的Promise对象,供下层中间件异步控制
return next().then(() => {
console.log('middleware1 end')
})
}

得益于 async 函数的自动异步流程控制,中间件也可以用如下方式来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// async函数自动返回Promise对象
const middleware2 = async (req, res, next) => {
console.log('middleware2 start')
await new Promise(resolve => {
setTimeout(() => resolve(), 1000)
})
await next()
console.log('middleware2 end')
}

const middleware3 = async (req, res, next) => {
console.log('middleware3 start')
await next()
console.log('middleware3 end')
}

执行结果如下:

以上描述了中间件机制中多个异步中间件的调用流程,实际中间件机制的实现还需要考虑异常处理、路由等。

express 框架中,中间件的实现方式为方式一,并且全局中间件和内置路由中间件中根据请求路径定义的中间件共同作用,不过无法在业务处理结束后再调用当前中间件中的代码。koa2 框架中中间件的实现方式为方式二,将 next() 方法返回值封装成一个 Promise,便于后续中间件的异步流程控制,实现了 koa2 框架提出的洋葱圈模型,即每一层中间件相当于一个球面,当贯穿整个模型时,实际上每一个球面会穿透两次。

koa2 框架的中间件机制实现得非常简洁和优雅,这里学习一下框架中组合多个中间件的核心代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
// index会在next()方法调用后累加,防止next()方法重复调用
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
// 核心代码
// 包装next()方法返回值为Promise对象
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
// 遇到异常中断后续中间件的调用
return Promise.reject(err)
}
}
}
}

三、中间件社区

在后续 Node.js 学习和应用中,建议使用 koa2 框架作为基础框架,这里列出了一些使用比较多的中间件。

koa-router:路由中间件 koa-bodyparser:http请求主体解析 koa-static:代理静态文件 koa-compress:gzip压缩 koa-logger:日志记录 koa-convert:转换koa1.x版本的中间件 kcors:跨域中间件 koa中间件列表地址:https://github.com/koajs/koa/wiki

四、总结

本文主要介绍了中间件的概念、为何引入中间件以及中间件机制的核心实现。中间件机制使得 Web 应用具备良好的可扩展性和组合性。

在实现中间件时,单个中间件应该足够简单,职责单一。由于每个请求都会调用中间件相关代码,中间件的代码应该高效,必要的时候可以缓存重复获取的数据。在对不同的路由使用中间件时,还应该考虑到不同的中间件应用到不同的路由上。

Node中的事件循环和异步API

1、介绍

单线程编程会因阻塞I/O导致硬件资源得不到更优的使用。多线程编程也因为编程中的死锁、状态同步等问题让开发人员头痛。 Node在两者之间给出了它的解决方案:利用单线程,远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以好使用CPU。

实际上,node只是在应用层属于单线程,底层其实通过libuv维护了一个阻塞I/O调用的线程池。

但是:在应用层面,JS是单线程的,业务代码中不能存在耗时过长的代码,否则可能会严重拖后续代码(包括回调)的处理。如果遇到需要复杂的业务计算时,应当想办法启用独立进程或交给其他服务进行处理。

1.1 异步I/O

在Node中,JS是在单线程中执行的没错,但是内部完成I/O工作的另有线程池,使用一个主进程和多个I/O线程来模拟异步I/O。

当主线程发起I/O调用时,I/O操作会被放在I/O线程来执行,主线程继续执行下面的任务,在I/O线程完成操作后会带着数据通知主线程发起回调。

1.2 事件循环

事件循环是Node的执行模型,正是这种模型使得回调函数非常普遍。

在进程启动时,Node便会创建一个类似while(true)的循环,执行每次循环的过程就是判断有没有待处理的事件,如果有,就取出事件及其相关的回调并执行他们,然后进入下一个循环。如果不再有事件处理,就退出进程。

Event loop是一种程序结构,是实现异步的一种机制。Event loop可以简单理解为:

  • 所有任务都在主线程上执行,形成一个执行栈(execution context stack)。
  • 主线程之外,还存在一个”任务队列”(task queue)。系统把异步任务放到”任务队列”之中,然后主线程继续执行后续的任务。
  • 一旦”执行栈”中的所有任务执行完毕,系统就会读取”任务队列”。如果这个时候,异步任务已经结束了等待状态,就会从”任务队列”进入执行栈,恢复执行。
  • 主线程不断重复上面的第三步。

Node中事件循环阶段解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   ┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

每个阶段都有一个FIFO的回调队列(queue)要执行。而每个阶段有自己的特殊之处,简单说,就是当event loop进入某个阶段后,会执行该阶段特定的(任意)操作,然后才会执行这个阶段的队列里的回调。当队列被执行完,或者执行的回调数量达到上限后,event loop会进入下个阶段。

Phases Overview 阶段总览

  • timers: 这个阶段执行setTimeout()、setInterval()设定的回调。
  • I/O callbacks: 执行几乎所有的回调,除了close callbacks、setTimeout()、setInterval()、setImmediate()的回调。
  • idle, prepare: 仅内部使用。
  • poll: 获取新的I/O事件;node会在适当条件下阻塞在这里。
  • check: 执行setImmediate()设定的回调。
  • close callbacks: 执行比如socket.on(‘close’, …)的回调。

1. timers

一个timer指定一个下限时间而不是准确时间,定时器setTimeout()和setInterval()在达到这个下限时间后执行回调。在指定的时间过后,timers会尽早的执行回调,但是系统调度或者其他回调的执行可能会延迟它们。

从技术上来说,poll阶段控制timers什么时候执行,而执行的具体位置在timers。

下限的时间有一个范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1。

2. I/O callbacks

执行除了close callbacks、setTimeout()、setInterval()、setImmediate()回调之外几乎所有回调,比如说TCP连接发生错误。

3. idle, prepare

系统内部的一些调用。

4. poll

这是最复杂的一个阶段。poll会检索新的I/O events,并且会在合适的时候阻塞,等待回调被加入。

poll阶段有两个主要的功能:一是执行下限时间已经达到的timers的回调,一是处理poll队列里的事件。 注:Node很多API都是基于事件订阅完成的,这些API的回调应该都在poll阶段完成。

当事件循环进入poll阶段:

  • poll队列不为空的时候,事件循环肯定是先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。
  • poll队列为空的时候,这里有两种情况。
    • 如果代码已经被setImmediate()设定了回调,那么事件循环直接结束poll阶段进入check阶段来执行check队列里的回调。
    • 如果代码没有被设定setImmediate()设定回调:
      • 如果有被设定的timers,那么此时事件循环会检查timers,如果有一个或多个timers下限时间已经到达,那么事件循环将绕回timers阶段,并执行timers的有效回调队列。
      • 如果没有被设定timers,这个时候事件循环是阻塞在poll阶段等待事件回调被加入poll队列。

Node的很多API都是基于事件订阅完成的,比如fs.readFile,这些回调应该都在poll阶段完成。

5. check setImmediate()在这个阶段执行。

这个阶段允许在poll阶段结束后立即执行回调。如果poll阶段空闲,并且有被setImmediate()设定的回调,那么事件循环直接跳到check执行而不是阻塞在poll阶段等待poll 事件们 (poll events)被加入。

注意:如果进行到了poll阶段,setImmediate()具有最高优先级,只要poll队列为空且注册了setImmediate(),无论是否有timers达到下限时间,setImmediate()的代码都先执行。

6. close callbacks 如果一个socket或handle被突然关掉(比如socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发。

1.3 请求对象

对于Node中的异步I/O调用而言,回调函数不由开发者来调用,从JS发起调用到I/O操作完成,存在一个中间产物,叫请求对象。

在JS发起调用后,JS调用Node的核心模块,核心模块调用C++内建模块,內建模块通过libuv判断平台并进行系统调用。在进行系统调用时,从JS层传入的方法和参数都被封装在一个请求对象中,请求对象被放在线程池中等待执行。JS立即返回继续后续操作。

1.4 执行回调

在线程可用时,线程会取出请求对象来执行I/O操作,执行完后将结果放在请求对象中,并归还线程。 在事件循环中,I/O观察者会不断的找到线程池中已经完成的请求对象,从中取出回调函数和数据并执行。

跑完当前执行环境下能跑完的代码。每一个事件消息都被运行直到完成为止,在此之前,任何其他事件都不会被处理。这和C等一些语言不通,它们可能在一个线程里面,函数跑着跑着突然停下来,然后其他线程又跑起来了。JS这种机制的一个典型的坏处,就是当某个事件处理耗时过长时,后面的事件处理都会被延后,直到这个事件处理结束,在浏览器环境中运行时,可能会出现某个脚本运行时间过长,页面无响应的提示。Node环境则可能出现大量用户请求被挂起,不能及时响应的情况。

2、非I/O的异步API

Node中除了异步I/O之外,还有一些与I/O无关的异步API,分别是:setTimeout()、setInterval()、process.nextTick()、setImmediate(),他们并不是像普通I/O操作那样真的需要等待事件异步处理结束再进行回调,而是出于定时或延迟处理的原因才设计的。

2.1 setTimeout()与setInterval()

这两个方法实现原理与异步I/O相似,只不过不用I/O线程池的参与。

使用它们创建的定时器会被放入timers队列的一个红黑树中,每次事件循环执行时会从相应队列中取出并判断是否超过定时时间,超过就形成一个事件,回调立即执行。

所以,和浏览器中一样,这个并不精确,会被长时间的同步事件阻塞。

值得一提的是,在Node的setTimeout的源码中:

1
2
3
4
5
6
7
8
// Node源码
after *= 1; // coalesce to number or NaN
if (!(after >= 1 && after <= TIMEOUT_MAX)) {
if (after > TIMEOUT_MAX) {
process.emitWarning(...);
}
after = 1; // schedule on next tick, follows browser behavior
}

意思是如果没有设置这个after,或者小于1,或者大于TIMEOUT_MAX(2^31-1),都会被强制设置为1ms。也就是说setTimeout(xxx,0)其实等同于setTimeout(xxx,1)。

2.2 setImmediate()

setImmediate()是放在check阶段执行的,实际上是一个特殊的timer,跑在event loop中一个独立的阶段。它使用libuv的API来设定在 poll 阶段结束后立即执行回调。

来看看这个例子:

1
2
3
4
5
6
setTimeout(function() {
console.log('setTimeout')
}, 0)
setImmediate(function() {
console.log('setImmediate')
}) // 输出不稳定

setTimeout与setImmediate先后入队之后,首先进入的是timers阶段,如果我们的机器性能一般或者加入了一个同步长耗时操作,那么进入timers阶段,1ms已经过去了,那么setTimeout的回调会首先执行。

如果没有到1ms,那么在timers阶段的时候,超时时间没到,setTimeout回调不执行,事件循环来到了poll阶段,这个时候队列为空,此时有代码被setImmediate(),于是先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout的回调函数。

1
2
3
4
5
6
7
8
setTimeout(function() {
console.log('set timeout')
}, 0)
setImmediate(function() {
console.log('set Immediate')
})
for (let i = 0; i < 100000; i++) {} // 可以保证执行时间超过1ms
// 稳定输出: setTimeout setImmediate

这样就可以稳定输出了。

再一个栗子:

1
2
3
4
5
6
7
const fs = require('fs')
fs.readFile('./filePath.js', (err, data) => {
setTimeout(() => console.log('setTimeout') , 0)
setImmediate(() => console.log('setImmediate'))
console.log('开始了')
for (let i = 0; i < 100000; i++) {}
}) // 输出 开始了 setImmediate setTimeout

这里我们就会发现,setImmediate永远先于setTimeout执行。

fs.readFile的回调是在poll阶段执行的,当其回调执行完毕之后,setTimeout与setImmediate先后入了timers与check的队列,继续到poll,poll队列为空,此时发现有setImmediate,于是事件循环先进入check阶段执行回调,之后在下一个事件循环再在timers阶段中执行setTimeout回调,虽然这个setTimeout已经到了超时时间。

再来个栗子:

同样的,这段代码也是一样的道理:

1
2
3
4
setTimeout(() => {
setImmediate(() => console.log('setImmediate') )
setTimeout(() => console.log('setTimeout') , 0)
}, 0)

以上的代码在timers阶段执行外部的setTimeout回调后,内层的setTimeout和setImmediate入队,之后事件循环继续往后面的阶段走,走到poll阶段的时候发现队列为空,此时有代码被setImmedate(),所以直接进入check阶段执行响应回调(注意这里没有去检测timers队列中是否有成员到达超时事件,因为setImmediate()优先)。之后在下一个事件循环的timers阶段中再去执行相应的回调。

2.3 process.nextTick()与Promise

对于这两个,我们可以把它们理解成一个微任务。也就是说,它们其实不属于事件循环的一部分。

有时我们想要立即异步执行一个任务,可能会使用延时为0的定时器,但是这样开销很大。我们可以换而使用process.nextTick(),它会将传入的回调放入nextTickQueue队列中,下一轮Tick之后取出执行,不管事件循环进行到什么地步,都在当前执行栈的操作结束的时候调用,参见Nodejs官网。

process.nextTick方法指定的回调函数,总是在当前执行队列的尾部触发,多个process.nextTick语句总是一次执行完(不管它们是否嵌套),递归调用process.nextTick,将会没完没了,主线程根本不会去读取事件队列,导致阻塞后续调用,直至达到最大调用限制。

相比于在定时器中采用红黑树树的操作时间复杂度为0(lg(n)),而process.nextTick()的时间复杂度为0(1),相比之下更高效。

来举一个复杂的栗子,这个栗子搞懂基本上就全部理解了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setTimeout(() => {
process.nextTick(() => console.log('nextTick1'))

setTimeout(() => {
console.log('setTimout1')
process.nextTick(() => {
console.log('nextTick2')
setImmediate(() => console.log('setImmediate1'))
process.nextTick(() => console.log('nextTick3'))
})
setImmediate(() => console.log('setImmediate2'))
process.nextTick(() => console.log('nextTick4'))
console.log('sync2')
setTimeout(() => console.log('setTimout2'), 0)
}, 0)

console.log('sync1')
}, 0)
// 输出: sync1 nextTick1 setTimout1 sync2 nextTick2 nextTick4 nextTick3 setImmediate2 setImmediate1 setTimout2

2.4 结论

  • process.nextTick(),效率最高,消费资源小,但会阻塞CPU的后续调用;
  • setTimeout(),精确度不高,可能有延迟执行的情况发生,且因为动用了红黑树,所以消耗资源大;
  • setImmediate(),消耗的资源小,也不会造成阻塞,但效率也是最低的。