Koa 快速入门

Koa 快速入门

  • Koa 快速入门
    • 1. Koa1.x 与 Koa2
    • 2. context 对象
    • 3. middleware
      • 3.1 中间件的概念
      • 3.2 中间件的功能
      • 3.3 中间件的加载
      • 3.4 Express 中的中间件
      • 3.5 next 方法
      • 3.6 中间件的串行调用
      • 3.7 一个例子:如何实现超时响应

Koa 快速入门

Koa 是 Express 的原班开发人马使用 ES2015 中的新特性(主要是 Generator )重新打造了新的 Web 框架——Koa,Koa 的初衷就是彻底解决在Node Web开发中的异步问题,在 ES2015 还没有被 Node 完全支持的时候,运行 Koa 项目需要在启动 Node 时加上--harmony参数。

Koa 的理念与 Connect 更加相似,内部没有提供任何中间件,Express 中保留的静态文件和路由也被剔除,仅仅作为中间件的调用的脚手架。

Koa 的发展存在 Koa1.x 和 Koa2 两个阶段,两者之间的区别在于 Koa2 使用了 ES2017 中 async 方法来处理中间件的调用(Koa1.x 使用的是 generator),该特性已经在 v7.6.0 之后的 Node 版本中提供原生支持。

1. Koa1.x 与 Koa2

前面已经提到,Koa1.x 和 Koa2 的主要区别在于前者使用 Generator,后者使用 async 方法来进行中间件的管理。

在 Web 开发中,尽管 Node 本身是异步的,但我们还是希望能够顺序执行某些操作,而且代码实现要尽可能简洁。例如在收到 HTTP 请求时,我们希望先将请求信息写入日志,接着进行数据库相关的操作,最后返回对应的结果。

在实际开发中,这些操作会抽象为一个个中间件,通常都是异步进行调用的,我们的问题就回到了如何控制中间件的调用顺序上。

在 Koa1.x 的版本中,因此使用了 ES2015 提案中的 Generator 函数来作为异步处理的主要方式。

下面是一个 Koal1x 的例子。

const Koa = require('Koa');
const app = Koa();

app.use(function *(next) {
  const start = new Date();
  // 调用下一个中间件,即向前端响应 'Hello world'
  yield next;
  const ms = new Date() - start;
  // 打印从请求到响应的耗时
  console.log('%s %s - %s', this.method, this.url, ms);
});

app.use(function *() {
  this.body = 'Hello world';
});

app.listen(3000);

当用户访问 localhost:3000 时,首先打印出 hello world ,再输出log 信息。

Koa1.x 对中间件的处理基于 co 模块,ES2017 的草案里增加了 async 函数,Koa 为此发布了 2.0 版本,这个版本舍弃了 Genrator 函数和 co 模块,完全是使用 async 函数来实现的,async 函数在 Node v7.6.0 之后才得到了完整的支持,因此要使用 Koa2 进行开发,本地的 Node 环境最好大于 7.6.0。

除此之外,Koa 和 Express 最大的不同之处在于 Koa 剥离了各种中间件,这种做法的优点是可以让框架变得更加轻量,缺点就是 Koa 发展时间还较短,各种中间件质量参差不齐,1.x 和 2.x 的中间件也存在一些兼容性问题,但对于多数常用的中间件来说,都已经实现了对Koa2.0 的支持。

在 Koa 项目的 GitHub 主页 https:/github.com/Koajs 中,列出了 Koa 项目本身和被一些官方整理的中间件列表,开发者也可以在GitHub 中搜索,查找比较活跃的中间件。

在本文中,我们主要介绍 Koa2 的使用,在后面内容里提到的 Koa 均代表 Koa2.0。

2. context 对象

首先,我们从最简单的入门例子来看 Koa 的使用。

const Koa = require('Koa');
const app = Koa();

