梦入琼楼寒有月,行过石树冻无烟

📖 earlier posts 📖

Node.js 工作原理

运行时(Rutime System)与 node vm

Runtime system

运行时系统(Runtime system),又称之为 “运行环境”,指将半编译的代码运行的环境。在 node 核心概念中指的是 数据类型的确定 由编译时推迟到了运行时,因此就出现了运行时系统(Rutime System)来处理编译后的代码。

因此这个概念和流程就是 JavaScript 引擎负责解析和即时编译(JIT),结合 Rutime 因此可以实现了 Window 、DOM API,存在与浏览器的 Runtime 中。

所以 Node.js 中的 Runtime 也包含了不同的库,例如 Cluster、FileSystem API,这里面的 Runtime 都包含了内置的数据类型和常用的工具,因此 Chrome 和 Node.js 同样使用 V8 引擎但不会使用同一个 Runtime。

Cluster (集群)在 node v0.8 开始集成的内置模块,为解决单线程设计的瓶颈而生。

System Runtime :https://designfirst.io/systemruntime/,常见的运行时系统需要通过 C 或汇编而成,通过此项目可以简单构建

VM

VM 是 Node 核心模块之一,通过使用 V8 的 Virtual Machine Contexts 动态编译和执行代码,但代码的运行在上下文中是与当前隔离的,但他不是安全的,因此不要使用他来运行不受信任的代码。

不同与浏览器的沙箱环境, vm 提供了一系列的 API 用于在 V8 虚拟机中编译和运行代码,可以让 JavaScript 的代码被编译且立即运行或编译后保存运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 定义常量
const util = require('util');
const vm = require('vm');

// 创建一个新的 vm.Script 对象,将会编译 code 但不会运行,之后可以多次运行。
const script = new vm.Script('globalVar += 1; anotherGlobalVar = 1;');

// 创建沙盒并绑定上下文
const sandbox = {globalVar: 1};

// 创建上下文并将沙盒(sandbox)绑定至到环境中
const contextifiedSandbox = vm.createContext(sandbox);

// 运行 vm.script 内包含的上下文对象,并返回结果
const result = script.runInContext(contextifiedSandbox);

// 是否沙盒绝对等于上下文沙盒 (true)
console.log(`sandbox === contextifiedSandbox ? ${sandbox === contextifiedSandbox}`);

// sandbox: { globalVar: 2, anotherGlobalVar: 1 }
console.log(`sandbox: ${util.inspect(sandbox)}`);

// result: 1
console.log(`result: ${util.inspect(result)}`);

vm.Script 是新增于 node.js v0.3.1 类,用于绑定任何全局对象,之后可以多次运行,也就是说我们将 sandbox 对象创建了环境的上下文,将对象绑定到环境中,因此最后输出 globalVar: 2, anotherGlobalVar: 1

这也就表明了 script.runInContextcontextifiedSandbox 上下文中运行,从而当 console.log 时,他的值已经改变了。

util.inspect 用于检查常量,除了 script.runInContext 以外,在 node.js v6.3.0 中也完全支持了更为简单的方法 vm.runlnContext

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

// 设置上下文全局变量为 1
const context = {globalVar: 1};

// 创建上下文方法准备该对象,以便可以调用上下文
vm.createContext(context);

// 通过 vm 方法编译 code,并运行上下文并返回结果
vm.runInContext('globalVar += 1;', context)

// globalVar:2
console.log('globalVar:' + context.globalVar);

回调\同步\异步\阻塞\非阻塞

回调函数

在计算机的设计之初,基本上都是通过异步来进行的,在此之前我们需要知道异步的相关类别

Name Info
同步 按照需求内的一步一步进行执行
异步 可以同时执行多种事情
堵塞 按照需求内一步一步进行,当一个程序没有执行,下面所排队等待的程序都处于排队状态
非堵塞 当一个程序没有执行完毕,后面所排队的程序都会进行插队

node.js 异步内的直接体现就在于回调,而回调就是,当你不知道用户何时会点击按钮,因此你为事件所定义了一个事件的处理程序,该事件会在处理时被触发调用即“回调”,我们以文件读取为例:

1
2
3
4
5
6
7
8
9
10
var fs = require('fs');

// 异步读取文件内容,通过 readFile() 缓冲整个文件
fs.readFile('package.json', function (err,data) {
if (err) {
console.log(err.stack);
return;
}
console.log(data.toString() + '\n File echo success!');
});

以上的 js 脚本中那个,主要通过 fs.readFile 函数来异步读取文件,如果文件不存在则直接通过 err 输出错误信息,如没有发生错误则会忽略 err对象进行输出,因此文件的内容就通过了回调函数输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ node vm.js 
{
"name": "demo",
"version": "1.0.0",
"description": "node demo",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://gitee.com/sif_one/node-demo"
},
"keywords": [
"null"
],
"author": "sun likun",
"license": "ISC"
}

File echo success!

假设 package.json 文件并不存在,那么在经过 s.readFile() 方法时,err 对象就会输出错误信息:

1
2
$ node vm.js 
Error: ENOENT: no such file or directory, open 'pac1kage.json'

同步和异步的区别

同步可以说是一个进程在执行某一个请求的时候,另一个进程则需要等待,等上一个请求完后,这个进程才可以执行。而异步则是另一个进程无需等待,无论其他进程状态是否,直接进程执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function __test() {
setTimeout(()=> {
console.log(1000);
},1000)
}

async function test() {
for (let i=0; i<15; i++) {
try {
await __test();
} catch (e) {
console.log('a', e);
}
}
}
test();

首先我们需要注意的是,为了防止异步的出错,需要将 await 放到 try……catch 中,这就让 __testtest 两个函数同步起来,这样就会让输出的结果是__test 运行完了,test 还在等待的效果,正因这样,一个正常的运行结果才不会混乱。

堵塞

堵塞与非堵塞

1
2
3
4
5
6
7
8
9
10
11
12
var fs = require("fs")

var data = fs.readFileSync('hello.text')
fs.readFile('hello.text', (err,data)=> {
if (err != null) {
console.log('没有此文件')
return
}
})

console.log(data.toString())
console.log("程序执行结束!")

输出:

1
2
hello
程序执行结束!

在以上的code中,首先并不存在 hesllo.text 文件,处理回调错误的同时,在node中,任何回调函数中的地一个参数都为错误对象在Node官方文档中被标注为错误优先与回调的说法。

非堵塞

1
2
3
4
5
6
7
8
9
10
var fs = require("fs")

fs.readFile("hello.text", function(err,data) {
if (err !== null) {
console.log(err)
return
}
console.log(data.toString())
})
console.log('程序执行完成')

输出:

1
2
程序执行完成
hello

本文通过堵塞与非堵塞来进行演示,通常情况下堵塞代码是按照需求一步一步的执行,当一个步骤或没有完成时等待的程序处于排队状态,而非堵塞则是不当一步骤没有执行完毕后面所排队的程序都会依次进行插队即处理更加的效率

事件循环

Node.js 基本上所以的事件机制都使用类似与设计模式中的观察者模式实现,而观察者模式又是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时通知多个观察者的对象或其他对象。

观察者模式会在他本身的状态改变时发出通知,而这种呼叫各类观察者所提供的方式来实现,因此这种模式也被称之为事件处理系统。

在 node.js 中,所提供了 events 库,而他最主要的部分就是创建对象、事件处理、绑定事件以及最后的触发事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var events = require('events');

// 创建事件
var eventEmitter = new events.EventEmitter();

// 创建事件处理
var connectHandler = function connected() {
console.log('1.连接成功')

// 触发 data_received 事件
eventEmitter.emit('data_received');
}

// 将 connectHandler 绑定 connection
eventEmitter.on('connection', connectHandler);

// 绑定 'data_receiver' 事件
eventEmitter.on('data_received', function () {
console.log("2.接受触发成功\n3.流程执行完毕")
});

eventEmitter.emit('connection')
1
2
3
1.连接成功
2.接受触发成功
3.流程执行完毕

上述 code 中 eventEmitter.emit() 用于触发事件,同时他也允许将任意一组参数传递给监听器函数,之后通过eventEmitter.on() 函数来注册监听器 connection ,最后通过 data_received 的事件绑定被监听器发现,从而输出整套流程。

Node.js MySql at Sequelize

