Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。
底层实现
koa、express这些使用node环境作为服务端,其实底层都还是用的node的http模块实现的。
const http = require('http');
const server = http.createServer((req, res) => {
res.end('hello world');
});
server.listen(3000);
这样我们就完成了一个服务端的创建,访问http://localhost:3000/
即可看到一个hello world。koa在此基础上进行了封装,方便我们对请求进行更加精细化的控制。
创建Application
// application.js
const http = require('http')
class Application {
constructor() {
this.middleware = [];
}
callback(req, res) {
this.middleware.forEach(fn => fn(req, res))
}
use(fn) {
typeof fn === 'function' && this.middleware.push(fn);
}
listen(...args) {
const server = http.createServer(this.callback.bind(this))
return server.listen(...args)
}
}
module.exports = Application;
现在我们有了use、listen方法,请求进来时会执行use中传入的中间件方法,现在中间件传入的是(req,res)
,但koa入参是(ctx,next)
,next先不考虑。
接下来我们需要去创建ctx,我们先看下ctx的关键几个属性。
- ctx.req: Node 的 request 对象
- ctx.res: Node的 response 对象
- ctx.request: koa 的
Request
对象
- ctx.response: koa 的
Response
对象.
- ctx.app: 应用程序实例引用
结合上面我们来定义一个context.js,在这之前需要先定义Request、Response对象,因为ctx引用了它们。
创建Request、Response
Request、Response对象是对node上的request、response进行了封装。我们可以直接通过它们对node上的request和response进行取值、赋值如:
request.header
请求头对象。
request.header=
设置请求头对象。
response.body
获取响应体。
response.body=
设置响应体
koa中使用了getter/setter去获取和设置Request,Response上的属性。这样做相比直接去操作node上的request、response的好处是,开发者的所用获取、设置都需要先经过我们的getter/setter方法,这样我们就可以对获取属性时做一些封装返回,对设置属性值做一层验证,提前将不规范的值抛出错误。
// request.js
module.exports = {
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
};
// response.js
module.exports = {
get body() {
return this._body;
},
set body(val) {
this._body = val;
this.res.end(val);
},
};
接下来我们还需要加入context,也就是传入use中的方法的ctx实例。
创建上下文Context
Context 将 node 的 request
和 response
对象封装在一个单独的对象里面,其为编写 web 应用和 API 提供了很多有用的方法。 这些操作在 HTTP 服务器开发中经常使用,因此其被添加在上下文这一层,而不是更高层框架中,因此将迫使中间件需要重新实现这些常用方法。
// context.js
module.exports = {
get header() {
return this.request.headers;
},
set header(val) {
this.request.headers = val;
},
get body() {
return this.response.body;
},
set body(val) {
this.response.body = val;
},
};
// application.js
const http = require("http");
const request = require("./request");
const response = require("./response");
const context = require("./context");
class Application {
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;
return context;
}
callback(req, res) {
const ctx = this.createContext(req, res);
this.middleware.forEach((fn) => fn(ctx));
}
...
}
module.exports = Application;
这样就实现了通过ctx取值,以及赋值的操作了,但是如果context通过这种对Response、Request上的方法重写来实现代理,显然是很麻烦的很麻烦,且不易维护。所以Koa2使用了delegate来实现对Response、Request的代理。
// context.js
const delegate = require("delegates");
const proto = {
/**
* 这里可以写一些context特有的方法,如错误处理
* 以及需要同时用到res和req的方法,如cookies的处理
*/
};
delegate(proto, "response").access("body");
delegate(proto, "request").getter("headers");
module.exports = proto;
至此,一个最最最最基本的koa能正常跑起来了。
const Koa = require("../src/application");
const app = new Koa();
app.use(async (ctx) => {
ctx.body = "Hello World";
});
app.listen(3000);
打开浏览器输入http://localhost:3000/
,能看到Hello World。
实现next功能
koa的一个亮点设计在于对中间件的处理,Koa 中间件以更传统的方式级联,您可能习惯使用类似的工具 - 之前难以让用户友好地使用 node 的回调。然而,使用 async 功能,我们可以实现 “真实” 的中间件。对比 Connect 的实现,通过一系列功能直接传递控制,直到一个返回,Koa 调用“下游”,然后控制流回“上游”。
这种模式其实就是洋葱模型,koa团队为此实现了一个洋葱模型的工具库koa-compose,实现十分简洁,不到50行代码。
利用递归实现了 Promise 的链式执行,不管中间件中是同步还是异步都通过 Promise 转成异步链式执行。这边贴一下koa-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!')
}
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)
}
}
}
}
compose的入参是我们之前实现的中间件数组,返回了一个函数,接受两个参数,context 和 next。context 是 koa 中的 ctx,next 是所有中间件执行完后,框架使用者来最后处理请求和返回的回调函数。最后我们只要执行该函数,就使中间件与中间件之间进行级联。
我们需要重新实现一下Application的callback方法
// application.js
class Application {
...
callback(req, res) {
const ctx = this.createContext(req, res);
const fn = compose(this.middleware);
fn(ctx);
}
...
}
现在我们来试下使用异步的中间件
const Koa = require("../src/application");
const app = new Koa();
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('test-next');
console.log(rt);
});
// x-response-time
app.use(async (ctx, next) => {
ctx.set('test-next', 'sucess');
await next();
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);