https://blog.csdn.net/weixin_45857341/article/details/136226235

主流框架介绍
目前比较流行的 Node.js 框架有Express、KOA 和 Egg.js,其次是另外一个正在兴起的与 TypeScript 相关的框架——Nest.js,接下来我们分析三个主流框架之间的关系。

在介绍框架之前,我们先了解一个非常重要的概念——洋葱模型,这是一个在 Node.js 中比较重要的面试考点,掌握这个概念,当前各种框架的原理学习都会驾轻就熟。无论是哪个 Node.js 框架,都是基于中间件来实现的,而中间件(可以理解为一个类或者函数模块)的执行方式就需要依据洋葱模型来介绍。Express 和 KOA 之间的区别也在于洋葱模型的执行方式上。

洋葱模型
洋葱我们都知道,一层包裹着一层,层层递进,但是现在不是看其立体的结构,而是需要将洋葱切开来,从切开的平面来看,如图 1 所示。

图 1 洋葱切面图

可以看到要从洋葱中心点穿过去,就必须先一层层向内穿入洋葱表皮进入中心点,然后再从中心点一层层向外穿出表皮,这里有个特点:进入时穿入了多少层表皮,出去时就必须穿出多少层表皮。先穿入表皮,后穿出表皮,符合我们所说的栈列表,先进后出的原则。

然后再回到 Node.js 框架,洋葱的表皮我们可以思考为中间件:

从外向内的过程是一个关键词 next();

而从内向外则是每个中间件执行完毕后,进入下一层中间件,一直到最后一层。

中间件执行
为了理解上面的洋葱模型以及其执行过程,我们用 Express 作为框架例子,来实现一个后台服务。在应用 Express 前,需要做一些准备工作,你按照如下步骤初始化项目即可。

mkdir myapp
cd myapp
npm init
npm install express --save
touch app.js
1
2
3
4
5
AI写代码
java
运行
1
2
3
4
5
然后输入以下代码,其中的 app.use 部分的就是 3 个中间件,从上到下代表的是洋葱的从外向内的各个层:1 是最外层,2 是中间层,3 是最内层。

接下来我们运行如下命令,启动项目。

node app.js
1
AI写代码
java
运行
1
启动成功后,打开浏览器,输入如下浏览地址:

http://127.0.0.1:3000/
1
AI写代码
java
运行
1
然后在命令行窗口,你可以看到打印的信息如下:

Example app listening on port 3000!
first
second
third
third end
second end
first end
1
2
3
4
5
6
7
AI写代码
java
运行
1
2
3
4
5
6
7
这就可以很清晰地验证了我们中间件的执行过程:

先执行第一个中间件,输出 first;

遇到 next() 执行第二个中间件,输出 second;

再遇到 next() 执行第三个中间件,输出 third;

中间件都执行完毕后,往外一层层剥离,先输出 third end;

再输出 second;

最后输出 first end。

以上就是中间件的执行过程,不过 Express 和 KOA 在中间件执行过程中还是存在一些差异的。

Express & KOA
Express 框架出来比较久了,它在 Node.js 初期就是一个热度较高、成熟的 Web 框架,并且包括的应用场景非常齐全。同时基于 Express,也诞生了一些场景型的框架,常见的就如上面我们提到的 Nest.js 框架。

随着 Node.js 的不断迭代,出现了以 await/async 为核心的语法糖,Express 原班人马为了实现一个高可用、高性能、更健壮,并且符合当前 Node.js 版本的框架,开发出了 KOA 框架。

那么两者存在哪些方面的差异呢:

Express 封装、内置了很多中间件,比如 connect 和 router ,而 KOA 则比较轻量,开发者可以根据自身需求定制框架;

Express 是基于 callback 来处理中间件的,而 KOA 则是基于 await/async;

在异步执行中间件时,Express 并非严格按照洋葱模型执行中间件,而 KOA 则是严格遵循的。

为了更清晰地对比两者在中间件上的差异,我们对上面那段代码进行修改,其次用 KOA 来重新实现,看下两者的运行差异。

因为两者在中间件为异步函数的时候处理会有不同,因此我们保留原来三个中间件,同时在 2 和 3 之间插入一个新的异步中间件,代码如下:

