前段时间使用 koa 开发了一个小应用,觉得koa使用起来还是非常简单方便的,于是想写一篇总结,增进对 koa 的理解。
node http
在学习 koa 之前,我们可以先看看使用 node http 模块怎么构建一个 web 服务。
const http = require("http");
const server = http.createServer((request, response) => {
// 在这里处理所有的 http 请求
});
server.listen(3000);
复制代码
代码很简单,通过 http.createServer 创建一个 http 服务,然后通过 server.listen
监听端口,这样外部就可以访问了。
http.createServer
接收一个函数,并且它将在每次接收到 http 请求时执行。我们需要注意的是这个函数有两个参数,request
和 response
,它们非常重要!
request
request
是 IncomingMessage 的实例,其中包含了所有和请求有关的信息,如请求方法(get、port...),请求路径,请求头等。同时,request
中实现了 ReadableStream 接口,以获取 body 中的内容,方法如下:
let body = [];
request.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body).toString();
// 这里 `body` 包含了所有请求体的内容,保存在一个字符串中
});
复制代码
response
response
是 ServerResponse 的实例,我们可以通过它来设置请求的返回。如状态码,响应头等。
// 设置状态码
response.statusCode = 404;
// 设置响应头
esponse.setHeader('Content-Type', 'application/json');
复制代码
最后,向客户端返回内容。这依赖了 WritableStream 对象。
response.write('Hello World');
response.end();
// 也可以简化为
response.end('Hello World');
复制代码
了解了以上一些内容,我们就可以丰富一下前面基础示例的功能了。如下:
const http = require('http');
http.createServer((request, response) => {
request.on('error', (err) => { // 加上一个错误处理
console.error(err);
response.statusCode = 400;
response.end();
});
response.on('error', (err) => { // 加上一个错误处理
console.error(err);
});
// 处理方法为 “POST”,路径为 “/echo” 的请求
if (request.method === 'POST' && request.url === '/echo') {
let body = [];
request.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body).toString();
response.end(body); // 将所有 body 内容返回
});
} else {
response.statusCode = 404;
response.end();
}
}).listen(3000);
复制代码
上面的示例只对方法是 POST,访问路径是 /echo
的请求作应答,并返回所有 body 的内容,其他请求均返回 404。同时,由于 request
和 response
也是 EventEmitter 对象,我们可以通过监听 error 事件来处理内部可能发生的错误。
koa
接下里就是我们的主角 koa 了。下面是一个最简单的 koa 示例,它的功能是对所有的请求返回一个“Hello World”字符串。
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
复制代码
我们可以结合前面 node http 的示例来看这个例子的代码,最显眼的就是这里的 app.use
,它其实就是注册 koa 的中间件(middleware)。
middleware
中间件大家都不陌生了,多个中间件是有顺序的,它们会在收到请求后依次执行。如下面的示例:
const Koa = require('koa');
const app = new Koa();
// 1. logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// 2. x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// 3. response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
复制代码
这里依次注册了 3 个中间件:日志(logger),计算请求时间(x-response-time)和返回(response)。在一个 web 应用中,各个中间件都负责一个独立的功能,最后合成一个完整的应用。
koa 中间件的形式如下:
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
复制代码
它有两个参数,ctx
和 next
。ctx
是 koa 中的 Context 上下文,它包含了前面所说的 request
和 response
,同时封装了其他一些有用的方法(Context 会放到后面说)。而 next
即是下一个中间件,这里通过调用 await next()
来运行它。
不难发现,每个中间件都用了 async
和 await
,这也是官方推荐的用法。为什么要这样用呢?其实主要是针对 node 中的异步行为的。
如下示例,我们在 “response” 中间件中添加一个异步的 Promise,如果 “x-response-time” 中不使用 await
,结果是如何呢?
// 2. x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
next(); // 不使用 await!!
const ms = Date.now() - start;
ctx.set("X-Response-Time", `${ms}ms`);
});
// 3. response
app.use(async (ctx) => {
const wait2seconds = () =>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 2000);
});
await wait2seconds();
ctx.body = "Hello World";
});
复制代码
结果是 “response” 还没执行完,“x-response-time” 就已经计算完它的执行时间了😫~
使用 await
可以使当前函数“完全运行完成”后再运行后面的逻辑。所以,为了更好的控制中间件的行为:上游调用下游,下游执行结束后,控制又返回上游,我们必须要使用 async
,await
来控制。
不过它也不是万能的,如果你的 “response” 写成这样,那就真的“失去控制”了。
app.use(async (ctx) => {
// 谁也不知道我什么时候运行结束~
setTimeout(() => {
ctx.body = "Hello World";
}, 2000);
});
复制代码
其实我们可以简单理解为,中间件就是一串有顺序的函数,它们会在请求到来时先后执行。一般一个中间件只负责一个简单的功能,比如“body-parser”只做 body 的解析,“logger”只处理请求日志等等。上游中间件能够“控制”下游中间件在很多时候都是很有用的,这在计算耗时和错误处理上就体现得很明显。
对于 koa 中间件,我们要习惯使用 async
和 await
,这在一些时候也能避免出现一些“奇怪”的行为,就比如中间件的返回值。
app.use(async (ctx, next) => {
const res = next(); // 不使用 await
console.log("res", res); // 这里是什么呢??
});
app.use(() => {
return "hello world";
});
复制代码
例子中打印的结果不是"hello world"字符串,而是一个 Promise。这是由于 koa 将所有的中间件的返回都用 Promise 封装了一层,源码如下:
想了解更多可以看 koa-compose,源码很少很友好~它的作用便是将所有的中间件串联在一起,让它们先后执行。
app.use
再看示例中 app.use
,其实它的工作太简单了,就是把函数塞到中间件队列里。
app.listen
例子中是 app.listen(3000)
,它又做了什么呢?
listen
方法的源码如下:
它其实只简单的调用了
http.createServer
。
再看 this.callback
:
前面使用 app.use
把函数推到中间件队列里,这里使用 compose
(即 koa-compose)把这些中间件函数合并到一起。最后返回一个 handleRequest
函数。再回顾前面 node http 的示例:
const http = require("http");
const server = http.createServer((request, response) => {
// 在这里处理所有的 http 请求
});
server.listen(3000);
复制代码
handleRequest
其实就是传入 createServer
的这个函数了,所有的请求都会在这个 handleRequest
函数里处理。
在 this.handleRequest(ctx, fn)
中,koa 会把这个 ctx
传入到中间件函数 fn
中执行,即 fn(ctx)
。这样,整个流程就走通了,这个中间件函数串联了所有通过 app.use
注入的中间件,它们将在每个请求到来时执行,而这个 ctx
对象也将存在于这个请求的整个生命周期中,被各个中间件使用。
ctx
最后一个重点,就是 koa 中的上下文 context 了。它通过createContext
创建,并传入了 Node 的 request 和 response 对象。
createContext
源码如下:
它将 Node 的 request 和 response 对象都封装在了
context
这个对象中,同时也包含了 koa 中的 request
对象和 response
对象。
属性 | 含义 |
---|---|
ctx.req | Node 的 request 对象. |
ctx.res | Node 的 response 对象. |
ctx.request | koa 的 Request 对象. |
ctx.response | koa 的 Response 对象. |
ctx.app | 即 const app = new Koa() 这个实例 |
更多可参见 Koa Context。
总结
总结一下,本篇结合 node http 示例对 koa 的简单示例进行了分析,探究了 koa 中的部分原理。
node http 示例可以用一张图来表示:
关键点有三部分:
-
request。它是一个IncomingMessage 的实例,其中包含了所有和请求有关的信息,如 method,url,body 等。读取 body 的内容依赖了 ReadableStream 对象。
-
response。它是一个 ServerResponse 的实例。它可以设置请求的返回,如 header,statusCode 等。向客户端写内容依赖了 WritableStream 对象。
-
最后,有一个请求处理函数 handle request,它在所有请求到来时执行。在这个函数中可以获取到 request 和 response 对象,从而可以根据不同请求实现不同的处理行为。
回到 koa ,我们也可以用一张图来表示:
其中有两个最明显的区别,同时也是 koa 的要点:
- Context。koa 的上下文,它封装了 Node 的 request 和 response 对象和其他一些方法,便于在中间件中使用。
- middleware。koa 的中间件,相比使用一个大的处理函数,中间件系统将功能拆分成多个中间件函数,这样逻辑清晰而且便于维护。我们可以在 npm 社区中找到许多实用的 koa 中间件,帮助我们快速构建一个 web 应用。