前言

经过这几天的努力,整个node-router路由功能部分算是完全开发完了,剩下的就是测试用例的编写,跑测试覆盖率了,这些等过段时间在给全部补上。今天先来总结下关于路由部分的实现碰到的问题,以及解决办法!

思路及demo

首先,根据我预先的构想,先完成订阅/发布模块,然后在开发路由模块,对外提供路由注册api,那么所谓的路由注册,其实就是订阅事件,并且将路由的url和请求类型当成事件名称,那么当请求进入时,实际上路由只需要要将url和请求类型处理成对应的事件名称,然后发布事件就可以了!就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 批量注册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对象里,当成参数传递给回调函数,以方便回调函数直接使用!

参数处理问题

就想上面所说的那样,想先把参数先处理好,然后直接放到参数里给回调函数,这会遇到一个问题,那就是参数的传递方式问题,在现在我们常用的请求种一般有三种方式:

1
2
3
4
5
6
7
8
// 第一种 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私有方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
_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对象中,但是对于第三种方式,就会遇到上面提到的两个问题,这就要现在路由模块专门建立一个对象或者数组,专门用来登记带参数的路由:

1
2
// 记录路由中带参数的容器
this.paramUrl = [];

然后,需要在路由被触发前,对路由与登记的路由进行对比,看看是否符合匹配规则,如果符合就将路由中对应的部分拿出来,放到参数里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 路由分发
* @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上看我的项目源码!