node 简介

node.js 是⼀个 JS 的服务端运⾏环境,基于 V8,是在 JS 语⾔规范的基础上,封装了⼀些服务端的 runtime,能够简单实现⾮常多的业务功能。在 2009 年 (第一版 npm 被创建) 诞生之初是为了实现高性能的 web 服务器,再后来 node.js 慢慢演化为了一门服务端 “语言”
commonjs 是一个 规范,node.js 是 cjs 的实现

引入

node 作用

  • 跨平台开发: PC web H5 RN Weex
  • 后端开发: API, RPC
  • 前端开发: 前端工具链
  • 工具开发: 脚本、脚手架、命令行。

举例

  • 压缩: UglifyJS、JSMin
  • 管理: npm、yarn、bower
  • 模块系统: Commonjs, ESM
  • 模块构建: Babel、Browserify、Webpack、Gulp、Grunt
  • 生成器: yeoman、slush、CRA、CLI

node 缺陷

  • 单线程很脆弱,但是可以通过 cluster / pm2 多核并发实现负载均衡
  • node 对 MongoDB、Mysql、redis 的支持比较好,对 neo4j、tigerGraph 的支持比较差
  • 安全问题

vs 浏览器

  • Node 环境中是没有 DOM, BOM, 同样的,浏览器中也没有 fs, path 这些模块。
  • 事件循环
    • node 的事件循环
    • 浏览器: 微任务、宏任务、raf、 render、 requestIdleCallback
  • cjsesm
    • Node.js 使用 CommonJS 模块系统,而在浏览器中我们开始看到正在实施的 ESM 标准。

内核

npm

npm install 工作流程

npm CI

Continuous Integration (持续集成),npm 从 5.7.0 版本开始引入的一个命令,专门为自动化的持续集成环境设计。和 install 的不同点

  • 必须要有 package-lock.json 文件
  • 且下载完全依赖该文件
  • 会删除 node_modules
  • 如果和 package.json 冲突,则直接报错
  • 只能一次性安装
  • 永不改写 package.json 和 package-lock.json 文件

Dependencies

  • dependencies: 项目依赖 (lodash (debounce, deepMerge))
  • devDependencies: 开发依赖 (webpack, rollup, jest)
  • peerDependencies: 同版本依赖。比如 vue 组件库,如果说连 vue 都没,那这个项目没有意义
  • bundledDependencies: 捆绑依赖
  • optionalDependencies: 可选依赖

npm、cnpm、yarn、pnmp、npx

  1. npm: 包管理器,方便开发者分享和下载开源包。经历了许多重大版本的更新,各方面已经和 yarn 在同一水平

  2. npx: npm@5.2的产物,方便运行本地命令

  • npx 会帮你执行依赖包里的二进制文件: 不需要在 scripts 中声明命令
  • npx 原理: 运行的时候,会到 node_modules/.bin 路径和环境变量$PATH里面,检查命令是否存在。由于 npx 会检查环境变量$PATH,所以系统命令也可以调用。
  • 避免全局安装模块: npx 将 create-react-app 下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载 create-react-app。
  • —no-install: 如果想让 npx 强制使用本地模块,不下载远程模块,可以使用—no-install 参数。如果本地不存在该模块,就会报错。
  • —ignore-existing: 如果忽略本地的同名模块,强制安装使用远程模块,可以使用—ignore-existing 参数。
  • 使用不同版本的 node: $ npx node@0.12.8 -v,原理是从 npm 下载这个版本的 node,使用后再删掉。某些场景下,这个方法用来切换 Node 版本,要比 nvm 那样的版本管理器方便一些。
  • -p: 用于指定 npx 所要安装的模块。$ npx -p node@0.12.8 node -v,先指定安装node@0.12.8,然后再执行 node -v 命令。
  • -c: 如果 npx 安装多个模块,默认情况下,所执行的命令之中,只有第一个可执行项会使用 npx 安装的模块,后面的可执行项还是会交给 Shell 解释。-c 参数可以将所有命令都用 npx 解释
  1. cnpm: 方便中国开发者下载依赖包而诞生的下载器

  2. yarn: 解决了 npm@5 之前的一些让人诟病的问题,同时拥有一些其它的优点。例如离线安装、失败自动重试安装和并行下载等

  3. pnpm: 通过连接的方式,让多个项目的依赖公用同一个包,大大节省了磁盘空间

  • pnpm 运行起来非常的快,超过了 npm 和 yarn
  • pnpm 采用了一种巧妙的方法,利用硬链接和符号链接来避免复制所有本地缓存源文件, yarn 的最大的性能弱点之一
  • 使用链接并不容易,会带来一堆问题需要考虑
  • pnpm 继承了 yarn 的所有优点,包括离线模式和确定性安装

node API

Buffer

Buffer 是一种计算机中数据流结构。计算机中是以二进制的方式,进行数据存取的。而 js 在一开始,没有文件读写能力的,就要借助 Buffer 来实现一些缓冲区的内容。
Buffer 一般用于表示固定长度的缓冲区序列。浏览器中使用 File new Blob。

  1. Buffer 的声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let buf1 = Buffer.alloc(5); // 单位是字节 00000000
