大家好,我是半夏👴,一个刚刚开始写文的沙雕程序员.如果喜欢我的文章,可以关注➕ 点赞 👍 加我微信:frontendpicker,一起学习交流前端,成为更优秀的工程师~关注公众号:搞前端的半夏,了解更多前端知识! 点我探索新世界!
前言
在上一篇大前端,你说你不会Koa?进来带你手撸源码,看完不会,尽管喷!,介绍了Koa上下文的实现,以及Koa对node原生http模块request和response的封装。
为了新读者可以直接阅读这篇文章,我们再来梳理一下。
导出Koa类,以及listen,use函数的实现。
let http = require('http')
class Application {
use(fn) {
this.fn=fn
}
callback = (req, res) => {
this.fn(req, res)
}
listen() {
const server = http.createServer(this.callback);
server.listen(...arguments)
}
}
module.exports = Application
封装ctx,我们新增了一个函数,在callback中调用。让然后传给use的函数。
createContext = (req, res) => {
// 这里主要是将原生的req和res绑定到ctx上。
}
callback = (req, res) => {
let ctx = this.createContext(req, res)
this.fn(ctx)
x
响应用户请求,返回内容
callback = (req, res) => {
let ctx = this.createContext(req, res)
this.fn(ctx)
let body = ctx.body;
if (body) {
res.end(body);
} else {
res.end('Not Found')
}
}
这样简单一看,Koa还是很简单的。当然不是,这只是最基本的源码,本文,我们就要来手撕Koa的中间件系统的实现。
洋葱模型
这个概念基本是每一篇关于Koa的文章都会介绍的。并且都会放下面这张图。
这玩意是就是洋葱模型,不看代码的话,我们最简单的理解是啥呢:
koa通过app.use加载外部的函数,在创建完上下文之后,按照上面我们正常的操作,我们是直接响应用户请求,返回内容,但是洋葱模型干了啥?我们此时并不会直接去响应,而是先把加载的外部函数执行完,再去响应用户请求,返回内容。
callback = (req, res) => {
let ctx = this.createContext(req, res)
use1的函数执行
use2的函数执行
use3的函数执行
let body = ctx.body;
if (body) {
res.end(body);
} else {
res.end('Not Found')
}
}
上面的代码只是我们最简单的状态,不存在异步的情况。
app.use与中间件
Koa中间件其实就是函数,通过app.use来进行调用。app.use() 返回 this, 因此可以链式表达.
这里的中间件可以时普通的函数,也可以是async定义的异步函数。
app.use(async (ctx, next)=>{
console.log(1)
await next();
console.log(1)
});
这里的next是为了去执行下一个中间件,意思就是下面的console.log(1)不执行,执行别的中间件去。
再聊聊洋葱模型。上面我们简单的猜测了一下洋葱模型的逻辑。下面我们通过一个例子再来说明一下。
app.use(async (ctx, next) => {
console.log(1)
await next();
console.log(1)
});
app.use(async (ctx, next) => {
console.log(2)
next();
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
})
对于这个例子输出的结果是 1 2 3 2 1。 再来看下这幅图,是不是知道洋葱模型的大概了。
上面我们说了next的作用是去执行下一个中间件,在上面的例子我们做一个改动。
app.use(async (ctx, next) => {
console.log(1)
await next();
console.log(1)
});
app.use(async (ctx, next) => {
console.log(2)
next();
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
})
app.use((ctx) => {
console.log('koa')
ctx.body = "原生koad"
})
app.use(async (ctx, next) => {
console.log(4)
next();
console.log(4)
})
对于这个例子,最后两个中间件是无法执行到的。
话不多说;我们总结一下洋葱模型: 中间的执行,并不是一层一层的执行,而是以next为界限,先执行next上面的代码,知道所有的上层执行完,再执行next下层代码。
一个洋葱结构,从上往下进来,再从下往上回去。
步步升级
最简单
我们上面已经知道了洋葱模型的大概实现,那么我们就先简单的实现一下。 首先是next的作用,这里next的作用就像是一个占位符,或者说是执行器,遇到next就会去执行下一个函数,我们来简单的模拟一下:
function fn1() {
console.log(1)
fn2();
console.log(1)
}
function fn2() {
console.log(2)
fn3();
console.log(2)
}
function fn3() {
console.log(3)
return;
}
fn1();
我们将函数放在next的位置,就可以达到效果。
升级-函数包裹
上面我们把函数放在了next的位置,是可以实现效果。 那么我们现在把next放回去。
async function fn1(next) {
console.log(1);
await next();
console.log(1);
}
async function fn2(next) {
console.log(2);
await next();
console.log(2);
}
async function fn3() {
console.log(3);
}
let next1 = async function () {
await fn2(next2);
}
let next2 = async function() {
await fn3();
}
fn1(next1);
这里我们将函数包裹起来赋值给next。然后传给函数。再调用。
升级-封装共通
上面的例子我们将函数包裹起来,其实也就是将函数传给另一个函数,如果我们有N个函数,如果你不嫌麻烦也可以这样写,但是我们可以根据上面的思路提出一个共通的函数。
首先:
- 中间件是普通函数也可以是async函数。
- 中间件接受next来调用下一个中间件
- 先执行第一个中间件
在上面我们进行next传参的时候,创建了next1和next2二次封装中间价,最后执行第一个函数。现在我们想要使用一个next来进行封装,第一反应肯定是在for或者while中,这里怎么会出现for,既然我们有这么多中间件,把他们收集起来,然后循环对他们传参,是不是就可以了。
收集函数
const middlewares = [fn1, fn2, fn3];
传参next
这个函数的作用就是给每一个中间价传参。
function compose(middleware, next) {
return async function() {
await middleware(next);
}
}
循环传参
这里定义了一个next,来保存上一个已经接受next的函数。
let next;
for (let i = middlewares.length - 1; i >= 0; i--) {
next = compose(middlewares[i], next);
}
经过这个for循环,我们已经完整的给每个中间件传递了next。效果大概就是这种。
next = async function(){
await fn1(async function() {
await fn2(async function() {
await fn3(async function(){
return Promise.resolve();
});
});
});
};
async function fn1(next) {
console.log(1);
await fn2();
console.log(1);
}
async function fn2(next) {
console.log(2);
await fn3();
console.log(2);
}
async function fn3() {
console.log(3);
}
这里有一个点,我们for是从最后一个开始的,这是为了,能够执行函数,我们传递结束参数,那么肯定要执行函数,next最后保存的就是第一个函数。
完整代码
async function fn1(next) {
console.log(1);
await fn2();
console.log(1);
}
async function fn2(next) {
console.log(2);
await fn3();
console.log(2);
}
async function fn3() {
console.log(3);
}
function compose(middleware, oldNext) {
return async function() {
await middleware(oldNext);
}
}
const middlewares = [fn1, fn2, fn3];
let next ;
for (let i = middlewares.length - 1; i >= 0; i--) {
next = compose(middlewares[i], next);
}
next();
Koa的实现
上面简单的实现了一个洋葱模型。 在Koa中实现的逻辑其实大致相同。
this.middlewares
constructor() {
this.middlewares = [];
}
之前我们use是
use(fn) {
this.fn=fn
}
现在只要:
use(fn) {
this.middlewares.push(fn)
}
将中间件收集起来
compose
之前我们在callback中执行了
callback = (req, res) => {
this.fn(ctx)
}
现在有了中间件的概念我们肯定就不能再这样写了。 同样的,我们将所有的中间件封装到一个函数中,然后执行。
callback = (req, res) => {
this.compose(ctx).then(() => {
let body = ctx.body;
if (body) {
res.end(body);
} else {
res.end('Not Found')
}
}).catch((e)=>{
})
}
compose内部就是上面的for循环/while循环。在Koa中使用的是while循环。这里我们也用while循环实现一遍。
- while循环时从头往后,同时一个中间件中只有一个next。
- 当走到最后一个中间件,返回一个promsie
- 最后执行第一个中间件
compose(ctx){
// index
let index = -1
const dispatch = (i)=>{
if(i <= index){
return Promise.reject('next() 只允许调用一次')
}
index = i;
if(this.middlewares.length == i) return Promise.resolve();
let middleware = this.middlewares[i];
try{
return Promise.resolve(middleware(ctx,()=> dispatch(i+1)));
}catch(e){
return Promise.reject(e)
}
}
return dispatch(0);
}
至此整个洋葱模型的实现结束。