Skip to content

异步编程模型

Node.js 是一个异步的世界,官方 API 支持的都是 callback 形式的异步编程模型,这带来了许多问题,例如:

  • callback hell:最臭名昭著的 callback 嵌套问题。
  • release zalgo:异步函数中可能同步调用 callback 返回数据,导致不一致性。

因此,社区提供了各种异步的解决方案。最终,Promise 胜出,并内置到了 ECMAScript 2015 中。基于 Promise 和 Generator 提供的切换上下文能力,出现了 co 等第三方类库,让我们以同步的写法来编写异步代码。同时,async function 这个官方解决方案也在 ECMAScript 2017 中发布,并在 Node.js 8 中得到实现。

async function

async function 是语言层面提供的语法糖。在 async function 中,我们可通过 await 关键字等待一个 Promise 被 resolve(或 reject,在此情况下会抛出异常)。

Node.js 现在的 LTS 版本(8.x)已原生支持 async function。

js
const fn = async function () {
  const user = await getUser();
  const posts = await fetchPosts(user.id);
  return { user, posts };
};
fn()
  .then((res) => console.log(res))
  .catch((err) => console.error(err.stack));

Koa

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造,致力于成为 web 应用和 API 开发领域中的更小、更富有表现力、更健壮的基础设施。

Koa 和 Express 的设计风格十分相似,底层也都是共用同一套 HTTP 基础库。但它们存在几个明显的区别。除了上面提到的默认异步解决方案之外,Koa 的主要特点还包括以下几个:

Middleware

Koa 的中间件和 Express 不同,Koa 选择了洋葱圈模型。

  • 中间件洋葱图:

  • 中间件执行顺序图:

每个请求在经过一个中间件时都会执行两次,与 Express 形式的中间件相对,Koa 的模型可以方便地实现后置处理逻辑。比较 Koa 与 Express 的 Compress 中间件,便可明显感受到 Koa 中间件模型的优势。

Context

与 Express 只有 Request 和 Response 两个对象不同,Koa 增加了一个 Context 对象,作为该次请求的上下文对象(在 Koa 1 中为中间件的 this,在 Koa 2 中作为中间件的第一个参数传入)。我们可将一次请求相关的上下文全部挂载至此对象上。例如,traceId 这种需贯穿整个请求(之后在任何地方进行其他调用都需使用)的属性便可挂载上去。这比单独的 request 和 response 对象更加符合语义。

同时,Context 上也挂载了 Request 和 Response 两个对象。和 Express 类似,两者都提供了许多便捷方法,辅助开发。例如:

  • get request.query
  • get request.hostname
  • set response.body
  • set response.status

异常处理

通过同步方式编写异步代码的另一个很大好处是,异常处理变得非常自然。我们可以使用 try catch 来捕获规范编写代码中的所有错误,从而非常容易地编写自定义的错误处理中间件。

js
async function onerror(ctx, next) {
  try {
    await next();
  } catch (err) {
    ctx.app.emit('error', err);
    ctx.body = 'server error';
    ctx.status = err.status || 500;
  }
}

只需将此中间件放在其他中间件前,便可捕获所有同步或异步代码中抛出的异常。

Egg 继承于 Koa

如上所述,Koa 是一个非常优秀的框架。然而,对于企业级应用来说,它还比较基础。

而 Egg 选择了 Koa 作为其基础框架,在它的模型基础上,对其进行了进一步的增强。

扩展

在基于 Egg 的框架或者应用中,我们可以通过定义 app/extend/{application,context,request,response}.js 来扩展 Koa 中对应的四个对象的原型。通过这个功能,我们可以快速增加更多的辅助方法。举例,我们在 app/extend/context.js 中写入以下代码:

javascript
// app/extend/context.js
module.exports = {
  get isIOS() {
    const iosReg = /iphone|ipad|ipod/i;
    return iosReg.test(this.get('user-agent'));
  },
};

在 Controller 中,我们就可以使用刚才定义的这个便捷属性了:

javascript
// app/controller/home.js
exports.handler = (ctx) => {
  ctx.body = ctx.isIOS
    ? 'Your operating system is iOS.'
    : 'Your operating system is not iOS.';
};

更多关于扩展的内容,请查看扩展章节。

插件

众所周知,在 Express 和 Koa 中,我们经常会引入众多中间件来提供各种功能,如引入 koa-session 提供 Session 支持,引入 koa-bodyparser 解析请求体。Egg 提供了强大的插件机制,让这些独立领域的功能模块更易于编写。

一个插件可以包含:

  • extend:扩展基础对象的上下文,提供工具类、属性等。
  • middleware:加入一个或多个中间件,提供请求的前置、后置逻辑处理。
  • config:配置不同环境下插件的默认配置项。

在一个独立领域下实现的插件,可以在维护性非常高的情况下提供完善的功能。插件还支持配置各个环境下的默认(最佳)配置,使得使用插件时几乎无需修改配置项。

@eggjs/security 插件是一个典型的例子。

更多关于插件的内容,请查看插件章节。

Egg 与 Koa 的版本关系

Egg 1.x

Egg 1.x 发布时,Node.js 的 LTS 版本尚不支持 async function,因此 Egg 1.x 基于 Koa 1.x 开发。在此基础上,Egg 全面增加了对 async function 的支持。再加上 Egg 对 Koa 2.x 的中间件完全兼容,应用层代码可以完全基于 async function 开发。

  • 底层基于 Koa 1.x,异步解决方案基于 co 封装的 generator function。
  • Egg 核心和官方插件使用 generator function 编写,通过 co 包装兼容 async function。
  • 开发者可以根据 Node.js 版本选择使用 async function 或 generator function。

Egg 2.x

Node.js 8 正式进入 LTS 后,async function 在 Node.js 中无性能问题,Egg 2.x 基于 Koa 2.x 开发。框架底层和所有内置插件都采用 async function 编写,对 Egg 1.x 和 generator function 保持完全兼容。应用层只需升级到 Node.js 8,即可从 Egg 1.x 迁移到 Egg 2.x。

  • 底层基于 Koa 2.x,异步解决方案采用 async function。
  • Egg 核心和官方插件使用 async function 编写。
  • 建议业务层迁移到 async function 解决方案。
  • 仅支持 Node.js 8 及以上版本。

Born to build better enterprise frameworks and apps