/**
* 异步中间件
*/
app.use(async (req, res, next) => {
console.log('async');
await next();
await new Promise(
(resolve) =>
setTimeout(
() => {
console.log(`wait 1000 ms end`);
resolve()
},
1000
)
);
console.log('async end');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
AI写代码
javascript
运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
然后将其他中间件修改为 await next() 方式,如下中间件 1 的方式:

/**
* 中间件 1
*/
app.use(async (req, res, next) => {
console.log('first');
await next();
console.log('first end');
});
1
2
3
4
5
6
7
8
AI写代码
javascript
运行
1
2
3
4
5
6
7
8
接下来,我们启动服务:

node app
1
AI写代码
java
运行
1
并打开浏览器访问如下地址:

http://127.0.0.1:3000/
1
AI写代码
java
运行
1
然后再回到打印窗口,你会发现输出如下数据:

Example app listening on port 3000!
first
second
async
third
third end
second end
first end
wait 1000 ms end
async end
1
2
3
4
5
6
7
8
9
10
AI写代码
java
运行

1
2
3
4
5
6
7
8
9
10
可以看出,从内向外的是正常的,一层层往里进行调用,从外向内时则发生了一些变化,最主要的原因是异步中间件并没有按照顺序输出执行结果。

接下来我们看看 KOA 的效果。在应用 KOA 之前,我们需要参照如下命令进行初始化。

mkdir -p koa/myapp-async
cd koa/myapp-async
npm init
npm i koa --save
touch app.js
1
2
3
4
5
AI写代码
java
运行
1
2
3
4
5
然后我们打开 app.js 添加如下代码,这部分我们只看中间件 1 和异步中间件即可,其他在 GitHub 源码中,你可以自行查看。

const Koa = require('koa');
const app = new Koa();
/**
* 中间件 1
*/
app.use(async (ctx, next) => {
console.log('first');
await next();
console.log('first end');
});
/**
* 异步中间件
*/
app.use(async (ctx, next) => {
console.log('async');
await next();
await new Promise(
(resolve) =>
setTimeout(
() => {
console.log(`wait 1000 ms end`);
resolve()
},
1000
)
);
console.log('async end');
});
app.use(async ctx => {
ctx.body = 'Hello World';
});

app.listen(3000, () => console.log(`Example app listening on port 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
AI写代码
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
25
26
27
28
29
30
31
32
33
34
和 express 代码基本没有什么差异,只是将中间件中的 res、req 参数替换为 ctx ,如上面代码的第 6 和 14 行,修改完成以后,我们需要启动服务:

node app
1
AI写代码
java
运行
1
并打开浏览器访问如下地址:

http://127.0.0.1:3000/
1
AI写代码
java
运行
1
然后打开命令行窗口,可以看到如下输出:

Example app listening on port 3000!
first
second
async
third
third end
wait 1000 ms end
async end
second end
first end
1
2
3
4
5
6
7
8
9
10
AI写代码
java
运行

1
2
3
4
5
6
7
8
9
10
你会发现,KOA 严格按照了洋葱模型的执行,从上到下,也就是从洋葱的内部向外部,输出 first、second、async、third;接下来从内向外输出 third end、async end、second end、first end。

因为两者基于的 Node.js 版本不同,所以只是出现的时间点不同而已,并没有孰优孰劣之分。Express 功能较全,发展时间比较长,也经受了不同程度的历练,因此在一些项目上是一个不错的选择。当然你也可以选择 KOA,虽然刚诞生不久,但它是未来的一个趋势。

KOA & Egg.js
上面我们说了 KOA 是一个可定制的框架,开发者可以根据自己的需要,定制各种机制,比如多进程处理、路由处理、上下文 context 的处理、异常处理等,非常灵活。而 Egg.js 就是在 KOA 基础上,做了各种比较成熟的中间件和模块,可以说是在 KOA 框架基础上的最佳实践,用以满足开发者开箱即用的特性。

我们说到 KOA 是未来的一个趋势,然后 Egg.js 是目前 KOA 的最佳实践,因此在一些企业级应用后台服务时,可以使用 Egg.js 框架,如果你需要做一些高性能、高定制化的框架也可以在 KOA 基础上扩展出新的框架。本专栏为了实践教学,我们会在 KOA 基础上将上一讲的框架进行优化和扩展。

原理实现
以上简单介绍了几个框架的知识点,接下来我们再来看下其核心实现原理,这里只介绍底层的两个框架Express和KOA,如果你对 Egg.js 有兴趣的话,可以参照我们的方法进行学习。

Express
Express 涉及 app 函数、中间件、Router 和视图四个核心部分,这里我们只介绍 app 函数、中间件和 Router 原理,因为视图在后台服务中不是特别关键的部分。

我们先来看一个图,图 2 是 Express 核心代码结构部分:

图 2 Express 核心代码

它涉及的源码不多,其中:

middleware 是部分中间件模块;

router 是 Router 核心代码;

appliaction.js 就是我们所说的 app 函数核心处理部分;

express.js 是我们 express() 函数的执行模块,实现比较简单,主要是创建 application 对象,将 application 对象返回;

request.js 是对 HTTP 请求处理部分;

response.js 是对 HTTP 响应处理部分;

utils.js 是一些工具函数;

view.js 是视图处理部分。

express.js

在 express 整个代码架构中核心是创建 application 对象,那么我们先来看看这部分的核心实现部分。在 Express 中的例子都是下面这样的:

const express = require('express')
const app = express()
const port = 3000
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
app.get('/', (req, res) => res.send('Hello World!'))
1
2
3
4
5
AI写代码
javascript
运行
1
2
3
4
5
其中我们所说的 app ,就是 express() 函数执行的返回,该 express.js 模块中核心代码是一个叫作 createApplication 函数,代码如下:

function createApplication() {
var app = function(req, res, next) {
app.handle(req, res, next);
};
1
2
3
4
mixin(app, EventEmitter.prototype, <span class="hljs-literal">false</span>);
mixin(app, proto, <span class="hljs-literal">false</span>);

<span class="hljs-comment">// expose the prototype that will get set on requests</span>
app.request = <span class="hljs-built_in">Object</span>.create(req, {
<span class="hljs-attr">app</span>: { <span class="hljs-attr">configurable</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">enumerable</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">writable</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">value</span>: app }
})

<span class="hljs-comment">// expose the prototype that will get set on responses</span>
app.response = <span class="hljs-built_in">Object</span>.create(res, {
<span class="hljs-attr">app</span>: { <span class="hljs-attr">configurable</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">enumerable</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">writable</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">value</span>: app }
})

app.init();
<span class="hljs-keyword">return</span> app;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AI写代码
AI写代码
AI写代码
javascript
运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

AI写代码
AI写代码
AI写代码
javascript
运行
1
2
3
4
代码中最主要的部分是创建了一个 app 函数,并将 application 中的函数继承给 app 。因此 app 包含了 application 中所有的属性和方法,而其中的 app.init() 也是调用了 application.js 中的 app.init 函数。在 application.js 核心代码逻辑中,我们最常用到 app.use 、app.get 以及 app.post 方法,这三个原理都是一样的,我们主要看下 app.use 的代码实现。

application.js

app.use,用于中间件以及路由的处理,是我们常用的一个核心函数。

在只传入一个函数参数时,将会匹配所有的请求路径。

当传递的是具体的路径时,只有匹配到具体路径才会执行该函数。

如下代码所示:

const express = require('express')
const app = express()
const port = 3000
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
app.use((req, res, next) => {
console.log('first');
next();
console.log('first end');
});
app.use('/a', (req, res, next) => {
console.log('a');
next();
console.log('a end');
});
app.get('/', (req, res) => res.send('Hello World!'))
app.get('/a', (req, res) => res.send('Hello World! a'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AI写代码
javascript
运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
当我们只请求如下端口时,只执行第 6 ~ 10 行的 app.use。

http://127.0.0.1:3000/
1
AI写代码
java
运行
1
而当请求如下端口时,两个中间件都会执行。

http://127.0.0.1:3000/a
1
AI写代码
java
运行
1
再来看下 Express 代码实现,如图 3 所示:

图 3 Express app.use 代码实现

当没有传入 path 时,会默认设置 path 为 / ,而 / 则是匹配任何路径,最终都是调用 router.use 将 fn 中间件函数传入到 router 中。

接下来我们看下 router.use 的代码实现。

router/index.js

这个文件在当前目录 router 下的 index.js 中,有一个方法叫作 proto.use,即 application.js 中调用的 router.use 。

图 4 中间件 push 实现

图 4 中的代码经过一系列处理,最终将中间件函数通过 Layer 封装后放到栈列表中。就完成了中间件的处理,最后我们再来看下用户请求时,是如何在栈列表执行的。

所有请求进来后都会调用 application.js 中的 app.handle 方法,该方法最终调用的是 router/index.js 中的 proto.handle 方法,所以我们主要看下 router.handle 的实现。在这个函数中有一个 next 方法非常关键,用于判断执行下一层中间件的逻辑,它的原理是从栈列表中取出一个 layer 对象,判断是否满足当前匹配,如果满足则执行该中间件函数,如图 5 所示。

图 5 中间件执行逻辑

接下来我们再看看 layer.handle_request 的代码逻辑,如图 6 所示。

图 6 handle_request 代码实现

图 6 中的代码释放了一个很重要的逻辑,就是在代码 try 部分,会执行 fn 函数,而 fn 中的 next 为下一个中间件,因此中间件栈执行代码,过程如下所示:

(()=>{
console.log('a');
(()=>{
console.log('b');
(()=>{
console.log('c');
console.log('d');
})();
console.log('e');
})();
console.log('f');
})();
1
2
3
4
5
6
7
8
9
10
11
12
AI写代码
javascript
运行

1
2
3
4
5
6
7
8
9
10
11
12
如果没有异步逻辑,那肯定是 a → b → c → d → e → f 的执行流程,如果这时我们在第二层增加一些异步处理函数时,情况如下代码所示:

(async ()=>{
console.log('a');
(async ()=>{
console.log('b');
(async ()=>{
console.log('c');
console.log('d');
})();
await new Promise((resolve) => setTimeout(() => {console.log(`async end`);resolve()}, 1000));
console.log('e');
})();
console.log('f');
})();
1
2
3
4
5
6
7
8
9
10
11
12
13
AI写代码
javascript
运行

1
2
3
4
5
6
7
8
9
10
11
12
13
再执行这部分代码时,你会发现整个输出流程就不是原来的模式了,这也印证了 Express 的中间件执行方式并不是完全的洋葱模型。

Express 源码当然不止这些,这里只是介绍了部分核心代码,其他部分建议你按照这种方式自我学习。

KOA
和 Express 相似,我们只看 app 函数、中间件和 Router 三个部分的核心代码实现。在 app.use 中的逻辑非常相似,唯一的区别是,在 KOA 中使用的是 await/async 语法,因此需要判断中间件是否为异步方法,如果是则使用 koa-convert 将其转化为 Promise 方法,代码如下:

use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
1
2
3
4
5
6
7
8
9
10
11
12
AI写代码
javascript
运行

1
2
3
4
5
6
7
8
9
10
11
12
最终都是将中间件函数放入中间件的一个数组中。接下来我们再看下 KOA 是如何执行中间件的代码逻辑的,其核心是 koa-compose 模块中的这部分代码:

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)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
AI写代码
javascript
运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在代码中首先获取第一层级的中间件,也就是数组 middleware 的第一个元素,这里不同点在于使用了 Promise.resolve 来执行中间件,根据上面的代码我们可以假设 KOA 代码逻辑是这样的:

new Promise(async (resolve, reject) => {
console.log('a')
await new Promise(async (resolve, reject) => {
console.log('b');
await new Promise((resolve, reject) => {
console.log('c');
resolve();
}).then(async () => {
await new Promise((resolve) => setTimeout(() => {console.log(`async end`);resolve()}, 1000));
console.log('d');
});
resolve();
}).then(() => {
console.log('e')
})
resolve();
}).then(() => {
console.log('f')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AI写代码
javascript
运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
可以看到所有 next() 后面的代码逻辑都包裹在 next() 中间件的 then 逻辑中,这样就可以确保上一个异步函数执行完成后才会执行到 then 的逻辑,也就保证了洋葱模型的先进后出原则,这点是 KOA 和 Express 的本质区别。这里要注意,如果需要确保中间件的执行顺序,必须使用 await next()。

————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/weixin_45857341/article/details/136226235

大主流系统框架:由浅入深分析 Express、KoA和 Egg.js的更多相关文章

  1. 6 大主流 Web 框架优缺点对比:15篇前端热文回看

    摘自:http://blog.csdn.net/VhWfR2u02Q/article/details/78993079 注:以下文章,点击标题即可阅读 <6 大主流 Web 框架优缺点对比> ...

  2. [转]Node.js框架对比:Express/Koa/Hapi

    本文转自:https://www.cnblogs.com/souvenir/p/6039990.html 本文翻译自: https://www.airpair.com/node.js/posts/no ...

  3. [译]Node.js框架对比:Express/Koa/Hapi

    本文翻译自: https://www.airpair.com/node.js/posts/nodejs-framework-comparison-express-koa-hapi 1.介绍 直至今日, ...

  4. 6 大主流 Web 框架优缺点对比(转)

    英文: Kit Kelly   译文:oschina https://www.oschina.net/translate/web-frameworks-conclusions 是该读些评论和做一些总结 ...

  5. 大数据分析处理框架——离线分析(hive,pig,spark)、近似实时分析(Impala)和实时分析(storm、spark streaming)

    大数据分析处理架构图 数据源: 除该种方法之外,还可以分为离线数据.近似实时数据和实时数据.按照图中的分类其实就是说明了数据存储的结构,而特别要说的是流数据,它的核心就是数据的连续性和快速分析性: 计 ...

  6. 基于 Egg.js 框架的 Node.js 服务构建之用户管理设计

    前言 近来公司需要构建一套 EMM(Enterprise Mobility Management)的管理平台,就这种面向企业的应用管理本身需要考虑的需求是十分复杂的,技术层面管理端和服务端构建是架构核 ...

  7. Express/Koa/Hapi

    Express/Koa/Hapi 本文翻译自: https://www.airpair.com/node.js/posts/nodejs-framework-comparison-express-ko ...

  8. 主流Java数据库连接池分析(C3P0,DBCP,TomcatPool,BoneCP,Druid)

    主流数据库连接池 常用的主流开源数据库连接池有C3P0.DBCP.Tomcat Jdbc Pool.BoneCP.Druid等 C3p0: 开源的JDBC连接池,实现了数据源和JNDI绑定,支持JDB ...

  9. BAT资深工程师 由浅入深分析 Tp5&Tp6底层源码 - 分享

    BAT资深工程师由浅入深分析Tp5&Tp6底层源码 第1章 课程简介 本章主要让大家知道本套课程的主线, 导学内容,如何学习源码等,看完本章要让小伙伴觉得这个是必须要掌握的,并且对加薪有很大的 ...

  10. BAT资深工程师由浅入深分析Tp5&Tp6底层源码☆

    第1章 课程简介 本章主要让大家知道本套课程的主线, 导学内容,如何学习源码等,看完本章要让小伙伴觉得这个是必须要掌握的,并且对加薪有很大的帮助. 第2章 [TP5灵魂]自动加载Loader 深度分析 ...

随机推荐

  1. 报表自动生成程序:ZREPORT_GENERATOR

    群里看到的,抄别人的...源自哪里就不清楚了.这里申明一下:非本人所写,如果侵权,你打群主好了. 这里说明一下:这个程序不太好用,没有SQVI友好.(以下问题可能是没用字段别名,不想试验了) 1.生成 ...

  2. 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-21- 操作鼠标拖拽 - 中篇(详细教程)

    1.简介 上一篇中,主要是介绍了拖拽的各种方法的理论知识以及实践,今天宏哥讲解和分享一下划取字段操作.例如:需要在一堆log字符中随机划取一段文字,然后右键选择摘取功能. 2.划取字段操作 划取字段操 ...

  3. websocket h5控制 pc 大屏 进行协同操作

    接到产品需求单 ,哎说多都是累 这几天接口写个没停 今天捣鼓一下这个需求 需求简要说明:客户找我们做的pc端可视化大屏 需要扫码 到 h5端移动端控制pc端大屏 移动端当然也是一个网页 一对一操控 分 ...

  4. C# winForm打包的的麻烦过程

    https://www.cnblogs.com/qiantao/p/9468570.html 作为研发人员,在本机上开发的winform.wpf或者控制台程序需要发给其他人测试时候,一般需要对其进行打 ...

  5. leetcode 901 股票价格跨度

    简介 简单 code class StockSpanner { public: vector<int> v; vector<int> vv; StockSpanner() { ...

  6. java 线程知识问答

    简介 一个线程的核心线程数是4, 最大线程数是8, 有一个任务提交过来, 迅速执行王弼, 如果再来一个任务, 那么线程池是新建一个线程去执行还是会复用之前的线程? 参考链接 https://blog. ...

  7. vue v-show 失效

    简介 RT 解决方案 采用 直接 操控dom赋值属性的方式

  8. ETLCloud结合kafka的数据集成

    一.ETLCloud中实时数据集成的使用 在ETLCloud中数据集成有两种方式,一种是离线数据集成,另一种便是我们今天所要介绍的实时数据集成了,两者的区别从名字便可以得知,前者处理的数据是离线的没有 ...

  9. MCU/CPU/*PU的 WatchDog/看门狗 使用注意事项

    MCU/CPU/*PU的 WatchDog/看门狗 使用注意事项 类比于 Heartbeat/心跳 检测多用在软件及服务领域, WatchDog/看门狗 多用在硬件与系统领域(硬件看门狗), 也有用在 ...

  10. freeswitch笔记(8)-esl outbound 填坑笔记

    github上的esl-client已经N年未更新了,上面有一堆bug,记录一下: 一.内存泄露 org.freeswitch.esl.client.transport.message.EslFram ...