前言

用了那么多年的express.js,终于有时间来深入学习express,然后顺便再和koa2的实现方式对比一下。

老实说,还没看express.js源码之前,一直觉得express.js还是很不错的,无论从api设计,还是使用上都是可以的。但是这次阅读完express代码之后,我可能改变想法了。

虽然express.js有着精妙的中间件设计,但是以当前js标准来说,这种精妙的设计在现在可以说是太复杂。里面的层层回调和递归,不花一定的时间还真的很难读懂。而koa2的代码呢?简直可以用四个字评论:精简彪悍!仅仅几个文件,用上最新的js标准,就很好实现了中间件,代码读起来一目了然。

老规矩,读懂这篇文章,我们依然有一个简单的demo来演示: express-vs-koa

1、express用法和koa用法简单展示

如果你使用express.js启动一个简单的服务器,那么基本写法应该是这样:

const express = require('express')const app = express()
const router = express.Router()app.use(async (req, res, next) => {console.log('I am the first middleware')next()console.log('first middleware end calling')
})
app.use((req, res, next) => {console.log('I am the second middleware')next()console.log('second middleware end calling')
})router.get('/api/test1', async(req, res, next) => {console.log('I am the router middleware => /api/test1')res.status(200).send('hello')
})router.get('/api/testerror', (req, res, next) => {console.log('I am the router middleware => /api/testerror')throw new Error('I am error.')
})app.use('/', router)app.use(async(err, req, res, next) => {if (err) {console.log('last middleware catch error', err)res.status(500).send('server Error')return}console.log('I am the last middleware')next()console.log('last middleware end calling')
})app.listen(3000)
console.log('server listening at port 3000')

换算成等价的koa2,那么用法是这样的:

const koa = require('koa')
const Router = require('koa-router')const app = new koa()
const router = Router()app.use(async(ctx, next) => {console.log('I am the first middleware')await next()console.log('first middleware end calling')
})app.use(async (ctx, next) => {console.log('I am the second middleware')await next()console.log('second middleware end calling')
})router.get('/api/test1', async(ctx, next) => {console.log('I am the router middleware => /api/test1')ctx.body = 'hello'
})router.get('/api/testerror', async(ctx, next) => {throw new Error('I am error.')
})app.use(router.routes())app.listen(3000)
console.log('server listening at port 3000')

如果你还感兴趣原生nodejs启动服务器是怎么使用的,可以参考demo中的这个文件:node.js

于是二者的使用区别通过表格展示如下(知乎不支持markdown也是醉了~表格只能截图了~):

上表展示了二者的使用区别,从初始化就看出koa语法都是用的新标准。在挂载路由中间件上也有一定的差异性,这是因为二者内部实现机制的不同。其他都是大同小异的了。

那么接下去,我们的重点便是放在二者的中间件的实现上。

2、express.js中间件实现原理

我们先来看一个demo,展示了express.js的中间件在处理某些问题上的弱势。demo代码如下:

const express = require('express')const app = express()const sleep = (mseconds) => new Promise((resolve) => setTimeout(() => {console.log('sleep timeout...')resolve()
}, mseconds))app.use(async (req, res, next) => {console.log('I am the first middleware')const startTime = Date.now()console.log(`================ start ${req.method} ${req.url}`, { query: req.query, body: req.body });next()const cost = Date.now() - startTimeconsole.log(`================ end ${req.method} ${req.url} ${res.statusCode} - ${cost} ms`)
})
app.use((req, res, next) => {console.log('I am the second middleware')next()console.log('second middleware end calling')
})app.get('/api/test1', async(req, res, next) => {console.log('I am the router middleware => /api/test1')await sleep(2000)res.status(200).send('hello')
})app.use(async(err, req, res, next) => {if (err) {console.log('last middleware catch error', err)res.status(500).send('server Error')return}console.log('I am the last middleware')await sleep(2000)next()console.log('last middleware end calling')
})app.listen(3000)
console.log('server listening at port 3000')

该demo中当请求/api/test1的时候打印结果是什么呢?

I am the first middleware
================ start GET /api/test1
I am the second middleware
I am the router middleware => /api/test1
second middleware end calling
================ end GET /api/test1 200 - 3 ms
sleep timeout...

如果你清楚这个打印结果的原因,想必对express.js的中间件实现有一定的了解。

我们先看看第一节demo的打印结果是:

I am the first middleware
I am the second middleware
I am the router middleware => /api/test1
second middleware end calling
first middleware end calling

