首页
/ Koa 从 v1 迁移到 v2 完整指南:中间件签名变更、生成器兼容与源码级原理解析

Koa 从 v1 迁移到 v2 完整指南:中间件签名变更、生成器兼容与源码级原理解析

2026-09-05 22:31:58作者:齐添朝

本篇技术文章基于 Koa 仓库中的官方迁移文档 docs/migration-v1-to-v2.md 展开,系统讲解 Koa 从 v1.x 升级到 v2.x 的全部破坏性变更:新的中间件签名(async (ctx, next) => await next())、生成器中间件的 koa-convert 过渡方案、业务代码重构建议,以及 new 关键字、错误处理与依赖变化等细节。读完后,你将掌握一条可落地的中间件升级路径,并能结合当前仓库源码(lib/application.jstests/application/use.test.js)验证迁移后的行为是否正确。

一、迁移背景:v2 为何重写中间层

Koa v2 的核心动机是用 ES2017 的 async/await 替换 v1 基于 co + 生成器(generator)的协程模型。从仓库的 History.md 可以看到这条演进脉络:

  • 2.0.0-alpha.1将中间件签名改为 async (ctx, next) => await next(),放弃 Node < 4 支持,用 ES6 重写整个代码库
  • 2.0.0引入 koa-convert,让生成器函数在 v2 中继续可用,但明确标注"generator functions are deprecated in v2 and will be removed in v3";
  • 3.0.0-alpha.0(当前仓库状态,版本 3.2.1):彻底移除生成器支持,"Koa no longer asserts if generators are used"。

因此,这份 v1→v2 迁移文档对今天仍有价值:它是理解当前 Koa 中间件模型(Readme.md 中描述的 "async function / common function" 两种形态)的起点,而 v2 中保留的生成器兼容层,则正是后来 v3 移除的对象(详见 docs/migration-v2-to-v3.md)。

二、新中间件签名:从 function* (next)async (ctx, next)

v1 的中间件签名是 function* (next) { ... yield next ... },通过 this 访问上下文。v2 的新签名是:

// 使用 async 箭头函数
app.use(async (ctx, next) => {
  try {
    await next() // next 现在是函数
  } catch (err) {
    ctx.body = { message: err.message }
    ctx.status = err.status || 500
  }
})

app.use(async ctx => {
  const user = await User.getById(this.session.userid) // await 替代 yield
  ctx.body = user // ctx 替代 this
})

新签名有三个关键变化:

  1. await 替代 yield:所有异步等待点从 yield promise 变为 await promise
  2. ctx 通过显式参数传入,替代 this:这是最重要的语义变化。迁移文档特别指出,显式传参让 Koa 与 ES6 箭头函数完全兼容——箭头函数捕获外部作用域的 this,v1 中依赖 this 的写法在箭头函数里会失效,而 v2 的 ctx 参数则没有这个问题;
  3. next 从"可 yield 的值"变为"返回 Promise 的函数":调用方式是 await next()next().then(...)

需要注意:Koa 并不强制使用 async function任何返回 Promise 的函数都合法Readme.md 明确列出了两种被接受的中间件形态——async function 与普通函数:

// 普通函数,直接返回 next() 的 Promise
app.use((ctx, next) => {
  const start = Date.now();
  return next().then(() => {
    const ms = Date.now() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  });
});

源码佐证:Koa 如何执行中间件

从当前仓库的 lib/application.js 看,app.use() 的校验逻辑非常简洁——只要求是函数,不做任何生成器检测(v2 时代的自动转换层在 v3 已被移除):

use (fn) {
  if (typeof fn !== 'function') { throw new TypeError('middleware must be a function!') }
  debug('use %s', fn._name || fn.name || '-')
  this.middleware.push(fn)
  return this
}

真正驱动中间件执行的是 callback() 中通过 koa-compose 组合出的 fn,以及 handleRequest()fnMiddleware(ctx).then(handleResponse).catch(onerror) 的 Promise 链——这里要求组合结果必须是 thenable,这正是"返回 Promise 的普通函数也能当中间件"的底层原因。测试文件 tests/application/use.test.js 中的 should compose mixed middleware 用例专门验证了 async 函数与普通函数混合堆叠时,调用顺序为 [1, 2, 3, 4, 5, 6](洋葱模型:先逐层进入,再逐层返回)。

三、在 v2 中使用 v1 中间件:koa-convert 过渡方案

v2 发布时并未一刀切砍掉生成器中间件。迁移文档说明:Koa v2 会在 app.use 时尝试用 koa-convert 把旧签名的生成器中间件自动转换History.md2.0.0 的 changelog 也印证了这一点("include koa-convert so that generator functions still work")。因此一段纯 v1 代码在 v2 中可以直接运行:

// Koa (v2) 会自动转换
app.use(function *(next) {
  const start = Date.now();
  yield next;
  const ms = Date.now() - start;
  console.log(`${this.method} ${this.url} - ${ms}ms`);
});

也可以手动转换,手动转换后 Koa 不再介入:

const convert = require('koa-convert');

app.use(convert(function *(next) {
  const start = Date.now();
  yield next;
  const ms = Date.now() - start;
  console.log(`${this.method} ${this.url} - ${ms}ms`);
}));

但文档同时给出明确建议:应尽快把所有 v1 中间件迁移到新签名——这个"尽快"后来被证实是准确的:当前仓库(v3)已在 History.md3.0.0-alpha.0 记录中移除了生成器支持,lib/application.js 中也已不存在任何 convert 调用。若你的目标环境是 v3,请直接阅读 docs/migration-v2-to-v3.md,其中给出了 koa-convert 用法到 async/await 的逐行对照改写。

