Koa和Express的区别

前置核心概念

  1. Express :线性瀑布式中间件模型,基于回调,无原生 async/await 支持;
  2. Koa2 :洋葱圈中间件模型,基于 async/await + compose,原生支持异步;
  3. 同步:代码无定时器、IO、数据库、Promise;
  4. 异步:定时器、文件读写、数据库、网络请求、Promise、async 函数。

统一测试需求:

3 层中间件,要求:

  • 中间件1:前置拼接 a,后置输出最终结果
  • 中间件2:拼接 b
  • 中间件3:拼接 c
    最终在中间件1后置打印 message = abc

一、同步场景:Express vs Koa(无异步操作)

1. Express 同步中间件

js 复制代码
const express = require('express')
const app = express()

app.use((req, res, next) => {
  req.msg = 'a'
  console.log('Express 中间件1 前置')
  next()
  // next之后的代码,所有下游中间件执行完才执行
  console.log('Express 中间件1 后置:', req.msg)
})

app.use((req, res, next) => {
  req.msg += 'b'
  console.log('Express 中间件2 前置')
  next()
  console.log('Express 中间件2 后置')
})

app.use((req, res) => {
  req.msg += 'c'
  console.log('Express 中间件3 执行完毕')
  res.send(req.msg)
})

app.listen(3000)

执行顺序(同步无任何异步)

复制代码
Express 中间件1 前置
Express 中间件2 前置
Express 中间件3 执行完毕
Express 中间件2 后置
Express 中间件1 后置: abc

同步下 Express 特点

  1. 同步代码下,执行流程和Koa洋葱模型表现一致
  2. next() 是同步跳转,会立刻执行下一个中间件;
  3. next() 后面代码会等所有下游中间件全部走完再回头执行;
  4. 同步场景二者行为几乎无差异。

2. Koa 同步中间件

js 复制代码
const Koa = require('koa')
const app = new Koa()

app.use((ctx, next) => {
  ctx.msg = 'a'
  console.log('Koa 中间件1 前置')
  next()
  console.log('Koa 中间件1 后置:', ctx.msg)
})

app.use((ctx, next) => {
  ctx.msg += 'b'
  console.log('Koa 中间件2 前置')
  next()
  console.log('Koa 中间件2 后置')
})

app.use(ctx => {
  ctx.msg += 'c'
  console.log('Koa 中间件3 执行完毕')
  ctx.body = ctx.msg
})

app.listen(8000)

输出顺序和Express完全一致

复制代码
Koa 中间件1 前置
Koa 中间件2 前置
Koa 中间件3 执行完毕
Koa 中间件2 后置
Koa 中间件1 后置: abc

同步场景小结

同步代码时,Express 和 Koa 表现完全相同

自上而下执行 next() 前代码,全部走完后,自下而上执行 next() 后代码。

差异只在异步场景爆发。


二、异步场景:Express(巨大缺陷)vs Koa(完美可控)

场景设定:中间件2中加入异步延时操作(模拟数据库/接口请求)

js 复制代码
// 模拟异步IO
function sleep(time) {
  return new Promise(resolve => setTimeout(resolve, time))
}

1. Express 异步中间件(重大问题)

js 复制代码
const express = require('express')
const app = express()

app.use((req, res, next) => {
  req.msg = 'a'
  console.log('Express 中间件1 前置')
  next()
  // 异步导致此处提前执行,拿不到完整abc
  console.log('Express 中间件1 后置:', req.msg)
})

app.use(async (req, res, next) => {
  req.msg += 'b'
  console.log('Express 中间件2 前置')
  // 异步延时2秒
  await sleep(2000)
  next()
  console.log('Express 中间件2 后置')
})

app.use((req, res) => {
  req.msg += 'c'
  console.log('Express 中间件3 执行完毕')
  res.send(req.msg)
})

app.listen(3000)

执行输出顺序(错误)

less 复制代码
Express 中间件1 前置
Express 中间件2 前置
Express 中间件1 后置: a  // 重点:异步阻塞前直接回头执行后置!
// 等待2秒后
Express 中间件3 执行完毕
Express 中间件2 后置

问题分析

  1. Express 不识别 async/awaitnext() 调用后直接同步返回,不会等待异步代码完成;
  2. 中间件2内部 await sleep 阻塞时,事件循环让出,Express 直接回到中间件1执行 next() 后面的代码;
  3. 中间件1后置代码提前执行,req.msg 只有 a,拿不到最终拼接结果;
  4. 无法实现"等所有下游逻辑完成再执行后置"的需求。

Express 异步解决方案(丑陋)

必须手动在异步结束后调用 next(),强行嵌套回调,回调地狱:

js 复制代码
app.use((req, res, next) => {
  req.msg += 'b'
  console.log('中间件2 前置')
  setTimeout(() => {
    req.msg += '延迟b'
    next() // 异步完成后再放行
  }, 2000)
})

缺点:多层异步会无限嵌套,流程不可读、难以维护。

2. Koa 异步中间件(原生支持,流程不乱)

js 复制代码
const Koa = require('koa')
const app = new Koa()

function sleep(time) {
  return new Promise(resolve => setTimeout(resolve, time))
}