这个打印符合大家的期望,但是为什么刚才的demo打印的结果就不符合期望了呢?二者唯一的区别就是第二个demo加了异步处理。有了异步处理,整个过程就乱掉了。因为我们期望的执行流程是这样的:

I am the first middleware
================ start GET /api/test1
I am the second middleware
I am the router middleware => /api/test1
sleep timeout...
second middleware end calling
================ end GET /api/test1 200 - 3 ms

那么是什么导致这样的结果呢?我们在接下去的分析中可以得到答案。

2.1、express挂载中间件的方式

要理解其实现,我们得先知道express.js到底有多少种方式可以挂载中间件进去?熟悉express.js的童鞋知道吗?知道的童鞋可以心里默默列举一下。

目前可以挂载中间件进去的有:(HTTP Method指代那些http请求方法,诸如Get/Post/Put等等)

  • app.use
  • app.[HTTP Method]
  • app.all
  • app.param
  • router.all
  • router.use
  • router.param
  • router.[HTTP Method]

2.2、express中间件初始化

express代码中依赖于几个变量(实例):app、router、layer、route,这几个实例之间的关系决定了中间件初始化后形成一个数据模型,画了下面一张图片来展示:

图中存在两块Layer实例,挂载的地方也不一样,以express.js为例子,我们通过调试找到更加形象的例子:

结合二者,我们来聊聊express中间件初始化。为了方便,我们把上图1叫做初始化模型图,上图2叫做初始化实例图

看上面两张图,我们抛出下面几个问题,搞懂问题便是搞懂了初始化。

  • 初始化模型图Layer实例为什么分两种?
  • 初始化模型图Layer实例中route字段什么时候会存在?
  • 初始化实例图中挂载的中间件为什么有7个?
  • 初始化实例图中圈2和圈3的route字段不一样,而且name也不一样,为什么?
  • 初始化实例图中的圈4里也有Layer实例,这个时候的Layer实例和上面的Layer实例不一样吗?

首先我们先输出这样的一个概念:Layer实例是path和handle互相映射的实体,每一个Layer便是一个中间件。

这样的话,我们的中间件中就有可能嵌套中间件,那么对待这种情形,express就在Layer中做手脚。我们分两种情况挂载中间件:

  • 使用app.userouter.use来挂载的

    • app.use经过一系列处理之后最终也是调用router.use
  • 使用app.allapp.[Http Method]app.routerouter.allrouter.[Http Method]router.route来挂载的
    • app.allapp.[Http Method]app.routerouter.allrouter.[Http Method]经过一系列处理之后最终也是调用router.route

因此我们把焦点聚焦在router.userouter.route这两个方法。

2.2.1、router.use

该方法的最核心一段代码是:

for (var i = 0; i < callbacks.length; i++) {var fn = callbacks[i];if (typeof fn !== 'function') {throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))}// add the middlewaredebug('use %o %s', path, fn.name || '<anonymous>')var layer = new Layer(path, {sensitive: this.caseSensitive,strict: false,end: false}, fn);// 注意这个route字段设置为undefinedlayer.route = undefined;this.stack.push(layer);
}

此时生成的Layer实例对应的便是初始化模型图1指示的多个Layer实例,此时以express.js为例子,我们看初始化实例图圈1的所有Layer实例,会发现除了我们自定义的中间件(共5个),还有两个系统自带的,看初始化实例图的Layer的名字分别是:queryexpressInit。二者的初始化是在[application.js]中的lazyrouter方法:

