koa是现在我们最常用的node框架,它是一个轻量的web框架,只提供了http的协议的解析和中间件功能。我们要实现路由、静态页面托管和文件上传等功能均需要插件来实现。
koa源码结构
上图是koa
的源码结构,lib放着koa的核心文件::application.js、context.js、request.js、response.js。
application.js
application.js是koa的入口文件,它向外到处了Koa
类,即函数。Koa继承了node的事件模块event
,因此,我们new Koa()
的实例app,可以基于事件来实现观察订阅的功能。Koa还有内置了常用的几个函数:listen、use、createContext、toJSON。
listen方法是通过http.createServer开启并监听了http服务,并且它里面还进行了中间件的合并、上下文context的初始化,并且每次请求来的中件合并、context都会重新初始化。
context.js
这部分是对中间件上下对象ctx
封装和暴露,里面的重点在delegate,这个就是代理,比如我们要访问ctx.repsponse.status但是我们通过delegate,可以直接访问ctx.status访问到它。
1 | // 暴露出来的对象 |
request.js、response.js
这两个文件是对原生对象req和res的解析,主要是使用了Getter
和setter
的方式来对http协议完成解析,便于我们使用.
1 | // request |
上面的this.req 也是application.js 里面的application 方法挂载的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18createContext(req, res) {
const context = Object.create(this.context);
// 初始化上下文ctx对象 的 request和repoonse 属性
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
// 保留app 实例
context.app = request.app = response.app = this;
// 保留原生的 req 和 res 对象 也是 上面 request.js文件里面 写法的原因
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
//保留上下文
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
Koa整体流程梳理
1 | const http = require("http"); |
中间件的原理
koa的中间件机制是一个剥洋葱式的模型,多个中间件通过use放进一个数组队列然后从外层开始执行,遇到next后进入队列中的下一个中间件,所有中间件执行完后开始回帧,执行队列中之前中间件中未执行的代码部分,这就是剥洋葱模型。koa的中间件机制,是基于async/await + Promise实现的.
compose
函数实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function compose(middlewares) {
return function (ctx) {
// 初始执行第一个中间件函数
return disPatch(0)
function disPatch(i) {
let fn = middlewares[0];
if(!fn) {
return Promise.resolve()
}
// 返回promise
return Promise.resolve(fn(ctx,function next(){
return disPatch(++i)
}))
}
}
}
koa-compose源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29function 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) {
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]
// 边界处理 这里的next 为undefined
if (i === middleware.length) fn = next
// 当fn为undefined 不执行 直接resolved
if (!fn) return Promise.resolve()
try {
// 实际app.use 中的 next 就是 disptch函数,它对应这当前的中间件函数
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
module.exports = compose
来看下洋葱模型的执行结果:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20async function fn1(next) {
console.log("fn1");
await next();
console.log("end fn1");
}
async function fn2(next) {
console.log("fn2");
await delay();
await next();
console.log("end fn2");
}
function fn3(next) {
console.log("fn3");
}
function delay() {
return new Promise((reslove, reject) => {
setTimeout(() => { reslove(); }, 2000);
});
}
const middlewares = [fn1, fn2, fn3]; const finalFn = compose(middlewares); finalFn();
总结
koa的核心对象:Koa类构造函数、request、response、context都有梳理,koa的源码做了许多细节处理,这样处理有什么好处,还需和大家共同探讨