// string ==> buffer
let buf2 = Buffer.from("麓一"); // node 中一般编码使用的是 utf-8, 所以一个汉字,是3个字节。
let buf3 = Buffer.from([0xe9, 0xba, 0x93]);

console.log(buf1); // -> <Buffer 00 00 00 00 00>
console.log(buf2); // -> <Buffer e9 ba 93 e4 b8 80>
// buffer ==> string
console.log(buf3.toString()); // -> 麓
// copy
// 第一个0, 表示从0这个位置开始拷贝
// 第二和第三个数字,表示拷贝从几到几的长度。
buf2.copy(buf1, 0, 0, 2);
// concat
let bigBuffer = Buffer.concat([buf1, buf2], 6);
// slice
buf1.slice(0, 6);
// 类型判断
Buffer.isBuffer(buf);

Stream

防止淹没可用内存: Buffer 不适合大文件的读取,适合比较小的文件,对于大文件,需要使用流

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
const fs = require("fs");
const path = require("path");

// 创建可读流
const res = fs.createReadStream(path.resolve(__dirname, "../package.json"), {
flags: "r",
start: 0,
end: 20,
highWaterMark: 5, // 默认是 64K
autoClose: true,
emitClose: true,
});

let arr = [];

res.on("open", function (fd) {
console.log("fd", fd);
});

res.on("data", function (data) {
console.log("data", data);
arr.push(data);
});

res.on("end", function () {
console.log("end", Buffer.concat(arr).toString());
});

res.on("close", function () {
console.log("close");
});

res.on("error", function () {
console.log("error");
});

cluster

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const fs = require("fs");
const path = require("path");
const http = require("http");
// 子进程,开子进程。

const os = require("os");
const { default: cluster } = require("cluster");

const cpu_num = os.cpus();

if (cluster.isMaster) {
for (let i = 0; i < cpu_num.length; i++) {
cluster.fork();
}
} else {
http
.createServer((req, res) => {
res.end("childPid", process.pid);
})
.listen(3000);
}

事件循环

浏览器的事件循环

messageBump: 宏任务 -> 微任务 -> RAF -> Layout -> RequestIdleCallback

Node 的事件循环

基于 Libuv。Libuv 是一个高性能的、事件驱动的 I/O 库,为 Node.js 提供了跨平台的异步 I/O 能力,使之能够高效地处理大量并发请求

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
             同步的代码
|
process.nextTick / promise...
|
┌───────────────────────────┐
┌─>│ timers │ 定时器: setTimeout / setInterval
│ └─────────────┬─────────────┘
| process.nextTick / promise...
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │ 执行延迟到下一个循环迭代的I/O回调
│ └─────────────┬─────────────┘
| process.nextTick / promise...
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │ 系统内部使用
│ └─────────────┬─────────────┘ ┌───────────────┐
| process.nextTick / promise...
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
| process.nextTick / promise...
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │ setImmediate
│ └─────────────┬─────────────┘
| process.nextTick / promise...
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │ 关闭回调函数
└───────────────────────────┘
  • 定时器: 本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
  • 待定回调: 执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare: 仅系统内部使用。
  • 轮询: 检索新的 I/O 事件; 执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • 检测: setImmediate() 回调函数在这里执行。
  • 关闭的回调函数: 一些关闭的回调函数,如: socket.on(‘close’, …)。

总结
Node.js: microtask 在事件循环的各个阶段之间执行 > 浏览器: microtask 在事件循环的 macrotask 执行完之后执行

  1. node 的初始化
  • 初始化 node 环境
  • 执行输入代码
  • 执行 process.nextTick 回调
  • 执行微任务队列
  1. 进入 event-loop
  • 进入 timers 阶段(执行 setTimeout 和 SetInterval)
    • 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerid 升序进行
    • 检查是否有 process.nextTick 任务,如果有全部执行
    • 检查是否有 microtask,有全部执行
    • 退出该阶段
  • 进入 IO 阶段
    • 检查是否有 pending 的 io 回调,如果有,执行回调,如果没有,退出该阶段
    • 检查是否有 process.nextTick()任务,如果有,全部执行
    • 检查是否有 MicroTask,如果有全部执行
    • 退出该阶段
  1. 进入 idle,prepare 阶段
  2. 进入 poll 阶段
  • 首先检查是否存在尚未完成的回调,存在,则
    1. 如果 有可用回调,
    • 那么执行
    • 检查是否有 process.nextTick()回调,有全部执行
    • 检查是否有 MicroTask,如果有全部执行
    • 退出该阶段
    1. 如果没有可用回调
    • 检查是否有 immediate 回调,如果有退出 poll 阶段,如果没有,阻塞在此阶段,等待新的事件通知
  • 如果不存在尚未完成的回调,退出 poll 阶段
  1. 进入 check 阶段
  • 如果有 immediate 回调,执行所有回调
  • 检查是否有 process.nextTick()回调,如果有,全部执行
  • 检查是否有 MicroTask,如果有全部执行
  • 退出该阶段
  1. 进入 closing 阶段
  • 如果有 immediate 回调,则执行所有 immediate 回调。
  • 检查是否有 process.nextTick 回调,如果有,全部执行。
  • 检查是否有 microtaks,如果有,全部执行。
  • 退出 closing 阶段
  1. 检查是否有活跃的 handles(定时器、io 等事件)
  • 如果有,继续下一轮循环
  • 没有则结束事件循环,退出程序

