关于源码:Koa源码解析手写

78次阅读

共计 20674 个字符,预计需要花费 52 分钟才能阅读完成。

本文基于 koa 3.0.0-alpha.1 版本源码进行剖析

因为 koa 的源码量非常少,然而体现的思维十分经典和难以记忆,如果忽然要手写 koa 代码,可能还不肯定能很快写进去,因而本文将集中于如何了解以及记忆 koa 的代码

本文一些代码块为了演示不便,可能有一些语法排列谬误,因而本文所有代码均能够视为伪代码

文章内容

  1. 0 到 1 推导 koa 3.0.0-alpha.1 版本源码的实现,一步一步欠缺简化版 koa 的手写逻辑
  2. 剖析罕用中间件 koa-router 的源码以及进行对应的手写
  3. 剖析罕用中间件 koa-bodyparser 的源码以及进行对应的手写

外围代码剖析 & 手写

2.1 koa-compose

const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {console.log("中间件 1 start");
await next();
console.log("中间件 1 end");
});
app.use(async (ctx, next) => {console.log("中间件 2 start");
await next();
console.log("中间件 2 end");
});
app.use(async (ctx, next) => {console.log("中间件 3 start");
await next();
console.log("中间件 3 end");
});
app.listen(3000);

下面代码块中间件运行流程如下所示

下面的运行流程看起来就跟咱们平时开发不太一样,咱们能够看一个类似的场景,比方上面

  • 咱们在 fn1() 中执行一系列的业务逻辑
  • 然而咱们在 fn1() 遇到了 await fn2(),因而咱们得期待fn2() 执行结束后能力持续前面的业务逻辑
function fn1() {console.log("fn1 执行业务逻辑 1");
await fn2();
console.log("fn1 执行业务逻辑 2")
}
async function fn2() {console.log("fn2 执行业务逻辑 1");
}

咱们将 fn2 作为参数传入

async function fn2() {console.log("fn2 执行业务逻辑 1");
}
function fn1(fn2) {console.log("fn1 执行业务逻辑 1");
await fn2();
console.log("fn1 执行业务逻辑 2")
}

如果咱们有 fn3fn4 呢?

async function fn1(fn2) {console.log("fn1 执行业务逻辑 1");
await fn2();
console.log("fn1 执行业务逻辑 2")
}
async function fn2(fn3) {console.log("fn2 执行业务逻辑 1");
await fn3();
console.log("fn2 执行业务逻辑 2")
}
async function fn3(fn4) {console.log("fn3 执行业务逻辑 1");
await fn4();
console.log("fn3 执行业务逻辑 2")
}
async function fn4() {console.log("fn4 执行业务逻辑 1");
console.log("fn4 执行业务逻辑 2")
}

那如果咱们还有fn5fn6…. 呢?

咱们应用怎么的逻辑进行这种 function 的嵌套?

咱们能够从下面代码发现,每一个 fnX() 传递的都是上一个fn(X+1)()

2.1.1 应用 middleware 遍历所有 fn

咱们能够先应用一个数组进行 fn 的增加

middleware.push(fn);

当咱们取出一个 fn 时,咱们应该传入下一个fn,即

let fn = middleware[i];
fn(middleware[i+1]);

如果咱们想要程序传入context

let fn = middleware[i];
fn(context, middleware[i+1]);

应用 middleware 整合下面的逻辑,如上面所示

  • 咱们应用 app.use((ctx, next)) 传入的 next() 须要强制返回一个 Promise,因为它能够应用await,因而咱们应用Promise.resolve() 包裹 fn() 返回的值,避免返回的不是Promise
  • 在调用 fn() 的时候,会传入下一个中间件作为第二个参数:middleware[i + 1]
let middleware = [];
let context = {};
let app = {use(fn) {middleware.push(fn);
},
listen(...args) {this.callback();
},
callback() {
// 要求每一个 fn 返回都是一个 Promise
function dispatch(i) {let fn = middleware[i];
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据
return Promise.resolve(fn(context, middleware[i + 1]));
}
return dispatch(0);
}
};
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
console.log("fn1 执行业务逻辑 2")
});
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
console.log("fn2 执行业务逻辑 2")
});
app.listen(200);

2.1.2 链式调用

