KOA洋葱模型-01
前言
- 看了很多关于koa的文章,都不太理解,所以本文从源码入手,希望能够提供给有需要的人一点点帮助
什么是koa
- 也许你会觉得我会讲一大堆概念,附上中文官网地址:www.koajs.com.cn/#introducti…,简单来说就是就是基于node的web框架,直白点就是让你用javascript语言去编写接受请求,响应请求的逻辑代码。(自己说的大白话,如有错请各位大神指出)
- 另外一个问题就是koa,koa2,express有什么区别,这种问题自行百度,框架无所谓好坏,只有适不适合
koa初探
-
如果你用过其他的web框架如express,或者node提供的原生http服务,会对下面的代码很熟悉
-
const Koa = require('koa'); const app = new Koa(); //中间件1 const mid1 = async (ctx, next) => { ctx.body = `<h3>请求 => 第一层中间件</h3>` await next() ctx.body += `<h3>响应 <= 第一层中间件</h3>` } //中间件2 const mid2 = async (ctx, next) => { ctx.body += `<h3>请求 => 第二层中间件</h3>` await next() ctx.body += `<h3>响应 <= 第二层中间件</h3>` } app.use(mid1) app.use(mid2) app.listen(5000, () => { console.log('starting at http://localhost:5000'); }); -
上面的代码无非就是
引包=> 书写两个中间件=> 把中间件加入服务处理队列=> 开启服务 -
把本段代码粘贴到任意一个空白的js文件中,然后用
node 文件路径(相对或绝对路径),接着在浏览器地址栏访问http://localhost:5000,就能看到结果了(结果请自行尝试) -
乍一看,就这啊,我也会,但只会写这个这还远远不够,一个服务器应该能处理各种各样的请求,但是我们这个服务只能处理 访问了
http://localhost:5000这一个请求,如果你学过ajax,那你对各种请求肯定不默认。 -
但是,本文暂不处理各种各样的请求,咱们谈论的是,为什么请求这个地址会返回相对应的数据
源码分析
- 在node_modules找到koa这个包,打开package.json,可以发现入口文件是
./lib/application.js
- 打开
application.js文件,发现会依赖当前目录的request.js , response.js context.js包。为了统一好调试,我们把这四个包统一复制出来,放入一个单独的文件夹中,然后我们把application.js
- 重点关注的是
application.js文件,在源码31行我们可以看到,对外暴露了一个类,这个就是我们new的koa实例
- 在源码第86行可以看到该类的listen方法,这就是我们上面代码listen方法源码,底层就是基于node提供的原生api开启的服务
- 如果你写过node原生的api,应该对下面的代码很熟悉
const http = require('http');
const hostname = "localhost",
port = 8080
//创建服务
const server = http.createServer((req,res) => {
console.log('有人访问了我们的网站')
res.end('hello world');
})
//监听请求 , http.createServer里面的函数和这个监听等价,只要有人请求了,那么就会触发
// server.on('request', (req,res) => {
// console.log('有人访问了我们的网站');
// res.end('hello world');
// })
//启动服务,协议默认是http
server.listen(port, hostname, () => {
console.log('服务已经启动了');
})
- 做完了对比,可以发现koa传入的是
this.callback(),所以我们可以知道这个函数调用后返回了一个函数,看callback源码(application.js第150行)
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
//定义了一个函数
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
//返回了一个函数 ,和结论一致,只要有人访问了定义的那个地址,这个函数就会被调用
return handleRequest;
}
handleRequest方法:
/* 第一行就是组装原生res,req对象成ctx上下文对象,这不是简单的组装,ctx有很多实
* 用的属性和方法,比如请求参数,设置响应头等等,具体可以自行查阅,本文不做介绍。
* 第二行调用的是类本身的 handleRequest方法,而不是这个局部的,所以不是递归
*/
- 接下来看一下这个参数,第一个是ctx上下文对象,不做过多介绍,第二个fn,回头看一下fn的由来,到了本文的重头戏了,看一下compose,因自
koa-compose,这就是大名鼎鼎的koa引用库之一,代码不到40行,看一下这个包,就暴露了一个函数,先不看这个函数,先看参数,this.middleware, 可以在application.js文件中第59行看到在构造函数中初始化了一个空数组
- 这就是存放中间件的容器,我们写的所有的中间件函数都存在在这里,前提是我们得use一下,既然提到了use,就简单看一下use方法(第129行),可以看到,首先做了一下判断,只要你写的规范,就不用在意这些,倒数第二行就是直接往我们这个数组里面添加这个函数返回this是为了链式调用,这里不深究
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;
}
- 我们知道了compose的参数,以及参数是怎么形成的,接下来就讨论洋葱模型的内核,
koa-compose库的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!')
}
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
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)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
- 简要看一下这个函数:首先判断是不是数组=》不是=》报异常;然后遍历数组,每一个元素都必须是函数,不是=》报异常,最后就是返回了一个匿名函数。
- 然后回到上面的
callback方法,我们知道了fn是一个匿名函数,传递给了this.handleRequest方法(别忘了callback返回了handleRequest这个局部方法,在有人请求的时候就会触发这个函数,上文已经提到了) - 接下来如果有人请求了,
handleRequest触发,this.handleRequest触发,看一下this.handleRequest方法application.js第176行)
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
- 最后一行就是我们
compose函数返回的那个匿名函数的调用了,现在可以分析该函数了。下面的代码就是callback里面的fn,就是this.handleRequest方法第第二个参数fnMiddleware(具体可以看上面的callback方法)
function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
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)));
} catch (err) {
return Promise.reject(err)
}
}
}
- 中间件数组我们分为空数组和传递两个中间件讨论,方便理解
传递空数组
- 先说结论,如果是空数组的话,页面上看到的是404.
this.handleRequest调用=>fnMiddleware调用(就上面这个函数),该函数里面创建了dispatch方法,返回了dispatch(0)的调用,通过代码分析很容易的得到把0传进去的时候,let fn = middleware[i](fn是undefined, 所以直接返回成功的promise ,值是undefined),=>if (!fn) return Promise.resolve(),执行完了,又回到了this.handleRequest方法,看一下喽- 根据promise的链式调用,会执行then回调,所以又执行了
respond方法,先别急着看respond方法,上面还有两行,设置状态码为404,虽然不是给上下文对象设置的,但是在上下文对象中可以拿到,然后看respond方法(application第252行),这里就不深究了,如果需要看的话我下次再讲
function respond(ctx) {
// 省略代码...
if (null == body) {
if (ctx.response._explicitNullBody) {
ctx.response.remove('Content-Type');
ctx.response.remove('Transfer-Encoding');
return res.end();
}
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
//这就是返回的body内容,404,至于为什么上面的分支都不会走,因为我是一步一步调试到这里的,如果细看的话还有很多其他需要说的,这里就不深究了
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
//返回body,页面上就看到了404
return res.end(body);
}
//省略代码...
}
传递多个中间件(本文以两个为例)
- 再把这个函数拿来
function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
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)));
} catch (err) {
return Promise.reject(err)
}
}
}
- 同理,调用上面的函数,首先取出第一个中间件赋值给fn =>
let fn = middleware[i],然后进入try...catch,返回了fn的调用返回值作为成功的promise值,注意,这个fn就是我们压入数组的第一个中间件,fn执行传入了两个参数,一个是上下文对象,一个是新函数,bind就改变了 dispatch函数的this指向,没有执行dispatch函数,然后数组索引加1,这就是我们在中间件执行的next方法。 - 所以就应该听过一句话,如果不在中间件调用next方法,是不会形成中间件执行链的,就是在这里截断了。
- 好了,现在我们按照正常的逻辑走,第一个中间件执行了 ,body添加了内容,然后执行了next方法,这个next执行,相当于执行了
dispatch(1)(注意这时候第一个中间件还没执行完,因为next在这里会阻塞,等待next执行完才会执行剩余代码) dispatch(1)执行,就能取出第二个中间件,fn执行,所以第二个中间件执行了,第二个中间件又调用了next方法,这时候就相当于执行了dispatch(2),但是这时候let fn = middleware[2],返回值是undefined,因为只有两个中间件,后面有一一个判断,if (i === middleware.length) fn = next,成立,所以执行,需要注意的是,这个next这个局部函数的外部变量
- 回头看我们一开始是怎么调用这个匿名函数的,是不是在
this.handleRequest里面的fnMiddleware函数,他只传了一个参数ctx,所以这个next就是undefined,所以下一条件语句执行if (!fn) return Promise.resolve(),返回值为undefined的成功promise - 所以,这时候我们在第二个中间件里next方法就此完成,await的结果是undefined,执行下面的代码,执行完了,如果没有返回值,默认也是undefined,这时候第一个中间件的next方法执行完,执行剩余代码
- 至此,所有中间件代码执行完毕,整理数据返回,所以能够在页面看到如下结果
结语
本文基于自己的理解,写的第一篇文章,如有错误之处,还请大神们不吝赐教!