这是通过了 npm 注册中心所发布的 node.js 模块,并由 node.js 驱动,以 JavaScript 编写且并不需要编译,并 100% 遵循了 MIT 许可证,可直接使用 npm 进行安装:

npm install mysql

在 MySQL 创建一个表和库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> create databases node
> use node
> create table user (
-> id int COMMENT "编号",
-> name varchar(40) COMMENT "名称"
-> );
Query OK, 0 rows affected (0.015 sec)

> INSERT INTO user(id,name) value ('2','mysql_user');
Query OK, 1 row affected (0.003 sec)

> INSERT INTO user(id,name) value ('1','users_node');
Query OK, 1 row affected (0.003 sec)

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
const mysql = require('mysql')

var connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'toor',
database: 'node'
})

connection.connect(function (err) {
if (err) {
console.error('connection is error' + err.stack)
return
}
console.log('threadId:' + connection.threadId)
})

var showUser = "SELECT * FROM user;"

connection.query(showUser,function (err, result) {
if (err) throw err;
console.log('show user:',result[0])
})

connection.end() // or connection.destory()

Sequelize

在 node.js 所提供的 mysql 库中,多基于命令形式并通过 connection,query 来实现与数据库的交互,并提供了很多链接方法,如连接池等,而对于开发项目来说这种方式和并不利于项目在生产环境的发布。

因此基于 mysql 所提供的接口,Node 提供了 ORM(Object-Relational Mapping) 来将数据库的表结构来映射到数据库中,这种方式不仅统一了风格还使得与 SQL 的操作变得规范,但核心还是基于 mysql 库。

Sequelize 是一个基于 Promise 的 Node.js ORM,并适用与 Postgres、MySQL、MariaDB、SQLite、Microsoft SQL Server。相对于直接使用原生 mysql 库,直接使用 ORM 格式显得更加的规范和简洁。在 Node.js 中使用 Sequelize 可以直接通过 npm 进行安装,同样的由于 Sequelize 是一个方法约定,因此需要安装 mysql2 作为主要依赖:

npm install sequelize
npm install mysql2

首先需要创建一个 config.js 文件用于作为配置文件并通过 app.js 进行连接。

config.js

1
2
3
4
5
6
7
8
9
10

var config = {
databases: 'node',
username: 'root',
password: 'toor',
host: 'localhost',
port: '3306'
}

module.exports = config

app.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
const Sequelize = require('sequelize')
const mysql = require('mysql2')

const config = require('./config')

var sequelize = new Sequelize(config.databases, config.username, config.password, {
host: config.host,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 30000 // 连接在被释放之前可以空闲的最长时间(以毫秒为单位)。
}
})

sequelize
.authenticate()
.then(() => {
// Connection has been established successfully
console.log('连接已成功建立')
})
.catch(err => {
// Unable to connect to the database
console.error('无法连接到数据库', err)
})

Data Types

Sequelize 的数据类型主要分为 8 种,均代替原生数据库语法以进行统一并适配多种数据库写法,其中 mysql 的对应关系为:

Id Name MySql Type Info
1 DataTypes.STRING VARCHAR(255) Strings 长度在 1 到 ~ 字符之间的可变长度的字符串(VARCHAR 字段在创建时必须定义长度,Sequelize 默认的 VARCHAR 为 255)
DataTypes.STRING(1234) VARCHAR(1234)
DataTypes.SIRING.BINARY VARCHAR BINARY BLOB 是“二进制大对象”,用于存储大量二进制数据,例如图像或其他类型的文件。
2 DataTypes.TEXT TEXT 最大长度为 65535 个字符的字段。BLOB 是“二进制大对象”,用于存储大量二进制数据,例如图像或其他类型的文件, 定义为 TEXT 的字段也包含大量数据。 两者的区别在于对存储数据的排序和比较在 BLOB 中区分大小写,而在 TEXT 字段中不区分大小写。
DataTypes.TEXT(‘tiny’) TINYTEXT 最大长度为 255 个字符的 BLOB 或 TEXT 列
4 DataTypes.Boolean TINYINT(1) Boolean 一个可以有符号或无符号的非常小的整数,如果有符号,则允许的范围是从 -128 到 127。如果是无符号,则允许的范围是从 0 到 255。
5 DataTypes.INTEGER INTEGER Numbers 一个可以有符号或无符号的非常小的整数,如果有符号,则允许的范围是从 -128 到 127。如果是无符号,则允许的范围是从 0 到 255。
DataTypes.INTEGER.UNSIGNED Unsigned & Zerofill integers(无符号和零填充整数) 一个可以有符号或无符号的非常小的整数
DataTypes.ZEROFILL 零填充
DataTypes.INTEGER.UNSIGNED.ZEROFILL 一个可以有符号或无符号的非常小的整数,且零填充
6 DataTypes.BIGINT BIGINT 可以有符号或无符号的大整数。,如果有符号,则允许范围为 -9223372036854775808 到 9223372036854775807。如果没有符号,则允许范围为 0 到 18446744073709551615。
7 DataType.FLOAT FLOAT 不能无符号的浮点数,可以定义显示长度 (M) 和小数位数 (D),这不是必需的,默认为 10,2,其中 2 是小数位数,10 是总位数(包括小数), 对于 FLOAT,十进制精度可以达到 24 位。
8 DataType.DOUBLE DOUBLE 不能无符号的双精度浮点数,您可以定义显示长度 (M) 和小数位数 (D),这不是必需的,默认为 16,4,其中 4 是小数位数。 DOUBLE 的十进制精度可以达到 53 位, REAL 是 DOUBLE 的同义词。
9 DataType.DECIMAL DECIMAL 无法无符号的解压缩浮点数,在未压缩的十进制中,每个十进制对应一个字节。 需要定义显示长度 (M) 和小数位数 (D)。 NUMERIC 是 DECIMAL 的同义词。
10 DataType.DATE DATETIME Dates 日期和时间的组合。 格式:YYYY-MM-DD hh:mm:ss。 支持的范围是从“1000-01-01 00:00:00”到“9999-12-31 23:59:59”。 在列定义中添加 DEFAULT 和 ON UPDATE 以获取自动初始化并更新到当前日期和时间
11 DataType.DATEONLY DATE 一个日期,格式:YYYY-MM-DD。支持的范围从“1000-01-01”到“99999 -12-31”

sequelize 可通过 sequlize.define 来定义一个模型,之后通过 model.create 来创建模型的数据信息,之后使用 sequlize.sync 同步到数据库中。

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
const Sequelize = require('sequelize')
const mysql = require('mysql2')

const config = require('./config')

var sequelize = new Sequelize(config.databases, config.username, config.password, {
host: config.host,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 30000 // 连接在被释放之前可以空闲的最长时间(以毫秒为单位)。
}
})

sequelize
.authenticate()
.then(() => {
// Connection has been established successfully
console.log('连接已成功建立')
})
.catch(err => {
// Unable to connect to the database
console.error('无法连接到数据库', err)
})

// 定义一个模型
var users = sequelize.define('users', {
id: {
type: Sequelize.STRING(40),
primaryKey: true, // 作为主键的字段一部分不可以包含 null value 且一个表中只能有一个
uniqueKey: true // 唯一键,该表没有两个不同的行或这些列的值相同的记录
},
name: Sequelize.STRING(90),
gender: Sequelize.BOOLEAN,
createAt: Sequelize.BIGINT,
updateAt: Sequelize.BIGINT
}, {
timestamps: false
})

// 当前时间戳的默认值
var now = Date.now()

users.create({
id: '1',
name: 'Administrator',
gender: false,
createAt: now,
updateAt: now
}).then(function (p) {
console.log('created:' + JSON.stringify(p))
}).catch(function (err) {
console.log('failed:' + err)
})

// 同步链接
sequelize.sync()

CURD

Select

Sequelize 提供了两种查询方法,一种分别为 sequlize.fn 以及 WHERE 子句以及更为负责的 Operator,本文我们以最为常用的 WHERE 子句作为演示:

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
const Sequelize = require('sequelize')
const mysql = require('mysql2')

const config = require('./config')

var sequelize = new Sequelize(config.databases, config.username, config.password, {
host: config.host,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 30000 // 连接在被释放之前可以空闲的最长时间(以毫秒为单位)。
}
})