async function fn1(fn2) {console.log("fn1 执行业务逻辑 1");
await fn2();
console.log("fn1 执行业务逻辑 2")
}
async function fn2(fn3) {console.log("fn2 执行业务逻辑 1");
await fn3();
console.log("fn2 执行业务逻辑 2")
}
async function fn3() {console.log("fn3 执行业务逻辑 1");
console.log("fn3 执行业务逻辑 2")
}

咱们如何实现 fn1->fn2->fn3 的链式调用呢?

fn1(fn2(fn3))

回到咱们下面实现的 koa 源码

let middleware = [];
let context = {};
let app = {use(fn) {middleware.push(fn);
},
listen(...args) {this.callback();
},
callback() {
// 要求每一个 fn 返回都是一个 Promise
function dispatch(i) {let fn = middleware[i];
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据
return Promise.resolve(fn(context, middleware[i + 1]));
}
return dispatch(0);
}
};
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
console.log("fn1 执行业务逻辑 2")
});
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
console.log("fn2 执行业务逻辑 2")
});
app.listen(200);

如下面所示,咱们执行了 fn1(context, fn2),然而咱们fn2() 并没有传入 fn3,这导致了链式调用被中断了,而且fn2() 也不肯定会返回Promise,因而咱们须要对上面代码进行调整

let middleware = [];
let context = {};
let app = {use(fn) {middleware.push(fn);
},
listen(...args) {this.callback();
},
callback() {
// 要求每一个 fn 返回都是一个 Promise
function dispatch(i) {let fn = middleware[i];
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据
return Promise.resolve(fn(context, dispatch(i + 1)));
}
return dispatch(0);
}
};
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
console.log("fn1 执行业务逻辑 2")
});
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
await next();
console.log("fn2 执行业务逻辑 2")
});
app.listen(200);

fn(context, middleware[i + 1])调整为 fn(context, dispatch(i + 1))
这样咱们就能够实现

  • fn2()返回的是 Promise.resolve(),无论fn2() 返回什么,都是一个Promise
  • fn2(context, dispatch(i + 1))的第二个参数传入了 fn3,并且fn3 是一个Promise

2.1.3 细节优化

2.1.3.1 app.use 返回 this

app.use()返回本人自身,能够应用链式调用

let app = {use(fn) {middleware.push(fn);
return this;
}
}
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
console.log("fn1 执行业务逻辑 2")
}).use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
console.log("fn2 执行业务逻辑 2")
});
2.1.3.2 dispatch()返回办法

dispatch(i + 1)返回的是一个执行结束的 Promise 状态,不是一个办法,须要改成bind

let middleware = [];
let context = {};
let app = {use(fn) {middleware.push(fn);
return this;
},
listen(...args) {this.callback();
},
callback() {
// 要求每一个 fn 返回都是一个 Promise
function dispatch(i) {let fn = middleware[i];
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
}
return dispatch(0);
}
};
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
console.log("fn1 执行业务逻辑 2")
});
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
await next();
console.log("fn2 执行业务逻辑 2")
});
app.listen(200);
2.1.3.3 最初一个中间件返回空的 Promise.resolve

最初一个中间件调用 next() 时没有执行的办法,应该间接返回一个空的办法,比方下面代码中

  • console.log("fn2 执行业务逻辑 1")
  • await next(): 此时的 next() 应该是一个空的 Promise 办法
  • console.log("fn2 执行业务逻辑 2")
let middleware = [];
let context = {};
let app = {use(fn) {middleware.push(fn);
return this;
},
listen(...args) {this.callback();
},
callback() {
// 要求每一个 fn 返回都是一个 Promise
function dispatch(i) {let fn = middleware[i];
if (i === middleware.length) {return Promise.resolve();
}
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
}
return dispatch(0);
}
};
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
console.log("fn1 执行业务逻辑 2")
});
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
await next();
console.log("fn2 执行业务逻辑 2")
});
app.listen(200);
2.1.3.4 阻止中间件中反复调用 next()

阻止一个中间件反复调用 next() 办法,应用 index 记录以后的 i,如果发现i<=index,阐明反复调用了某一个中间件的next() 办法

