手撸一个 Koa
An Example¶
const MyKoa = require('./application');
const app = new MyKoa();
async function f1(ctx, next) {
console.log('f1');
await next();
}
async function f2(ctx, next) {
console.log('f2');
await next();
}
async function f3(ctx, next) {
console.log('f3');
throw new Error('抛出一个 TEST!错误');
}
app.use(f1);
app.use(f2);
app.use(f3);
app.listen(3000, () => {
console.log('[example] listening on port 3000');
});
Koa¶
文件树¶
├── README.md
├── package.json
└── lib
├── application.js // 入口文件
├── context.js
├── request.js
└── response.js
application.js¶
const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
class Application {
constructor () {
this.context = context;
this.request = request;
this.response = response;
this.callbackFunc;
}
/**
* 开启http server并传入callback
*/
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
/**
* 获取http server所需的callback函数
* @return {Function} fn
*/
callback() {
return (req, res) => {
// req, res 分别为 node 的原生 request,response 对象
console.log('[Application] callback in listen');
const ctx = this.createContext(req, res);
function respond() {
// ctx.res.end(ctx.body);
const content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
} else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content));
}
}
this.callbackFunc(ctx).then(respond);
}
}
/**
* 构造 context:
* 将 koa request/response 对象挂载在 ctx 上,并返回
*/
createContext(req, res) {
console.log('[Application] createContext');
const ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
}
/**
* 挂载回调函数
* @param {Function} fn 回调处理函数
*/
use (fn) {
console.log('[Application] use: 注册中间件 fn')
this.callbackFunc = fn
}
}
module.exports = Application;
context.js¶
context 作为请求的代理,挂载了 koa 的 request/response 对象
- 代码
module.exports = {
get query() {
return this.request.query;
},
get body() {
console.log('[context.js] getting body.')
return this.response.body;
},
set body(data) {
console.log('[context.js] setting body.')
this.response.body = data;
},
get status() {
return this.response.status;
},
set status(statusCode) {
this.response.body = data;
}
};
- 改造一下
let proto = {};
// 为 proto 名为 property 的属性设置getter
function delegateGet(property, name) {
proto.__defineGetter__(name, function () {
console.log('[context.js] delegateGet: ' + property + '.'+ name)
return this[property][name];
});
}
// 为proto名为property的属性设置setter
function delegateSet(property, name) {
proto.__defineSetter__(name, function (val) {
console.log('[context.js] delegateSet: ' + property + '.'+ name + ' 为 '+val)
this[property][name] = val;
});
}
// 代理的 setter/getter
// request
delegateGet('request', 'query');
// response
delegateGet('response', 'body');
delegateSet('response', 'body');
delegateGet('response', 'status');
delegateSet('response', 'status');
module.exports = proto;
request.js¶
const url = require('url');
module.exports = {
get query() {
console.log('[request.js] getter query')
return url.parse(this.req.url,true).query;
}
};
response.js¶
module.exports = {
get body() {
console.log('[response.js] getter body')
return this._body;
},
set body(data) {
console.log('[response.js] setter body: '+ data)
this._body = data;
},
get status() {
return this.res.statusCode;
},
set status(data) {
if(typeof data !== 'number') {
throw Error('status must be a number!'
)
this.res.statusCode = data
}
};
Koa-Compose¶
a more real application.js¶
中间件回调函数,变成一个顺序执行的回调数组。
const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
const compose = require('./compose');
class Application {
constructor () {
this.context = context;
this.request = request;
this.response = response;
// this.callbackFunc;
this.middleware = [];
}
/**
* 开启http server并传入callback
*/
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
/**
* 获取http server所需的callback函数
* @return {Function} fn
*/
callback() {
return (req, res) => {
// req, res 分别为 node 的原生 request,response 对象
console.log('[Application] callback in listen');
const ctx = this.createContext(req, res);
function respond() {
// ctx.res.end(ctx.body);
const content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
} else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content));
}
}
// this.callbackFunc(ctx).then(respond);
compose(this.middleware)(ctx).then(respond);
}
}
/**
* 构造 context:
* 将 koa request/response 对象挂载在 ctx 上,并返回
*/
createContext(req, res) {
console.log('[Application] createContext');
const ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
}
/**
* 挂载回调函数
* @param {Function} fn 回调处理函数
*/
use (fn) {
console.log('[Application] use: 注册中间件 fn')
// this.callbackFunc = fn
this.middleware.push(fn);
}
}
module.exports = Application;
step1. compose conception¶
/*
* 愿望: 依次执行 f1, f2, f3
*/
async function f1(next) {
console.log('f1');
await next();
console.log('/f1');
}
async function f2(next) {
console.log('f2');
await next();
console.log('/f2');
}
async function f3(next) {
console.log('f3');
if (next) await next();
console.log('/f3');
}
/*
* 构造出一个函数,实现让三个函数依次执行的效果
*/
var next2 = async function () {
await f3();
};
var next1 = async function () {
await f2(next2);
};
module.exports = function () {
console.log('compose')
f1(next1);
}
step2. compose design¶
function createNext(fn, next) {
return async function () {
await fn(next)
}
}
const middlewares = [f1, f2, f3];
const len = middlewares.length;
let next = async () => {
return Promise.resolve();
};
for(let i = len-1; i>=0; i--){
next = createNext(middlewares[i], next);
}
module.exports = function () {
console.log('compose');
next();
}
step3. compose realization¶
module.exports = function(middlewares) {
console.log('-- enter compose --');
return async function(ctx) {
// next 工厂
function createNext(fn, next) {
return async function () {
await fn(ctx, next)
}
}
let next = async () => {
return Promise.resolve();
};
const len = middlewares.length;
for(let i = len - 1; i >= 0; i--){
next = createNext(middlewares[i], next);
}
await next();
console.log('-- exit compose --');
};
}
step4. compose recursion¶
思想: 所有的迭代与递归都能相互转换。
module.exports = function(middlewares) {
return async function(ctx) {
// next 工厂 递归化
function createNext(i) {
fn = middlewares[i];
if (!fn) {
return Promise.resolve();
}
return Promise.resolve(
fn(ctx, async () => await createNext(i + 1))
);
}
await createNext(0);
};
}
simplified source code¶
'use strict'
// const fn = compose(this.middleware);
// return fn(ctx).then(handleResponse).catch(onerror);
/**
* Expose compositor.
*/
// 暴露compose函数
module.exports = compose
// compose函数需要传入一个数组队列 [fn,fn,fn,fn]
function compose (middleware) {
console.log('compose')
// compose函数调用后,返回的是以下这个匿名函数
// 匿名函数接收两个参数,第一个随便传入,根据使用场景决定
// 第一次调用时候第二个参数next实际上是一个undefined,因为初次调用并不需要传入next参数
// 这个匿名函数返回一个promise
return function (context, next) {
// last called middleware #
// 初始下标为-1
let index = -1
return dispatch(0)
function dispatch (i) {
// 如果传入i为负数且<=-1 返回一个Promise.reject携带着错误信息
// 所以执行两次next会报出这个错误。将状态rejected,就是确保在一个中间件中next只调用一次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// 执行一遍next之后,这个index值将改变
index = i
// 根据下标取出一个中间件函数
let fn = middleware[i]
// next在这个内部中是一个局部变量,值为undefined
// 当i已经是数组的length了,说明中间件函数都执行结束,执行结束后把fn设置为undefined
// 问题:本来middleware[i]如果i为length的话取到的值已经是undefined了,为什么要重新给fn设置为undefined呢?
if (i === middleware.length) fn = next
//如果中间件遍历到最后了。那么。此时return Promise.resolve()返回一个成功状态的promise
// 方面之后做调用then
if (!fn) return Promise.resolve()
// 调用后依然返回一个成功的状态的Promise对象
// 用Promise包裹中间件,方便await调用
// 调用中间件函数,传入context(根据场景不同可以传入不同的值,在KOa传入的是ctx)
// 第二个参数是一个next函数,可在中间件函数中调用这个函数
// 调用next函数后,递归调用dispatch函数,目的是执行下一个中间件函数
// next函数在中间件函数调用后返回的是一个promise对象
// 读到这里不得不佩服作者的高明之处。
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
}
}
}