上一讲我们没有应用任何框架实现了一个简单后台服务,以及一个简单版本的 MSVC 框架。本讲将介绍一些目前主流框架的设计思想,同时介绍其核心代码部分的实现,为后续使用框架优化我们上一讲实现的 MSVC 框架做一定的准备。

主流框架介绍

目前比较流行的 Node.js 框架有ExpressKOAEgg.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

然后输入以下代码,其中的 app.use 部分的就是 3 个中间件,从上到下代表的是洋葱的从外向内的各个层:1 是最外层2 是中间层3 是最内层

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

node app.js

启动成功后,打开浏览器,输入如下浏览地址:

http://127.0.0.1:3000/

然后在命令行窗口,你可以看到打印的信息如下:

Example app listening on port 3000!
first
second
third
third end
second end
first end

这就可以很清晰地验证了我们中间件的执行过程:

  • 先执行第一个中间件,输出 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');
});

然后将其他中间件修改为 await next() 方式,如下中间件 1 的方式:

/*** 中间件 1*/
app.use(async (req, res, next) => {console.log('first');await next();console.log('first end');
});

接下来,我们启动服务:

node app

并打开浏览器访问如下地址:

http://127.0.0.1:3000/

然后再回到打印窗口,你会发现输出如下数据:

Example app listening on port 3000!
first
second
async
third
third end
second end
first end
wait 1000 ms end
async end

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

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

mkdir -p koa/myapp-async
cd koa/myapp-async
npm init
npm i koa --save
touch app.js

