node-router开发之:路由开发(完结)
2018年9月5日
前言
经过这几天的努力,整个node-router路由功能部分算是完全开发完了,剩下的就是测试用例的编写,跑测试覆盖率了,这些等过段时间在给全部补上。今天先来总结下关于路由部分的实现碰到的问题,以及解决办法!
思路及demo
首先,根据我预先的构想,先完成订阅/发布模块,然后在开发路由模块,对外提供路由注册api,那么所谓的路由注册,其实就是订阅事件,并且将路由的url和请求类型当成事件名称,那么当请求进入时,实际上路由只需要要将url和请求类型处理成对应的事件名称,然后发布事件就可以了!就像下面这样:
const url = require('url');
const querystring = require('querystring');
import EventManage from './eventManage';
const eventManage = new EventManage;
class Router {
constructor() {
this.req = null;
this.res = null;
this.method = 'GET';
this.path = ''
}
init(req, res) {
this.req = req;
this.res = res;
this.method = req.method;
}
add(name, fn = () => {}, type) {
eventManage.on(`${type}.${name}`, fn);
}
tiggerRouter() {
eventManage.tigger(`${this.method}.${this.path}`, this.req, this.res);
}
}
const router = Router();
router.add('/index', (req, res) {
res.end('xxxx')
}, 'get');
当然上面只是最简单的例子,实际上实现起来还是有很多细节要处理,如路由的注册方式,尽可能的多提供一写方便使用的api,比如:
// 批量注册get路由
router.get({
'/': (res, req) => {
xxx
}
})
// 批量注册post路由
router.post({
'/': (res, req) => {
xxx
}
})
// 批量注册任何请求方式的路由
router.routes({
'/': (res, req) => {
xxx
}
}, 'put')
// 注册404
router.notFound((req, res, param) => {
res.end('hello 404');
});
// 注册全局路由
router.all((req, res, param) => {
res.end(`hello all, id=${param.id}`);
});
等等提供一系列的更方便的api,当然这些算是比较好处理的,还有一下类似于参数处理,就是在触发路由之前,先把请求的参数单独抽离处理,放到param对象里,当成参数传递给回调函数,以方便回调函数直接使用!
参数处理问题
就想上面所说的那样,想先把参数先处理好,然后直接放到参数里给回调函数,这会遇到一个问题,那就是参数的传递方式问题,在现在我们常用的请求种一般有三种方式:
// 第一种 get请求方式
'http://www.xxx.com/xxx?id=xxx'
// 第二种 post方式
res.body = {id: 111}
// 第三种 路由方式
'http://www.xxx.com/xxx/:id/xxx/:name'
前两种方式还比较好处理,直接用node的querystring模块直接分情况处理get和post接口的请求就好,但是对于第三种方式处理就比较麻烦了,首先,路由模块里是不记录路由注册情况的,都是直接注册到事件订阅模块,由事件模块来进行管理,直接意味着在路由模块是拿不到路由注册表的,也就无法处理这种带参数的路由。然后,就算拿到了路由注册表,怎么去对比实际请求的链接和注册的路由是否匹配?
为了处理上面说到的问题,首先对于前两种参数处理,我添加了一个_parseParam私有方法:
_parseParam() {
const urlObject = url.parse(this.req.url, true);
this.path = urlObject.pathname;
// 处理请求链接上的参数
this.param = urlObject.query || {};
// 处理url结尾的/
if (this.path !== '/') {
this.path = this.path.replace(/\/$/, '');
}
// 发布get事件
if (this.method === 'GET') {
return this._triggerRoute();
}
// 处理GET请求参数
// 发布事件
if (this.method === 'POST') {
let data = '';
this.req.on('data', (chunk) => {
data += chunk;
});
this.req.on('end', () => {
const param = data ? querystring.parse(data) : {};
Object.keys(param).map((key) => {
this.param[key] = param[key];
});
this._triggerRoute();
});
return;
}
// 处理PUT, DELETE等其他类型请求
this._triggerRoute();
}
这样就解决了前两种方式的参数处理问题,并且将参数放到param对象中,但是对于第三种方式,就会遇到上面提到的两个问题,这就要现在路由模块专门建立一个对象或者数组,专门用来登记带参数的路由:
// 记录路由中带参数的容器
this.paramUrl = [];
然后,需要在路由被触发前,对路由与登记的路由进行对比,看看是否符合匹配规则,如果符合就将路由中对应的部分拿出来,放到参数里:
/**
* 路由分发
* @private 私有方法
*/
_triggerRoute() {
const events = [`${this.method}._!_.${this.path}`, this.path];
// 处理路由中带参数如:/:id
// 筛选带参数路由登记容器中规则符合当前请求path的
const paramUrl = this.paramUrl.filter((event) => {
// 先处理请求的类型:get\post\put...
let method = '';
if (event.indexOf('._!_.') !== -1) {
method = event.split('._!_.')[0];
if (this.method !== method) {
return false;
}
}
// 将path以/进行切分
// 循环对比每个切分后的元素是否匹配
// 将匹配上的路由中的参数放入this.param中
const url = event.replace(/.+\._!_\./, '').replace(/^\//, '');
const pathArr = url.split('/');
const curPath = this.path.replace(/^\//, '').split('/');
let isCur = true;
let param = {};
// 循环对比每个切分后的元素是否匹配
pathArr.map((item, index) => {
if (/^:[^/]+$/.test(item)) {
return param[item.replace(':', '')] = curPath[index];
}
if (item !== curPath[index]) {
return isCur = false;
}
});
// 将匹配上的路由中的参数放入this.param中
if (isCur) {
Object.keys(param).map((key) => {
this.param[key] = param[key];
});
}
return isCur;
});
paramUrl.map((event) => eventManage.trigger(event, this.req, this.res, this.param));
events.map((event) => eventManage.trigger(event, this.req, this.res, this.param));
}
好了,到这里基本也就解决了参数问题了,写到这里基本整个路由也就没有太多难点了。剩下的,如果感兴趣的可以回去自己实践一下,或者直接去我的github上看我的项目源码!