let middleware = [];
let context = {};
let app = {use(fn) {middleware.push(fn);
return this;
},
listen(...args) {this.callback();
},
callback() {
// 要求每一个 fn 返回都是一个 Promise
let index = -1;
function dispatch(i) {if (i <= index) {return new Promise.reject(new Error("next()反复调用屡次"));
}
index = i;
let fn = middleware[i];
if (i === middleware.length) {return Promise.resolve();
}
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
}
return dispatch(0);
}
};
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
await next();
console.log("fn1 执行业务逻辑 2")
});
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
await next();
console.log("fn2 执行业务逻辑 2")
});
app.listen(200);
2.1.3.5 欠缺错误处理逻辑
  • 反复调用 next() 抛出谬误
  • 执行 fn() 过程中出错

dispatch() 的外层再包裹一个新的 function(),而后咱们就能够应用这个function() 进行对立的 then()catch()解决,即上面代码中的

  • let fn = compose(this.middleware)
  • fn().then(() => {}).catch(err => {})
function compose(middleware) {// 返回也是一个 Promise,可能是 Promise.resolve(),也有可能是 Promise.reject()
return function (context) {
// 要求每一个 fn 返回都是一个 Promise
let index = -1;
function dispatch(i) {if (i <= index) {return Promise.reject(new Error("next()反复调用屡次"));
}
index = i;
let fn = middleware[i];
if (i === middleware.length) {return Promise.resolve();
}
try {// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {return Promise.reject(err)
}
}
return dispatch(0);
}
}
let app = {middleware: [],
use(fn) {this.middleware.push(fn);
return this;
},
listen(...args) {this.callback();
},
callback() {let fn = compose(this.middleware);
let context = {};
fn(context).then(() => {
// 失常执行最终触发
console.log("fn 执行结束!");
}).catch(error => {console.error("fn 执行谬误", error);
});
}
};
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
await next();
console.log("fn1 执行业务逻辑 2")
});
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
await next();
console.log("fn2 执行业务逻辑 2")
});
app.listen(200);

运行下面代码,失去的后果为:

2.1.3.6 兼容 compose 传入 next()办法

compose()返回的 function(context) 减少传入参数next,能够在内部进行定义传入,而后判断

  • i 等于 middleware.length 时,middleware[i]必定为空,判断最初一个 next() 是否为空
  • 如果最初一个 next() 不为空,则继续执行最初一次next()
  • 如果最初一个 next() 为空,则间接返回空的 Promise.resolve,跟下面咱们解决 i 等于 middleware.length 时的逻辑一样
function compose(middleware) {// 返回也是一个 Promise,可能是 Promise.resolve(),也有可能是 Promise.reject()
return function (context, next) {
// 要求每一个 fn 返回都是一个 Promise
let index = -1;
function dispatch(i) {if (i <= index) {return Promise.reject(new Error("next()反复调用屡次"));
}
index = i;
let fn = middleware[i];
if (i === middleware.length) {// middleware[i]必定为空,判断最初一个 next()是否为空
// 如果不为空,则继续执行最初一次
// 如果为空,则返回 Promise.resolve()
fn = next;
}
if (!fn) {return Promise.resolve();
}
try {// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {return Promise.reject(err)
}
}
return dispatch(0);
}
}
let app = {middleware: [],
use(fn) {this.middleware.push(fn);
return this;
},
listen(...args) {this.callback();
},
callback() {let fn = compose(this.middleware);
let context = {};
const next = function () {console.log("最初一个 next()!");
}
fn(context, next).then(() => {
// 失常执行最终触发
console.log("fn 执行结束!");
}).catch(error => {console.error("fn 执行谬误", error);
});
}
};
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
await next();
console.log("fn1 执行业务逻辑 2")
});
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
await next();
console.log("fn2 执行业务逻辑 2")
});
app.listen(200);
2.1.3.7 解决 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!')
}
// 返回也是一个 Promise,可能是 Promise.resolve(),也有可能是 Promise.reject()
return function (context, next) {
// 要求每一个 fn 返回都是一个 Promise
let index = -1;
function dispatch(i) {if (i <= index) {return Promise.reject(new Error("next()反复调用屡次"));
}
index = i;
let fn = middleware[i];
if (i === middleware.length) {// middleware[i]必定为空,判断最初一个 next()是否为空
// 如果不为空,则继续执行最初一次
// 如果为空,则返回 Promise.resolve()
fn = next;
}
if (!next) {return Promise.resolve();
}
try {// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {return Promise.reject(err)
}
}
return dispatch(0);
}
}
let app = {middleware: [],
use(fn) {this.middleware.push(fn);
return this;
},
listen(...args) {this.callback();
},
callback() {let fn = compose(this.middleware);
let context = {};
const next = function () {console.log("最初一个 next()!");
}
fn(context, next).then(() => {
// 失常执行最终触发
console.log("fn 执行结束!");
}).catch(error => {console.error("fn 执行谬误", error);
});
}
};
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
await next();
console.log("fn1 执行业务逻辑 2")
});
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
await next();
console.log("fn2 执行业务逻辑 2")
});
app.listen(200);

