首页
/ Koa Application 核心 API 详解:app.use 中间件栈、应用配置与错误处理机制

Koa Application 核心 API 详解:app.use 中间件栈、应用配置与错误处理机制

2026-09-05 21:12:54作者:彭桢灵Jeremy

本篇技术指南以 Koa 官方 API 文档 docs/api/index.md 为主体,系统讲解 Application 应用对象的全部核心能力——安装与环境要求、中间件级联(Cascading)执行模型、应用配置项、app.listen / app.callback / app.use / app.keys / app.context / app.currentContext 以及错误处理机制。读完本文,你可以从零搭建一个符合 Koa 3.x 规范的 Web 应用,理解 next() 上下游控制流的真实执行过程,并能结合 源码实现 掌握每个 API 的底层行为与适用前提。

安装与环境要求

Koa 要求 Node.js v18.0.0 及以上版本,以获得 ES2015 与 async function 支持。这一点与 package.json"engines": { "node": ">= 18" } 的声明完全一致,当前仓库版本为 3.2.1

你可以使用喜欢的版本管理器(如 nvm)快速安装受支持的 Node 版本:

$ nvm install 22
$ npm i koa
$ node my-koa-app.js

Application:Koa 应用的核心对象

一个 Koa 应用是一个包含中间件函数数组的对象,这些中间件会在收到请求时被组合(compose)并以栈的方式执行。Koa 与 Ruby 的 Rack、Connect 等中间件体系类似,但做了一个关键设计决策:在原本低级的中间件层之上提供高层"语法糖",包括内容协商(content-negotiation)、缓存新鲜度判断(cache freshness)、代理(proxy)支持、重定向(redirection)等常用任务的方法。尽管提供了相当多的辅助方法,Koa 依然保持小巧的体积——它不捆绑任何中间件

最经典的 hello world 应用如下:

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

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

app.listen(3000);

从源码结构看,这个示例的每一步都有对应的实现:

  • new Koa() 创建的是 class Application extends Emitter,见 构造函数,它继承自 Node.js 的 EventEmitter——这正是后文 app.on('error', ...) 可用的原因;
  • app.use(fn) 只是把函数推入 this.middleware 数组并返回 this,见 use 实现,非函数参数会抛出 TypeError('middleware must be a function!')
  • ctx.body = 'Hello World' 赋值后由 respond 辅助函数 统一写入 res,支持字符串、Buffer、Stream、Blob、JSON 等 body 形态。

中间件级联(Cascading):next() 的上下游执行流

Koa 的中间件以传统方式级联,这在 Node 早期基于回调的阶段很难写得友好,而 async 函数让"真正的"中间件成为可能。与 Connect 那种"把控制权沿函数序列传递直到某个函数返回"的实现不同,Koa 的中间件会调用 next() 向下游(downstream)走,之后控制流再向上游(upstream)回流

官方示例:应用最终响应 "Hello World",但请求先流经 x-response-timelogging 两个中间件标记请求开始时间,然后把控制权让给响应中间件。当一个中间件调用 next() 时,该函数挂起并把控制权交给下一个定义的中间件;当下游没有更多中间件可执行时,调用栈展开,每个中间件被恢复执行以完成它的上游逻辑。

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

// logger

app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// 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`);
});

// response

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

app.listen(3000);

这段示例的执行顺序是 logging(前) → x-response-time(前) → response → x-response-time(后) → logging(后)。测试用例 compose.test.jscalls 数组验证了这一点:两个中间件按"前置、后置"交错记录,最终断言 calls 深等于 [1, 2, 3, 4]

底层组合逻辑在 app.callback() 中完成:

callback () {
  const fn = this.compose(this.middleware)
  // ...
  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res)
    // ...
    return this.ctxStorage.run(ctx, async () => {
      return await this.handleRequest(ctx, fn)
    })
  }
  return handleRequest
}

