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

27229bfed20f130585e2dc821c6febbe.png

前言

用了那么多年的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也是醉了~表格只能截图了~):

1d2c5536e39ce1d65e430f4a84f01b6c.png

上表展示了二者的使用区别,从初始化就看出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,这几个实例之间的关系决定了中间件初始化后形成一个数据模型,画了下面一张图片来展示:

da9cc3a99454e2fc1f5655097f3fa21a.png

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

b4345c74f6d34b306c620a3252bcb52a.png

结合二者,我们来聊聊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()实现了这一切,这里做了一张流程图,希望对你理解这个有点用处:

e334103048366296ff090d9c2bbcb459.png

我们再把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中间件的执行流程吧:

11ed05536f00d415fbf3784fbcda63ea.gif

最后

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

参考

  1. koa
  2. express
  3. http

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/559503.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

没有工作经验找it_校招和社招有什么区别?没有工作经验,如何找工作?

我们的招聘一般分为社招和校招。社招就是社会上招聘&#xff0c;绝大多数都要有相关的工作经验。而校招则是针对在校大学生的&#xff0c;所以应届毕业生的身份很重要&#xff0c;失去了这个身份&#xff0c;就要进入社招的渠道&#xff0c;面临的压力和要求都要高得多。那么&a…

python输入个人所得税计算_个税起征点上调至5000,用Python算一算少交多少税

原标题&#xff1a;个税起征点上调至5000&#xff0c;用Python算一算少交多少税今天出了一个重磅消息&#xff0c;个税起征点从3500上调到5000啦&#xff01; 广大IT农民工的生活压力又减轻了一些&#xff0c;有没有 晚上加一个鸡腿&#xff0c;要不要~ 开心归开心&#xff0c;…

hive 行转列和列转行的方法_面试常考!SQL行转列和列转行

关注上方“数据挖掘工程师”&#xff0c;选择星标&#xff0c;关键时间&#xff0c;第一时间送达&#xff01;行转列&#xff0c;列转行是我们在开发过程中经常碰到的问题。行转列一般通过CASE WHEN 语句来实现&#xff0c;也可以通过 SQL SERVER 的运算符PIVOT来实现。用传统的…

.net core 装了没生效_王者荣耀:辅助装出现惊天bug?辅助光环对自己无效?

大家好&#xff0c;我是老刘备。今天说的这个bug我已经发现许久&#xff0c;从新版本以来就一直在出现&#xff0c;起初我以为是辅助装的光环改动了&#xff0c;但是最近和我的小伙伴开黑之后也遇到这个bug&#xff0c;才发现不是如此。本期我就来带大家一起看看。辅助光环技我…

python sns绘制回归线_Python数分实战:员工流失情况预测

在很久之前&#xff0c;我有写一个Excel数据分析的实战项目&#xff0c;不晓得大家还记不记得&#xff0c;感兴趣的童鞋可以回看&#xff1a;A九姑娘&#xff1a;Excel数分实战&#xff1a;员工流失率分析​zhuanlan.zhihu.com本次的项目数据依旧是这个&#xff0c;但是我们这次…

go var 一个整数_Go语言学习基础-值、变量、常量

Hello world第一个程序是打印经典的“hello world”信息。保存为hello-world.go文件&#xff0c;在命令行中使用go run命令运行程序。若要构建为二进制文件&#xff0c;则使用go build来实现&#xff0c;然后直接执行二进制文件。结果&#xff1a;$ go run hello-world.gohello…

怎么取消自动格式化_iPhone自动扣费怎么取消?App Store、微信、支付宝关闭自动扣费教程...

经常有网友反馈&#xff0c;自己的手机每月会被不知不觉的被扣钱&#xff0c;但又不知道如何关闭&#xff0c;烦恼不已。其实&#xff0c;现在很多视频、音乐、购物等APP都有会员充值功能&#xff0c;其中不少还包含“自动续费”功能&#xff0c;一旦开启会员&#xff0c;今后就…

c# selenium chrome 文件下载_Selenium 框架原理以及环境安装

Selenium 框架原理图片来自转载 蓝色的图Selenium Language Binding就是我们平时使用的编码语言&#xff0c;包括java、python、C#等。可以看到这些语言并没有直接与Browser Drivers进行通信&#xff0c;而是通过了JSON WireProtocol&#xff0c;这就是webdriver协议 &#xff…

怎么打包图片_怎么将许多张照片打包发到邮箱?

怎么将许多张照片打包发到邮箱&#xff1f;所谓的打包&#xff0c;就是需要你把大量照片文件变成一个压缩文件&#xff0c;类似于放到一个文件夹内&#xff0c;也可以理解为把多张纸质照片装订成一个相册&#xff0c;而在装订过程中需要相应工具才能完成。使用压缩包发送文件&a…