app.lazyrouter = function lazyrouter() {if (!this._router) {this._router = new Router({caseSensitive: this.enabled('case sensitive routing'),strict: this.enabled('strict routing')});this._router.use(query(this.get('query parser fn'))); // 最终调用的就是router.use方法this._router.use(middleware.init(this)); // 最终调用的就是router.use方法}
};

于是回答了我们刚才的第三个问题。7个中间件,2个系统自带、3个APP级别的中间、2个路由级别的中间件

2.2.2、router.route

我们说过app.allapp.[Http Method]app.routerouter.allrouter.[Http Method]经过一系列处理之后最终也是调用router.route的,所以我们在demo中的express.js,使用了两次app.get,其最后调用了router.route,我们看该方法核心实现:

proto.route = function route(path) {var route = new Route(path);var layer = new Layer(path, {sensitive: this.caseSensitive,strict: this.strict,end: true}, route.dispatch.bind(route));layer.route = route;this.stack.push(layer);return route;
};

这么简单的实现,与上一个方法的实现唯一的区别就是多了new Route这个。通过二者对比,我们可以回答上面的好几个问题:

  • 初始化模型图Layer实例为什么分两种? 因为调用方式的不同决定了Layer实例的不同,第二种Layer实例是挂载在route实例之下的。
  • 初始化模型图Layer实例中route字段什么时候会存在?使用router.route的时候就会存在
  • 初始化实例图中圈2和圈3的route字段不一样,而且name也不一样,为什么?圈2的Layer因为我们使用箭头函数,不存在函数名,所以name是anonymous,但是圈3因为使用的router.route,所以其统一的回调函数都是route.dispath,因此其函数名字都统一是bound dispatch,同时二者的route字段是否赋值也一目了然

最后一个问题,既然实例化route之后,route有了自己的Layer,那么它的初始化又是在哪里的?初始化核心代码:

// router/route.js/Route.prototype[method]
for (var i = 0; i < handles.length; i++) {var handle = handles[i];if (typeof handle !== 'function') {var type = toString.call(handle);var msg = 'Route.' + method + '() requires a callback function but got a ' + typethrow new Error(msg);}debug('%s %o', method, this.path)var layer = Layer('/', {}, handle);layer.method = method;this.methods[method] = true;this.stack.push(layer);}

可以看到新建的route实例,维护的是一个path,对应多个method的handle的映射。每一个method对应的handle都是一个layer,path统一为/。这样就轻松回答了最后一个问题了。

至此,再回去看初始化模型图,相信大家可以有所明白了吧~

2.3、express中间件的执行逻辑

整个中间件的执行逻辑无论是外层Layer,还是route实例的Layer,都是采用递归调用形式,一个非常重要的函数next()实现了这一切,这里做了一张流程图,希望对你理解这个有点用处:

我们再把express.js的代码使用另外一种形式实现,这样你就可以完全搞懂整个流程了。

为了简化,我们把系统挂载的两个默认中间件去掉,把路由中间件去掉一个,最终的效果是:

((req, res) => {console.log('I am the first middleware');((req, res) => {console.log('I am the second middleware');(async(req, res) => {console.log('I am the router middleware => /api/test1');await sleep(2000)res.status(200).send('hello')})(req, res)console.log('second middleware end calling');})(req, res)console.log('first middleware end calling')
})(req, res)

因为没有对await或者promise的任何处理,所以当中间件存在异步函数的时候,因为整个next的设计原因,并不会等待这个异步函数resolve,于是我们就看到了sleep函数的打印被放在了最后面,并且第一个中间件想要记录的请求时间也变得不再准确了~

但是有一点需要申明的是虽然打印变得奇怪,但是绝对不会影响整个请求,因为response是在我们await之后,所以请求是否结束还是取决于我们是否调用了res.send这类函数

至此,希望整个express中间件的执行流程你可以熟悉一二,更多细节建议看看源码,这种精妙的设计确实不是这篇文章能够说清楚的。本文只是想你在面试的过程中可以做到有话要说~

接下去,我们分析牛逼的Koa2,这个就不需要费那么大篇幅去讲,因为实在是太太容易理解了。

3、koa2中间件

koa2中间件的主处理逻辑放在了koa-compose,也就是仅仅一个函数的事情:

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 = -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)}}}
}

每个中间件调用的next()其实就是这个:

dispatch.bind(null, i + 1)

还是利用闭包和递归的性质,一个个执行,并且每次执行都是返回promise,所以最后得到的打印结果也是如我们所愿。那么路由的中间件是否调用就不是koa2管的,这个工作就交给了koa-router,这样koa2才可以保持精简彪悍的风格。

再贴出koa中间件的执行流程吧:

最后

有了这篇文章,相信你再也不怕面试官问你express和koa的区别了~

参考

  1. koa
  2. express
  3. http