sequelize
.authenticate()
.then(() => {
// Connection has been established successfully
console.log('连接已成功建立')
})
.catch(err => {
// Unable to connect to the database
console.error('无法连接到数据库', err)
})

// 定义一个模型
var users = sequelize.define('users', {
id: {
type: Sequelize.STRING(40),
primaryKey: true, // 作为主键的字段一部分不可以包含 null value 且一个表中只能有一个
uniqueKey: true // 唯一键,该表没有两个不同的行或这些列的值相同的记录
},
name: Sequelize.STRING(90),
gender: Sequelize.BOOLEAN,
createAt: Sequelize.BIGINT,
updateAt: Sequelize.BIGINT
}, {
timestamps: false
})

/*
find 1 tousers
{"id":"1","name":"Administrator","gender":false,"createAt":1631729482972,"updateAt":1631729482972}
*/
tofind = async () => {
var tousers = await users.findAll({
where: {
id: 1
}
})
console.log(`find ${tousers.length} tousers`)
for (let u of tousers) {
console.log(JSON.stringify(u))
}
}

tofind()

Add

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
const Sequelize = require('sequelize')
const mysql = require('mysql2')

const config = require('./config')

var sequelize = new Sequelize(config.databases, config.username, config.password, {
host: config.host,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 30000 // 连接在被释放之前可以空闲的最长时间(以毫秒为单位)。
}
})

sequelize
.authenticate()
.then(() => {
// Connection has been established successfully
console.log('连接已成功建立')
})
.catch(err => {
// Unable to connect to the database
console.error('无法连接到数据库', err)
})

// 定义一个模型
var users = sequelize.define('users', {
id: {
type: Sequelize.STRING(40),
primaryKey: true, // 作为主键的字段一部分不可以包含 null value 且一个表中只能有一个
uniqueKey: true // 唯一键,该表没有两个不同的行或这些列的值相同的记录
},
name: Sequelize.STRING(90),
gender: Sequelize.BOOLEAN,
createAt: Sequelize.BIGINT,
updateAt: Sequelize.BIGINT
}, {
timestamps: false
})

var now = Date.now()

// create:{"id":2,"name":"User","gender":true,"createAt":1631731389401,"updateAt":1631731389401}
tousers = async () => {
var addusers = await users.create({
id: 2,
name: 'User',
gender: true,
createAt: now,
updateAt: now
})
console.log('create:' + JSON.stringify(addusers))
}

tousers()

Delete

通过 Sequelize 所提供的方法直接使用 destory() 对查询到的数据进行破坏即可完成删除需求:

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
const Sequelize = require('sequelize')
const mysql = require('mysql2')

const config = require('./config')

var sequelize = new Sequelize(config.databases, config.username, config.password, {
host: config.host,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 30000 // 连接在被释放之前可以空闲的最长时间(以毫秒为单位)。
}
})

sequelize
.authenticate()
.then(() => {
// Connection has been established successfully
console.log('连接已成功建立')
})
.catch(err => {
// Unable to connect to the database
console.error('无法连接到数据库', err)
})

// 定义一个模型
var users = sequelize.define('users', {
id: {
type: Sequelize.STRING(40),
primaryKey: true, // 作为主键的字段一部分不可以包含 null value 且一个表中只能有一个
uniqueKey: true // 唯一键,该表没有两个不同的行或这些列的值相同的记录
},
name: Sequelize.STRING(90),
gender: Sequelize.BOOLEAN,
createAt: Sequelize.BIGINT,
updateAt: Sequelize.BIGINT
}, {
timestamps: false
})

todelete = async () => {
var delUser = await users.findAll({
where: {
id: 2
}
})
for (let d of delUser) {
await d.destroy()
}
}

todelete()

Updete

对于 Sequlize 的修改同样是先进行查询后使用其 Model 进行修改数据,并通过 .save() 进行保存,从而达到更改数据字段的效果

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
const Sequelize = require('sequelize')
const mysql = require('mysql2')

const config = require('./config')

var sequelize = new Sequelize(config.databases, config.username, config.password, {
host: config.host,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 30000 // 连接在被释放之前可以空闲的最长时间(以毫秒为单位)。
}
})

sequelize
.authenticate()
.then(() => {
// Connection has been established successfully
console.log('连接已成功建立')
})
.catch(err => {
// Unable to connect to the database
console.error('无法连接到数据库', err)
})

// 定义一个模型
var users = sequelize.define('users', {
id: {
type: Sequelize.STRING(40),
primaryKey: true, // 作为主键的字段一部分不可以包含 null value 且一个表中只能有一个
uniqueKey: true // 唯一键,该表没有两个不同的行或这些列的值相同的记录
},
name: Sequelize.STRING(90),
gender: Sequelize.BOOLEAN,
createAt: Sequelize.BIGINT,
updateAt: Sequelize.BIGINT
}, {
timestamps: false
})

var now = Date.now()

/*
MariaDB [node]> select * from users;
+----+---------------+--------+---------------+---------------+
| id | name | gender | createAt | updateAt |
+----+---------------+--------+---------------+---------------+
| 1 | Administrator | 1 | 1631729482972 | 1631731823059 |
+----+---------------+--------+---------------+---------------+
*/
toupdete = async() => {
var upAdmin = await users.findAll({
where: {
id: 1
}
})
for (let u of upAdmin) {
u.gender = true
u.updateAt = now
await u.save()
}
}

toupdete()

Node.js Mocha

Node.js 是一个面向服务端的 JavaScript,为了能够有效的持续保证代码质量,通过单元测试(Unit Testing) 来进行正确性校验的测试工作,Node.js 也有 MochaShould 以及 SuperTest 模块用于。

单元测试主要的目的就是这对程序模块中进行正确性检验测试工作,而程序单元是应用的最小测试部件。

一个单元就是单个程序或者函数、过程等,而对于面向对象编程,则最小单元就是方法和超类、子类中的方法。

在此之前,JavaScript 主要用于在前端,自从 node.js 起始以来 JavaScript 开始面向客户端以及跨平台的使用,这时单元测试就显得尤为重要。we

BDD or TDD

TDD

单元测试主要分为 BDD(Behavior-Driven Development, 行为驱动开发),以及TDD(Test-Driven Development,测试驱动开发)

其中 TDD 原理是指在开发功能之前先编写单元测试的用例代码,TDD 也是 极限编程(Extreme programming,xp)的实现.

极限编程是软件工程方法学中敏捷软件开发的一种,他更倾向与适应性以及可预测性,即在任何阶段去适应变化。

BDD

行为驱动开发(Behavior-driven development,BDD)是敏捷开发技术之一,他也是极限开发(Extreme Programming)鼓励软件项目的开发者和非技术人员和以及商业参与者之间的协作并保证最后的 QA(Quality Assurance)

这主要是出于 2009 年 Dan North 对 BDD 给出的定义:

BDD是第二代的、由外及内的、基于拉(pull)的、多方利益相关者的(stakeholder)、多种可扩展的、高自动化的敏捷方法。它描述了一个交互循环,可以具有带有良好定义的输出(即工作中交付的结果):已测试过的软件。

Mocha

Mocha 主要是用于 JavaScript 的测试框架,使得异步测试变得简单且 “有趣”。他最为重要的是 Mocha 测试是连续的,在正确的测试条件中遇到未捕获的异常时将会给出灵活且准确的报告。

Assert

断言(Assertion)是单元测试中用来保证最小单元是否正常的测试方法之一,目的是验证开发者所预期的效果,当程序运行到断言位置时会返回布尔值,其中返回为真则表示正常,否则将会终止运行并给出错误消息。

是 Mocha 中的核心方法之一,这由 Node.js 官方库所进行提供,并在 Node.js 的 API 中定义了多种检测方法供开发者使用,将方法与 Mocha 进行配合来以低成本的方式进行单元测试。

Id Name Info
1 assert(value,[message]) 当 value 为 false 时终止运行并返回错误
assert.ok(value,[message]) assert 的别名
2 assert.ifError(value) 如果 value 不是 nullundefined 则报错并抛出 value
3 assert.match(value,regExp) value 是否匹配正则表达式
4 assert.notEqual(actual,expected[,message]) 严格匹配 actual != expected 则正常运行,否则抛出异常错误
assert.notStrictEqual(actual,expected[,message]) assert.notEqual 的别名
assert.notDeepStricEqual(actual,expect[,message]) 深度匹配 actual != expected 是否相等,assert.notEqual 的别名
5 assert.equal(actual,expected[,message]) 严格匹配 actual 与 expected 是否相等
assert.strictEqual(actual,expected[,message]) Euqal 别名
assert.deepEqual(actual,expected[,message]) Equal 别名
assert.deepStrictEqual(actual,expected[,message]) Equal 别名
6 assert.throws(function[,error][,message]) 判断代码快是否抛出异常并自定义异常错误
assert.doesNotThrow(function[,error][,message]) 捕获错误然后重新抛出错误

