Q1:什么是中间件
中间件(Middleware),也叫中介层,是提供系统软件和应用软件之间连接的软件,便于软件各部分之间的沟通。
中间件只是一种服务,没有这种服务 系统也能够存在。
一般提到中间件这个概念就必须要提到AOP(一个中间件一般有两个切面,遵循先进后出的切面执行顺序)。
koa中的中间件函数能够访问请求对象和响应对象以及应用程序的请求/响应循环中的下一个中间件函数。类似过滤器,在请求和响应到来前,先进行处理掉一些逻辑。
Q2:什么是AOP
面向切面编程AOP(Aspect Oriented Programmming)是一种非侵入式扩充对象、方法和函数行为的技术。
- 侵入式是需要知道框架中的代码,与框架代码紧密结合在一起。
- 非侵入式是可以自由选择和组装各个功能模块,没有过多的依赖。
AOP就是在现有代码程序中,在程序生命周期或横向流程中加入/减去一个或多个功能,不影响原有功能。
(⚠️继承、组合、委托等也可以用来增加和合并行为,但是多数情况下,AOP被证明是更灵活和更少侵入的方式)
场景描述:我们需要在
thing.doSomething()
中做一些数据分析,需要收集当前函数执行的时间等信息,应该如何扩展呢?
var originDoSomething = Thing.prototype.doSomething;
Thing.prototype.doSomething = function(){
doSomethingBefore(); //增加行为
return originDoSomething.apply(this, arguments);
}
上述实现有效的为thing.doSomthing();
增加了行为。在调用thing.doSomthing();
时,将首先调用doSomethingBefore()
,然后再执行原来的行为。
上述实现方案的好处:
- Thing的源代码没有被修改。(VS 粗暴的直接将要增加的行为添加到
Thing.prototype.doSomething
中) - Thing的使用方无需修改调用代码。(VS 为不侵入Thing实现,将行为增加到Thing的每个调用位置;继承的话也需调用方修改因为引入了新的构造函数)
- doSomething的原本行为得以保留。
- Thing并不知道
doSomethingBefore
的存在,并且不依赖它。因此,Thing的单测也无需更新。
从AOP的角度,可以说doSomethingBefore()
是应用于this.doSomething()
的一个行为切面,被称为"before advice",即thing.doSomething()
在执行原来的行为之前会先执行doSomethingBefore
。(AOP通常可以实现多种类型,如before、after、afterReturning、afterThrowing、around)。
其实现在很多跨端兼容的框架都是采用AOP的思想实现的。对源码无侵入,采用拦截器的形式对原型进行拦截,增加行为处理逻辑。
AOP VS OOP
AOP(面向切面编程)是OOP(面向对象编程)的延续。二者在字面是虽然非常类似,但却是面向不同领域的两种设计思想。
- OOP针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更佳清晰高效的逻辑单元划分;
- AOP则针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合的隔离效果。
Q3:什么是洋葱模型?
koa2最出色的就是基于洋葱模型的HTTP中间件处理流程。
通过
next()
把多个中间件串联执行的效果。所有中间件都会执行两遍,就像洋葱一样,从洋葱的一侧进入就会从另一侧出去。
Koa2.js的源码阅读笔记
Koa中间件采用堆栈形式先进后出(first-in-last-out)的执行顺序。
先来看一段koa的使用用例:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('...fun1 begin');
const start = Date.now();
await next();
console.log('...fun1 end');
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
app.use(async ( ctx, next) => {
const start = Date.now();
console.log('...fun2 begin');
await next();
console.log('...fun2 end');
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
app.use(async ctx => {
console.log('...fun3 begin');
ctx.body = "Hello World";
});
app.listen(3000);
/* 代码输出结果如下:
...fun1 begin
...fun2 begin
...fun3 begin
返回 respond: ctx.body是Hello world
...fun2 end ⚠️这里开始原路返回了
GET / - 4ms
...fun1 end
*/
根据上述代码的输出结果,️ async/await会暂停当前流程,next
参数是什么呢?
每碰到 await next
,代码会跳出当前中间件,执行下一个,最终还会原路返回,依次执行await next
下面的代码。实际上是一个递归返回Promise。
️如何实现中间件洋葱执行模型的?
- 基于
generator + co.js
(koa1)
function* fun1(){
console.log('fun1 begin');
yield *fun2Iterator;
console.log('fun1 end')
}
function* fun2(){
console.log('fun2 begin');
yield *fun3Iterator;
console.log('fun2 end')
}
function* fun3(){
console.log('fun3 begin');
}
var fun1Iterator = fun1(),
fun2Iterator = fun2(),
fun3Iterator = fun3();
fun1Iterator.next();
/**
fun1 begin
fun2 begin
fun3 begin
fun2 end
fun1 end
*/
es6中引入了Generator
函数,类似于一个状态机,封装了多个内部状态。通过yield
语句暂停,输出当前的状态。
⚠️在koa中使用的是yeild next
,而这里我们使用的是yield *next
;在koa中yeild next
和yeild *next
是等价的,这主要得益于co库。
- 基于
async/await
(koa2)
node.js v7.6.0开始完全支持async/await,koa2 node环境需要7.6.0以上;
利用匿名函数自执行的特性结合Promise.resolve()
实现代码如下:
Promise.resolve((async()=>{
console.log('fun1 begin');
await Promise.resolve((async() => {
console.log('fun2 begin');
await Promise.resolve((async() => {
console.log('fun3 begin');
})());
console.log('fun2 end');
})());
console.log('fun1 end');
})());
/**
fun1 begin
fun2 begin
fun3 begin
fun2 end
fun1 end
*/
看到输出结果,不就是洋葱模型的输出嘛 到这里,你是否能清晰的感知到Promise
结合await async
后的强大能力呢?那么问题又来了,koa2是如何实现通过await next()
开始直接执行下一个中间件的呢?他是如何将中间件按顺序串联起来的呢?答案就藏在compose模块模块中。
下面一起看看koa2.js都做了些什么吧
1. 封装node http Server
先不要着急,我们先来看看不依赖于框架,直接使用Node.js提供的API如何实现一个Server:
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
res.end("Hello World");
})
.listen(9999);
请求一进来,就会执行http.createServer
的callback。
所以koa对callback模块进行了一些处理(主要由app.use
来注册回调函数),通过app.listen()
开启server并传入callback
,如下:
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
2. 构造resquest, response, context对象
callback()
初始化的时候会执行compose
对所有的中间件函数进行聚合,方便后续可以按顺序控制执行中间件函数调用,并返回新构建的handleRequest()
。
在请求进来的时候才会执行callback
即handleRequest
,这时会对req和res进行合并成为ctx
,并递归执行中间件。
callback() {
const fn = compose(this.middleware); //组合middleware
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn); //合并req和res到ctx上
};
return handleRequest;
}
- request:对node原生的request对象的封装;
- response:对node原生的response对象的封装;
使用JS的getter
和setter
属性,基于node的对象req/res
对象封装Koa的request/response
对象。 - context:回调函数的上下文,挂载了koa request和response对象;使用
delegates
模块对一些常用方法进行了代理。
参考代理机制实现
为什么需要
ctx
呢?
koa处理请求是按照中间件的形式的,而中间件并没有返回值的。那么如果一个中间件的处理结果需要下一个中间件使用的话,该怎么把这个结果告诉下一个中间件呢?如:有一个中间件是解析token的将它解析成userId,然后后面的中间件需要使用到,那么如果将它传递过去呢?
其实中间件就是一个函数,维护一个对象ctx
,给每个中间件都传入ctx
,所有中间件便能通过这个ctx
来进行交互了。
3. 中间件机制
由于对middleware中间件函数的整合处理compose()
,所以一旦有请求进来会把所有中间件函数执行一遍,具体实现如下:
function compose (middleware) {
return function (context, next) {
let index = -1
return dispatch(0) //派发执行第一个中间件
function dispatch (i) {
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))); //通过resolve执行async方法将下一个中间件函数传入进入,所以在app.use上可以接受到next(下一个中间件)
} catch (err) {
return Promise.reject(err)
}
}
}
}
下一个中间件函数以参数的形式传入进来了:
⚠️ 执行第一个中间件的await next()
的时候实际执行的是dispatch(1)
由于dispatch()
是一个闭包,所以它会拿到父级的index。
总结一下中间件机制的实现:koa2利用async + await
实现让中间件的洋葱模型;通过compose()
组合中间件数组构造next()
,实现 await next()
派发下一个中间件,控制中间件的执行顺序。
4. 错误处理
一个健壮的框架必须保证在发生错误的时候,能够捕获错误并有降级方案返回给客户端。细心的伙计应该发现了Koa2中的Application
继承自nodejs中的events
。
推荐阅读:
https://juejin.im/post/5decf130f265da339565d40e?utm_source=gold_browser_extension