揭秘Koa原理,带你实现一个mini-koa
上一篇讲解了koa洋葱模型是如何实现的,并写了一个最小实现方案,这一篇讲全面讲解koa源码实现的细节,看看有哪些点值得我们学习。
_koa@2.14.1@koa
├─History.md
├─LICENSE
├─Readme.md
├─package.json
├─lib
| ├─application.js
| ├─context.js
| ├─request.js
| ├─response.js
| └test.md
├─dist
| └koa.mjs
koa
库的文件结构非常简单,实现的逻辑都在lib
目录下面,一共有四个模块,分别是application
、context
、request
、response
。
delegates库是干什么用的
在讲解源码之前,先要介绍一下一个库:delegates。context
的部分实现就是用这个库,将一些方法和属性在访问的时候委托(或者说代理)到了request
和respone
上(类似于Object.defineProperty)。
delegates接收两个参数,一个是proto,一个是target,proto是访问对象,target是代理对象。
delegate(proto, target).setter(name)
注册setter的name,比如当给proto[name]赋值的时候,其实是给target[name]赋值delegate(proto, target).getter(name)
注册getter的name,比如当获取proto[name]的值的时候,得到的是target[name]的值delegate(proto, target).access(name)
access是既注册了setter,又注册了getter,实现了真正的完全代理,不管赋值还是取值,都是代理到了target对象上。delegate(proto, target).method(name)
注册method的name,当proto[name]()调用一个方法时,相当于target[method]()。 例如:
const delegate = require('delegates');
const proto = {}
let target = {
foo: () => {
return 'foo'
}
}
proto.target = target;
delegate(proto, 'target').method('foo');
// 当访问proto.foo()时,其实是代理到了target对象的foo方法
console.log(proto.foo()); // 'foo'
// Object.defineProperty实现
const proto = {}
let target = {
foo: () => {
return 'foo'
}
}
proto.target = target;
Object.defineProperty(proto, 'foo', {
get(){
return target['foo'];
}
})
console.log(proto.foo()); // 'foo'
new koa做了什么?
koa源码地址 让我们通过一个简单的demo,来一步步实现mini-koa。
const Koa = require('koa');
const app = new Koa();
app.use(async(ctx, next) => {
ctx.body = 'hello world'
});
app.listen(3000)
源码展示:
去除一些不影响主逻辑的代码,
constructor
函数其实只创建了四个变量。
// index.js
const Emitter = require('events');
const context = require('./context.js');
const request = require('./request.js');
const response = require('./response.js');
class Application extends Emitter {// 继承events模块,方便监听
constructor(options) {
super();
this.middleares = []; // 用于收集中间件
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
}
实现use和listen
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); // 将generators函数转成async函数
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
去掉一些边界判断条件,use
方法其实只做了两件事:1.收集中间件;2.返回this,支持链式调用。再看listen
方法。
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
listen
方法也很简单,就是利用http
模块,创建了一个服务,并传入了一个this.callback
回调函数,同时将参数透传给http
服务的listen
方法。
http.createServer接收一个回调函数,参数形式为:http.createServer((res, req) => {})
继续完善我们的mini-koa
// index.js
const Emitter = require('events');
const context = require('./context');
const request = require('./request');
const response = require('./response');
+ const http = require('http');
class Application extends Emitter {
constructor(options) {
this.middleares = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response)
}
+ use(middleare) {
+ this.middleares.push(middleare);
+ return this;
+ }
+ listen(...args) {
+ const server = http.createServer(this.callback());
+ return server.listen(...args);
+ }
+ callback() {
+ return (req, res) => {
+ // todo
+ }
+ }
}
实现完整版的compose函数
继续看看源码的callback
如何实现:
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); // 创建了context对象
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
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);
}
createContext(req, res) { // 创建一个context,并且定义了对外暴露context的属性
const context = Object.create(this.context);
const request = context.request = Object.create(this.request); // 挂在koa的request
const response = context.response = Object.create(this.response); // 挂载koa的response
context.app = request.app = response.app = this; // 挂载app实例
context.req = request.req = response.req = req; // 挂载原生的req对象
context.res = request.res = response.res = res; // 挂载原生的res对象
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
首先看callback
函数第一行代码const fn = compose(this.middleware)
,利用compose
函数,组合了中间件,返回了一个fn
,后面将fn
传给了this.handleRequest()
的第二个参数(第一个参数是context
)。在this.handleRequest
这个函数里面,fn
被命名为fnMiddleare
,最后this.handleRequest
是返回了fnMiddleware(ctx).then(handleResponse).catch(onerror)
,所以可以得到以下几点:
- 调用
compose
函数返回的是一个fn函数,这个函数接收context
作为参数 - 调用
fn
函数返回了一个promise
所以在上一篇原来koa实现洋葱模型只有11行代码文章的基础之上,稍微修改一下即可。
// compose.js
function compose(middleares) {
return function fn (context) { // 1. fn接收一个context作为参数
let index = -1;
function dispatch(i) { // 2. dispatch必须返回的是一个promise
if(i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i;
let middleare = middleares[i];
// 别忘记fn函数的返回值是一个promise
if(!middleare) return Promise.resolve();
try {
return Promise.resolve(middleare(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
return dispatch(0); // 返回dispatch的执行结果
}
}
实现Context(利用delegates委托机制)
koa的Context源码地址 伪代码如下:
const delegate = require('delegates');
const proto = module.exports = {
...
inspect(){
// ...
}
...
}
delegate(proto, 'response')
.method('attachment')
.access('body')
delegate(proto, 'request')
.method('get')
.access('method')
.getter('href')
前面已经简单讲过delegates
的使用方法,context
除了一些自身定义的方法之外,还有许多方法是直接委托到response
和request
上。
实现request/response
// request.js
module.exports = {
get method(){
return this.req.method;
},
set method(val) {
return this.req.method = val;
}
...
}
koa
的request
是在原生的req
基础之上,做了很多方法的封装,方便操作;response
也是同理的,这里不再展开讨论。
错误拦截
注册错误函数
// application.js
callback() {
...
// 检测有没有已经注册过error事件
if (!this.listenerCount('error')) this.on('error', this.onerror);
...
}
onerror(){
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error;
if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));
if (404 === err.status || err.expose) return;
if (this.silent) return;
const msg = err.stack || err.toString();
console.error(`\n${msg.replace(/^/gm, ' ')}\n`);
}
如上图,new koa
的时候,会先检测app
实例上有没有你自己写的error
事件,比如app.on('error', () => {})
,如果有的话,就用你的;如果没有的话,就注册this.onerror
函数。this.onerror
函数主要就是将错误信息用console.error
方法打印出来。
拦截中间件错误
在this.handleRequest
函数里面,可以看到fnMiddleare
是一个promise
,也就是说当中间件里面发生错误的时候,就会走到catch
函数,调用ctx.onerror
方法。
// application.js
handleRequest(ctx, fnMiddleware) {
...
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
继续看看ctx.onerror
方法
// context.js
onerror(err) {
if (null == err) return;
const isNativeError =
Object.prototype.toString.call(err) === '[object Error]' ||
err instanceof Error;
if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));
...
// delegate
this.app.emit('error', err, this); // 触发error事件,将错误信息发射到app.onerror
const { res } = this;
...
// respond
const code = statuses[statusCode];
const msg = err.expose ? err.message : code;
...
res.end(msg);
},
当中间件发生错误时,ctx.onerror
方法会调用this.app.emit('error', err, this)
方法,由于之前我们已经注册过app
的onerror
方法了,就会将错误信息打印出来。
emit 和 on 方法都是application继承自Events模块
ctx.body是如何做到的
// application.js
handleRequest(ctx, fnMiddleware) {
...
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
还是在application.js
的this.handleRequest
函数里面,当执行完fnMiddleare(ctx)
之后会调用then
方法,相当于调用了respond
方法。
function respond(ctx) {
...
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
...
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' === typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
可以看到,respond
函数主要就是对body
为Buffer
、String
、Stream
、Json
的类型分别判断处理。
总结
重新梳理一下koa
的运行流程
- 当
new koa
的时候,会实例化一些属性,主要有this.context
、this.request
、this.response
、this.middleares
; - 调用use的时候,将中间件收集起来,放到
this.middleares
里面,等待后续一起处理(发布订阅模式); - 调用
listen
的时候,会调用callback
方法,组合了中间件,返回了fn
函数,等待处理; - 当页面访问对应的端口的时候,就触发了
http
的回调,开始执行handleRequest
函数,然后依次执行中间件 - 如果中间件没有错误,就用调用
respond
方法,将body
内容传给http
服务器;否则,调用catch
方法
戳这里查看mini-koa源码,总体来说koa的源码并不复杂,唯一有点绕的地方可能就是compose函数了。有什么不懂或者我理解不对的地方,欢迎评论区讨论。
转载自:https://juejin.cn/post/7206871218031575077