assert or assert.ok

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

/*
AssertionError [ERR_ASSERTION]: 当 assert 为 false 时终止运行并返回错误
at Object.<anonymous> (/home/kunlun/Development/Web/Demo/node/test.js:3:8)
at Module._compile (internal/modules/cjs/loader.js:1072:14)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1101:10)
at Module.load (internal/modules/cjs/loader.js:937:32)
at Function.Module._load (internal/modules/cjs/loader.js:778:12)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12)
at internal/main/run_main_module.js:17:47 {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: false,
expected: true,
operator: '=='
}
*/

//assert.ok(false,'当 assert 为 false 时终止运行并返回错误') or
assert(false,'当 assert 为 false 时终止运行并返回错误')

assert.ifError()

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

assert.ifError(null)

/*
AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 'Error'
at Object.<anonymous> (/home/kunlun/Development/Web/Demo/node/test.js:4:8)
at Module._compile (internal/modules/cjs/loader.js:1072:14)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1101:10)
at Module.load (internal/modules/cjs/loader.js:937:32)
at Function.Module._load (internal/modules/cjs/loader.js:778:12)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12)
at internal/main/run_main_module.js:17:47 {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 'Error',
expected: null,
operator: 'ifError'
}
*/
assert.ifError('Error')

assert.match

1
2
3
4
5
6
7
8
9
10
const assert = require('assert')

// ok
assert.match('hey,assert!', /[a-z]/);

/*
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /[a-z]/. Input:
'2021'
*/
assert.match('2021', /[a-z]/)

assert.notEqual or assert.notStricteEqueal at assert.notDeepStrictEuqal

1
2
3
4
5
6
7
8
9
10
const assert = require('assert')

// ok
assert.notEqual(1,2,'ok')

// AssertionError [ERR_ASSERTION]: This is !=
assert.notStrictEqual(1,1,'This is !=')

// ok
assert.notDeepStrictEqual(1,'1','This is ok!')

assert.equal at assert.strictEqual() or deepEqual() and deepStrictEqual()

1
2
3
4
5
6
7
8
9
10
11
// ok
assert.equal(1,1,'This is ok')

// ok
assert.strictEqual(1,1,'This is ok')

// ok
assert.deepEqual(1,1,'This is ok')

// AssertionError [ERR_ASSERTION]: This is error
assert.deepStrictEqual(1,2,'This is error')

asset.throws()

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 assert = require('assert')

/*
AssertionError [ERR_ASSERTION]: Throw an exception error
at Object.<anonymous> (/home/kunlun/Development/Web/Demo/node/test.js:4:8)
at Module._compile (internal/modules/cjs/loader.js:1072:14)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1101:10)
at Module.load (internal/modules/cjs/loader.js:937:32)
at Function.Module._load (internal/modules/cjs/loader.js:778:12)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12)
at internal/main/run_main_module.js:17:47 {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: Error: Error value
at assert.throws.code (/home/kunlun/Development/Web/Demo/node/test.js:6:15)
at getActual (assert.js:765:5)
at Function.throws (assert.js:911:24)
at Object.<anonymous> (/home/kunlun/Development/Web/Demo/node/test.js:4:8)
at Module._compile (internal/modules/cjs/loader.js:1072:14)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1101:10)
at Module.load (internal/modules/cjs/loader.js:937:32)
at Function.Module._load (internal/modules/cjs/loader.js:778:12)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12)
at internal/main/run_main_module.js:17:47,
expected: {
code: 404,
name: 'TypeError',
message: 'Error value',
info: { nested: true, data: 'nested ok' }
},
operator: 'throws'
}
*/
assert.throws(
() => {
throw new Error('Error value')
},{
code: 404,
name: 'TypeError',
message: 'Error value',
info: {
nested: true,
data: "nested ok"
}
},'Throw an exception error'
)

assert.doesNotThrow()

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

// AssertionError [ERR_ASSERTION]: Got unwanted exception: This will print erroras
assert.doesNotThrow(
() => {
throw new TypeError('Error value')
},'This will print error'
)

demo

app.js

首先我们需要通过一个计算的函数来进行添加 tests.js 测试用例,并最终通过测试用例进行检测代码是否通过测试:

1
2
3
4
5
6
7
8
exports.add = function (i,j) {
return i + j
}

exports.mul = function (i,j) {
return i * j
}

appTests.js

在 Mocha 中,describe 方法的主要作用就是添加一段描述而 it 则是向测试回调方法中添加一个参数即 done,Mocha 将会等待调用此函数以完成测试,并接受 Error 实例。

需要注意的 it() 任何东西都是无效使用并引发错误,这通常会导致测试失败

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
const assert = require('assert')
const calc = require('./app')

/*
$ npm test
> node@1.0.0 test /home/kunlun/Development/Web/Demo/node
> mocha work.js

Calculator Tests
Addition Tests
✔ returns 1 + 1 = 2
✔ should returns 1 + -1 = 0
✔ returns 0 * 4 = 4
Multiplication Tests
✔ returns 2 * 2 = 4
*/
describe('Calculator Tests', function () {
describe('Addition Tests', function () {
it('returns 1 + 1 = 2', function (done) {
assert.equal(calc.add(1,1),2)
done();
})

it('should returns 1 + -1 = 0', function (done) {
assert.equal(calc.add(1,-1),0)
done()
});

describe('Multiplication Tests', function () {
it('returns 2 * 2 = 4', function (done) {
assert.equal(calc.mul(2,2),4)
done()
});
});

it('returns 0 * 4 = 4', function (done) {
assert.equal(calc.mul(2,2),4)
done()
});
});
});

在上述 code 中,我们使用了 node.js 那种的 assert 模块,通常如果抛出一个 Error 就被执行工作,并向 app.js 添加 ij 参数值。

istanbul

istanbul 是一个使用行计数器检测 ES5、ES6 的 JavaScript 代码,来跟踪单元测试和代码的执行情况(也就是单元测试对代码的覆盖率),可直接根据 mocha 来添加 istanbul:

1
npm install --save-dev nyc

之后在 package.json 中将 nyc 放入 test 之前即可:

1
2
3
"scripts": {
"test": "nyc mocha work.js"
},

通过 npm test 运行后可以看到 instanbul 所提供的单元测试覆盖率检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  Calculator Tests
Addition Tests
✔ returns 1 + 1 = 2
✔ should returns 1 + -1 = 0
✔ returns 0 * 4 = 4
Multiplication Tests
✔ returns 2 * 2 = 4


4 passing (5ms)

----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
app.js | 100 | 100 | 100 | 100 |
work.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------

Node.js error

对于错误的处理主要分为代码的错误和系统本身的问题其中包含内存不足或者打开文件过多、系统配置以及网络和远程服务问题,错误的处理需要加入到没有任何错误处理的程序中。

需要考虑到任何会导致失败所可能产生的结果,错误是一种 Error 的实例,从而抛出一个异常,或者通过 callback 进行无抛异常进而通过异常。

Error function

Id Name Info
1 throw 以同步的方式传递异常,如果调用者通过 try/catch 异常就会被捕获,如果异常未被捕获则会被 domains 或者进程级的 uncaughtExceptin 捕获
2 callback 是基础的异步传递方式,用户传进来一个函数(callback) 之后当某个异步操作完成后调用 callback ,在正常情况下 callback(err,result) 的形式被调用,这两个参数自然有一个是非空的,这取决与成功还是失败
3 EventEmitter 最为主要的是 EventEmitter 是没有 callback 而是返回 EventEmitter 对象。
error 当做一个可能会产生多个错误或多个结果的复杂操作时所返回的错误
row callback 所返回的 EventEmitter 的结果将会触发一个 row 事件
endrow 事件被触发的时候将会触发或返回一个 end 事件

