You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Layer.prototype.param=function(param,fn){varstack=this.stack;varparams=this.paramNames;varmiddleware=function(ctx,next){returnfn.call(this,ctx.params[param],ctx,next);};middleware.param=param;varnames=params.map(function(p){returnString(p.name);});varx=names.indexOf(param);// 获得indexif(x>-1){stack.some(function(fn,i){// param handlers are always first, so when we find an fn w/o a param property, stop here// if the param handler at this part of the stack comes after the one we are adding, stop here// 两个策略// 1. param处理器总是在最前面的,当前fn.param不存在。则直接插入 [a,b] mid => [mid, a, b]// 2. [mid, a, b] mid2 => [mid, mid2, a, b]保证按照params的顺序排列// 保证在正常中间件前// 保证按照params顺序排列if(!fn.param||names.indexOf(fn.param)>x){// 在当前注入中间件stack.splice(i,0,middleware);returntrue;// 停止some迭代。}});}returnthis;};
Router.prototype.allowedMethods=function(options){options=options||{};varimplemented=this.methods;// 返回一个中间件用于 app.use注册。returnfunctionallowedMethods(ctx,next){returnnext().then(function(){varallowed={};// 判断ctx.status 或者状态码为404console.log(ctx.matched,ctx.method,implemented);if(!ctx.status||ctx.status===404){// routes方法生成的ctx.matched// 就是筛选出来的layer匹配组ctx.matched.forEach(function(route){route.methods.forEach(function(method){allowed[method]=method;});});varallowedArr=Object.keys(allowed);// 实现了的路由匹配if(!~implemented.indexOf(ctx.method)){// 位运算符 ~(-1) === 0 !0 == true// options参数 throw如果为true的话则直接扔出错误// 这样可以给上层中间价做处理// 默认是抛出一个HttpErrorif(options.throw){varnotImplementedThrowable;if(typeofoptions.notImplemented==='function'){notImplementedThrowable=options.notImplemented();// set whatever the user returns from their function}else{notImplementedThrowable=newHttpError.NotImplemented();}thrownotImplementedThrowable;}else{// 否则跑出501// 501=>服务器未实现方法ctx.status=501;ctx.set('Allow',allowedArr.join(', '));}// 如果允许的话}elseif(allowedArr.length){// 对options请求进行操作。// options请求与get请求类似,但是请求没有请求体 只有头。// 常用语查询操作if(ctx.method==='OPTIONS'){ctx.status=200;ctx.body='';ctx.set('Allow',allowedArr.join(', '));}elseif(!allowed[ctx.method]){// 如果允许方法if(options.throw){varnotAllowedThrowable;if(typeofoptions.methodNotAllowed==='function'){notAllowedThrowable=options.methodNotAllowed();// set whatever the user returns from their function}else{notAllowedThrowable=newHttpError.MethodNotAllowed();}thrownotAllowedThrowable;}else{// 405 方法不被允许ctx.status=405;ctx.set('Allow',allowedArr.join(', '));}}}}});};};
title: '声明式与命令式编程'
date: '2019-08-13T07:08:47Z'
author:
name: ZWkang
koa router 实现原理
path-to-regexp
如何使用其来匹配识别路由?
最直观肯定是路径字符串全匹配
当路由全匹配 /string 的时候我们可以做出一些反馈操作。例如执行一个 callback 等。
我们还可以利用正则匹配特性
这样子匹配模式显然可操作方式更多元,匹配路径也更多
例如对路径 path:
试想一下如果我们要对路径解析匹配,我们需要自己再去写正则表达式。从而达到匹配效果。
可以写吗?
肯定可以,可是太费时了。
path-to-regexp 它可以帮助我们简单地完成这种操作。
简介 path-to-regexp 的一些 api
主要 api
path-to-regexp api demo
捋一捋使用步骤
koa-router
不知道你是否曾使用过 koa-router
router 实现实际上也是一种基于正则的访问路径匹配。
如果是使用 koa 原生代码
例子:
匹配路径/simple 返回一个 body 为 {name:'zwkang'}的 body string
一个简单的例子,如
以上我们自己实现 url 的模式就是这样,单一的匹配,如果多元化匹配,甚至匹配参数,需要考虑正则的书写。
缺点,较为单一,设定方法较为简陋,功能弱小
如果我们使用 koa-router 的话
题外话:app.callback()
callback 是 koa 的运行机制。方法代表了啥? 代表了其 setup 的过程
而我们的常用 listen 方法 实际上也是调用了 http.createServer(app.callback()) 这么一步唯一
让我们来看看这koa-router到底做了些什么
前置知识
从 demo 入手进行分析
调用 koa 时候调用的实例方法包括
考虑因为是 koa,use 调用,那么我们可以肯定是标准的 koa 中间件模式
返回的函数类似于
源码的开头注释给我们讲述了基本的一些用法
我们可以简单提炼一下
router.verb()
根据 http 方法指定对应函数例如 router.get().post().put()
.all 方法支持所有 http 方法
当路由匹配时,ctx._matchedRoute 可以在这里获取路径,如果他是命名路由,这里可以得到路由名 ctx._matchedRouteName
请求匹配的时候不会考虑 querystring(?xxxx)
允许使用具名函数
允许多路由使用
允许嵌套路由
允许路由前缀匹配
捕获命名的参数添加到 ctx.params 中
代码整体分析
不妨先从 layer 文件理解。
layer.js
前面说了,这个文件主要是用来处理对 path-to-regexp 库的操作
文件只有 300 行左右 方法较少,直接截取方法做详细解释。
layer 构造函数
输入:path, methods, middleware, opts
输出:对象 属性包括(opts, name, methods, paramNames, stack, path, regexp)
我们之前说过了 layer 是根据 route path 做处理 判断是否匹配,连接库 path-to-regexp,这一点很重要。
stack 应该与传入的 middleware 一致。stack 是数组形式,以此可见我们的 path 对应的route是允许多个的。
我们接下来关注下
根据 path-to-regexp 结合自身需要的 middleware, koa-router 给我们处理了什么封装
params
在构造函数初始化的时候,我们生成 this.regexp 的时候通过传入 this.paramNames 从而将其根据 path 解析出的 param 填出
输入: 路径,捕获组,已存在的参数组
输出: 一个参数键值对对象
处理方式很普通。因为 params 与 captures 是位置相对应的。所以直接可以循环即可。
match
首先看的也是输入值与返回值
输入: path
输出: 是否匹配的 boolean
我们可以看这个 this.regexp 是属性值,证明我们是有能力随时改变 this.regexp 从而影响这个函数的返回值
captures
输入: path 路径
输出: 捕获组数组
返回整个捕获组内容
url
layer 实例的 url 方法
实际上一个例如/name/:id
我们解析后会获得一个{id: xxx}的 params 对象
根据/name/:id 跟 params 对象我们是不是可以反推出实际的 url?
这个 url 方法提供的就是这种能力。
param
这个方法的作用是在当前的 stack 中添加针对单个 param 的处理器
实际上就是对 layer 的 stack 进行一个操作
setPrefix
对当前的 path 加上前缀并且重置当前的一些实例属性
safeDecodeURIComponent
保证 safeDecodeURIComponent 不会抛出任何错误
Layer 总结。
layer 的 stack 主要是存储实际的 middleware[s]。
主要的功能是针对 pathToRegexp 做设计。
提供能力给上层的 Router 做调用实现的。
Router
Router 构造函数
给 Router 原型注册上
http method 的方法,如:Router.prototype.get = xxx
当我们使用实例的时候可以更方便准确使用
router.get('name', path, cb)
这里的 middleware 显然是可以多个。例如 router.get(name, path, cb)
我们可以留意到,这里的主要是调用了另一个方法
带着疑惑我们可以进入到 register 方法内。
register 方法
我们可以看到整个 register 方法,是设计给注册单一路径的。
针对多路径在forEach 调用 register 方法。这种写法在 koa-router 实现里并不少见。。
看了 register 方法,我们的疑惑得到了证实,果然入参大多是用来初始化 layer 实例的。
初始化 layer 实例后,我们将它放置到 router 实例下的 stack 中。
根据一些 opts 再进行处理判断。不多大抵是无伤大雅的。
这样一来我们就知道了register 的用法。
我们知道我们调用 router 实例时候。
要使用中间件 我们往往需要完成两步
我们知道一个极简的中间件调用形式总是
我们的不管 koa-body 还是 koa-router
传入 app.use 总是一个
这样的函数,是符合 koa 中间件需求的。
带着这样的想法
我们可以来到 routes 方法中一探究竟。
routes 原型方法
我们知道路由匹配的本质是实际路由与定义路径相匹配。
那么 routes 生成的中间件实际上就是在考虑做这种匹配的处理。
从返回值我们可以看到
=> dispatch 方法。
这个 dispacth 方法实际上就是我们前面说的极简方式。
可以说是相差无几。
我们知道 stack 当前存储的是多个 layer 实例。
而根据路径的匹配,我们可以知道
一个后端路径,简单可以分为 http 方法,与路径定义匹配。
例如:/name/:id
这个时候来了个请求/name/3
是不是匹配了。(params = {id: 3})
但是请求方法如果是 get 呢? 定义的这个/name/:id 是个 post 的话。
则此时虽然路径匹配,但是实际并不能完全匹配。
原型方法 match
看看这个 match 方法吧。
对 stack 中的 layaer 进行判断。
返回的 matched 对象中
path 属性: 仅仅路径匹配即可。
pathAndMethod 属性: 仅仅 http 方法与路径匹配即可。
route 属性: 需要 layer 的方法长度不为 0(有定义方法。)
所以 dispatch 中我们首先
ctx.matched = matched.path
得到路径匹配的 layer
实际中间件处理的,是 http 方法且路径匹配的 layer
这种情况下。而实际上,所谓中间件就是一个个数组
它的堆叠方式可能是多维的,也可能是一维的。
如果一个 route 进行了匹配
ctx._matchedRoute 代表了它的路径。
这里 ctx._matchedRoute 是方法且路径匹配数组的 layer 的最后一个。
相信取最后一个大家也知道为什么。多个路径,除开当前处理,在下一个中间件处理时候,总是返回最后一个即可。
最后将符合的 layer 组合起来
例如 如果有多个 layer 的情况下,layer 也有多个 stack 的情况下
运行顺序就会如上所示
相当于在将多个 layer 实例的 stack 展平,且在每一个 layer 实例前,添加 ctx 属性进行使用。
最后用 compose 将这个展平的数组一起拿来使用。
其实在这里我们可以留意到,所谓的中间件也不过是一堆数组罢了。
但是这里的在每个 layer 实例前使用 ctx 属性倒是个不错的想法。
对中间件的操作例如 prefix 等。就是不断的对内部的 stack 位置属性的调整。
allowedMethods 方法
这个方法主要是默认的给我们路由中间件添加 404 405 501 的这些状态控制。
我们也可以在高层中间件统一处理也可以。
使用位运算符+indexOf 也是一种常见的用法。
全文总结
至此整篇的 koa-router 源码基本就解析完毕了。
虽然 Router 的源码还有很多方法本文没有写出,但是大多都是给上层提供 layer 实例的方法连接,欢迎到 github 链接从源码处查看。
总的来说能吸收的点可能是挺多的。
如果看完了整篇。
我的博客 zwkang.com
源码地址(注释解析版) koa-router 分支
The text was updated successfully, but these errors were encountered: