本文基于
koa 3.0.0-alpha.1
版本源码进行分析
由于
koa
的源码量非常少,但是体现的思想非常经典和难以记忆,如果突然要手写koa
代码,可能还不一定能很快写出来,因此本文将集中于如何理解以及记忆koa
的代码本文一些代码块为了演示方便,可能有一些语法排列错误,因此本文所有代码均可以视为伪代码
1. 文章内容
- 从
0到1
推导koa 3.0.0-alpha.1
版本源码的实现,一步一步完善简化版koa
的手写逻辑 - 分析常用中间件
koa-router
的源码以及进行对应的手写 - 分析常用中间件
koa-bodyparser
的源码以及进行对应的手写
2. 核心代码分析&手写
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")
}
如果我们有fn3
、fn4
呢?
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")
}
那如果我们还有fn5
、fn6
….呢?
我们使用怎样的逻辑进行这种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,说明重复调用了某一个中间件的
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 {
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 {
// 正常执行最终触发
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 {
// 正常执行最终触发
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 {
// 正常执行最终触发
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 {
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
的形式,然后在构造函数中初始化context
、request
、response
的初始化,在callback ()
进行http.createServer()
回调函数req
和res
的赋值
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 {
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
原生模块的req
和res
都放入到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. 常见中间件分析&手写
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
判断是否符合注册的路由,如果符合,则触发注册的方法 - 我们需要根据
path
、methods
进行对应数据结构的构建
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.path
和ctx.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请求
-
query
是格式化好的参数对象,比如query={a:1, b: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 = `
koa2 request post demo
userName
nickName
email
`;
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.method
是POST
请求时,自动解析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;
参考
- Koa.js 设计模式-学习笔记
- Koa2进阶学习笔记
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net