至此,咱们曾经齐全实现了官网 koa-compose 的残缺代码!

2.2 Node.js 原生 http 模块

Koa是基于中间件模式的 HTTP 服务框架,底层原理就是封装了 Node.js 的 http 原生模块

在下面实现 koa-compose 中间件的根底上,咱们减少 Node.js 的 http 原生模块,根本就是 Koa 的外围代码的实现

2.2.1 原生代码示例

const http = require('http');
const server = http.createServer((req, res)=> {res.end(`this page url = ${req.url}`);
});
server.listen(3001, function() {console.log("the server is started at port 3001")
})

2.2.2 减少原生 http 模块的相干代码

欠缺 listen()callback()的相干办法,减少原生 http 模块的相干代码

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!')
}
// 返回也是一个 Promise,可能是 Promise.resolve(),也有可能是 Promise.reject()
return function (context, next) {
// 要求每一个 fn 返回都是一个 Promise
let index = -1;
function dispatch(i) {if (i <= index) {return Promise.reject(new Error("next()反复调用屡次"));
}
index = i;
let fn = middleware[i];
if (i === middleware.length) {// middleware[i]必定为空,判断最初一个 next()是否为空
// 如果不为空,则继续执行最初一次
// 如果为空,则返回 Promise.resolve()
fn = next;
}
if (!next) {return Promise.resolve();
}
try {// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {return Promise.reject(err)
}
}
return dispatch(0);
}
}
let app = {middleware: [],
use(fn) {this.middleware.push(fn);
return this;
},
listen(...args) {const server = http.createServer(this.callback());
return server.listen(...args);
},
callback() {let fn = compose(this.middleware);
return (req, res) => {let context = {};
this.handleRequest(context, fn);
}
},
handleRequest(context, fn) {const next = function () {console.log("最初一个 next()!");
}
fn(context, next).then(() => {
// 失常执行最终触发
console.log("fn 执行结束!");
}).catch(error => {console.error("fn 执行谬误", error);
});
}
};
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
await next();
console.log("fn1 执行业务逻辑 2")
});
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
await next();
console.log("fn2 执行业务逻辑 2")
});
app.listen(200);

2.3 初始化 context

app={} 的模式欠缺为 class Koa 的模式,而后在构造函数中初始化 contextrequestresponse 的初始化,在 callback() 进行 http.createServer() 回调函数 reqres的赋值

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;
}

残缺代码如下所示

const context = require("./context.js");
const request = require("./request.js");
const response = require("./response.js");
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!");
}
// 返回也是一个 Promise,可能是 Promise.resolve(),也有可能是 Promise.reject()
return function (context, next) {
// 要求每一个 fn 返回都是一个 Promise
let index = -1;
function dispatch(i) {if (i <= index) {return Promise.reject(new Error("next()反复调用屡次"));
}
index = i;
let fn = middleware[i];
if (i === middleware.length) {// middleware[i]必定为空,判断最初一个 next()是否为空
// 如果不为空,则继续执行最初一次
// 如果为空,则返回 Promise.resolve()
fn = next;
}
if (!next) {return Promise.resolve();
}
try {// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {return Promise.reject(err);
}
}
return dispatch(0);
};
}
class Koa {constructor() {this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
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;
}
use(fn) {this.middleware.push(fn);
return this;
}
listen(...args) {const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {let fn = compose(this.middleware);
return (req, res) => {let context = this.createContext(req, res);
this.handleRequest(context, fn);
};
}
handleRequest(context, fn) {const next = function () {console.log("最初一个 next()!");
};
fn(context, next)
.then(() => {
// 失常执行最终触发
console.log("fn 执行结束!");
})
.catch((error) => {console.error("fn 执行谬误", error);
});
}
}
const app = new Koa();
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1");
await next();
await next();
console.log("fn1 执行业务逻辑 2");
});
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1");
await next();
console.log("fn2 执行业务逻辑 2");
});
app.listen(200);