通常 callbackEventEmitter 将会归到同一个异步的错误传递,使用 rhrowcallback 以及 EventEmitter 取决与 该一场是操作失败还是系统所造成的 BUG 以及 该函数是同步和异步的

其中即可以同步传递错误并抛出,又可以传递错误(通过传给一个回调函数或者触发 EventEmittererror 事件)。或者选择回调函数 try.catch 两种具体取决与异常的传递。

同步的异常捕获 (try/catch)

1
2
3
4
5
6
7
8
9
10
11
/*
捕获异常
Error: error!!
*/
try {
throw new Error('error!!')
} catch (e) {
console.log('捕获异常')
console.log(e)
}

process uncaughtException

假设 try/catch 未被捕获,则可以通过 process 所提供的 uncaughtException 事件来监听未捕获的异常:

1
2
3
4
5
6
7
8
9
10
11
try {
setTimeout(() => {
throw new Error('Error info')
})
} catch (e) {
console.error('error is:', e.message)
}

process.on('uncaughtException', (e) => {
console.log('process error is:', e.message)
})

events

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

/*
error is code
Error: Code is error
*/
const emitter = new events.EventEmitter()

emitter.addListener('error', (e) => {
console.log('error is code')
console.log(e)
})

emitter.emit('error', new Error(('Code is error')))

通过 events 库所提供的监听器来监听 error 的事件以及使用 emitter.emit 来发送错误并回调错误信息。

callback

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

/*
Error this process
*/
fs.mkdir('/dir',(e) => {
if (e) {
console.log("Error this process")
} else {
console.log('create dir')
}
})

Promise

promise 的操作返回 promise 其主要是在异步中操作完成后或失败所最终完成的结果值,他提供了两个值 resolve 以及 rejectreject 方法返回一个带有拒绝原因的 Promise 对象,而 resolve 则是返回一个指定值解析后的 promise 对象。

1
2
3
4
5
6
7
8
9
10
11
12
/*
This is error
Error: error
*/
new Promise((resolve, reject) => {
throw new Error('error')
}).then(() => {
// 逻辑处理
}).catch((e) => {
console.log('This is error')
console.log(e)
})

Async/Await

Async/Await 是基于 Promise 的更简洁的风格所编写,其中 async 韩式使用 async 关键字声明函数,从而避免了显式配置承诺链的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const func = function () {
return new Promise((resolve, reject) => {
throw new Error('error')
})
}

/*
This is error
Error: error
*/

async function funAsync() {
try {
await func()
} catch (e) {
console.log('This is error')
console.log(e)
}
}

funAsync()

domain

domain 模块可以处理多个不同 IO 的操作作为一个组,并注册到事件和回调到 domain,当发生一个错误事件或错误时,domain 对象将会被通知,从而不会丢失上下文环境,也不会导致程序立即退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const doman = require('domain')
const d = doman.create()

d.on('error', (err) => {
console.log('err',err.message)
console.log(needSend.message)
})

const needSend = {message: 'To error data'}
d.add(needSend)

function excute() {
try {
setTimeout(() => {
throw new Error('Error info')
})
} catch (e) {
console.log('error is', e.message)
}
}

d.run(excute)

Node.js 多线程

进程\线程\协程

进程

在此之前,我们需要了解到进程、协程、以及并发、并行的概念。首先进程(process) 是指计算机中已运行的程序,进程的出现是指为了更好的利用 CPU 资源使得并发的可能性。

假设有两个任务,A 和 B ,A 遇到 I/O 操作的时候 CPU 就会默默的等待任务 A 读取完成再去执行 B 任务,这样救护导致 CPU 资源造成了浪费。

因此就会有人在想,将任务 A 读取数据时让 B 任务执行,当任务 B 读取完整数据后又切换到任务 A 执行。

这时候因为 “切换” 的问题,需要涉及到保存和状态回复,以及 A 与 B 的系统资源不一,就需要有一个东西记录 A 和 B 分别需要什么样的资源和如何识别 A 和 B 等,因此进程就被创建出来

通过进程可以分配资系统资源、标识任务,以及分配 CPU 执行进程为调度,进程的状态记录、恢复。其中切换又被称之为 “上下文切换”,其中进程是指系统分配的最小单位,进程所占用的资源共有地址空间,全局变量、文件描述符、各种硬件资源等。

线程

线程(tread) 是操作系统能够进行运算调度的最小单位,大部分情况下他被包含在进程中,他的出现主要是为了降低上下文的消耗以提高系统的并发性,并解决一个进程只能完成一件任务的问题,使得进程内并发成为可能。

在之前进程处理任务的时候,当只有一个进程的时候只能干一件事,而假设有了多个进程,每个进程只能负责一个任务,A 负责接收一个信息,B 负责显示信息,C 负责保存信息,这里的各个进程之间就会涉及到进程通信的问题,假设共同维护一个文本内容或切换,则会造成性能上的消耗。

此时线程主要的作用就是使得 A、B、C 共享资源,并参与 CPU 的调度,同时进程也是线程的容器,当一个现成挂掉则全部线程也会失效。

协程

协程(coroutine) 通过线程中实现调度以避免陷入内核级别的上下文切换所造成的性能损失,进而突破了线程在 I/O 上的性能瓶颈。

当涉及到较大的并发连接时,以线程作为处理单元(系统调度开销开始过大),当连接数过多时则需要大量的线程,可能部分的线程处于 ready(准备) 状态,系统会不断的进行上下文切换。

而瓶颈就是上下文切换,在线程中自己实现调度从而不陷入内核级的上下文切换,这也是协程的主要作用。

并发与并行

并发

并发计算(Concurrent computing) 是一种程序的计算形式,通常是指有两个以上工作单位的计算在同时运行,这也是多核的前提,但单线程或单核是很难利用多进程达到并行状态。

并行

并行(parallel computing) 一般是指许多工作任务同时进行的计算模式,可以将计算的过程分为许多小部分,之后以并发来解决。

总结来说并发是指可以同时出发,而并行则是指一起执行,你可以理解为一边一吃饭一边玩游戏是并发。而你将外卖摆在桌上和游戏,你先吃饭再打游戏是并发。

JavaScript 单线程的异步和非阻塞

线程安全

单线程的 JavaScript 保证了线程的绝对安全,不会单行同一个变量被多个线程读写从而造成崩溃,从而免去了在多线程开发中忘记对变量加锁或者解锁所造成的问题。

单线程的异步非阻塞

Node.js 是一个单进程的,因此提供了一个 libuv 库来进行实现多进程,如在 fs 库中的多进程就通过 libuv 库进行实现,他主要提供了异步 I/O 。

虽然 node.js 是单线程异步非阻塞的,但在某一个情况下他采用的还是阻塞式的单线程

libuv


libuv 是一个跨平台的 I/O 库,用于实现和让 node.js 支持多线程,同时也被 fs 使用他主要提供事件循环(Event Loops)、文件系统(Filesystem)、网络支持(Networking)、线程(Threads)、进程(Process)以及其他工具(Utilities)

Web Worker 实现多进程

实现多进程可以通过 Node API 中的 child_process ,所提供的 fork 方法来创建一个 Node.js 文件并将它作为 worker 文件,类似与 express 库:

app.js

.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const express = require('express')
const fork = require('child_process').fork
const app = express()

app.get('/', function (req,res) {
var worker = fork('./work.js') // 创建进程
worker.on('message', function (m) { // 接收计算结果
if ('object' === typeof m && m.type == 'fibo') {
// 发送关闭进程信号
worker.kill()
res.send(m.result.toString()) // 返回结果
}
})
worker.send({type:'fibo', num:~~req.query.n || 1})
})
app.listen(8210)

其中 worker.kill 的用法是通过 work.js 中的 process.on 监听 SIGHUP 事件函数并通过 process.exit 从而使子进程推出的

work.js

.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义算法
var fibo = function fibo(n) {
return n > 1 ? fibo(n -1) + fibo(n -2) :1
}

// 接受 app.js 主进程所发送的消息
process.on('message', function (m) {
if (typeof m === 'object' && m.type() === 'fibo') {
// 计算 fibo
var num = fibo(~~m.num)

// 发送结果
process.send({type:'fibo', result:num})
}
})

// 收到消息后进程退出
process.on('SIGHUP',function () {
process.exit()
})

Node.js 异步 I/O

