上一篇文章我们讲了Koa的基本架构,可以看到Koa的基本架构只有中间件内核,并没有其他功能,路由功能也没有。要实现路由功能我们必须引入第三方中间件,本文要讲的路由中间件是@koa/router,这个中间件是挂在Koa官方名下的,他跟另一个中间件koa-router名字很像。其实@koa/router是fork的koa-router,因为koa-router的作者很多年没维护了,所以Koa官方将它fork到了自己名下进行维护。这篇文章我们还是老套路,先写一个@koa/router的简单例子,然后自己手写@koa/router源码来替换他。
本文可运行代码已经上传GitHun,拿下来一边玩代码,一边看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaRouter
简单例子我们这里的例子还是使用之前Express文章中的例子:
访问跟路由返回Hello World
get /api/users返回一个用户列表,数据是随便造的
post /api/users写入一个用户信息,用一个文件来模拟数据库
这个例子之前写过几次了,用@koa/router写出来就是这个样子:
const fs = require("fs"); const path = require("path"); const Koa = require("koa"); const Router = require("@koa/router"); const bodyParser = require("koa-bodyparser"); const app = new Koa(); const router = new Router(); app.use(bodyParser()); router.get("http://www.likecs.com/", (ctx) => { ctx.body = "Hello World"; }); router.get("/api/users", (ctx) => { const resData = [ { id: 1, name: "小明", age: 18, }, { id: 2, name: "小红", age: 19, }, ]; ctx.body = resData; }); router.post("/api/users", async (ctx) => { // 使用了koa-bodyparser才能从ctx.request拿到body const postData = ctx.request.body; // 使用fs.promises模块下的方法,返回值是promises await fs.promises.appendFile( path.join(__dirname, "db.txt"), JSON.stringify(postData) ); ctx.body = postData; }); app.use(router.routes()); const port = 3001; app.listen(port, () => { console.log(`Server is running on :${port}/`); });上述代码中需要注意,Koa主要提倡的是promise的用法,所以如果像之前那样使用回调方法可能会导致返回Not Found。比如在post /api/users这个路由中,我们会去写文件,如果我们还是像之前Express那样使用回调函数:
fs.appendFile(path.join(__dirname, "db.txt"), postData, () => { ctx.body = postData; });这会导致这个路由的处理方法并不知道这里需要执行回调,而是直接将外层函数执行完就结束了。而外层函数执行完并没有设置ctx的返回值,所以Koa会默认返回一个Not Found。为了避免这种情况,我们需要让外层函数等待这里执行完,所以我们这里使用fs.promises下面的方法,这下面的方法都会返回promise,我们就可以使用await来等待返回结果了。
手写源码本文手写源码全部参照官方源码写成,方法名和变量名尽可能与官方代码保持一致,大家可以对照着看,写到具体方法时我也会贴上官方源码地址。手写源码前我们先来看看有哪些API是我们需要解决的:
Router类:我们从@koa/router引入的就是这个类,通过new关键字生成一个实例router,后续使用的方法都挂载在这个实例下面。
router.get和router.post:router的实例方法get和post是我们定义路由的方法。
router.routes:这个实例方法的返回值是作为中间件传给app.use的,所以这个方法很可能是生成具体的中间件给Koa调用。
@koa/router的这种使用方法跟我们之前看过的有点像,如果之前看过Express.js源码解析的,看本文应该会有种似曾相识的感觉。
先看看路由架构我讲过他的路由架构,本文讲的@koa/router的架构跟他有很多相似之处,但是也有一些改进。在进一步深入@koa/router源码前,我们先来回顾下Express.js的路由架构,这样我们可以有一个整体的认识,可以更好的理解后面的源码。对于我们上面这个例子来说,他有两个API:
get /api/users
post /api/users
这两个API的path是一样的,都是/api/users,但是他们的method不一样,一个是get,一个是post。Express里面将path这一层提取出来单独作为了一个类----Layer。一个Layer对应一个path,但是同一个path可能对应多个method。所以Layer上还添加了一个属性route,route上也存了一个数组,数组的每个项存了对应的method和回调函数handle。所以整个结构就是这个样子:
const router = { stack: [ // 里面很多layer { path: '/api/users' route: { stack: [ // 里面存了多个method和回调函数 { method: 'get', handle: function1 }, { method: 'post', handle: function2 } ] } } ] }整个路由的执行分为了两部分:注册路由和匹配路由。
注册路由就是构造上面这样一个结构,主要是通过请求动词对应的方法来实现,比如运行router.get('/api/users', function1)其实就会往router上添加一个layer,这个layer的path是/api/users,同时还会在layer.route的数组上添加一个项:
{ method: 'get', handle: function1 }