# koa源码分析-从入门到看不懂
# 前言
本文用来记录一下自己学习koa的时候阅读源码和相关学习资料的心得和总结,但是写的很乱,大致的意思没有体现出来,不建议阅读下去,如果想更加清楚的理解koa的核心源码,可以移步下一章节,自己实现一个mini版的koa,这会让你对源码的认知提升到一个很好的地步。
# koa是什么?
引用官网的话,koa是基于Node.js平台的下一代web开发框架,致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
用简单的话来总结就是:
1、基于node原生req和res为request和response对象赋能,并基于它们封装成一个context对象。
2、基于async/await(generator)的中间件洋葱模型机制。
# 基本用法
const Koa = require('koa')
const app = new Koa()
app.use(async(ctx, next) => {
ctx.body = 'hello world'
next()
})
app.listen(5050, () => {
console.log('sever start')
})
以上是最简单的 hello world
, 通过代码我们大致可以发现koa的使用并不难,而且koa的源码也并不多,但是设计的很抽象简洁,刚开始阅读的时候还是有点难接受的。
以下是网上借鉴来的一张koa结构示意图,可以更加直观的了解到koa的结构。
# 源码结构
如果你看了koa的源码,你会发现koa源码其实很简单,就4个文件
── lib
├── application.js
├── context.js
├── request.js
└── response.js
结合示意图,发现这个结构很简单,其中 context 、request 和 response 就是 3 个字面量形式创建的简单对象,上面封装了一些列方法(其实绝大部分是属性的赋值器(setter)和取值器(getter))。
这4个文件也就对应着koa的4个对象
── lib
├── new Koa() || ctx.app
├── ctx
├── ctx.req || ctx.request
└── ctx.res || ctx.response
那我们先对这4个文件进行初步的认识。
# application.js
从koa的package.json中我们发现,application.js是koa的入口,也是koa的核心所在。
{
"name": "koa",
"version": "2.7.0",
"description": "Koa web app framework",
"main": "lib/application.js",
---
}
下面对核心代码进行注释
/**
* 依赖模块,包括但不止于下面的,只列出核心需要关注的内容
*/
const response = require('./response');
const compose = require('koa-compose');
const context = require('./context');
const request = require('./request');
const Emitter = require('events');
/**
* `Application`继承于`Emitter`,说明具有异步处理的能力
*/
module.exports = class Application extends Emitter {
constructor() {
super();
this.middleware = [];//存放中间件函数
// context 、request 和 response 就是 3 个字面量形式创建的简单对象,他们将作为app的相应属性的原型
// Object.create() 让两者不会指向同一片内存,不会相互影响。
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
/**
* 创建服务器
*/
listen(...args) {
// 调用原生的node创建服务器的方法起一个服务
const server = http.createServer(this.callback());
return server.listen(...args);
}
/**
* use函数用于将中间件函数收集起来,存放在middleware中
*/
use(fn) {
// 兼容koa1 的写法,因为koa1主要使用generator,而koa2主要使用 async await
if (isGeneratorFunction(fn)) {
fn = convert(fn);
}
// 将中间件函数收集起来,存放在middleware中
this.middleware.push(fn);
// 返回当前实例,支持链式调用
return this;
}
/**
* http.createServer的参数,返回一个类似于 (req, res) => {} 的函数,作为服务请求的处理函数
*/
callback() {
// 将所有use函数收集的中间件函数集合形成超集,实现洋葱模型中间件机制
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
// 基于req、res封装出更强大的ctx超集
const ctx = this.createContext(req, res);
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);
}
/**
* 将req res 进行组合,形成强大的ctx
*/
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
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;
}
/**
* 错误处理函数
*/
onerror(err) {
if (!(err instanceof Error)) 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();
console.error(msg.replace(/^/gm, ' '));
console.error();
}
};
通过application.js
的初步解读,我们可以总结一下application.js
的作用
1、框架入口
2、实现洋葱模型的中间件机制
3、将原生req、res处理成一个强大的超集ctx
4、错误的统一处理
# context.js
const util = require('util');
const createError = require('http-errors');
const httpAssert = require('http-assert');
const delegate = require('delegates');
const proto = module.exports = {
// 省略了一些不甚重要的函数
onerror(err) {
// 触发application实例的error事件
this.app.emit('error', err, this);
},
};
/*
在application.createContext函数中,
被创建的context对象会挂载基于request.js实现的request对象和基于response.js实现的response对象。
下面2个delegate的作用是让context对象代理request和response的部分属性和方法
*/
delegate(proto, 'response')
.method('attachment')
...
.access('status')
...
.getter('writable')
...;
delegate(proto, 'request')
.method('acceptsLanguages')
...
.access('querystring')
...
.getter('origin')
...;
从代码中我们可以总结context的作用: 1、错误的处理 2、代理request、response的属性和方法
# request.js 和 response.js
module.exports = {
// 在application.js的createContext函数中,会把node原生的req作为request对象(即request.js封装的对象)的属性
// request对象会基于req封装很多便利的属性和方法
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
// 省略了大量类似的工具属性和方法
};
request对象基于node原生req封装了一系列便利属性和方法,也提供了一些原生上没有的属性和方法,如 path
。
所以当你访问ctx.request.xxx的时候,实际上是在访问request对象上的赋值器(setter)和取值器(getter)。
response对象和request对象类似,但是需要注意的是返回的body支持Buffer、Stream、String以及最常见的json
# 深入了解源码机制
上文中我们对koa的源码结构和对应文件的功能做了一些小的总结,下面我们从初始化、启动应用、中间件处理、错误处理来更加深入了解一下koa。
# 初始化
const Koa = require('koa');
const app = new Koa();
首先我们创建了 Koa 的实例 app,其构造函数十分简单,如下:
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
结合上文中的示意图,在创建实例的时候context
request
response
分别被初始化,也在实例上挂载了一些常用的如 subdomainOffset
env
等属性
我们可以总结一下,koa在初始化的时候,koa获得处理异步事件的能力,并且挂载一些属性和方法。
# 启动应用
app.listen(5050, () => {
console.log('sever start')
})
app.listen()做的事件也很简单, 我们已经知道,通过向 http.createServer 创建一个服务,部分源码如下
listen() {
const server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
}
唯一需要我们关注的就是这个 this.callback ,也是理解 koa 应用的核心所在。
this.callback() 执行返回一个类似于 (req, res) => {} 的函数
那么这个函数具体是怎么做的呢?首先,它基于 req 和 res 封装出我们中间件所使用的 ctx 对象,再将 ctx 传递给中间件所组合成的一个嵌套函数。中间件组合的嵌套函数返回的是一个 Promise 的实例,等到这个组合函数执行完( resolve ),通过 ctx 中的信息(例如 ctx.body )想 res 中写入数据,执行过程中出错 (reject),这调用默认的错误处理函数。
原理还是很简单,看一下代码:
callback() {
const fn = compose(this.middleware);
return (req, res) => {
res.statusCode = 404;
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
onFinished(res, onerror);
fn(ctx).then(() => respond(ctx)).catch(onerror);
};
}
就像我们阅读源码发现时候一样,通过 compose
方法对中间件函数组合成一个大的嵌套函数供后序执行的时候调用
而 createContext
根据 req 和 res 封装中间件所需要的 ctx
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
// 省略一点无关紧要的代码
return context;
}
简单的说就是创建了3个简单的对象,并且将他们的原型指定为我们 app 中对应的对象。然后将原生的 req 和 res 赋值给相应的属性,这也是为何以下结构得到的结果是一样的原因。
console.log(ctx.req.url);
console.log(ctx.request.req.url);
console.log(ctx.request.url);
console.log(ctx.url); // ctx.request.url
如我们示意图,整个 Koa 的结构就完整了。
但是,ctx 上不是暴露出来很多属性吗?它们在哪?他们就在我们示意图的最右边,一开始我们略过的 3 个简单对象。通过原型链的形式,我们 ctx.request 所能访问属性和方法绝大部分都在其对应的 request 这个简单的对象上面。request 又是怎么封装的呢?我只需要简单的贴一点源码,大家就秒懂了。
module.exports = {
//...
get method() {
return this.req.method;
},
set method(val) {
this.req.method = val;
}
//...
}
# 中间件处理
---略
# 错误处理
---略