其中 this.compose 默认为 koa-composepackage.json 依赖中为 ^4.1.0),负责把中间件数组组合成一个洋葱模型的可调用函数;handleRequest 先把 res.statusCode 预置为 404(无中间件设置 body 时的兜底状态),再执行 fnMiddleware(ctx).then(handleResponse).catch(onerror),即中间件 Promise 链成功后调用 respond(ctx) 写响应,失败则交给 ctx.onerror(err)。值得注意的是,构造函数支持传入自定义的 options.compose(见 构造函数 的 JSDoc 参数列表),测试用例 compose.test.js 中的第二个用例就演示了用自实现组合函数替代默认 compose 并得到相同的 [1, 2, 3, 4] 调用顺序。

应用配置项(Settings)

应用配置是 app 实例上的属性。官方文档列出当前支持的配置项,并与 构造函数源码 中的默认值逐一对应:

配置项 默认值 说明 源码依据
app.env NODE_ENV,缺省为 "development" 应用运行环境 this.env = options.env || process.env.NODE_ENV || 'development'
app.keys 签名 Cookie 使用的密钥数组 构造函数中 if (options.keys) this.keys = options.keys
app.proxy false true 时信任代理头字段 this.proxy = options.proxy || false
app.subdomainOffset 2 ctx.subdomains 忽略的偏移量 this.subdomainOffset = options.subdomainOffset || 2
app.proxyIpHeader 'X-Forwarded-For' 读取代理 IP 的请求头名 this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'
app.maxIpsCount 0(表示不限) 从代理 IP 头中最多读取的 IP 数量 this.maxIpsCount = options.maxIpsCount || 0
app.asyncLocalStorage 未启用 true 或一个 AsyncLocalStorage 实例以启用异步本地存储 构造函数中创建 this.ctxStorage

配置既可以通过构造函数传入:

const Koa = require('koa');
const app = new Koa({ proxy: true });

也可以动态修改:

const Koa = require('koa');
const app = new Koa();
app.proxy = true;

两种写法的等价性由测试用例 index.test.js 验证,其中分别覆盖了 envproxykeyssubdomainOffset 等选项经构造函数注入后属性值正确的断言。

proxy / proxyIpHeader / maxIpsCount 的实际作用:当 app.proxytrue 时,ctx.ips 才会解析代理头。见 request.js 中的 ips getter

get ips () {
  const proxy = this.app.proxy
  const val = this.get(this.app.proxyIpHeader)
  let ips = proxy && val
    ? splitCommaSeparatedValues(val)
    : []
  if (this.app.maxIpsCount > 0) {
    ips = ips.slice(-this.app.maxIpsCount)
  }
  return ips
}

即:只有 proxy 为真且存在 proxyIpHeader 头时才解析逗号分隔的 IP 列表(如 "client, proxy1, proxy2" 得到 ["client", "proxy1", "proxy2"],其中 proxy2 是最下游);maxIpsCount > 0 时只保留列表末尾的 N 个 IP。ctx.ip 则取 ips[0],否则回退到 socket.remoteAddress(见 ip getter)。

subdomainOffset 的实际作用ctx.subdomains 默认把主机名的最后两段视为主域名。如主机名为 tobi.ferrets.example.com,未设置时 this.subdomains["ferrets", "tobi"];设置 app.subdomainOffset = 3 后变为 ["tobi"]。实现见 subdomains getterhostname.split('.').reverse().slice(offset)

app.listen(...)

Koa 应用与 HTTP 服务器不是一一对应的关系:一个或多个 Koa 应用可以挂载在一起,共享同一个 HTTP 服务器。

app.listen(...) 创建一个 HTTP 服务器并返回,参数透传给 Node.js 的 Server#listen()。最简示例是把应用绑定到端口 3000:

const Koa = require('koa');
const app = new Koa();
app.listen(3000);

app.listen(...) 本质上是下面代码的语法糖(与源码 listen 实现 完全一致):

const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);

这意味着同一个应用可以同时以 HTTP 和 HTTPS 启动,或监听多个地址:

const http = require('http');
const https = require('https');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
https.createServer(app.callback()).listen(3001);

app.callback()