2.4 欠缺响应数据的逻辑

由下面初始化 context 的代码能够晓得,咱们曾经将 http 原生模块的 reqres都放入到 context 中,因而咱们在执行结束中间件后,咱们应该对 context.res 进行解决,返回对应的值

handleRequest(context, fn) {const next = function () {console.log("最初一个 next()!");
};
const handleResponse = () => {return this.handleResponse(context);
};
fn(context, next)
.then(handleResponse)
.catch((error) => {console.error("fn 执行谬误", error);
});
}
handleResponse(ctx) {
const res = ctx.res;
let body = ctx.body;
if (!body) {return res.end();
}
if (typeof body !== "string") {body = JSON.stringify(body);
}
res.end(body);
}

至此,咱们实现了一个简化版本的Koa,残缺代码放在 github mini-koa

常见中间件剖析 & 手写

3.1 koa-router

3.1.1 不应用 koa-router

在不应用 koa-router 中间件时,咱们须要手动依据 ctx.request.url 去判断路由,如上面代码所示

const Koa = require("koa");
const fs = require("fs");
const app = new Koa();
function readFile(path) {return new Promise((resolve, reject) => {let htmlUrl = `../front/${path}`;
fs.readFile(htmlUrl, "utf-8", (err, data) => {if (err) {reject(err);
} else {resolve(data);
}
});
});
}
async function parseUrl(url) {
let base = "404.html";
switch (url) {
case "/":
base = "index.html";
break;
case "/login.html":
base = "login.html";
break;
case "/home.html":
base = "home.html";
break;
}
// 从本地读取出该门路下 html 文件的内容,而后返回给客户端
const data = await readFile(base);
return data;
}
app.use(async (ctx) => {
let url = ctx.request.url;
// 判断这个 url 是哪一个申请
const htmlContent = await parseUrl(url);
ctx.status = 200;
ctx.body = htmlContent;
});
app.listen(3000);
console.log("[demo] route is starting at port 3000");

因而咱们手写 koa-router 时,咱们须要关注几个问题:

  • 依据 ctx.path 判断是否合乎注册的路由,如果合乎,则触发注册的办法
  • 咱们须要依据 pathmethods 进行对应数据结构的构建

    3.1.2 应用 koa-router 的具体示例