Node 还有 异步/IO、事件驱动、单线程等特点,其中 “单线程” 是指程序的主线程,也就是将所有阻塞的部分交给线程池进行处理,之后一个队列跟线程池进行协作。

对于 JavaScript Code 不再需要过多关注线程的问题,主要通过 callback 回调构成,当程序运行时主线开始不停的循环适中调用。

至于 node.js 所采用的单线程异步非阻塞模式,就是在异步非阻塞模式执行的过程中,I/O 操作不会阻塞后面的程序执行或计算,当完成 I/O 时,以事件的方式通知,继续执行。

异步 I/O

异步 I/O 其实就是用户空间中的程序不用依赖内核空间中的 I/O 即可操作完成并进行下一步操作,以同步作为对比来讲,假设两个任务的时间按分别为 mn,如果程序采用同步的方式完成此操作则需要的任务时间为 m+n

但如果是采用异步方式处理程序,则可以在两种 I/O 并行的情况下完成任务的时间最大限度的不超过本身的处理最大限度,即同步完成任务的时间 max(m,n)

运行空间

在操作系统中,程序的运行空间(又被称之为核心空间)主要分为内核空间(Kernel space)和用户空间,以 Linux 系统对自身进行划分为例,对于内核空间,是指对硬件设备具有访问权限,这个空间一部分核心软件独立于普通的应用程序,运行在最高或较高的权限级别中。

于此相对的还有一个用户空间(User space),在操作系统中,用户空间又被称之为 “虚拟内存” 或者 “使用者空间” 与核心空间共为运行空间中的两个区块

同样以 Linux 系统作为例子,除了对硬件设备具有访问的通常被称之为内核空间外,对于普通应用程序这部分的,就被称之为用户空间。

必要性

以编程语言为例,为了设计的让应用程序调用方便,将程序设计为同步的 I/O 模型,这就意味着程序中的后续任务都需要等待 I/O 的完成。从而在等待的过程中无法充分的利用 CUP。

但为了充分利用 CPU 以使 I/O 可以并行,可通过实现多线程、单线程以及单线成多进程来达到目的。

多线程与单进程

多线程主要的设计理念就是为了共享在程序空间中实现并行处理任务,从而达到充分利用 CPU 空间的效果,但缺点就是在执行时上下文交换开销较大和同步的问题,但这样会让程序变得复杂化。

单线程与多进程

此方法用于解决多线程与单进程使用的问题,有的语言为了单线程保持了调用的简单化和用户体验,采用多进程的方式来达到充分利用 CPU 和提升总体并行处理能力的行为,但缺点在于当应用程序稍微复杂时,因为业务逻辑无法分布多个进程之间,所以事物的处理能力较高。

但在设计程序时应该选择前者,后者相对与性能若后与前者,且没有优化的余地,而前者可以通过优化来解决开销较大和同步的问题。

还有一个非常主要的问题,在分布式系统或通过 API 方式来处理数据,因此数据的传递很难保持同步的状态,且干扰原因有很多,因此如果网络速度有边,则 mn 的值都会变大,这时候同步的 I/O 语言就会露出弱势。

在分布式系统场景下,异步 I/O 将会体现出优势max(m,n) 的时间最大开销可以有效的缓解值增长所带来的问题。

I/O 线程和状态

对于计算机内核 I/O 而言,同步与异步是线程之间的关系,而阻塞、非阻塞则是对于某一个时刻中的一个状态,线程只能是阻塞或异步的状态,要么线程处于阻塞状态或非阻塞状态。

I/O 的阻塞与非阻塞

阻塞模式的 I/O 自如其名会造成应用程序的等待,直到阻塞完成,同时操作系统也将 I/O 操作设置为非阻塞模式.

在设置非阻塞模式时,可能在应用程序调用时没有拿到真正数据就返回了,为此需要多次调用才能确认 I/O 操作完成

I/O 的同步和异步

如果做阻塞的 I/O 调用,应用程序的等待的调用完成的过程就是同步状态,反而 I/O 为非阻塞模式时,应用程序将是异步的。

异步 I/O 轮询

在进行非阻塞 I/O 调用时,需要读到完整的数据,应用程序需要多次进行论寻,才可确保读取数据的完成,并进行下一步操作

缺点在于应用程序需要主动调用,会造成占用较多的 CUP 时间片,性能较为低下

read

read 论寻技术用于同步非阻塞 I/O 模型,每次论需只能获取一个 I/O 操作对应的状态,轮询操作由用户进行负责,可通过 read() 系统调用,即用户线程需要在一定间隔内非阻塞的持续发起论寻请求来获取 I/O 操作当前状态,所以他是由用户所负责的。

虽然用户线程不会被阻塞,但他需要被不停的触发来引起系统调用,直到用户数据可用为止,因此他是一个较为原始且性能较低(含数据吞吐量较低)且想对于阻塞式系统调用,消耗了额外的 CPU 资源。

I/O 多路复用

I/O 多路复用(I/O Multiplexing)是为实现和满足用户线程不需要进行不停的轮询,以避免消耗额外的 CPU资源,同时在数据就绪时,用户线程处于休眠状态(sleep)。

而当一个或多个文件描述符对应的 I/O 操作数据准备完成时,用户线程将会被唤醒来处理这些 I/O 操作,目前包括 select、poll、epoll 等都是采用多路服用技术来提高性能。

文件描述符是一个非负整数,实际上是一个索引值,指向内核为没一个进程所维护的该进程打开文件的记录表,当程序打开一个现有的文件爱你或创建新文件时,该内核向进程返回一个文件描述符。

select

select 是一种 read 改进方案,也是一种 I/O 多路复用模型,通过对文件描述符上的事件状态来判断。可以让内核在多个文件描述符对应的 I/O 操作中任何一个完成或经过指定时间后唤醒(wake)并通知用户线程。

在唤醒之前用户线程因为被阻塞而处于休眠状态。

poll

poll 是 select 基础之上进行的改进,并改善了不需要的检查,但相对与文件描述符较多的时候,性能还是较为底下的,通过 poll 实现的轮询与 select 较为相似,但性能和限制都有所改善。

epoll

epool 为解决 select 和 poll 中每一次检查监听的文件描述符变化时,都需要遍历一次用户传入的描述符集合,当文件描述符较多时,一次集合的遍历时间会非常耗时的问题。

为了解决这个问题,epool 在 select 和 poll 上的基础进行优化在 Linux kernel 2.6 中引入了 epoll(event poll)方法,他将文件描述符的检测和真正的检测进行了分离。

从实现的角度上来说 epoll 会创建一个 epoll 实例,之后 epoll 实例会床将一个对应的文件描述符(这个 epoll 实例中包含了文件描述符的集合,被称之为了 epoll set \ interest list)。

集合中存储了所有希望被监听的文件描述符,当某个描述符对应的 I/O 操作时,文件描述符又会被放入 ready list

ready list 是 epoll set 的子集

kqueue\IOCP\event ports

kqueue

kqueue 方法实现与 epoll 类似,不过他仅仅在 FreeBSD 系统下存在,但 macOS 也会使用

IOCP

IOCP 方法相当与 Windows 下的 epoll

event ports

event ports 相当与 Solaris 下的 epoll

Node.js console at debug

console API

log\error\info\warn

Node 主要提供了四种常用的 API,其中主要分为 console.logconsole.errorconsole.infoconsole.warn 等/14.Node.js%20console%20at%20debug.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
/*
log: hey
log: hey world
log: hey world

error: hey
error: hey error
error: hey error

info: hey
info: world
info: world

warn: hey
warn: hey error
warn: hey error
*/
console.log('log: hey')
console.log('log: hey', 'world')
console.log('log: hey %s', 'world')

console.error('error: hey')
console.error('error: hey', 'error')
console.error('error: hey %s', 'error')

console.info('info: hey')
console.info('info: world')
console.info('info: %s','world')

console.warn('warn: hey')
console.warn('warn: hey', 'error')
console.warn('warn: hey %s', 'error')

在这 console.log 同等于 console.errorconsole.infoconsole.warn 等 API,当然可根据不同的场景和语法结构使用响应的方法。

console.time at timeEnd

通常 console.timeconsole.timeEnd 将用于评估 timetimeEnd 两点之间的时间差(单位为:毫秒)

1
2
3
// timeLabel: 0.069ms
console.time('timeLabel')
console.timeEnd('timeLabel')

