上一节把选型说清了,这一节动手:装环境、初始化项目、写下第一颗洋葱,并用命令行完成一次请求穿透。本节是全册唯一从空目录讲起的动手节,后面所有章节都默认这里的工程已经就位——所以哪怕你经验丰富,也建议快速跟跑一遍,确认环境口径一致。
Koa 2.x 要求 Node.js 7.6 以上(async/await 可用),但生产口径建议直接上 Node.js 18 或更高版本——既拿到长期支持,又避开旧版在 HTTP 解析器上的已知问题。检查本机版本:
node -v # 输出形如 v18.19.0 或更高 npm -v
然后建项目目录并安装 Koa。全程不需要全局安装任何东西:
mkdir koa-onion && cd koa-onion npm init -y npm install koa
装完的 package.json 里会出现一个 dependencies 条目,形如 "koa": "^2.15.0"。^ 表示接受同主版本的向上兼容更新;团队协作时建议再补一个锁文件(npm 会自动生成 package-lock.json),保证每个人装到完全相同的版本。
在项目根新建 app.js,写下:
const Koa = require('koa'); const app = new Koa(); // 第一层:记录进出时间,演示洋葱的进层与出层 app.use(async (ctx, next) => { const start = Date.now(); console.log(`--> 进层 ${ctx.method} ${ctx.url}`); await next(); // 把控制权交给下一层,并在出层时回到这里 const ms = Date.now() - start; console.log(`<-- 出层 ${ctx.method} ${ctx.url} 耗时 ${ms}ms`); }); // 第二层:业务响应 app.use(async (ctx) => { ctx.body = { hello: 'koa', now: Date.now() }; }); app.listen(3000, () => { console.log('服务已启动:http://localhost:3000'); });
启动并访问:
node app.js # 另开一个终端 curl http://localhost:3000/ # {"hello":"koa","now":1735689600000} # 服务端日志 # --> 进层 GET / # <-- 出层 GET / 耗时 1ms
对着日志看这段代码,洋葱就活了:请求先进入第一层(打印进层日志),await next() 把控制权交给第二层,第二层给 ctx.body 赋值;第二层结束后控制权回到第一层 next() 之后的位置,第一层在出层阶段算出耗时。进层时还不存在响应,出层时响应已经定型——这就是 Koa 一切机制的舞台,后面九章都在这个舞台上展开。
如果用浏览器访问后立刻刷新页面,你还会注意到两次 now 值不同、且没有缓存——这是因为 Koa 对 JSON 响应默认不加缓存头,缓存策略是第 5.3 节的主题。
教程前几章为了聚焦不拆目录,但真实项目建议一开始就立好最小约定。一个被大量 Koa 项目验证过的结构长这样:
koa-onion/ ├── app.js 入口:创建实例、装配中间件、监听端口 ├── middleware/ 自定义中间件:日志、错误兜底、鉴权 ├── routes/ 路由定义:按资源拆文件 ├── services/ 业务逻辑:路由层保持薄,逻辑下沉 ├── package.json
约定只有一条原则:入口文件只负责装配顺序,不写业务。app.js 里应该一眼看清这个应用有几层洋葱、按什么顺序叠——第 3 章讲层序控制时,你会感谢这条约定。services 与 routes 的分工同理:路由层只做"接参数、调服务、回响应",逻辑一旦超过几行就下沉,这是后续测试章节能顺利进行的前提。
新手前三天的报错基本被下面四种包圆,逐个过一遍:
其一,端口被占用。 报错形如 Error: listen EADDRINUSE: address already in use 3000。说明有别的进程占着端口,换一个端口(app.listen(3001))或找到占用进程结束它:
# macOS 与 Linux lsof -i :3000 # Windows(Git Bash 也可用 netstat) netstat -ano | findstr :3000
其二,Node 版本过低。 报错形如 SyntaxError: Unexpected token 且指向 async 函数那一行,多半是 Node 6/7 的老环境。升级 Node 即可,别试图用转译工具绕过——本册所有示例都按现代 Node 口径编写。
其三,require 找不到包。 报错 Cannot find module 'koa',说明当前目录没装依赖:检查是否在项目根目录执行的安装,以及 node_modules 是否真的存在。从别处复制项目忘了装依赖,是最常见的翻车姿势。
其四,访问返回 404。 服务正常启动、请求也进来了,却得到 Not Found——说明没有任何一层给 ctx.body 赋过值。Koa 的默认响应就是 404:请求穿过所有层、没人写 body、出层时内核发现无内容可发。记住这个机制,它会在调试路由时反复帮你定位"到底是没匹配上,还是匹配了没响应"。
关键直觉:Koa 里 404 不是错误,是"所有层都没表态"的默认结果。看到 404 先别查错误处理,先查是不是漏写了
ctx.body。
await next() 前后分别是进层与出层阶段;至此第一颗洋葱已经转起来了。下一章我们把它剖到芯:Application 实例如何组织中间件队列,Context 对象从哪里来,错误又如何沿层冒泡——内核的每个零件都会在显微镜下过一遍。