四、升级自有中间件:逐步替换路线图

对于自己维护的生成器中间件,需要逐个改写为 async 函数。以迁移文档中的示例为例,改写前后对照:

// v2 目标写法
app.use(async (ctx, next) => {
  const user = await Users.getById(this.session.user_id);
  await next();
  ctx.body = { message: 'some message' };
})

改写要点:function* (next)async (ctx, next)yield nextawait next()this.xxxctx.xxxyield 异步操作await 异步操作

文档给出了一条经过验证的、可按项目规模伸缩的升级路径——先全部套壳、再逐个拔壳

  1. 把所有现有中间件用 koa-convert 包裹(应用立即可在 v2 上跑起来);
  2. 完整测试;
  3. 运行 npm outdated,查看哪些 Koa 生态中间件版本过旧;
  4. 升级其中一个过旧中间件到 v2 版本,并移除它外面的 koa-convert 包裹;
  5. 再次测试;
  6. 重复第 3–5 步,直到全部完成。

这条路径的价值在于:每一步之后应用都处于可测试、可回滚的状态,避免了"一次性大爆炸式重写"。

五、业务代码重构建议:为迁移铺路

迁移文档还提出了若干在 v1 阶段就可以提前执行的重构建议,用来降低后续迁移成本:

  • 让代码处处返回 Promise:这是从 yield 迁移到 await 的前提,因为 await 只能作用于 thenable;
  • 不要使用 yield*:生成器解包语法在 Promise 世界没有对应物,提前消灭可减少迁移时的排查面;
  • 不要使用 yield {}yield []co 对对象/数组的并行解包):
    • yield [] 改写为 yield Promise.all([])
    • yield {} 改写为 yield Bluebird.props({})
  • 把逻辑移出中间件函数:创建形如 function* someLogic(ctx) {} 的独立函数,在中间件中调用 const result = yield someLogic(this)。这样做的额外收益是——显式传参 ctx 后不再依赖 this,与 v2 新签名"不传 this"的设计天然对齐,迁移时中间件主体几乎不用动。

六、Application 构造函数:必须使用 new

v1 中 Application 是一个可以不带 new 直接调用的构造函数;v2 改用 ES6 class 实现,new 变成强制要求。改写对比:

// v1.x:不加 new
var koa = require('koa');
var app = module.exports = koa();

// v2.x:必须加 new
var koa = require('koa');
var app = module.exports = new koa();

当前仓库源码印证了这一设计:lib/application.jsmodule.exports = class Application extends Emitter,构造函数 constructor(options) 完成了 envproxysubdomainOffsetmiddleware = []context/request/response 原型初始化等全部工作。类继承自 events.Emitteronerror 通过 if (!this.listenerCount('error')) this.on('error', this.onerror) 挂载——ES6 class 无法像 v1 那样被无 new 调用,这是语言层面的必然结果。

七、其他破坏性变更

移除 ENV 特定的日志行为

v1 的错误处理中有一段针对 test 环境的显式判断(测试环境下静默)。v2 移除了该检查。从当前 onerror 实现 看,如今决定"是否打印错误"的条件只剩三类:err.status === 404 || err.expose 时直接返回、this.silent 为真时返回、非原生 Error 对象则抛出 TypeError;不存在任何基于 this.env === 'test' 的分支。这意味着迁移后,测试环境下的未处理错误也会像生产环境一样走到默认错误处理器,需要自行保证测试隔离。

依赖变化

  • co 不再随 Koa 打包:若业务代码仍直接用 co 运行 Promise/生成器,需自行 require('co')。当前 package.jsondependencies 中已无 co,取而代之的是 koa-compose: ^4.1.0(见 lib/application.jsconst compose = require('koa-compose')),中间件组合逻辑完全 Promise 化;
  • composition 不再使用且已废弃:v1 时代的 thenables/composition 依赖被彻底移除。

八、迁移验证清单与后续路线

完成迁移后,可以用以下仓库中的测试用例作为行为基线进行自检:

  1. 洋葱模型顺序:多层中间件的进入/返回顺序应为 [1, 2, 3, 4, 5, 6],对应 tests/application/use.test.js
  2. async 与普通函数混用:两种形态的中间件可以共存于同一应用,见 tests/application/use.test.jsshould compose mixed middleware
  3. 非 Promise 中间件的异常捕获:普通(非 async)函数中抛出的错误同样会被中间件链捕获(should catch thrown errors in non-async functions),这保证了错误处理代码在 yieldawait 改写后行为一致;
  4. 类型校验app.use 传入非函数会抛出 TypeError: middleware must be a function!lib/application.js对应测试)。

v1.x 长期分支与后续文档

按迁移文档说明,v1.x 分支虽继续受支持,但不再接收功能更新(除该迁移指南外,官方文档一律面向最新版本)。对当前仓库的读者而言,完整的版本演进链条是:

  • 本仓库 docs/migration-v1-to-v2.md:v1 → v2 的签名与依赖迁移(本文主体);
  • docs/migration-v2-to-v3.md:v2 → v3 的破坏性变更(Node.js >= 18、移除生成器支持、ctx.throw(status, error, properties) 新签名、ctx.back() 替代 redirect('back')、querystring 换用 URLSearchParams 等),当前 package.jsonengines.node >= 18version: 3.2.1 即对应该文档描述的 v3 状态。

如果你在实际迁移中遇到本指南未覆盖的问题,文档建议向仓库提交文档 PR——这类实战经验本身也是迁移指南持续完善的主要来源。

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