1、Koa2初体验:Node.js 异步进化史
大家好,欢迎来到《Koa2框架实战》的第一章。
说实话,每次带新人入门Node.js后端,我都会先问一个问题:「你被回调地狱折磨过吗?」 如果你写过一段时间的Node.js,大概率会心一笑。今天我们就从这段「血泪史」开始,一步步走到Koa2的世界里。
1.1 异步进化史:从回调到async/await
Node.js从诞生起就带着异步基因。但早期的异步写法,嗯,确实有点「原始」。
1.1.1 回调函数时代
我刚入行那会儿,项目里到处都是嵌套的回调。举个例子,读取一个文件再处理内容:
const fs = require('fs');
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取失败:', err);
return;
}
// 处理数据...
console.log(data);
});
单看这个还好。但如果你要依次读取三个文件呢?
fs.readFile('a.txt', 'utf8', (err, dataA) => {
fs.readFile('b.txt', 'utf8', (err, dataB) => {
fs.readFile('c.txt', 'utf8', (err, dataC) => {
// 终于拿到所有数据了...
console.log(dataA, dataB, dataC);
});
});
});
这就是传说中的「回调地狱」。代码往右偏移,逻辑越来越乱。我在项目中遇到过最夸张的一次,嵌套了7层回调,改个bug花了整整一下午。为什么会这样?因为回调函数把控制流打散了,你很难一眼看出执行顺序。
1.1.2 Promise 登场
后来社区搞出了 Promise,说白了就是把回调「扁平化」。还是刚才的例子:
const fs = require('fs').promises;
fs.readFile('a.txt', 'utf8')
.then(dataA => {
return fs.readFile('b.txt', 'utf8');
})
.then(dataB => {
return fs.readFile('c.txt', 'utf8');
})
.then(dataC => {
console.log(dataC);
})
.catch(err => {
console.error('出错了:', err);
});
你看,嵌套变成了链式调用。错误处理也统一到了 catch 里。我个人习惯用 Promise 处理一些简单的异步流程,比如并发请求用 Promise.all:
Promise.all([
fs.readFile('a.txt', 'utf8'),
fs.readFile('b.txt', 'utf8'),
fs.readFile('c.txt', 'utf8')
]).then(([dataA, dataB, dataC]) => {
console.log(dataA, dataB, dataC);
});
但 Promise 也有个小问题——then 链一长,可读性还是会下降。你想想看,如果中间有 5 个异步步骤,每个步骤还要处理数据,代码就变成了一长串 .then()。
1.1.3 async/await 终极方案
ES2017 带来了 async/await,这简直就是异步编程的「语法糖炸弹」。它让异步代码长得像同步代码:
const fs = require('fs').promises;
async function readFiles() {
try {
const dataA = await fs.readFile('a.txt', 'utf8');
const dataB = await fs.readFile('b.txt', 'utf8');
const dataC = await fs.readFile('c.txt', 'utf8');
console.log(dataA, dataB, dataC);
} catch (err) {
console.error('出错了:', err);
}
}
readFiles();
是不是清爽多了?await 关键字会暂停函数执行,直到 Promise 完成。 注意,await 只能在 async 函数里用。我建议你从现在开始,所有异步操作都优先考虑 async/await,除非你非要兼容老掉牙的 Node.js 版本。
const [a, b, c] = await Promise.all([
fs.readFile('a.txt'),
fs.readFile('b.txt'),
fs.readFile('c.txt')
]);
1.2 Koa2 核心概念:洋葱模型
好,异步进化史讲完了。现在聊聊 Koa2 最核心的东西——洋葱模型。
Koa2 的中间件执行机制,就像切洋葱一样。请求从外层中间件进入,经过内层,再一层层返回。我画个简单的示意图:
请求 --> 中间件1(进入) --> 中间件2(进入) --> 中间件3(处理)
<-- 中间件2(返回) <-- 中间件3(返回)
<-- 中间件1(返回) --> 响应
每个中间件都有两次执行机会:一次在请求进入时,一次在响应返回时。 这是通过 next() 函数实现的。
来看个例子:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('1. 请求进入第一个中间件');
await next(); // 交给下一个中间件
console.log('5. 响应返回第一个中间件');
});
app.use(async (ctx, next) => {
console.log('2. 请求进入第二个中间件');
await next();
console.log('4. 响应返回第二个中间件');
});
app.use(async (ctx) => {
console.log('3. 处理请求');
ctx.body = 'Hello Koa2';
});
app.listen(3000);
访问一下,控制台输出顺序是:1 → 2 → 3 → 4 → 5。这就是洋葱模型!
我在项目中遇到过一个问题:有人把 await next() 写成了 next(),结果中间件顺序全乱了。记住:一定要加 await,否则 next 返回的是 Promise,不会等待它完成。
1.3 搭建第一个 Koa2 应用
理论说完了,咱们动手搭一个真正的 Koa2 应用。别怕,就几步。
1.3.1 环境准备
首先确保你装了 Node.js(建议 v12 以上)。然后创建项目目录:
mkdir my-koa-app
cd my-koa-app
npm init -y
安装 Koa2:
npm install koa
嗯,就这么简单。Koa2 本身非常轻量,核心代码才几百行。它不像 Express 那样内置了路由、模板引擎等,但这也给了我们最大的自由度。
1.3.2 编写入口文件
创建 app.js:
const Koa = require('koa');
const app = new Koa();
// 第一个中间件:记录请求耗时
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// 第二个中间件:设置响应头
app.use(async (ctx, next) => {
ctx.set('X-Powered-By', 'Koa2');
await next();
});
// 第三个中间件:返回响应
app.use(async (ctx) => {
ctx.body = 'Hello, Koa2 World!';
});
app.listen(3000, () => {
console.log('Server running at http://localhost:3000');
});
运行一下:
node app.js
打开浏览器访问 http://localhost:3000,你会看到「Hello, Koa2 World!」。控制台会输出类似:
GET / - 3ms
我个人习惯把中间件按功能拆分到不同文件,但刚开始学,先写在一起就好。
1.3.3 理解 ctx 对象
上面的代码里有个 ctx,它是 Koa2 的上下文对象,封装了 Node.js 的 request 和 response。常用属性:
| 属性 | 说明 | 示例 |
|---|---|---|
| ctx.request | 请求对象 | ctx.request.method, ctx.request.url |
| ctx.response | 响应对象 | ctx.response.body, ctx.response.set() |
| ctx.method | 请求方法(快捷方式) | ctx.method === 'GET' |
| ctx.url | 请求路径(快捷方式) | ctx.url === '/api/users' |
| ctx.body | 响应体(快捷方式) | ctx.body = { name: 'Koa' } |
| ctx.query | 查询参数 | ctx.query.id |
注意,ctx.body 可以赋多种类型:字符串、对象、Buffer、Stream 等。Koa2 会自动设置 Content-Type。比如赋对象时,会自动转为 JSON 并设置 application/json。
1.4 本章小结
这一章我们走完了 Node.js 异步编程的进化史:从回调地狱到 Promise 链式调用,再到优雅的 async/await。然后深入理解了 Koa2 的洋葱模型——这种设计让中间件可以灵活地做前置和后置处理。最后,我们亲手搭建了第一个 Koa2 应用,跑通了从请求到响应的完整流程。
下一章,我们会深入中间件机制,教你如何编写可复用的中间件,以及如何处理错误。到时候见!