示例

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
async function async1() {
console.log("async1 started");
await async2();
console.log("async end"); // m 1
}
async function async2() {
console.log("async2");
}
console.log("script start.");
setTimeout(() => {
console.log("setTimeout0");
setTimeout(() => {
console.log("setTimeout1");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});
}, 0);

async1();
process.nextTick(() => {
console.log("nextTick"); // m 0
});

new Promise((resolve) => {
console.log("promise1");
resolve();
console.log("promise2");
}).then(() => {
console.log("promise.then"); // m2
});
console.log("script end.");

// script start.
// async1 started
// async2
// promise1
// promise2
// script end

// nextTick
// async end
// promise.then

// setTimeout0
// setImmediate
// setTimeout1

Node 框架

express / koa

express 是一个基于 node.js 平台的一个灵活的 web 应用开发框架,connect 中间件,内置了视图、static 等部分
koa2 相对来说更新一些,也是由 express 原班人马打造的框架,通过中间件来实现

1
2
3
4
5
6
7
8
const express = require("express");
const path = require("path");

const app = express();

app.use(express.static(path.resolve(__dirname, "./public")));

app.listen(3000, () => console.log("server is running in 3000"));
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 Koa = require("koa");
const app = new Koa();

const api = () =>
new Promise((resolve) => {
setTimeout(() => {
console.log("timing...");
resolve(100);
}, 100);
});

app.use(async (ctx, next) => {
console.log("querying start 1");
const result = await api();
ctx.result = result;
await next();
console.log("querying end 1");
});

app.use(async (ctx, next) => {
console.log("querying start 2", ctx.result);
next();
console.log("querying end 2");
});

app.use(async (ctx, next) => {
console.log("querying start 3");
next();
console.log("querying end 3");
});

const main = (ctx) => {
ctx.body = "hello world";
};

app.use(main);
app.listen(3008);

// querying start 1
// timing...
// querying start 2 100
// querying start 3
// querying end 3
// querying end 2
// querying end 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
function num(ctx, next) {
console.log("starting num ...");
next(ctx * 10);
console.log("ending num ...");
}

function discount(ctx, next) {
console.log("starting discount ...");
next(ctx * 0.8);
console.log("ending discount ...");
}

function express(ctx, next) {
console.log("starting express ...");
next(ctx + 12); // 不包邮,12运费
console.log("ending express ...");
}

// compose实现.
function compose(args) {
let result;
return function (ctx) {
// ctx 初始化时,是我们放进去的 150 元,我们要不断地计算这个值。
let i = 0;
let dispatch = function (i, ctx) {
let fn;
if (i < args.length) fn = args[i]; // fn 就是我每一个函数。
if (i === args.length) {
result = ctx;
return;
}
return fn(ctx, dispatch.bind(null, ++i));
};
dispatch(i, ctx);
return result;
};
}

const sell = compose([num, discount, express]);

console.log(sell(150)); // 1212
// starting num ...
// starting discount ...
// starting express ...
// ending express ...
// ending discount ...
// ending num ...

koa 框架原理

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
// ⼊⼝⽅法
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
// (res, req) => { }
callback() {
// 处理中间件,等一下看compose和this.middleware
const fn = compose(this.middleware);
// 错误处理,listenerCount是EventEmitter类的函数
if (!this.listenerCount('error')) this.on('error', this.onerror);
// 传递给createServer的就是下面这个函数
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};

return handleRequest;
}

handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
// 这里等到response再看
const handleResponse = () => respond(ctx);
// 给请求结束增加一个回调,这个onerror是ctx的onerror,不是app的onerror
onFinished(res, onerror);
// 等一下看这个
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

// 添加中间件⽅法
use(fn) {
...
this.middleware.push(fn);
return this;
}

createContext(req, res) {
// 每次请求,ctx都是一个新的对象
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
// 原生的req和res
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
// koa生成的request和response
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}

koa-compose

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
"use strict";

/**
* Expose compositor.
*/

module.exports = compose;

/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/

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!");
}

/**
* @param {Object} context
* @return {Promise}
* @api public
*/

return function (context, next) {
// last called middleware #
let index = -1;
return dispatch(0);
function dispatch(i) {
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 {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}

koa 常用中间件

  • koa2-cors
  • koa-static
  • koa-bodyparser

BFF

Backends For Frontends

在后端普遍采用微服务的情况下,作为适配层,更好的为前端服务。
优势: 降低沟通成本,提升用户体验
问题: 资源浪费,增加架构复杂度,管理复杂

Sequelize

Sequelize 是一个基于 promise 的 Node.js ORM, 目前支持 Postgres, MySQL, MariaDB, SQLite 以及 Microsoft SQL Server. 它具有强大的事务支持, 关联关系, 预读和延迟加载,读取复制等功能