然后我们打开 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!`));

和 express 代码基本没有什么差异,只是将中间件中的 res、req 参数替换为 ctx ,如上面代码的第 6 和 14 行,修改完成以后,我们需要启动服务:

node app

并打开浏览器访问如下地址:

http://127.0.0.1:3000/

然后打开命令行窗口,可以看到如下输出:

Example app listening on port 3000!
first
second
async
third
third end
wait 1000 ms end
async end
second end
first end

你会发现,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 基础上将上一讲的框架进行优化和扩展。

原理实现

以上简单介绍了几个框架的知识点,接下来我们再来看下其核心实现原理,这里只介绍底层的两个框架ExpressKOA,如果你对 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!'))

其中我们所说的 app ,就是 express() 函数执行的返回,该 express.js 模块中核心代码是一个叫作 createApplication 函数,代码如下:

function createApplication() {var app = function(req, res, next) {app.handle(req, res, next);};
  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;
}

代码中最主要的部分是创建了一个 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'))

当我们只请求如下端口时,只执行第 6 ~ 10 行的 app.use。

http://127.0.0.1:3000/

而当请求如下端口时,两个中间件都会执行。

http://127.0.0.1:3000/a

再来看下 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');
})();

如果没有异步逻辑,那肯定是 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');
})();

再执行这部分代码时,你会发现整个输出流程就不是原来的模式了,这也印证了 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;}

最终都是将中间件函数放入中间件的一个数组中。接下来我们再看下 KOA 是如何执行中间件的代码逻辑的,其核心是 koa-compose 模块中的这部分代码:

return function (context, next) {// last called middleware #let index = -1return dispatch(0)function dispatch (i) {if (i <= index) return Promise.reject(new Error('next() called multiple times'))index = ilet fn = middleware[i]if (i === middleware.length) fn = nextif (!fn) return Promise.resolve()try {return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));} catch (err) {return Promise.reject(err)}}}

在代码中首先获取第一层级的中间件,也就是数组 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')})

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

Express 和 KOA 的源代码还有很多,这里就不一一分析了,其他的部分你可以自行学习,在学习中,可以进一步提升自己的编码能力,同时改变部分编码陋习。在此过程中有任何问题,都欢迎留言与我交流。

总结

本讲先介绍了洋葱模型,其次根据洋葱模型详细分析了 Express 和 KOA 框架的区别和联系,最后介绍了两个核心框架的 app 函数、中间件和 Router 三个部分的核心代码实现。学完本讲,你需要掌握洋葱模型,特别是在面试环节中要着重介绍;能够讲解清楚在 Express 与 KOA 中的洋葱模型差异;以及掌握 Express 和 KOA 中的核心代码实现部分原理;其次了解 Egg.js 框架。

下一讲,我们将会介绍 Node.js 多进程方案,在介绍该方案时,我们还会应用 KOA 框架来优化我们第 03 讲的基础框架,使其成为一个比较通用的框架。



精选评论

console_man:

老师讲解的太仔细了,值得反复阅读。

**飞:

可以的

**留影:

干货满满,

Node.js 应用开发详解04 3 大主流系统框架:由浅入深分析 Express、Koa 和 Egg.js相关推荐

  1. SpringCloud Alibaba史上最强详解与史上最系统框架搭建

    框架实现代码资源地址:springCloud_dataservice_bus.zip_springcloudalibaba搭建-Java文档类资源-CSDN下载 目录 一.官网集合: Springbo ...

  2. 微信公众号 开发详解02【自动回复、发布文章、自定义菜单、3种链接跳转】

    [微信公众号--零基础全面系统教学] [P01-P07]微信公众号 开发详解01[目的.起名.准备工作.注册流程.后台功能] [P08-P12]微信公众号 开发详解02[自动回复.发布文章.自定义菜单 ...

  3. 微信公众号 开发详解01【目的、起名、准备工作、注册流程、后台功能】

    [微信公众号--零基础全面系统教学] [P01-P07]微信公众号 开发详解01[目的.起名.准备工作.注册流程.后台功能] [P08-P12]微信公众号 开发详解02[自动回复.发布文章.自定义菜单 ...

  4. EasyPR中文开源车牌识别系统 开发详解

     在上篇文档中作者已经简单的介绍了EasyPR,现在在本文档中详细的介绍EasyPR的开发过程. 正如淘宝诞生于一个购买来的LAMP系统,EasyPR也有它诞生的原型,起源于CSDN的taotao ...

  5. 酒店点餐系统开发详解(二)

    酒店点餐系统开发详解 --系统分析 一个好的系统,必须要进行完善而且全面的系统分析.系统分析做好了,那就能够指导项目良好地发展下去.当然,系统分析需要以需求分析为基础,不过,在本项目中并未做仔细的需求 ...

  6. Node.js中Async详解

    Node.js中Async详解:流程控制 安装 npm install async --save 地址 https://github.com/caolan/async Async的内容主要分为三部分 ...

  7. NodeJS+Express+mySQL服务端开发详解

    NodeJS+Express+mySQL服务端开发详解 随着NodeJS的发展,现在已经被很多人熟知,NodeJS已经成为了前端开发人员必备的技能.本文不会对NodeJS过多介绍 如果你感兴趣可以访问 ...

  8. php和js调试,JS调试使用详解

    这次给大家带来JS调试使用详解,JS调试的注意事项有哪些,下面就是实战案例,一起来看一下. 试想一下:出现了某个bug,有人用几分钟就搞定了,有人用了半天或者一天都找不到原因所在.你愿意当前者还是后者 ...

  9. 【OpenCV 4开发详解】保存和读取XML和YMAL文件

    本文首发于"小白学视觉"微信公众号,欢迎关注公众号 本文作者为小白,版权归人民邮电出版社发行所有,禁止转载,侵权必究! 经过几个月的努力,小白终于完成了市面上第一本OpenCV 4 ...

最新文章

  1. AI研习丨专题:因果推断与因果性学习研究进展
  2. 中国学者变革300多年来的活塞!MIT、哈佛联手,动力3倍以上,可节能40%
  3. 首次安装mysql57需要注意的问题
  4. darknet: ./src/parser.c:348: parse_region: Assertion `l.outputs == params.inputs' failed.yolov3训练问题
  5. 索尼a5100_索尼相机分类,买相机不再发愁
  6. Linux KVM 虚拟化技术
  7. 【图论】【最短路】【SPFA】【USACO题库】2.4.4 Bessie Come Home回家(jzoj 1274)
  8. 用指针完成函数参数的调用
  9. 垃圾回收算法与垃圾回收器
  10. 图解法求最优解的例题_初一上学期,方程的解互为相反数,两种方法求解参数的值...
  11. 【CodeSnippet】Gradle
  12. 概率论与数理统计图解
  13. redies成神之路
  14. dorado 7 注意总结
  15. 【论文写作技巧】Endnote参考文献统一输出格式
  16. 区块链安全分析:安全是重中之重,需要各方企业共同守护 | 新加坡Meetup
  17. android虚拟机固定横屏幕竖屏,用VBox虚拟机安装Android 屏幕90度翻转竖屏设置
  18. RBP中新增自由格式报表资源之自由格式风格
  19. SpringBoot核心注解
  20. 100道Python编程练习题

热门文章

  1. 初识设计模式(策略模式)
  2. iNFTnews丨澳航推出NFT系列,全球还有哪些航空公司涉足NFT?
  3. c++:算术运算符、赋值运算符、比较运算符、逻辑运算符
  4. 【呆子谈管理】:管理的基本思维(二)
  5. 服务器主板数码管显示ff,关于数码管显示编码
  6. HbuilderX快捷键
  7. 聊聊 Kafka: Consumer 源码解析之 Rebalance 机制
  8. 【WAF绕过-权限控制工具】菜刀、蚁剑、冰蝎 下载、使用方法
  9. 计算机x6接口,机械革命MR X6有什么接口?有几个USB接口?
  10. STM32F 单片机硬件I2C Busy标志导致的I2C卡死的处理办法