console.assert

该 API 方法主要使用其 console.assert 进行断言(assertion),而断言主要是一种放在程序中的一阶逻辑,也就是结果为真或假的逻辑判断式。目的是为了表示与验证程序预期的结果,当程序运行到断言位置时,对应断言应为 true,假设程序终止并给出错误信息。

1
2
3
4
5
6
/*
Assertion failed: start wrong
*/

console.assert(true,'start','wrong')
console.assert(false,'start','wrong')

或者直接通过异常进行处理:

1
2
3
4
5
6
// Assertion failed: error
try {
console.assert(false,'error')
} catch (e) {
console.log(e.message)
}

console.trace

这主要用于将字符串打印到 stderr(标准错误)中,然后以 util.format() 格式的消息和代码到当前的位置堆栈跟踪

1
2
3
4
5
6
7
8
9
10
11
/*
Trace: This will output one end of the error stack
at Object.<anonymous> (/home/kunlun/Development/Web/Demo/node/app.js:1:9)
at Module._compile (internal/modules/cjs/loader.js:1072:14)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1101:10)
at Module.load (internal/modules/cjs/loader.js:937:32)
at Function.Module._load (internal/modules/cjs/loader.js:778:12)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12)
at internal/main/run_main_module.js:17:47
*/
console.trace('This will output one end of the error stack')

console.dir

该方法主要用于深层次的打印,其中可以通过 depth 进行深层次的打印层级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var obj = {
data: {
main: {
info: {
hey: 'hey.dir'
}
}
}
}

/*
{ data: { main: { info: [Object] } } }
{
data: { main: { info: { hey: 'hey.dir' } } }
}
*/
console.dir(obj)
console.dir(obj, {depth:3})

自定义 stdout

自定义 stdocut 即标准输出,基于 console.Console 将数据输出到指定的文件中,当然这其中会使用到 fs 模块。

1
2
3
4
5
const fs = require('fs')
const file = fs.createWriteStream('desfile.txt')

const logger = new console.Console(file, file);
logger.log('hello,world')

当运行之后,通过 logger.log 的内容数据将会直接写入到 fs.createWriteStream 方法中的 path 参数路径中。

desfile.txt:“hello,world”

debug

对于日志的调试,可以通过 console.log 输出调试日志,由于 console.log 可以支持服务端以及浏览器中使用,且非常的简洁易用,因此这种方法对日志的调试很好。

在开发中由于需要调试就需要多个 console.log 进行输出,之后上传到服务端中又注释 console.log,因此这就是一个加注释和去注释的过程。

但由于上述方法太过于低效,因此 debug 库就可以实现控制输出的调试,他主要的作用就是判断 DEBUG 环境变量,来调整程序的运行环境即可控制日志的是否输出,但他最为核心的还是对 DEBUG 环境变量进行解析,允许开发者选性的控制输出日志。

project

app.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
var debug = require('debug')('http')
, http = require('http')

/*
$ DEBUG=worker:* node app.js
worker:a This is worka +0ms
worker:b This is workb +0ms
worker:a This is worka +239ms
worker:a This is worka +40ms
$ DEBUG=worker:a node app.js
worker:a This is worka +0ms
worker:a This is worka +260ms
worker:a This is worka +42ms
$ DEBUG=worker:b node app.js
worker:b This is workb +0ms
worker:b This is workb +1s
worker:b This is workb +976ms
*/
http.createServer(function(req, res){
debug(req.method + ' ' + req.url);
res.end('hello\n');
}).listen(3000, function(){
debug('listening');
});

// 引入 console.log 工厂
require('./worker');

worker.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = require('debug')('worker:a')
, b = require('debug')('worker:b');

function work() {
a('doing lots of uninteresting work');
setTimeout(work, Math.random() * 1000);
}

work();

function workb() {
b('doing some work');
setTimeout(workb, Math.random() * 2000);
}

workb();

条件的定义

有些方法和事件可以通过 --conditions flag 进行使用,但同样可以在 debug 下进行引入参数,来实现对生产环境日志开启条件,在本地环境下开启日志:

Id Name Info
1 browser(浏览器) 任何环境,实现了 Web 浏览器中 JavaScript 所提供的全局浏览器 API
2 development(开发环境/本地环境) 可用于开发环境入口点,必须始终不与生产环境互斥
3 production(生产环境) 用于定义生产环经的切入点,并与本地/开发环境互斥
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const _ = require('lodash')
const debug = require('debug')
const debugA = debug('A:')
const debugB = debug('B:')

/*
$ DEBUG=A: NODE_ENV=production node app.js
A: hey,world! +0ms
$ DEBUG=B: NODE_ENV=production node app.js
B: I am new to debug +0ms
*/
if (process.env.NODE_ENV === 'production') {
debugA.enable = false
}

debugA('hey,world!')
debugB('I am new to debug')

命名空间

Node debug 支持对日志进行分类打印以及命名空间及通佩符,在下文中 DEBUG=ap* 表示将所有以 ap 为开头的调试日志都将会被打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const debug = require('debug')
var appDebug = debug('app')
var apiDebug = debug('api')

/*
$ DEBUG=api node app.js
api hey,ApiDebug +0ms
$ DEBUG=app node app.js
app hey,AppDebug +0ms
$ DEBUG=app,api node app.js
app hey,AppDebug +0ms
api hey,ApiDebug +0ms
$ DEBUG=ap* node app.js
app hey,AppDebug +0ms
api hey,ApiDebug +0ms
*/
appDebug('hey,AppDebug')
apiDebug('hey,ApiDebug')

Node events 事件触发器

Node 是一个主要通过 API 使用异步事件模型的 I/O 操作,某些类型对象或触发器周期的触发一个命名事件到 事件队列,如 net.Server 对象在每次连接时出发一个事件,以及 fs.readStream 对象在文件打开时也触发一个事件。

在这些过程中,所有能产生事件的对象都是 events.EventEmitter 实例,其中 events 模块中就只提供了一个 events.EventEmitter 对象,因此 EventEmitter 的核心就是事件的触发和事件的监听功能的封装。

注册事件 & 监听器

通过 event.emit 对象创建一个 some_event 对象,并在 3000ms 后触发该事件,然后通过 event.on 来触发该事件,此时 event.on 将是一个监听事件触发的作用。

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

// 调用 some_event 对象时触发回调
event.on('some_event', function () {
console.log('some_event 事件触发')
})

// 3000ms 后向 "event" 对象发送事件 "some_event"
setTimeout(function () {
event.emit('some_event')
},3000)

除此之外,EventEmitter 的每个事件由一个事件名和诺干的参数组成,事件名则是一个字符串形式,这通常表示一定的语义,对于每个事件、EventEmitter 还提供了一个支持多个事件的监听器,当该事件触发时,事件的监听器依次都会被调用,事件的参数会作为回调参数传递。

对于多个事件,当事件被触发时将会依次被输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const events = require('events')
const emitter = new events.EventEmitter()

/*
some_event1 参数1 参数2
some_event2 参数1 参数2
*/
emitter.on('some_event', function (args1,args2) {
console.log('some_event1', args1, args2)
})

emitter.on('some_event', function (args1,args2) {
console.log('some_event2', args1, args2)
})
emitter.emit('some_event', '参数1','参数2')

this

emit 方法允许为监听器传入任意参数,之后事件监听器中的 this 方法将会用于输出当前的事件属性和传入的方法等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const events = require('events')
const eventEmitter = new events.EventEmitter

/*
some_event
a b EventEmitter {
_events: [Object: null prototype] { some_event: [Function (anonymous)] },
_eventsCount: 1,
_maxListeners: undefined,
[Symbol(kCapture)]: false
}
*/
eventEmitter.on('some_event', function (a,b) {
console.log('some_event')
console.log(a,b, this)
})

eventEmitter.emit('some_event','a','b')

on at once

event 中,监听事件的方法可以通过 ononce 进行实现,两者的主要区别是后者只可以监听单次的 listener 函数,当下次在触发事件时,将会移出此监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const event = require('events')
const eventEmitter = new event.EventEmitter

/*
eventEmitter some_event
eventEmitter some_event
eventEmitter someEvent
*/
eventEmitter.on('some_event', function () {
console.log('eventEmitter some_event');
})

eventEmitter.emit('some_event')
eventEmitter.emit('some_event')