app.callback() 返回一个适合传给 http.createServer() 的回调函数来处理请求。你也可以利用这个回调把 Koa 应用挂载(mount)到 Connect/Express 应用中,从而把 Koa 当作子应用嵌入既有 Node.js 服务。

从源码结构看,callback() 每次调用都会基于当前 this.middleware 重新组合出处理函数,并在首次调用时注册默认错误监听器 if (!this.listenerCount('error')) this.on('error', this.onerror)(见 callback 实现)。

app.use(function)

向应用添加给定的中间件函数。app.use() 返回 this,因此可以链式调用。以下两种写法等价:

app.use(someMiddleware)
app.use(someOtherMiddleware)
app.listen(3000)
app.use(someMiddleware)
  .use(someOtherMiddleware)
  .listen(3000)

链式能力正是 use 实现 末尾 return this 带来的。

app.keys=

设置签名 Cookie 的密钥。这些密钥会传递给 KeyGrip 组件,也可以直接传入你自己的 KeyGrip 实例。以下两种写法均可接受:

app.keys = ['OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg', 'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7'];
app.keys = new KeyGrip(['OEK5zjaAMPc3L6iK7PyUjCOziUH3rsrMKB9u8H07La1SkfwtuBoDnHaaPCkG5Brg', 'MNKeIebviQnCPo38ufHcSfw3FFv8EtnAe1xE02xkN1wkCV1B2z126U44yk2BQVK7'], 'sha256');

出于安全考虑,请确保密钥足够长且随机。这些密钥支持轮换(rotation),并在以 { signed: true } 选项签名 Cookie 时被使用:

ctx.cookies.set('name', 'tobi', { signed: true });

app.keys 如何被消费可以在 context.js 的 cookies getter 中看到:ctx.cookies 惰性创建 new Cookies(this.req, this.res, { keys: this.app.keys, secure: this.request.secure }),即签名密钥在每次访问 ctx.cookies 时从应用实例读取。

app.context

app.context 是创建每个 ctx 时所用的原型对象。你可以通过编辑 app.contextctx 增加额外属性。这在"给 ctx 添加跨整个应用共用的属性或方法"的场景下有用,可能更省性能(无需中间件)或更方便(减少 require()),代价是更深地依赖 ctx,这可能被视为反模式。

例如,从 ctx 上引用你的数据库:

app.context.db = db();

app.use(async ctx => {
  console.log(ctx.db);
});

注意两点(与 createContext 源码 相互印证):

  • ctx 上的许多属性是通过 getter、setter 和 Object.defineProperty() 定义的,你只能通过 Object.defineProperty()app.context 上编辑它们(且不推荐这么做);
  • 被挂载(mounted)的应用当前复用父应用的 ctx 与配置,因此挂载应用实际上只是一组中间件的集合。

从源码看,createContext 通过 Object.create(this.context) 创建新的 context 原型链,并一次性装配 requestresponseappreqresoriginalUrlstate 等字段,这解释了为什么修改 app.context 会影响之后所有请求的 ctx

app.currentContext(Koa v3 新特性)

如果启用了 asyncLocalStorageapp.currentContext 会返回当前请求的上下文。例如:

const app = new Koa({ asyncLocalStorage: true })

app.use(async (ctx, next) => {
  callSomeFunction()
})

function callSomeFunction () {
  const ctx = app.currentContext /* 即上面中间件的 ctx */
}

v3.1.0 开始,你还可以传入自己的 AsyncLocalStorage 实例:

const asyncLocalStorage = new AsyncLocalStorage()
const app = new Koa({ asyncLocalStorage })

app.use(async (ctx, next) => {
  callSomeFunction()
})

function callSomeFunction () {
  const ctx = asyncLocalStorage.getStore()
}

它的典型用途是把请求 ID、用户身份等关键请求信息传递给内部服务,而不必层层透传参数。

