上一篇文章 ExpressJs中间件原理学习 学习了Node.js Web 应用程序框架ExpressJs,这篇文章会学习Node.js 的另一个web框架- Koa。Koa是一个非常精简的node框架,由 Express 幕后的原班人马打造,旨在成为 Web 应用程序和 API开发领域的更小、更具表现力和更强大的基石。主要体现在:
- Koa 内部使用 ES6 编写,利用 async 函数,避免了回调函数的缺陷,也使得Koa 处理异常更加简单。
- 基于async/await(generator)的中间件洋葱模型机制,并且不提供其他中间件。
- 提供一个高内聚的ctx对象,该对象封装了node原生req和res对象。
Koa源码结构
- lib
- application.js ——> ctx.app
- context.js ——> ctx
- request.js. ——> ctx.request
- response.js. ——> ctx.response
Koa源码结构极其的简单。application.js就是入口文件,其余三个文件可以看作就是对node的原生req和res对象的封装。下面我们先来看看这四个文件的代码实现。
application.js
核心源码解读,注意注释
// 模块依赖,列出了核心的几个模块
// 封装了node原生res对象
const response = require("./response");
// 用于异步中间件
const compose = require("koa-compose");
// ctx对象的基础
const context = require("./context");
// 封装了node原生req对象
const request = require("./request");
// Application继承了node的Emitter模块,可以处理异步事件。app.on("XXX")
module.exports = class Application extends Emitter {
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || "X-Forwarded-For";
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || "development";
if (options.keys) this.keys = options.keys;
// 用于放置收集的中间件
this.middleware = [];
// Object.create,防止对象引用的污染,实现类似原型继承
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
// 创建server,封装了http.createServer
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
// 使用中间件
use(fn) {
if (typeof fn !== "function")
throw new TypeError("middleware must be a function!");
// 收集中间件
this.middleware.push(fn);
return this;
}
/**
* 返回(req, res) => void函数,作为参数传递给上面的listen函数中的http.createServer函数
*/
callback() {
// 组合中间件
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
// 重点:每一个请求都有唯一的ctx对象,这个对象就是封装了node的res和req对象
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);
}
// 创建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) {
const isNativeError =
Object.prototype.toString.call(err) === "[object Error]" ||
err instanceof Error;
if (!isNativeError)
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(`\n${msg.replace(/^/gm, " ")}\n`);
}
};
application.js 处理了如下的核心逻辑:
- 实现中间件的逻辑,包括中间件的错误处理。
- 为每个请求封装出一个十分强大的ctx对象。
context.js
context.js处理了两个核心逻辑:
- 创建ctx对象的原型(基础功能),包括代理了application.js 中的错误处理onerror
- 让ctx对象代理request和response的属性和方法
// 模块依赖
const util = require("util");
const createError = require("http-errors");
const httpAssert = require("http-assert");
const delegate = require("delegates");
const statuses = require("statuses");
const Cookies = require("cookies");
/**
* ctx对象的原型(基础功能)
*/
const proto = (module.exports = {
// ctx.throw
throw(...args) {
throw createError(...args);
},
// ctx.onerror。就是 this.app.emit("error")
/**
* this是什么?为啥能获取app?
* 其实在application.js的createContext中可以看出:context.app = request.app = response.app = this;
*/
onerror(err) {
// ...
// delegate
this.app.emit("error", err, this);
// ...
},
});
// 让ctx对象代理request和response的属性和方法
delegate(proto, "response")
.method("attachment")
.method("redirect")
.method("remove")
// ...
delegate(proto, "request")
.method("acceptsLanguages")
.method("acceptsEncodings")
.method("acceptsCharsets")
.method("accepts")
.method("get")
// ...
request.js和response.js
request.js和response.js两个文件很类似。就是分别封装了node原生的req对象作为request对象、res对象作为response对象,并提供了一些便捷的属性和方法。
以request.js为例:
//...模块依赖
// 提供了大量的Getter和Setter,大部分都是对this.req的代理
/**
* this.req究竟是什么?
* 其实在application.js的createContext中可以看出:
* context.req = request.req = response.req = req(node原生的req对象);
*/
module.exports = {
// ...
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
// ...
};
探究Koa中间件模型
在application.js中我们看到了Koa对于中间件的实现,主要用到了koa-compose这个来组合中间件,并且实现了洋葱圈模式的调用。
来一段Koa中间件的demo:
const Koa = require("koa");
const app = new Koa();
// first中间件
const first = async (ctx, next) => {
console.log("first start");
await next();
console.log("first end");
}
// second中间件
const second = async (ctx, next) => {
console.log("second start");
await next();
console.log("second end");
}
app.use(first);
app.use(second);
app.listen(3000);
// first中间件先执行,但是是最后执行完的。
// console.log("first start");
// console.log("second start");
// console.log("second end");
// console.log("first end");
- 如何理解Koa的洋葱圈模型
- 多个中间件会形成一个栈结构(middle stack),以”先进后出”的顺序执行。
- 在最外层的中间件首先执行。调用next函数,把执行权交给下一个中间件。以此递进,最外层的中间件收回执行权之后,执行next函数后面的代码。
- koa-compose
koa-compose是实现Koa的洋葱圈模型的关键。
module.exports = compose;
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!");
}
// 组合好后的中间件们返回一个接受context对象的函数,对于Koa可以忽略next参数
return function (context, next) {
// 每一个中间件会对应一个index,闭包变量
// 用于防止在一个中间件中重复调用 next() 函数
let index = -1;
// dispatch(0) 启动中间件的链式调用 -> 中间件接受的next参数就是dispatch,并且还绑定了对应的index
return dispatch(0);
function dispatch(i) {
// 有index对应不上
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 {
// dispatch.bind(null, i + 1) -> next -> 下一个中间件的执行权
// Promise.resolve强行Promise -> await next() / next().then()
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
// 处理中间件链执行的错误
return Promise.reject(err);
}
}
};
}
探究ctx.body
Koa设置响应可以用ctx.body,并且它可以响应多种类型的数据。
在response.js中实现了ctx.body的读取和设置:
const response = {
_body: "",
// 获取ctx.body
get body() {
return this._body;
},
// 设置ctx.body
set body(val) {
const original = this._body;
this._body = val;
// 处理数据类型,设置this.type 也就是Content-Type
// no content
if (null == val) {
if (!statuses.empty[this.status]) this.status = 204;
if (val === null) this._explicitNullBody = true;
this.remove("Content-Type");
this.remove("Content-Length");
this.remove("Transfer-Encoding");
return;
}
// 设置响应码
if (!this._explicitStatus) this.status = 200;
// string
if ("string" === typeof val) {
if (setType) this.type = /^\s*</.test(val) ? "html" : "text";
this.length = Buffer.byteLength(val);
return;
}
// buffer
if (Buffer.isBuffer(val)) {
if (setType) this.type = "bin";
this.length = val.length;
return;
}
// stream,处理流式数据
if (val instanceof Stream) {
onFinish(this.res, destroy.bind(null, val));
if (original != val) {
val.once("error", (err) => this.ctx.onerror(err));
// overwriting
if (null != original) this.remove("Content-Length");
}
if (setType) this.type = "bin";
return;
}
// json
this.remove("Content-Length");
this.type = "json";
},
};
最终在application.js 的respond方法中完成响应
/**
* Response helper.
*/
function respond(ctx) {
// ...
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' === typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// ...
}
参考: