上一篇文章咱们讲了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("/", (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 http://127.0.0.1:${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的路由模块有点像,如果之前看过Express.js
源码解析的,看本文应该会有种似曾相识的感觉。
先看看路由架构
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}
匹配路由就是当一个申请来了咱们就去遍历router
上的所有layer
,找出path
匹配的layer
,再找出layer
上method
匹配的route
,而后将对应的回调函数handle
拿进去执行。
@koa/router
有着相似的架构,他的代码就是在实现这种架构,先带着这种架构思维,咱们能够很容易读懂他的代码。
Router类
首先必定是Router
类,他的构造函数也比较简单,只须要初始化几个属性就行。因为@koa/router
模块大量应用了面向对象的思维,如果你对JS的面向对象还不相熟,能够先看看这篇文章。
module.exports = Router;function Router() { // 反对无new间接调用 if (!(this instanceof Router)) return new Router(); this.stack = []; // 变量名字都跟Express.js的路由模块一样}
下面代码有一行比拟有意思
if (!(this instanceof Router)) return new Router();
这种应用办法我在其余文章也提到过:反对无new
调用。咱们晓得要实例化一个类,个别要应用new
关键字,比方new Router()
。然而如果Router
构造函数加了这行代码,就能够反对无new
调用了,间接Router()
能够达到同样的成果。这是因为如果你间接Router()
调用,this instanceof Router
返回为false
,会走到这个if
外面去,构造函数会帮你调用一下new Router()
。
所以这个构造函数的次要作用就是初始化了一个属性stack
,嗯,这个属性名字都跟Express.js
路由模块一样。后面的架构曾经说了,这个属性就是用来寄存layer
的。
Router
构造函数官网源码:https://github.com/koajs/router/blob/master/lib/router.js#L50
申请动词函数
后面架构讲了,作为一个路由模块,咱们次要解决两个问题:注册路由和匹配路由。
先来看看注册路由,注册路由次要是在申请动词函数外面进行的,比方router.get
和router.post
这种函数。HTTP
动词有很多,有一个库专门保护了这些动词:methods。@koa/router
也是用的这个库,咱们这里就简化下,间接一个将get
和post
放到一个数组外面吧。
// HTTP动词函数const methods = ["get", "post"];for (let i = 0; i < methods.length; i++) { const method = methods[i]; Router.prototype[method] = function (path, middleware) { // 将middleware转化为一个数组,反对传入多个回调函数 middleware = Array.prototype.slice.call(arguments, 1); this.register(path, [method], middleware); return this; };}
下面代码间接循环methods
数组,将外面的每个值都增加到Router.prototype
上成为一个实例办法。这个办法接管path
和middleware
两个参数,这里的middleware
其实就是咱们路由的回调函数,因为代码是取的arguments
第二个开始到最初所有的参数,所以其实他是反对同时传多个回调函数的。另外官网源码其实是三个参数,还有可选参数name
,因为是可选的,跟外围逻辑无关,我这里间接去掉了。
还须要留神这个实例办法最初返回了this
,这种操作咱们在Koa
源码外面也见过,目标是让用户能够间断点点点,比方这样:
router.get().post();
这些实例办法最初其实都是调this.register()
去注册路由的,上面咱们看看他是怎么写的。
申请动词函数官网源码:https://github.com/koajs/router/blob/master/lib/router.js#L189
router.register()
router.register()
实例办法是真正注册路由的办法,联合后面架构讲的,注册路由就是构建layer
的数据结构可知,router.register()
的次要作用就是构建这个数据结构:
Router.prototype.register = function (path, methods, middleware) { const stack = this.stack; const route = new Layer(path, methods, middleware); stack.push(route); return route;};
代码跟预期的一样,就是用path
,method
和middleware
来创立一个layer
实例,而后把它塞到stack
数组外面去。
router.register
官网源码:https://github.com/koajs/router/blob/master/lib/router.js#L553
Layer类
下面代码呈现了Layer
这个类,咱们来看看他的构造函数吧:
const { pathToRegexp } = require("path-to-regexp");module.exports = Layer;function Layer(path, methods, middleware) { // 初始化methods和stack属性 this.methods = []; // 留神这里的stack寄存的是咱们传入的回调函数 this.stack = Array.isArray(middleware) ? middleware : [middleware]; // 将参数methods一个一个塞进this.methods外面去 for (let i = 0; i < methods.length; i++) { this.methods.push(methods[i].toUpperCase()); // ctx.method是大写,留神这里转换为大写 } // 保留path属性 this.path = path; // 应用path-to-regexp库将path转化为正则 this.regexp = pathToRegexp(path);}
从Layer
的构造函数能够看出,他的架构跟Express.js
路由模块曾经有点区别了。Express.js
的Layer
上还有Route
这个概念。而@koa/router
的stack
上存的间接是回调函数了,曾经没有route
这一层了。我集体感觉这种层级构造是比Express
的要清晰的,因为Express
的route.stack
外面存的又是layer
,这种互相援用是有点绕的,这点我在Express源码解析中也提出过。
另外咱们看到他也用到了path-to-regexp
这个库,这个库我在很多解决路由的库外面都见到过,比方React-Router
,Express
,真想去看看他的源码,加到我的待写文章列表外面去,空了去看看~
Layer
构造函数官网源码:https://github.com/koajs/router/blob/master/lib/layer.js#L20
router.routes()
后面架构提到的还有件事件须要做,那就是路由匹配。
对于Koa
来说,一个申请来了会顺次通过每个中间件,所以咱们的路由匹配其实也是在中间件外面做的。而@koa/router
的中间件是通过router.routes()
返回的。所以router.routes()
次要做两件事:
- 他应该返回一个
Koa
中间件,以便Koa
调用 - 这个中间件的次要工作是遍历
router
上的layer
,找到匹配的路由,并拿进去执行。
Router.prototype.routes = function () { const router = this; // 这个dispatch就是咱们要返回给Koa调用的中间件 let dispatch = function dispatch(ctx, next) { const path = ctx.path; const matched = router.match(path, ctx.method); // 获取所有匹配的layer let layerChain; // 定义一个变量来串联所有匹配的layer ctx.router = router; // 棘手把router挂到ctx上,给其余Koa中间件应用 if (!matched.route) return next(); // 如果一个layer都没匹配上,间接返回,并执行下一个Koa中间件 const matchedLayers = matched.pathAndMethod; // 获取所有path和method都匹配的layer // 上面这段代码的作用是将所有layer上的stack,也就是layer的回调函数都合并到一个数组layerChain外面去 layerChain = matchedLayers.reduce(function (memo, layer) { return memo.concat(layer.stack); }, []); // 这里的compose也是koa-compose这个库,源码在讲Koa源码的时候讲过 // 应用compose将layerChain数组合并成一个可执行的办法,并拿来执行,传入参数是Koa中间件参数ctx, next return compose(layerChain)(ctx, next); }; // 将中间件返回 return dispatch;};
上述代码中主体返回的是一个Koa
中间件,这个中间件外面先是通过router.match
办法将所有匹配的layer
拿进去,而后将这些layer
对应的回调函数通过reduce
放到一个数组外面,也就是layerChain
。而后用koa-compose
将这个数组合并成一个可执行办法,这里就有问题了。之前在Koa
源码解析我讲过koa-compose
的源码,这里再大抵贴一下:
function compose(middleware) { // 参数查看,middleware必须是一个数组 if (!Array.isArray(middleware)) throw new TypeError("Middleware stack must be an array!"); // 数组外面的每一项都必须是一个办法 for (const fn of middleware) { if (typeof fn !== "function") throw new TypeError("Middleware must be composed of functions!"); } // 返回一个办法,这个办法就是compose的后果 // 内部能够通过调用这个办法来开起中间件数组的遍历 // 参数模式和一般中间件一样,都是context和next return function (context, next) { return dispatch(0); // 开始中间件执行,从数组第一个开始 // 执行中间件的办法 function dispatch(i) { let fn = middleware[i]; // 取出须要执行的中间件 // 如果i等于数组长度,阐明数组曾经执行完了 if (i === middleware.length) { fn = next; // 这里让fn等于内部传进来的next,其实是进行收尾工作,比方返回404 } // 如果内部没有传收尾的next,间接就resolve if (!fn) { return Promise.resolve(); } // 执行中间件,留神传给中间件接管的参数应该是context和next // 传给中间件的next是dispatch.bind(null, i + 1) // 所以中间件外面调用next的时候其实调用的是dispatch(i + 1),也就是执行下一个中间件 try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err); } } };}
这段代码外面fn
是咱们传入的中间件,在@koa/router
这里对应的其实是layerChain
外面的一项,执行fn
的时候是这样的:
fn(context, dispatch.bind(null, i + 1))
这里传的参数合乎咱们应用@koa/router
的习惯,咱们应用@koa/router
个别是这样的:
router.get("/", (ctx, next) => { ctx.body = "Hello World";});
下面的fn
就是咱们传的回调函数,留神咱们执行fn
时传入的第二个参数dispatch.bind(null, i + 1)
,也就是router.get
这里的next
。所以咱们下面回调函数外面再执行下next
:
router.get("/", (ctx, next) => { ctx.body = "Hello World"; next(); // 留神这里});
这个回调外面执行next()
其实就是把koa-compose
外面的dispatch.bind(null, i + 1)
拿进去执行,也就是dispatch(i + 1)
,对应的就是执行layerChain
外面的下一个函数。在这个例子外面并没有什么用,因为匹配的回调函数只有一个。然而如果/
这个门路匹配了多个回调函数,比方这样:
router.get("/", (ctx, next) => { console.log("123");});router.get("/", (ctx, next) => { ctx.body = "Hello World";});
这里/
就匹配了两个回调函数,然而你如果这么写,你会失去一个Not Found
。为什么呢?因为你第一个回调外面没有调用next()
!后面说了,这里的next()
是dispatch(i + 1)
,会去调用layerChain
外面的下一个回调函数,换一句话说,你这里不调next()
就不会运行下一个回调函数了!要想让/
返回Hello World
,咱们须要在第一个回调函数外面调用next
,像这样:
router.get("/", (ctx, next) => { console.log("123"); next(); // 记得调用next});router.get("/", (ctx, next) => { ctx.body = "Hello World";});
所以有敌人感觉@koa/router
回调函数外面的next
没什么用,如果你一个路由只有一个匹配的回调函数,那的确没什么用,然而如果你一个门路可能匹配多个回调函数,记得调用next
。
router.routes
官网源码:https://github.com/koajs/router/blob/master/lib/router.js#L335
router.match()
下面router.routes
的源码外面咱们用到了router.match
这个实例办法来查找所有匹配的layer
,下面是这么用的:
const matched = router.match(path, ctx.method);
所以咱们也须要写一下这个函数,这个函数不简单,通过传入的path
和method
去router.stack
上找到所有匹配的layer
就行:
Router.prototype.match = function (path, method) { const layers = this.stack; // 取出所有layer let layer; // 构建一个构造来保留匹配后果,最初返回的也是这个matched const matched = { path: [], // path保留仅仅path匹配的layer pathAndMethod: [], // pathAndMethod保留path和method都匹配的layer route: false, // 只有有一个path和method都匹配的layer,就阐明这个路由是匹配上的,这个变量置为true }; // 循环layers来进行匹配 for (let i = 0; i < layers.length; i++) { layer = layers[i]; // 匹配的时候调用的是layer的实例办法match if (layer.match(path)) { matched.path.push(layer); // 只有path匹配就先放到matched.path下来 // 如果method也有匹配的,将layer放到pathAndMethod外面去 if (~layer.methods.indexOf(method)) { matched.pathAndMethod.push(layer); if (layer.methods.length) matched.route = true; } } } return matched;};
下面代码只是循环了所有的layer
,而后将匹配的layer
放到一个对象matched
外面并返回给里面调用,match.path
保留了所有path
匹配,然而method
并不一定匹配的layer
,本文并没有用到这个变量。具体匹配path
其实还是调用的layer
的实例办法layer.match
,咱们前面会来看看。
这段代码还有个有意思的点是检测layer.methods
外面是否蕴含method
的时候,源码是这样写的:
~layer.methods.indexOf(method)
而个别咱们可能是这样写:
layer.methods.indexOf(method) > -1
这个源码外面的~
是按位取反的意思,达到的成果与咱们前面这种写法其实是一样的,因为:
~ -1; // 返回0,也就是false~ 0; // 返回-1, 留神-1转换为bool是true~ 1; // 返回-2,转换为bool也是true
这种用法能够少写几个字母,又学会一招,大家具体应用的还是依据本人的状况来吧,选取喜爱的形式。
router.match
官网源码:https://github.com/koajs/router/blob/master/lib/router.js#L669
layer.match()
下面用到了layer.match
这个办法,咱们也来写一下吧。因为咱们在创立layer
实例的时候,其实曾经将path
转换为了一个正则,咱们间接拿来用就行:
Layer.prototype.match = function (path) { return this.regexp.test(path);};
layer.match
官网源码:https://github.com/koajs/router/blob/master/lib/layer.js#L54
总结
到这里,咱们本人的@koa/router
就写完了,应用他替换官网的源码也能失常工作啦~
本文可运行代码曾经上传到GitHub,大家能够拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaRouter
最初咱们再来总结下本文的要点吧:
@koa/router
整体是作为一个Koa
中间件存在的。@koa/router
是fork
的koa-router
持续进行保护。@koa/router
的整体思路跟Express.js
路由模块很像。@koa/router
也能够分为注册路由和匹配路由两局部。- 注册路由次要是构建路由的数据结构,具体来说就是创立很多
layer
,每个layer
上保留具体的path
,methods
,和回调函数。 @koa/router
创立的数据结构跟Express.js
路由模块有区别,少了route
这个层级,然而集体感觉@koa/router
的这种构造反而更清晰。Express.js
的layer
和route
的互相援用反而更让人纳闷。- 匹配路由就是去遍历所有的
layer
,找出匹配的layer
,将回调办法拿来执行。 - 一个路由可能匹配多个
layer
和回调函数,执行时应用koa-compose
将这些匹配的回调函数串起来,一个一个执行。 - 须要留神的是,如果一个路由匹配了多个回调函数,后面的回调函数必须调用
next()
能力持续走到下一个回调函数。
参考资料
@koa/router
官网文档:https://github.com/koajs/router
@koa/router
源码地址:https://github.com/koajs/router/tree/master/lib
文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和GitHub小星星,你的反对是作者继续创作的能源。
作者博文GitHub我的项目地址: https://github.com/dennis-jiang/Front-End-Knowledges
我也搞了个公众号[进击的大前端],不打广告,不写水文,只发高质量原创,欢送关注~