const app = new Koa();
const router = new Router();
router.get("/", (ctx, next) => {// ctx.router available});
router.get("/home", (ctx, next) => {// ctx.router available});
app.use(router.routes());

3.1.3 依据示例实现 koa-router

依据 methods 初始化所有办法,造成 this["get"]this["put"] 的数据结构,提供给内部调用注册路由

当有新的申请产生时,会触发中间件的逻辑执行,会依据目前 ctx.pathctx.method去寻找之前是否有注册过的门路,如果有则触发注册门路的 callback 进行逻辑的执行

function Router(opts) {this.register = function (path, methods, callback, opts) {
this.stack.push({
path,
methods,
middleware: callback,
opts,
});
return this;
};
this.routes = function () {
// 返回所有注册的路由
return async (ctx, next) => {// 每次执行中间件时,判断是否有合乎 register()的路由
const path = ctx.path;
const method = ctx.method.toUpperCase();
let callback;
for (const item of this.stack) {if (path === item.path && item.methods.indexOf(method) >= 0) {
// 找到对应的路由
callback = item.middleware;
break;
}
}
if (callback) {callback(ctx, next);
return;
}
await next();};
};
this.opts = opts || {};
this.methods = this.opts.methods || ["HEAD", "OPTIONS", "GET", "PUT", "PATCH", "POST", "DELETE"];
this.stack = [];
// 依据 methods 初始化所有办法,造成 this["get"]、this["put"]的数据结构
for (const _method of this.methods) {this[_method.toLowerCase()] = this[_method] = function (path, callback) {this.register(path, [_method], callback);
};
}
}

3.2 koa-bodyparser

该中间件能够简化申请体的解析流程

当咱们不应用 koa-bodyparser 时,如上面所示

3.2.1 不应用 koa-bodyparser

GET 申请

  1. query是格式化好的参数对象,比方query={a:1, b:2}
  2. querystring是申请字符串,比方querystring="a=1&b=2"
let request = ctx.request;
let query = request.query;
let queryString = request.querystring;
// 也能够间接省略 request,const {query, querystring} = request

POST 申请
没有封装具体的办法,须要手动解析 ctx.req 这个原生的 node.js 对象
如上面例子所示,ctx.req获取到 formData"userName=22&nickName=22323&email=32323"
咱们须要将 formData 解析为{userName: 22, nickName: 22323, email: 32323}

home.post("b", async (ctx) => {const body = await parseRequestPostData(ctx);
ctx.body = body;
});
async function parseRequestPostData(ctx) {return new Promise((resolve, reject) => {
const req = ctx.req;
let postData = "";
req.addListener("data", (data) => {postData = postData + data;});
req.addListener("end", () => {if (postData) {let parseData = transStringToObject(postData);
resolve(parseData);
} else {resolve("没有数据");
}
});
});
}
async function transStringToObject(data) {let result = {};
let dataList = data.split("&");
for (let [index, queryString] of dataList.entries()) {let itemList = queryString.split("=");
result[itemList[0]] = itemList[1];
}
return result;
}

3.2.2 应用 koa-bodyparser 的具体示例

const Koa = require("koa");
const fs = require("fs");
const app = new Koa();
const Router = require("koa-router");
const bodyParser = require("koa-bodyparser");
// post 申请参数解析示例
home.get("form", async (ctx) => {
let html = `
<h1>koa2 request post demo</h1>
<form method="POST" action="/b">
<p>userName</p>
<input name="userName" /><br/>
<p>nickName</p>
<input name="nickName" /><br/>
<p>email</p>
<input name="email" /><br/>
<button type="submit">submit</button>
</form>
`;
ctx.body = html;
});
home.post("b", async (ctx) => {
// 一般解析逻辑
// const body = await parseRequestPostData(ctx);
// ctx.body = body;
// 应用 koa-bodyparser 会主动解析表单的数据而后放在 ctx.request.body 中
let postData = ctx.request.body;
ctx.body = postData;
});
let router = new Router();
router.use("/", home.routes()); //http://localhost:3000
app.use(bodyParser()); // 这个中间件的注册应该放在 router 之前!app.use(router.routes());
app.listen(3002);

3.2.3 依据示例实现 koa-bodyparser

ctx.methodPOST申请时,主动解析ctx.request.body,次要分为:

  • form类型
  • json类型
  • text类型

依据不同的类型调用不同的解析办法,而后赋值给ctx.request.body

/**
* 注册对应的监听办法,进行 request 流数据的读取
* @param req
*/
function readStreamBody(req) {return new Promise((resolve, reject) => {
let postData = "";
req.addListener("data", (data) => {postData = postData + data;});
req.addListener("end", () => {if (postData) {resolve(postData);
} else {resolve("没有数据");
}
});
});
}
async function parseQuery(data) {let result = {};
let dataList = data.split("&");
for (let [index, queryString] of dataList.entries()) {let itemList = queryString.split("=");
result[itemList[0]] = itemList[1];
}
return result;
}
async function parseJSON(ctx, data) {let result = {};
try {result = JSON.parse(data);
} catch (e) {ctx.throw(500, e);
}
return result;
}
function bodyParser() {return async (ctx, next) => {if (!ctx.request.body && ctx.method === "POST") {let body = await readStreamBody(ctx.request.req);
// With Content-Type: text/html; charset=utf-8
// this.is('html'); // => 'html'
// this.is('text/html'); // => 'text/html'
// this.is('text/', 'application/json'); // => 'text/html'
//
// When Content-Type is application/json
// this.is('json', 'urlencoded'); // => 'json'
// this.is('application/json'); // => 'application/json'
// this.is('html', 'application/'); // => 'application/json'
//
// this.is('html'); // => false
let result;
if (ctx.request.is("application/x-www-form-urlencoded")) {result = await parseQuery(body);
} else if (ctx.request.is("application/json")) {result = await parseJSON(ctx, body);
} else if (ctx.request.is("text/plain")) {result = body;}
ctx.request.body = result;
}
await next();};
}
module.exports = bodyParser;

参考

  1. Koa.js 设计模式 - 学习笔记
  2. Koa2 进阶学习笔记

正文完
 0