nodejs express use 传值_再也不怕面试官问你express和koa的区别了相关推荐

  1. 看完这篇,我再也不怕面试官问垃圾收集了

    看完这篇,我再也不怕面试官问垃圾收集了 说在前面:本文的篇幅较长,看本文的时候最好先去上个厕所,先准备好一杯枸杞茶,慢慢品,本文将会讲解三种垃圾收集算法:标记-清除.复制.标记-整理算法,以及各种成熟 ...

  2. 存在就不插入_动画:面试官问我插入排序和冒泡排序哪个更牛逼?

    来自公众号:小鹿动画学编程 写在前边 排序对于每个开发者来讲,都多多少少知道几个经典的排序算法,比如我们之前以动画形式分享的冒泡排序,也包括今天要分享的插入排序.还有一些其他经典的排序,小鹿整理的共有 ...

  3. 手写Vuex核心原理,再也不怕面试官问我Vuex原理

    手写Vuex核心原理 文章目录 手写Vuex核心原理 一.核心原理 二.基本准备工作 三.剖析Vuex本质 四.分析Vue.use 五.完善install方法 六.实现Vuex的state 七.实现g ...

  4. 再也不怕面试官问我平时都从什么途径学习了

    上次面试,面试官问我平时都从哪些途径学习,我反手就给他看了经常学习的数百个公众号,下面这些更是优中选优的技术号 马哥Linux运维 开发者技术前线:马哥教育是国内顶级的 Linux 云计算.Pytho ...

  5. 图解 Java 线程的生命周期,看完再也不怕面试官问了

    文章首发自个人微信公众号: 小哈学Java www.exception.site/java-concur- 在 Java 初中级面试中,关于线程的生命周期可以说是常客了.本文就针对这个问题,通过图文并 ...

  6. 求职干货:再也不怕面试官问斐波那契数列了!

    ‍‍ 作者 | 守望 责编 | 胡巍巍 前言 假如面试官让你编写求斐波那契数列的代码时,是不是心中暗喜?不就是递归么,早就会了.如果真这么想,那就危险了. 递归解法 递归,在数学与计算机科学中,是指在 ...

  7. 15w+字的计算机网络知识核心总结!再也不怕面试官问我网络知识了,飘了!

    大家好,我是小林. 我在 csdn 输出了 15w+ 的图解网络系列文章,每一篇文章都有我高清无ma的手绘图. 每一篇都相当干活,很多同学跟我说,我的图解网络打破了他们对计算机网络的恐惧,甚至还助力他 ...

  8. java的map 使用string数组多了双引号_奥奥奥利给!!!再也不怕面试官问我String源码了!来吧...

    简述 字符串广泛应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了String 类来创建和操作字符串.字符串缓冲区支持可变字符串.因为String对象是不可变的,因此可以共享 ...

  9. babel原理_手写webpack核心原理,再也不怕面试官问我webpack原理

    手写webpack核心原理 一.核心打包原理 1.1 打包的主要流程如下 1.2 具体细节 二.基本准备工作 三.获取模块内容 四.分析模块 五.收集依赖 六.ES6转成ES5(AST) 七.递归获取 ...

最新文章

  1. 作为首席架构师,我是如何选择并落地架构方案的?
  2. 中国活性炭行业市场全景调研分析及需求趋势预测报告2022-2028年
  3. discuz数据库迁移,改密码后,相关配置文件修改
  4. 用状态机STATE MACHINE实现有选择的文件转换
  5. ASP.NET Core技术研究-全面认识Web服务器Kestrel
  6. python3发送邮件
  7. c++ 多字节 转换为 unicode
  8. dex2jar java 1.8_利用 dex2jar 反编译 dex文件
  9. ubuntu运行迅雷极速版
  10. ad采样频率_涨知识!模拟信号采样与AD转换详解
  11. 在Linux系统中增加一块SCSI硬盘,重新启动计算机。
  12. 3500元计算机基本硬件配置清单,电脑硬件中配配置清单
  13. 如何用Git将代码上传到GitHub
  14. 乐鑫Esp32学习之旅11 入门 乐鑫esp-adf 音频框架开发,造一个蓝牙耳机,实现切换歌曲,获取歌曲信息等功能。(附带Demo)
  15. C++设计模式——策略模式(高屋建瓴)
  16. 联通云的七大“场景云”,能让用户实现云计算自由吗?
  17. hiveserver2 的HA 配置
  18. Hastelloy C-276 (UNS N10276)含钨的镍-铬-钼合金
  19. mount挂载失败出现Structure needs cleaning
  20. ios开发 多人语音聊天_iOS语音通话功能实现流程(实时语音通话二)

热门文章

  1. 【Matlab】扩展卡尔曼滤波器原理及仿真(初学者入门专用)
  2. 关于数组首地址a、a+1、a[0]、a[0]+1、*a、*a、a+0的解析
  3. zoj 1078 palindrom numbers
  4. Max retries exceeded with URL报错小记
  5. 彩票模拟选号程序java_java课程设计-彩票购买抽奖程序
  6. 如何让nginx执行python代码_生产环境部署Python语言代码(django+uwsgi+nginx)
  7. linux删除文件_Linux删除文件夹用什么命令
  8. 单点登录 cas 设置回调地址_单点登录落地实现技术有哪些,有哪些流行的登录方案搭配?...
  9. 腾讯视频app怎么允许腾讯视频访问位置信息
  10. win7系统任务管理器被禁用怎么解决