app.use(ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

Node 提供了 request(IncomingMessage)和 response(ServerReponse)两个对象,Koa 把两者封装到了同一个对象中,即 context,缩写为 ctx

context 中封装了许多方法和属性,大部分是从 requestresponse 对象中使用委托方式得来的,下面列出了 ctx 对象封装的一些属性以及它们的来源:

  1. From request
  • ctx.header
  • ctx.headers
  • ctx.method
  • ctx.url
  • ctx.originalUrl
  • ctx.origin
  • ctx.href
  • ctx.path
  • ctx.query
  • ctx.querystring
  • ctx.host
  • ctx.hostname
  • ctx.fresh
  • ctx.stale
  • ctx.socket
  • ctx.protocol
  • ctx.secure
  • ctx.ip
  • ctx.ips
  • ctx.subdomains
  • ctx.is()
  • ctx.accepts()
  • ctx.acceptsEncodings()
  • ctx.acceptsCharsets()
  • ctx.acceptsLanguages()
  • ctx.get()
  1. From response
  • ctx.body
  • ctx.status
  • ctx.message
  • ctx.length
  • ctx.type
  • ctx.headerSent
  • ctx.redirect()
  • ctx.attachment()
  • ctx.set()
  • ctx.append()
  • ctx.remove()
  • ctx.lastModified
  • ctx.etag

除了自行封装的属性外,ctx 也提供了直接访问原生对象的手段,ctx.reqctx.res 即代表原生的 requestresponse 对象,例如 ctx.req.urlctx.url 就是同一个对象。

除了上面列出的属性之外,ctx 对象还自行封装了一些对象,例如 ctx.requestctx.response ,它们和原生对象之间的区别在于里面只有一部分常用的属性,我们可以试着将原生对象和 ctx 封装后的对象分别打印出来进行比较:

const Koa = require('Koa');
const app = Koa();

app.use(ctx => {
  console.log(ctx.request);
  console.log(ctx.response);
});

app.listen(3000);

访问 localhost:3000,可以看出,二者的结构和原生对象还是有很大区别的,ctx.response 只有最基本的几个属性,上面没有注册任何事件或方法,这表示下面的使用方法是错误的:

fs.createReadStream('foo.txt').pipe(ctx.response);

上面的代码会抛出 TypeError:dest.on is not a function 的错误,原因也很简单,ctx.response 只是一个简单的对象,没有定义任何事件,要使用 pipe 方法,代码要改成:

fs.createReadStream('foo.txt').pipe(ctx.res);
  1. ctx.state

state 属性是官方推荐的命名空间,如有开发者从后端的消息想要传递到前端,可以将属性挂在 ctx.state 下面,这和 react 中的概念有些相似,例如我们从数据库中查找一个用户 id

ctx.state.user = await User.find(id);
  1. 其他的一些属性方法
ctx.app // ctx 对 app 对象的引用
ctx.cookies.get(name, [options]) // 获取 cookie
ctx.cookies.set(name, value, [options]) // 设置 cookie
ctx.throw([msg], [status], [properties]) // 用来抛出异常的方法
// 例如
// ctx.throw('name required', 400);
// 这句代码相当于:
// const err = new Error('name required');
// err.status = 400;
// err.expose = true;
// throw err;
  1. 处理 http 请求

上面的内容也提到,Koa 在 ctx 对象中封装了 request 以及 response 对象,那么在处理 http 请求的时候,使用 ctx 就可以完成所有的处理。

在上面的代码中,我们使用:

ctx.body = "Hello World";

相当于:

res.statusCode = 200;
res.end("Hello World");

ctx 相当于 ctx.request 或者 ctx.response 的别名,判断 http 请求类型可以通过 ctx.method 来进行判断,get 请求的参数可以通过 ctx.query 获取。

例如,当用户访问 localhost:3000?kindName=Node 时,可以设置如下的路由。

app.get('/', async (ctx, next) => {
  console.log(ctx.method); // GET
  console.log(ctx.query); // { kindName: 'Node' }
  // TODO
  await next();
});

Koa 处理 get 请求比较简单,直接通过 ctx.req. 就能拿到 get 参数的值,post 请求的处理稍微麻烦一些,通常使用 bodyParser 这一中间件进行处理,但也仅限于普通表单,获取格式为 ctx.request.body.

例如我们构造一个简单的 fom 用来输入用户名和密码:

<form action="/login" method="post">
  <input name="name">
  <input name="password" type="password">
  <input type="submit" value="Submit">
form>

服务端相应路由的代码就可以写成:

router.post('/login', (ctx, next) => {
  const name = ctx.request.body.name;
  const password = ctx.request.body.password;
});

3. middleware

3.1 中间件的概念

在介绍 Koa 中间件之前,我们暂时先把目光投向 Express ,因为Koa 中间件的设计思想大部分来自 Connect,而 Express 又是基于 Connect 扩展而来的。

Express 本身是由路由和中间件构成的,从本质上来说,Express的运行就是在不断调用各种中间件。

中间件本质上是接收请求并且做出相应动作的函数,该函数通常接收 reqres 作为参数,以便对 requestresponse 对象进行操作,在 Web 应用中,客户端发起的每一个请求,首先要经过中间件的处理才能继续向下。

中间件的第三个参数一般写作 next,它代表一个方法,即下一个中间件。如果我们在中间件的方法体中调用了 next 方法,即表示请求会经过下一个中间件处理。

例如下面的函数就可以拿来做一个中间件。

function md(req, res, next) {
  console.log("I am a Middleware");
  next();
}

3.2 中间件的功能

由于中间件仍然是一个函数,那么它就可以做到 Node 代码能做到的任何事情,除此之外还包括了修改 requestresponse 对象、终结请求-响应循环,以及调用下一个中间件等功能,这通常是通过在内部调用 next 方法来实现的。如果在某个中间件中没有调用 next 方法,则表示对请求的处理到此为止,下一个中间件不会被执行。

3.3 中间件的加载

中间件的加载使用 use 方法来实现,该方法定义在 Express 或者 Koa 对象的实例上,例如加载上面定义的中间件 md

const app = express();
app.use(md);

3.4 Express 中的中间件

Express 应用可使用如下几种中间件:

  • 应用级中间件
  • 路由级中间件
  • 错误处理中间件
  • 内置中间件
  • 第三方中间件

上面是官网的分类,实际上这几个概念有一些重合之处。

  1. 应用级中间件

使用 app.use 方法或者 app.METHOD(Method表示 http 方法,即 get/post 等)绑定在 app 对象上的中间件。

const app = express();

// 没有挂载路径的中间件,前端每个请求都会经过该中间件
app.use(function (req, res, next) {
  console.log('Time:', Date.now())
  next()
});
app.use('/user/:id', function (req, res, next) {
  console.log('Request Type:', req.method);
});

在第一个中间件中调用了 next 方法,因此会转到第二个中间件,第二个由于没有调用 next 方法,其后的中间件都不会执行。

  1. 路由级中间件

和 Koa 不同,路由处理是 Express 的一部分,通常通过 router.use 方法来绑定到 router 对象上:

const app = express();
const router = express.Router();

// 将中间件挂载到 /1ogin 路径下,所有访问 /1ogin 的请求都会经过该中间件
router.use('/login', function (req, res, next) {
  console.log('Time:', Date.now())
  next();
});
  1. 错误级中间件

错误处理中间件有 4 个参数,即使不需要通过 next 方法来调用下一个中间件,也必须在参数列表中声明它,否则中间件会被识别为一个常规中间件,不能处理错误。

app.use(function (err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});
  1. 内置中间件

从 4.x 版本开始,Express 已经不再依赖 Connect 了。除了负责管理静态资源的 static 模块外,Express 以前内置的中间件现在已经全部单独作为模块安装使用。

  1. 第三方中间件

第三方中间件可以为 Express 应用增加更多功能,通常通过 npm 来安装,例如获取 Cookie 信息常用的 cookie-parser 模块,或者解析表单用的 bodyParser 等。

Koa 没有任何内置的中间件,连路由处理都没有包括在内,所有中间件都要通过第三方模块来实现,比起 Express 来,其实更像是 Connect。.

3.5 next 方法

无论是 Express 还是 Koa,中间件的调用都是通过 next 方法来执行的,该方法最早在 Connect 中提出,并被 Express 和 Koa 沿用。

当我们调用 app.use 方法时,在内部形成了一个中间件数组,在框架内部会将执行下一个中间件的操作放在 next 方法内部,当我们执行 next 方法时,就会执行下一个中间件。如果在一个中间件中没有调用 next 方法,那么中间件的调用会中断,后续的中间件都不会被执行。

对于整个应用来说,next 方法实现的无非就是嵌套调用,也可以理解成一个递归操作,执行完 next 对应的中间件后,还会返回原来的方法内部,继续向下执行后面的方法。

如下图所示,下面这张“洋葱图”很形象地解释了 Koa 中间件的工作原理,对于 request 对象,首先从最外围的中间件开始一层层向下,到达最底层的中间件后,再由内到外一层层返回给客户端。每个中间件都可能对 request 或者 response 对象进行修改。

Koa 快速入门_第1张图片

3.6 中间件的串行调用

接下来讲述的是 Koa 设计的核心部分,在 Web 开发中,我们通常希望一些操作能够串行执行,例如等待写入日志完成后再进行数据库操作,最后再进行路由处理。

在技术层面,上面的业务场景表现为串行调用某些异步中间件。比较容易想到的一种做法是把 next 方法放到回调里面。

下面的代码定义了两个 Express 中间件,和之前不同之处在于第二个中间件中调用了 process.nextTick() 表示这是一个异步操作。

const app = require('express')();
app.use(function (req, res, next) {
  next();
  console.log("I am middleware1 ");
});
app.use(function (req, res, next) {
  process.nextTick(function () {
    console.log("I am middleware2");
    next();
  });
});
app.listen(3000);
// 访问localhost:3000的输出结果
// I am middleware1
// I am middleware2

按照上面的原理,next 方法在执行完毕后返回上层的中间件,那么应该先执行 middleware2,然后再执行 middleware1;但由于第二个中间件内的 process.nextTick 是一个异步调用,因此马上返回到第一个中间件,继续输出 I am middleware1,然后中间件二的回调函数执行,输出 I am middleware2

我们前面也已经提到了,在有些情况下,我们可能希望等待 middleware2 执行结束之后再输出结果。而在 Koa 中,借助 async / await 方法,事情变得简单了。

const Koa = require("Koa");
const app = new Koa();
app.use(async (ctx, next) => {
  await next()
  console.log("I am middlewarel ");
});
app.use(async (ctx, next) => {
  process.nextTick(function () {
    console.log("I am middleware2")
    next();
  });
});
app.listen(3000);
// 访问localhost:3000的输出结果
// I am middleware2
// I am middleware1

使用 await 关键字后,直到 next 内部的异步方法完成之前,midddlware1 都不会向下执行。

下面我们来看一个具体例子的分析,这个例子反映了一种常见的需求,即设置整个 app 的响应时间。

3.7 一个例子:如何实现超时响应

  1. Express 中的超时响应

下面我们来介绍一个更贴近具体业务的例子。在 Web 开发中,我们希望能给长时间得不到响应的请求返回特定的错误信息。

如果是在 Express 中,可以使用 connect-timeout 这一第三方中间件来处理响应超时,该中间件实现很简单,下面是一段使用 connect-timeout进行超时响应的示意代码。

const express = require('express');
const timeout = require('connect-timeout');

// example of using this top-level; note the use of haltonTimedout
// after every middleware; it will stop the request flow on a timeout
const app = express();
app.use(timeout('5s'));
app.use(some middleware);
app.use(haltOnTimedout);
app.use(some middleware);
app.use(haltOnTimedout);

function haltonTimedout(req, res, next) {
  if (!reg.timedout) next();
}

app.listen(3000);

该中间件的实现很简单,timeout 内部定义了一个定时器方法,如果超过定时器规定的时间限制,就会触发错误事件并返回一个 503 状态码,并且 haltOnTimedout 后面的中间件不再执行。如果在定时器触发前完成响应,就会取消定时器。

这种做法虽然看起来能解决超时问题,但仔细想一想缺点也很明显,在 timeout 方法中只定义了一个简单的定时器,如果中间件中包含了一个异步操作,那么容易在调用回调方法时出现问题。

假设 timeout 加载后又引入了一个名为 queryDB 的中间件,该中间件封装了一个异步的数据库操作,并且将查询的结果作为响应消息返回。

queryDB 在大多数状态下很快(1秒内(就能完成,但有时会因为某些原因(例如被其他操作阻塞)导致执行时间变成了 10 秒,这时 timeout 中间件已经将超时信息返回给了客户端,如果queryDB 内部包含了一个 res.send 方法,就会出现 Can't set headers after they are sent 的错误。

要解决这个问题,比较妥当的方式是通过事件监听的方式,如果超时之后触发该事件,那么取消之后的全部操作,或者直接修改res.end 方法,在其中设置一个 flag 用来判断是否已经调用过。

上面问题的根本原因是 connect-time,或者是 Express 没办法对异步中间件的执行进行很好的控制。

  1. Koa 中的超时响应

借助 async 方法中间件会按照顺序来执行,这时进行 timeout 管理就比较方便了,目前社区也有 koa-timeout 等一些中间件,读者可以自行去探索使用,也可以考虑自己实现,毕竟这样的中间件实现难度并不大。

下面是我自己实现的一个例子,核心思想是使用 promise.race 方法来比较 setTimeout 和之后的中间件哪个会更快完成。

app.use(async (ctx, next) => {
  let tmr = null;
  const timeout = 5000; // 设置超时时间
  await Promise.race([
    new Promise(function (resolve, reject) {
      tmr = setTimeout(function () {
        const e = new Error('Request timeout');
        e.status = 408;
        reject(e);
      }, timeout);
    }),
    new Promise(function (resolve, reject) {
      // 执行后面加载的中间件
      (async function () {
        await next();
        clearTimeout(tmr);
        resolve();
      })();
    })
  ])
});

如果我们想用上面的代码管理超时,queryDB 需要返回一个Promise 对象或者是 async 方法。

你可能感兴趣的:(#,node.js,JavaScript,node.js)