eventEmitter.once('someEvent', function () {
console.log('eventEmitter someEvent');
})

eventEmitter.emit('someEvent')
eventEmitter.emit('someEvent')

添加/移出监听器事件

通过 once 创建一个事件,且侦听 start 事件,并通过 eventEmitter.emit 方法来进行调用此方法,因此输出的是 netListenerstart 事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const events = require('events')
const eventEmitter = new events.EventEmitter

eventEmitter.once('newListener', function (event, listener) {
if (event == 'start') {
eventEmitter.on('start', function () {
console.log('start 2')
})
console.log('start')
}
})

eventEmitter.on('start', function () {
console.log('eventEmitter start')
})

// 调用事件
eventEmitter.emit('start')

Node stream

流(Stream)是一种传输手段,其核心含义是指一个有始有终的数据传输结构,这通常是文件打开 -> 关闭的过程,在 Node.js 中分为四个基本的流类型,分别为:

Id Name Info
1 Writable 可写入流(fs.createWriteStream()
2 Readable 读取流 (fs.createReadStream())
4 Duplex 可读流 (net.Socket)
4 Transorm 在读写的过程中可以修改和变换数据的可读写流(Duplex,zlib.createDeflate()

对于体积较大的二进制文件,如音频、视频文件或以 GB 为单位来衡量大小的,则会容易造成 爆仓 ,为避免爆仓的发生,读取体积大的文件应使用读一部分写一部分的方式进行。

Readable

对于 node 的可读取留,即通过 fs.createReadStream 方法创建一个读取流,其参数大多数可通过 Node.js File System 进行参考,在这其中 highWaterMark 参数主要的作用是内部读取缓存区大小,这将在某些情况下,需要触发底层可读留机制的刷新,从而不消耗任何数据。

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
const fs = require('fs')

/*
Stream in open
file data => This is desfile.txt
Stream in end
Stream in close
*/
var rs = fs.createReadStream('desfile.txt',{
flags: 'r',
mode: 0o666, // https://www.xuchao.org/docs/freebsd/permissions.html
encoding: 'utf-8',
start: 0,
end: 19,
highWaterMark: 1
})

rs.on('open', function () {
console.log('Stream in open')
})

rs.setEncoding('utf-8')

// 将可读流传出 data 事件
rs.on('data', function (data) {
console.log('file data => ',data)
rs.pause() // 切出流模式
setTimeout(function () {
rs.resume() // 恢复流模式
}, 2000)
})

rs.on('error', function () {
console.log('Stream in error')
})

rs.on('end', function () {
console.log('Stream in end')
})

rs.on('close', function () {
console.log('Stream in close')
})

之后根据其 Stream 流程 open -> data -> error -> end -> close ,其中在监听 data 事件中,就可读取文件的内容并输出数据,边度边输的模式,这将会不停的通过 data 读取数据、触发 data 后再次读取。

在这个过程中主要的不是将文件的整体内容读取并输出,而是通过 fs.createReadStream 方法所设置的缓存区,大小默认为 64KB(这可通过 highWaterMark 参数进行设置)

Writable

可写流(Writable Stream),主要通过使用 fs.createWriteStream() 方法进行创建,并通过 ws.write 进行写入,并当 end 事件后后使用 finish 事件来回调方法。

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
const fs = require('fs')
var ws = fs.createWriteStream('desfile.txt', {
flags: 'w',
mode: 0o666,
start: 0,
highWaterMark: 1
})

// 打开流
ws.on('open', function () {
console.log('Stream in open')
})

// 写入流
ws.write('hey,fs.createWriteStream()','utf-8')

// 关闭流
ws.on('close', function () {
console.log('Stream in close')
})

// 结束流
ws.end()

// 当 end 事件后且以刷新所有底层系统,则触发回调
ws.on('finish', function () {
console.log('Stream in end')
})

管道流

管道流提供了一个输出和输入流的机制,这最为常用到的场景就是获取一个流的数据并写入另一个流当中。

1
2
3
4
5
6
7
const fs = require('fs')
var rs = fs.createReadStream('befile.txt')
var ws = fs.createWriteStream('desfile.txt')

rs.on('data', function (chunk) {
ws.write(chunk)
})

或者通过其 rs.pipe() 方法,将可读流写到可写流当中,并触发 pipe 事件:

1
2
3
4
5
6
7
const fs = require('fs')
var rs = fs.createReadStream('befile.txt')
var ws = fs.createWriteStream('desfile.txt')

rs.on('data', function (chunk) {
ws.write(chunk)
})

Node.js Websocket

Websocket 是从 HTML 5 开始所提供的一种浏览器与服务器之间的全双工通信网络技术,与 即WebSocket 协议在 HTML 5 上的实施,该协议可在 TCP 连接上进行全双工通信,该协议在 2111 年由 IETF 开始标准化为 RFC6455

双工(duplex)

双工被分为 全双工(full-duplex)半双工(half-duplex),其中半双工的系统允许两台设备之间的双向资料的传输(但不能同时进行),若另一方需要发送,则需要等先前发送的消息处理完成后即可发送。

全双工(full-duplex)即允许两台设备之间同时进行双向的消息传输。

频分双工(FDD,Frequency-Division Duplexing)通过频率分割多任务技术来分割发送和接收的信号,上传和下载区段之间所使用的 频率偏移(Frequency offset) 来分隔。

WebSocket 协议支持客户端之间的双向通信,在此之前,创建一个双向的 Web 应用程序客户端和服务器之间进行通信,需要通过 HTTP 论寻,同时以不同的方式发送上游通知 HTTP 调用。

于是一个更简单的解决方法就是单个 TCP 链接,两个方向的交通使用 WebSocket 协议进行提供,因此他代替了 HTTP 论寻的替代方法到远程服务器,实际上 WebSocket 协议旨在取代现有的使用 HTTP 作为传输的双向通信技术。

new WebSocket.Server

Server

Node 所提供的 ws (WebSocket,ws),可通过 new WebSocket.Server 方法创建一个新的服务器实例,之后在通过其事件与方法来实现服务端。

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
const WebSocket = require('ws')

const server = new WebSocket.Server({
port:8210
})

// 当服务关闭时所返回的数据
server.on('close', function close() {
console.log("Server close")
})

// 监听新的请求时返回回调函数
server.on('connection', function connection(ws,req) {
const ip = req.connection.remoteAddress
const port = req.connection.remotePort
const name = ip + port;
console.log(`%s is connected`, name)

// 发送欢迎消息
ws.send('Welcome' + name)

// 服务器接收到消息时回调
ws.on('message', function incoming(message) {
console.log('received: %s from %s', message, name)

server.clients.forEach(function (client) {
if (client.readyState === WebSocket.OPEN) {
client.send( name + " -> " + message);
}
})
})
})

Client

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket</title>
</head>
<body>
<script type="text/javascript">
var socket
if (!window.WebSocket) {
// Firefox 6.0 之前,部分引擎会认为 WebSocket 对象是错误的,因此被重命名为
// "MozWebSocket"
window.WebSocket = window.MozWebSocket
}
if (window.WebSocket) {
socket = new WebSocket("ws://localhost:8210/ws")

// 从服务器中接受到消息时回调
socket.onmessage = function (event) {
var ta = document.getElementById('responseText')
ta.value = ta.value + '\n' + event.data
}

// 打开时回调
socket.onopen = function (event) {
var ta = document.getElementById('responseText')
ta.value = "链接开启"
}

// 关闭时回调
socket.onclose = function (event) {
var ta = document.getElementById('responseText')
ta.value = ta.value + "服务关闭"
}
} else {
alert("浏览器不支持 WebSocket")
}

// 发送消息
function send(message) {
if (!window.WebSocket) {
return
}
if (socket.readyState == WebSocket.OPEN) {
socket.send(message)
} else {
alert("链接暂未开启")
}
}
</script>
<form onsubmit="return false;">
<textarea id="responseText"></textarea>
<br>
<input type="text" name="message" style="width: 300px" value="Welcome">
<input type="button" value="发送消息" onclick="send(this.form.message.value)">
<input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空聊天记录">
</form>
</body>
</html>

之后可以在其服务端和客户端中查看到发送的消息,这主要通过 send 方法来进行发送,但前提条件是浏览器支持 WebSocket 方法才可适用。

📖 more posts 📖