# koa2 原生路由实现
30 秒速记
Koa不带路由;原生路由 = 一个中间件里读ctx.path+ctx.method,用if/switch/ 正则分发- 匹配用
ctx.path(不含查询串),别用ctx.url:/todo?x=1会对不上 - 没人设置
ctx.body时Koa默认回404 Not Found;兜底页要显式设ctx.status = 404 - 路径存在但方法不对,应该回
405并带Allow头,不是404 - 手写适合理解原理;有
/user/:id参数、分组、方法区分时就上@koa/router
Koa 本身不带路由,原生路由就是在中间件里自己看请求路径,再决定返回什么。 最简单的写法是读 ctx.path,用 switch 或正则去匹配,命中了就给 ctx.body 赋值,没命中走兜底。我一般会提醒两件事:匹配用 ctx.path 而不是 ctx.url,不然带查询参数的请求全进 404;另外要把请求方法也算进去,GET /todo 和 POST /todo 通常是两回事。手写几条还好,路由一多、出现 /user/:id 这种参数,就该换路由库了。
理解「koa2 原生路由实现」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 middleware 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
把路径和方法都考虑进去,一个完整一点的原生路由大概是这样,404、405 都处理到了:
const Koa = require('koa')
const app = new Koa()
const routes = {
'/': { GET: ctx => { ctx.body = '首页' } },
'/todo': {
GET: ctx => { ctx.body = '待办列表' },
POST: ctx => { ctx.status = 201; ctx.body = '已创建' },
},
}
app.use(async ctx => {
const route = routes[ctx.path] // 用 path,不受查询串影响
if (!route) {
ctx.status = 404
ctx.body = '页面不存在'
return
}
const handler = route[ctx.method]
if (!handler) {
ctx.status = 405
ctx.set('Allow', Object.keys(route).join(', ')) // 告诉客户端支持哪些方法
return
}
await handler(ctx)
})
app.listen(3000)
// curl localhost:3000/todo?x=1 -> 200 待办列表
// curl -X DELETE localhost:3000/todo -> 405,响应头 Allow: GET, POST
// curl localhost:3000/abc -> 404 页面不存在
这张 routes 表其实就是路由库的雏形。路由库在此基础上多做了几件事:路径参数(/user/:id)、前缀和嵌套、路由级中间件、自动处理 HEAD 和 OPTIONS。理解了这一版,再看 koa-router 的源码会轻松很多。
面试官追问
追问 1/todo 能打开,/todo?from=home 却是 404 页面,为什么?
匹配用的是 ctx.url,它带着查询串,'/todo?from=home' !== '/todo'。换成 ctx.path,查询参数从 ctx.query 单独拿。
追问 2一个路由都没命中、也没写兜底,Koa 会返回什么?
404,响应体是 Not Found。Koa 在 ctx.body 从没被赋值时默认就是 404。反过来也要小心:兜底分支只要给 body 赋了值,状态就自动变成 200,所以兜底要连 ctx.status = 404 一起写。
追问 3/user/123 这种带 id 的路径,手写要怎么匹配?
用正则把参数抠出来:const m = ctx.path.match(/^\/user\/(\d+)$/),m[1] 就是 '123',注意是字符串。写到第三四个这种路由时就该换 @koa/router 了,它直接给你 ctx.params.id。
追问 4DELETE /todo 返回了 404,这样处理对吗?
不对。路径是存在的,只是不支持这个方法,应该回 405 Method Not Allowed,并在 Allow 头里写上 GET, POST,客户端一看就知道是方法用错了。koa-router 的 allowedMethods() 干的就是这件事。
追问 5路由测试全过了,但 CI 进程一直不退出,可能是什么原因?
服务 listen 了没关。测试里用 const server = app.listen(0) 拿随机端口,结束时 server.close();更省事的是用 supertest(app.callback()),根本不占端口。
# koa2 原生路由实现
# 简单例子
30 秒速记
ctx.request.url(简写ctx.url)= 路径 + 查询串,比如/hello/world?from=nav- 只要路径用
ctx.path,要参数用ctx.query/ctx.querystring - 原生路由的本质:拿到路径 →
if/ 正则判断 → 给ctx.body赋值 Koa按body类型自动设Content-Type:普通字符串text/plain,<开头的字符串text/html,对象JSON
这个例子就说明一件事:当前访问的地址就在 ctx.request.url 上,路由无非是拿它做判断。 访问 /hello/world,页面原样输出 /hello/world。要留意它带着查询串,访问 /hello/world?from=nav 输出的就是完整这一串,所以真正做匹配时我会用 ctx.path,参数另外从 ctx.query 拿。ctx.url 就是 ctx.request.url 的简写,Koa 把 request 上常用的属性都代理到了 ctx 上,平时直接写 ctx.xxx 就行。
const Koa = require('koa')
const app = new Koa()
app.use( async ( ctx ) => {
let url = ctx.request.url
ctx.body = url
})
app.listen(3000)
访问 http://localhost:3000/hello/world 页面会输出 /hello/world,也就是说上下文的请求request对象中url之就是当前访问的路径名称,可以根据ctx.request.url 通过一定的判断或者正则匹配就可以定制出所需要的路由。
面试官追问
追问 1访问 /hello/world?from=nav#top,页面会输出什么?
输出 /hello/world?from=nav,#top 不会出现。哈希只在浏览器里用,根本不会发到服务端。
追问 2以 /a/b?x=1 为例,ctx.url、ctx.path、ctx.querystring 分别是什么?
ctx.url 是 '/a/b?x=1',ctx.path 是 '/a/b',ctx.querystring 是 'x=1',不带问号;带问号的是 ctx.search。
追问 3为什么 ctx.body = url 显示的是纯文本,不是 HTML?
Koa 看 body 的内容自动设类型:字符串以 < 开头判成 text/html,否则是 text/plain;对象会序列化成 application/json。想强制按 HTML 返回就写 ctx.type = 'html'。
追问 4基于这个写法做首页、待办页两个页面,最少要补什么?
按 ctx.path 判断,每个分支设好 ctx.body,最后一定要有兜底分支,设 ctx.status = 404。不然随便输个地址都把路径回显出来,看着还像页面正常打开了。
追问 5ctx.url 和 ctx.req.url 有区别吗?
值一样,来源不同。ctx.url 是 Koa 封装的 request 上的属性,ctx.req.url 是 Node 原生 IncomingMessage 上的。业务代码用前者;如果有中间件改写过 ctx.url,想看最初的地址用 ctx.originalUrl。
# 定制化的路由
30 秒速记
- 完整示例是
route-simple:switch匹配路径 → 读view下对应的HTML→ 写进ctx.body - 映射关系:
/、/index→index.html;/todo→todo.html;其他 →404.html - 读文件包成
Promise,中间件里await拿内容;现在直接用fs.promises.readFile
