koa原理初探

·  阅读 826
koa原理初探

前段时间使用 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 请求时执行。我们需要注意的是这个函数有两个参数,requestresponse,它们非常重要!

request

requestIncomingMessage 的实例,其中包含了所有和请求有关的信息,如请求方法(get、port...),请求路径,请求头等。同时,request 中实现了 ReadableStream 接口,以获取 body 中的内容,方法如下:

let body = [];
request.on('data', (chunk) => {
  body.push(chunk);
}).on('end', () => {
  body = Buffer.concat(body).toString();
  // 这里 `body` 包含了所有请求体的内容,保存在一个字符串中
});
复制代码

response

responseServerResponse 的实例,我们可以通过它来设置请求的返回。如状态码,响应头等。

// 设置状态码
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。同时,由于 requestresponse也是 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`);
});

复制代码

它有两个参数,ctxnextctx 是 koa 中的 Context 上下文,它包含了前面所说的 requestresponse,同时封装了其他一些有用的方法(Context 会放到后面说)。而 next 即是下一个中间件,这里通过调用 await next() 来运行它。

不难发现,每个中间件都用了 asyncawait,这也是官方推荐的用法。为什么要这样用呢?其实主要是针对 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 可以使当前函数“完全运行完成”后再运行后面的逻辑。所以,为了更好的控制中间件的行为:上游调用下游,下游执行结束后,控制又返回上游,我们必须要使用 asyncawait来控制。

不过它也不是万能的,如果你的 “response” 写成这样,那就真的“失去控制”了。

app.use(async (ctx) => {
  // 谁也不知道我什么时候运行结束~
  setTimeout(() => {
    ctx.body = "Hello World";
  }, 2000);
});
复制代码

其实我们可以简单理解为,中间件就是一串有顺序的函数,它们会在请求到来时先后执行。一般一个中间件只负责一个简单的功能,比如“body-parser”只做 body 的解析,“logger”只处理请求日志等等。上游中间件能够“控制”下游中间件在很多时候都是很有用的,这在计算耗时和错误处理上就体现得很明显。

对于 koa 中间件,我们要习惯使用 asyncawait,这在一些时候也能避免出现一些“奇怪”的行为,就比如中间件的返回值。

app.use(async (ctx, next) => {
  const res = next(); // 不使用 await
  console.log("res", res); // 这里是什么呢??
});

app.use(() => {
  return "hello world";
});
复制代码

例子中打印的结果不是"hello world"字符串,而是一个 Promise。这是由于 koa 将所有的中间件的返回都用 Promise 封装了一层,源码如下: image-20220326112417045 想了解更多可以看 koa-compose,源码很少很友好~它的作用便是将所有的中间件串联在一起,让它们先后执行。

app.use

再看示例中 app.use,其实它的工作太简单了,就是把函数塞到中间件队列里。 image-20220326113450755

app.listen

例子中是 app.listen(3000),它又做了什么呢?

listen 方法的源码如下: image-20220326113108044 它其实只简单的调用了http.createServer

再看 this.callbackimage-20220326113423105

前面使用 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 对象。

image.png

createContext源码如下:

截屏2022-03-28 上午10.14.00.png 它将 Node 的 request 和 response 对象都封装在了 context 这个对象中,同时也包含了 koa 中的 request 对象和 response 对象。

属性含义
ctx.reqNode 的 request 对象.
ctx.resNode 的 response 对象.
ctx.requestkoa 的 Request 对象.
ctx.responsekoa 的 Response 对象.
ctx.appconst app = new Koa() 这个实例

更多可参见 Koa Context

总结

总结一下,本篇结合 node http 示例对 koa 的简单示例进行了分析,探究了 koa 中的部分原理。

node http 示例可以用一张图来表示:

image.png

关键点有三部分:

  • request。它是一个IncomingMessage 的实例,其中包含了所有和请求有关的信息,如 method,url,body 等。读取 body 的内容依赖了 ReadableStream 对象。

  • response。它是一个 ServerResponse 的实例。它可以设置请求的返回,如 header,statusCode 等。向客户端写内容依赖了 WritableStream 对象。

  • 最后,有一个请求处理函数 handle request,它在所有请求到来时执行。在这个函数中可以获取到 request 和 response 对象,从而可以根据不同请求实现不同的处理行为。

回到 koa ,我们也可以用一张图来表示:

image.png

其中有两个最明显的区别,同时也是 koa 的要点:

  • Context。koa 的上下文,它封装了 Node 的 request 和 response 对象和其他一些方法,便于在中间件中使用。
  • middleware。koa 的中间件,相比使用一个大的处理函数,中间件系统将功能拆分成多个中间件函数,这样逻辑清晰而且便于维护。我们可以在 npm 社区中找到许多实用的 koa 中间件,帮助我们快速构建一个 web 应用。

参考

  1. koajs
  2. Node指南-一次HTTP传输解析
  3. 10分钟理解 Node.js koa 源码架构设计
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改