源码实现印证了这一机制:

  • currentContext getter 只是 this.ctxStorage.getStore(),即 AsyncLocalStorage 中当前异步上下文存储的值;
  • 构造函数通过 getAsyncLocalStorage(options) 区分 true(新建实例)与自定义实例两种情况(见 构造函数);
  • callback 中的 handleRequestthis.ctxStorage.run(ctx, ...)ctx 绑定进整个请求处理过程的异步链路。

测试用例 currentContext.test.js 覆盖了几个关键行为:启用后中间件内(包括跨 setTimeout/setImmediate 等异步边界后)app.currentContext === ctx;请求处理之外(请求前后)该值为 undefined;未启用时恒为 undefined;错误处理器 app.on('error', (err, ctx) => ...) 中同样能取到 ctx;以及支持自定义 AsyncLocalStorage 实例。此外,源码对 v8 启动快照v8.startupSnapshot.isBuildingSnapshot())场景做了特殊处理——构建快照时推迟到反序列化回调中再创建 AsyncLocalStorage,相关测试也在同一文件中。

错误处理(Error Handling)

默认情况下,所有错误都会输出到 stderr,除非 app.silenttrue。默认错误处理器在 err.status404err.exposetrue 时也不输出。要执行自定义错误处理逻辑(如集中日志),可以添加 "error" 事件监听器:

app.on('error', err => {
  log.error('server error', err)
});

如果错误发生在 req/res 周期内、且已经无法响应客户端(如响应头已发送),Context 实例也会被一并传入:

app.on('error', (err, ctx) => {
  log.error('server error', err, ctx)
});

当错误发生且仍可以响应客户端(即没有数据写入 socket)时,Koa 会以合适的 500 "Internal Server Error" 响应;无论哪种情况,都会为日志目的触发应用级的 "error" 事件。

两个层面的源码实现对应这一行为:

应用级默认处理器 Application.prototype.onerror

onerror (err) {
  const isNativeError =
    Object.prototype.toString.call(err) === '[object Error]' ||
    err instanceof Error
  if (!isNativeError) { throw new TypeError(util.format('non-error thrown: %j', err)) }

  if (err.status === 404 || err.expose) return
  if (this.silent) return

  const msg = err.stack || err.toString()
  console.error(`\n${msg.replace(/^/gm, '  ')}\n`)
}

可见"非 Error 抛出物会被包装为 TypeError"、"404/expose/silent 时静默"均在此实现;行为细节由测试 onerror.test.js 逐项验证(如 app.onerror('foo')TypeError: non-error thrown: foostatus: 404 时不写 stderr、app.silent = true 时不写 stderr 等)。

上下文级处理器 Context.prototype.onerror 负责真正面向客户端的响应:先 this.app.emit('error', err, this)(err, ctx) 分发给应用级监听器;若响应头已发送(headerSent)则无从补救,直接返回;否则清空已有响应头、按 err.headers 重设、强制 text/plain 类型,把无效状态码归一化为 500,并根据 err.expose 决定对外暴露 err.message 还是标准状态码文案。而 socket 层面的错误(如连接中途被破坏)则经由 handleRequest 中的 onFinished(res, onerror) 注册,同样汇入 app.emit('error', ...)

小结

Application 类是 Koa 一切能力的入口:中间件栈(app.use)+ koa-compose 组合出洋葱模型执行流;一组可在构造时或运行时修改的配置项(envkeysproxysubdomainOffsetproxyIpHeadermaxIpsCountasyncLocalStorage)控制请求解析与 Cookie 签名行为;app.listen / app.callback 让同一应用可灵活挂载到多个 HTTP(S) 服务器;app.context 提供 ctx 原型级的扩展点;app.currentContext(v3)基于 AsyncLocalStorage 实现无侵入的请求上下文透传;app.on('error') 与两级 onerror 处理器构成默认错误处理链。上述所有行为均可在当前仓库的 lib/application.jslib/context.jslib/request.js__tests__/application/ 下的测试用例中逐行核对,适合作为二次开发(如自定义 compose、挂载子应用、接入内部服务)时的行为基线。

登录后查看全文
热门项目推荐
相关项目推荐