前言
从egg-core中可以看出,egg的核心类继承于Koa,本着事物的追溯到本源的过程,Koa自然也逃不过去。
**
目录结构
.
├── lib
│ ├── application.js
│ ├── context.js
│ ├── request.js
│ └── response.js
└── package.json
- application.js 是整个koa2 的入口文件,封装了context,request,response,以及最核心的中间件处理流程。
- context.js 处理应用上下文,里面直接封装部分request.js和response.js的方法
- request.js 处理http请求
- response.js 处理http响应
设计模式
- HTTP服务
- 处理HTTP请求request
- 处理HTTP响应response
- 中间件容器
- 中间件的加载
- 中间件的执行
HTTP服务:封装了nodejs原生的http模块。Nodejs http中文文档
// http模块使用例子
const http = require('http');
const PORT = 3001;
const router = (req, res) => {
res.end(`this page url = ${req.url}`);
}
const server = http.createServer(router)
server.listen(PORT, function() {
console.log(`the server is started at port ${PORT}`)
})
中间件容器
const Koa = require('koa');
let app = new Koa();
const middleware1 = async (ctx, next) => {
console.log(1);
await next();
console.log(6);
}
const middleware2 = async (ctx, next) => {
console.log(2);
await next();
console.log(5);
}
const middleware3 = async (ctx, next) => {
console.log(3);
await next();
console.log(4);
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.use(async(ctx, next) => {
ctx.body = 'hello world'
})
app.listen(3001)
// 启动访问浏览器
// 控制台会出现以下结果
// 1
// 2
// 3
// 4
// 5
// 6
以上结果主要是Koa.js的一个中间件引擎 koa-compose模块来实现的,也就是Koa.js实现洋葱模型的核心引擎。
+----------------------------------------------------------------------------------+
| |
| middleware 1 |
| |
| +-----------------------------------------------------------+ |
| | | |
| | middleware 2 | |
| | | |
| | +---------------------------------+ | |
| | | | | |
| action | action | middleware 3 | action | action |
| 001 | 002 | | 005 | 006 |
| | | action action | | |
| | | 003 004 | | |
| | | | | |
+---------------------------------------------------------------------------------------------------->
| | | | | |
| | | | | |
| | +---------------------------------+ | |
| +-----------------------------------------------------------+ |
+----------------------------------------------------------------------------------+
application.js源码阅读
module.exports = class Application extends Emitter { // 继承于event事件
constructor(options) {
super();
options = options || {};
this.middleware = []; // 存放中间件函数的数组
// 封装执行上下文的ctx,content文件主要做的是初始化原型,给定一些方法,例如:cookies、错误处理等等
this.context = Object.create(context);
// 封装request,给request封装了一些属性和方法,例如请求中可以用ctx.request.query等等
this.request = Object.create(request);
// 封装response,与request差不多,例如返回时候如果什么格式都可以ctx.body=xxxx
this.response = Object.create(response);
// ... 省略代码
}
listen(...args) { // 创建http服务
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
use(fn) { // 中间件使用方法 => app.use(middleware1);
// 必须是函数
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// 如果是generator函数用koa-convert处理
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 || '-');
// 将函数push到存放中间件函数的数组
this.middleware.push(fn);
return this;
}
callback() {
// 将存放的中间件函数用koa-compose处理
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror); // 继承于event监听错误,再做错误处理。
const handleRequest = (req, res) => {
// 重新封装原生的req和res到ctx中
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
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;
}
}
koa-compose
Koa.js实现洋葱模型的核心引擎。非常的重要,代码必须一行不漏的看。
// 在application.js将middleware的数组传入进来
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)
}
}
}
}
返回的匿名函数因为是最关键的部分,所以单独拿出来说。
第一次执行:
let index = -1
return dispatch(0)
看看dispatch(0)
执行的是什么。
function dispatch (i) {
// 首先显示判断 i<==index,如果 true 的话,则说明 next() 方法调用了多次。
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i // index = 0
let fn = middleware[i] //fn = middleware[0],执行的是第一个中间件
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)
}
}
再讲到return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
之前先看看之前的demo,我参考了许多资料,大部分资料都是以总结性的发言(比较官方),对学习者来说很不友好,所以我觉得以我个人的角度上来说,按着代码运行的逻辑上来写代码,写一下直白易懂的流程。
const middleware1 = async (ctx, next) => {
console.log(1);
await next();
console.log(6);
}
const middleware2 = async (ctx, next) => {
console.log(2);
await next();
console.log(5);
}
const middleware3 = async (ctx, next) => {
console.log(3);
await next();
console.log(4);
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
可以通过上述代码可以想象的出第一次执行的return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
其实是(middleware1)return Promise.resolve(async (context, dispatch(1)));
在middleware1里面执行了console.log(1);
然后就必须await ``dispatch(1)
了。如此反复,那么输出结果再最后一个await之前,执行的代码是console.log(1); console.log(2); console.log(3);
直到执行完middleware3的时候,那么运行了console.log(4);
middleware3执行完了之后,middleware2被await打通了,那么就执行console.log(5);
紧接着middleware1被await打通了,那么就执行console.log(6);
async (ctx, next=middleware2) => {
console.log(1);
await async (ctx, next=middleware3) => {
console.log(2);
await async (ctx, next=Promise.resolve()) => {
console.log(3);
await next();
console.log(4);
}
console.log(5);
}
console.log(6);
}
将上述的描述表达成代码就是上面的代码,这样的形式我觉得在我个人角度上是最好理解的方式了。
举个最简单的中间件的例子
const logger = async function(ctx, next) {
let res = ctx.res;
// 拦截操作请求 request
console.log(`<-- ${ctx.method} ${ctx.url}`);
await next();
// 拦截操作响应 request
res.on('finish', () => {
console.log(`--> ${ctx.method} ${ctx.url}`);
});
};
app.use(logger);
// 如果接受到请求的时候,控制台显示结果<-- GET /hello
// 返回请求的时候,控制台显示结果--> GET /hello
koa-convert
如果传入的中间件是generator函数用koa-convert处理,在了解koa-conver之前先了解一下co模块,co模块是保证generator函数自动执行。如果不了解generator函数的点Generator 函数的语法。
co模块源码,看看co是如何让generator函数自动执行的。
const co = require('co');
const sleep = (num) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num)
}, 1000);
})
}
var gen = function* () {
var f1 = yield sleep(1)
console.log(f1);
var f2 = yield sleep(2)
console.log(f2);
return 3
};
co(gen).then(function (){
console.log('Generator 函数执行完成');
});
上面是一个使用co模块的简单例子,方便后面讲解。
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1)
return new Promise(function(resolve, reject) {
// 在返回的 Promise 对象里面,co 先检查参数gen是否为 Generator函数。如果是,就执行该函数,得到一个内部指针对象;
// 如果不是就返回,并将 Promise 对象的状态改为resolved。
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
// 关键的地方:执行onFulfilled
onFulfilled();
function onFulfilled(res) {
var ret;
try {
// 第一次执行的时候 以上面例子为流程,ret = { value: Promise { <pending> }, done: false }
// 这个Promise就是sleep(1)
ret = gen.next(res);
} catch (e) {
return reject(e);
}
// 再将这个promise放入next函数
next(ret);
}
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
// 如果generator已经走到底了,那么直接resolve
if (ret.done) return resolve(ret.value);
// 确保每一步的返回值,是 Promise 对象。
var value = toPromise.call(ctx, ret.value);
// 使用then方法,为返回值加上回调函数,然后通过onFulfilled函数再次调用next函数。又会执行onFulfilled,从而返回是sleep(2)
// 知道处理完成,那么执行到该函数的最上面一步{ value: 3, done: true }
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
再看一下toPromise方法
function toPromise(obj) {
// 分别会对promis、对象、数组等写法做处理
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
所以说需要并发的时候可以有如下写法,都会被toPromise处理。
// 数组的写法
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);
// 对象的写法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).catch(onerror);
看完co模块后再来看koa-convert
function convert (mw) {
//首先判断该参数mw是不是一个函数,如果该mw不是一个函数的话,就直接抛出一个异常提示,该中间件必须是一个函数。
if (typeof mw !== 'function') {
throw new TypeError('middleware must be a function')
}
// 判断该 mw.constructor.name !== 'GeneratorFunction' 是不是一个Generator函数,如果不是Generator函数的话,就直接返回该mw。
if (mw.constructor.name !== 'GeneratorFunction') { // 如果是async函数是AsyncFunction
// assume it's Promise-based middleware
return mw
}
// 如果它是Generator函数的话,就会执行 converted 函数,该函数有2个参数,第一个参数ctx是运行Generator的上下文,第二个参数是传递给Generator函数的next参数。
const converted = function (ctx, next) {
// mw.call(ctx, createGenerator(next)),如果mw是一个Generator函数的话,就直接调用该Generator函数,返回return yield next(); 返回下一个中间件,然后使用调用co包,使返回一个Promise对象。该本身对象的代码会自动执行完
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
converted._name = mw._name || mw.name
return converted
}
总结
koa完成了http的封装和中间件容器,其他功能可以根据业务需求通过中间件加入进去,留下了很大的灵活空间,不过提高了Web服务的开发成本。例如接口必须携带token并解析完校验之后才能请求接口,就可以写一个登陆限制的中间件来拦截请求,并做验证。以上就是koa的源码解读,希望对读者理解koa有不错的帮助。