app.use(async (ctx, next) => {
  ctx.msg = 'a'
  console.log('Koa 中间件1 前置')
  await next() // 等待所有下游中间件全部执行完成才往下走
  console.log('Koa 中间件1 后置:', ctx.msg)
})

app.use(async (ctx, next) => {
  ctx.msg += 'b'
  console.log('Koa 中间件2 前置')
  await sleep(2000) // 异步阻塞,不会跳出当前中间件
  await next()
  console.log('Koa 中间件2 后置')
})

app.use(ctx => {
  ctx.msg += 'c'
  console.log('Koa 中间件3 执行完毕')
  ctx.body = ctx.msg
})

app.listen(8000)

正确输出顺序

arduino 复制代码
Koa 中间件1 前置
Koa 中间件2 前置
// 等待2秒
Koa 中间件3 执行完毕
Koa 中间件2 后置
Koa 中间件1 后置: abc

Koa 异步核心原理

  1. Koa 使用 compose 组合中间件,所有中间件被包装成 Promise 调用链;
  2. await next() 会暂停当前中间件,完整等待下游所有中间件(含异步)全部执行完毕,才恢复当前中间件剩余代码;
  3. 无论中间件内部有多少层异步、定时器、数据库操作,洋葱模型执行顺序永远稳定不变;
  4. 无回调嵌套,线性代码书写复杂异步流程。

三、同步/异步场景完整异同对照表

1. 相同点

  1. 同步代码场景
    • 两者执行顺序完全一致:自上而下前置 → 自下而上后置;
    • next() 同步跳转,下游全部执行完再回头执行后置逻辑;
    • 简单同步接口(无数据库/文件)表现无区别。
  1. 基础能力一致
    • 都基于 Node http 模块封装;
    • 都依靠中间件处理请求;
    • 都支持多层中间件堆叠。

2. 不同点(分同步、异步区分)

维度 Express Koa2
同步执行逻辑 线性流转,next后代码后置执行 洋葱流转,和Express同步表现一致
异步执行逻辑 next() 同步返回,不会等待异步;异步阻塞会直接跳出当前中间件,后置代码提前执行,流程错乱 await next() 阻塞等待全部下游异步完成,洋葱顺序永久稳定
异步语法支持 原生不支持async/await,需回调嵌套,极易产生回调地狱 原生基于Promise+async/await,无嵌套
中间件底层实现 数组顺序遍历,线性瀑布模型 compose递归调用,Promise链式洋葱模型
上下文对象 req、res分离,全局挂载数据容易冲突 统一ctx上下文,一次请求独立ctx,数据隔离
异步错误捕获 异步内部抛出错误无法被全局错误捕获,必须手动try/catch传err给next(err) 全局可捕获async中间件抛出的错误,统一error事件处理
多异步串联 多层异步必须嵌套,可读性极差 平铺书写,await顺序执行,逻辑清晰

四、异步错误处理对比(补充关键差异)

Express 异步错误捕获缺陷

js 复制代码
app.use(async (req, res, next) => {
  // 异步抛出错误,Express 捕获不到,直接崩溃
  throw new Error('数据库查询失败')
  next()
})

解决方式:必须手动 try/catch + next(err)

js 复制代码
app.use(async (req, res, next) => {
  try {
    await sleep(1000)
    throw new Error('异常')
  } catch (err) {
    next(err) // 手动传递错误
  }
})
// 错误中间件接收
app.use((err, req, res, next) => {
  res.status(500).send(err.message)
})

Koa 异步错误天然支持

js 复制代码
app.use(async (ctx, next) => {
  await sleep(1000)
  throw new Error('数据库异常')
})
// 全局统一捕获,无需手动传递
app.on('error', (err, ctx) => {
  ctx.status = 500
  ctx.body = { msg: err.message }
})

所有 async 中间件抛出的异常都会被 Koa 内部 Promise 捕获,自动触发全局 error 事件。

五、总结核心结论

  1. 只写同步代码:Express 和 Koa 几乎没有区别,洋葱/瀑布执行效果一样;
  2. 项目存在大量异步(数据库、Redis、文件、接口) :二者出现本质鸿沟:
    • Express:异步会打乱中间件执行顺序,回调嵌套、错误处理繁琐;
    • Koa:依靠 await next() 固定洋葱执行流,异步代码平铺书写,错误统一捕获;
  1. 核心根源:
    Express 中间件只是普通回调函数,无 Promise 封装;
    Koa 通过 compose 将所有中间件包装成 Promise 调用链,让异步流程可控。
相关推荐
MariaH1 小时前
Koa框架的使用
后端
luckdewei2 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某4 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy4 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom4 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
用户1474853079748 小时前
CodeX使用Skill生成游戏美术和音乐资源,一分钟入门
后端
Melody1238 小时前
用 abort 中断 AI 流式请求,我之前做错了
后端
onething3659 小时前
Spring Boot + Spring AI 从入门到实战:7天转型计划 Day 5 —— SSE 流式输出 + 打字机效果
人工智能·后端·全栈
一个做软件开发的牛马9 小时前
MyBatis-Plus 从零实战:完整搭建可运行 Demo,BaseMapper 零 SQL、Wrapper 条件构造、分页插件与代码生成器详解
java·后端