dram sram利用 利用_使用量子力学技术的新型超低功耗存储器或将取代DRAM和Flash...

雷锋网按&#xff1a;业界普遍认为未来从数据中将能挖掘出最大的价值&#xff0c;但要挖掘数据的价值除了需要很强的计算能力之外&#xff0c;数据的存储也非常关键。目前&#xff0c;新型存储器也是领先的企业非常关注的一个方向&#xff0c;兰开斯特大学(Lancaster Universit…

mac地址转换_mac电脑格式转化工具

软件下载【软件名称】&#xff1a;Permute【系统支持】&#xff1a;MAC电脑【位数支持】&#xff1a;32位/64位软件下载地址&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1OAEpnvmkQgqd9MMq3Rucow提取码&#xff1a;tgt7注&#xff1a;阿狸提供用户下载的所有内容均来…

电脑怎么打出冒号符号_Mac小技巧:教你如何在Mac电脑打出command?、option?等特殊符号...

很多Mac新用户不知道该如何打出command⌘、option⌥等特殊符号&#xff0c;而网上好多关于打出Mac特殊符号的教程已经过期&#xff0c;或者繁琐看不懂。今天为大家分享一份简单&#xff0c;一看就懂的Mac电脑怎么打出command⌘、option⌥等特殊符号教程。1.我们打开系统偏好设置…

arraylist下标从几开始_漫画:为什么计算机从 0 开始计数,而不是从 1 开始?

作者 | 漫话编程来源 | 漫话编程当我们想要写一个循环体&#xff0c;期望执行10次的时候&#xff0c;我们会使用以下方式&#xff1a;for (int i0; i<10; i){}可以看到&#xff0c;为了保证循环10次&#xff0c;我们定义了一个整数变量从0开始。还有&#xff0c;当我们定义数…

python 爬虫代码_Python 你见过三行代码的爬虫吗

Python 使用Lassie库&#xff0c;仅编写三行代码就能爬取静态页面上的图片和视频。Python实战教程每次讲爬虫的时候都会从“发送请求” 开始讲&#xff0c;讲到解析页面的时候可能大部分读者都会卡住&#xff0c;因为这部分确实需要一点XPATH或者CSS选择器的前置知识。那么有没…

接口传值后不起作用_聊一聊 API 接口测试

知其然亦知其所以然&#xff0c;接口测试没有那么复杂&#xff0c;但也没有那么简单。本文作者&#xff1a;张敏&#xff0c;软件测试工程师&#xff0c;就职于一家容器平台解决方案公司&#xff0c;负责 DevOps 产品的测试。什么是 APIAPI(Application Programming Interface)…

服务器无法在发送 http 标头之后设置内容类型。_python socket编程预知内容

socket&#xff1a;运用IP地址协议端口 标识一个进程我们知道两个进程如果需要进行通讯最基本的一个前提能能够唯一的标示一个进程&#xff0c; 在本地进程通讯中我们可以使用PID来唯一标示一个进程&#xff0c;但PID只在本地唯一&#xff0c;网络中的两个进程PID冲突几率很大 …

自定义键盘组件_一文读懂!iOS系统组件的设计规范全解

一说到组件&#xff0c;大部分初级设计师和中级设计师的脑海里只会蹦出弹窗、Toast及操作列表等具体的概念&#xff0c;没有一套属于自己的组件分类体系&#xff0c;这样对于视觉设计、交互设计或产品设计的系统学习来说都是不利的。iOS14概念设计iOS官方设计指南在介绍组件时是…

anaconda安装python视频_怎么安装anaconda?

安装 Anaconda步骤&#xff1a; 1、双击下载好的 Anaconda2-5.2.0-Windows-x86_64.exe文件&#xff0c;出现如下界面&#xff0c;点击 Next 即可。2、点击 I Agree &#xff08;我同意&#xff09;3、Install for: Just me还是All Users&#xff0c;假如你的电脑有好几个 Users…

office365 自定义_IT外包观察,足不出户,Office365打造教学新体验?

Office365无疑是当今最高效的集文档处理、联机协同、移动办公、快捷沟通、商业智能为一体的云端生产力服务。但office365仅仅只应用于移动办公吗&#xff1f;有没有可能实现移动办学呢&#xff1f;仅仅是商业智能吗&#xff1f;会不会也能做到教学智能&#xff1f;上海蓝盟连续…

python连接数据库mysql错误1045、手动登录可以_登录mysql错误1045解决方法

在命令提示符中登陆mysql时&#xff0c;提示1045报错解决方案&#xff1a; 安装好mysql&#xff0c;在cmd中输入mysql -uroot -p回车&#xff0c;输入密码后&#xff0c;出现1045错误&#xff0c;错误提示如下&#xff1a; ERROR 1045(28000): Access denied for user rootloca…