http-hash-router源码阅读

Posted by Juan on 2017-06-07

http-hash-router 模块基本使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var http = require('http');
var HttpHashRouter = require('http-hash-router');

var router = HttpHashRouter();

router.set('/health', function health(req, res) {
res.end('OK');
});

var server = http.createServer(function handler(req, res) {
router(req, res, {}, onError);

function onError(err) {
if (err) {
// use your own custom error serialization.
res.statusCode = err.statusCode || 500;
res.end(err.message);
}
}
});
server.listen(3000);

http-hash

HttpHash对象维护这样一个routerNode树,结点结构定义如下:

1
2
3
4
5
6
7
8
9
function RouteNode(parent, segment, isSplat) {
this.parent = parent || null; // 上级结点
this.segment = segment || null; // 当前结点元素
this.handler = null; // handler方法
this.staticPaths = {}; // 静态路径
this.variablePaths = null; // 可变路径
this.isSplat = !!isSplat; // 是否带*号路径
this.src = null; // 完整path
}

并提供了set,get两个方法

  • router.set(pathname, handler), 根据pathname, 遍历每层路径元素,不断在hash中插入路由结点
  • router.get(pathname), 同样通过可变路径优先找到访问的路径,调用相应结点的handler

set(pathname, handler)

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
55
56
57
58
59
60
61
62
63
64
65
66
function set(pathname, handler) {
var pathSegments = pathname.split('/');
var hash = this._hash;
var lastIndex = pathSegments.length - 1;
var splatIndex = pathname.indexOf('*');
var hasSplat = splatIndex >= 0;

if (hasSplat && splatIndex !== pathname.length - 1) {
throw SplatError(pathname);
}

for (var i = 0; i < pathSegments.length; i++) {
var segment = pathSegments[i];
// 路径元素为空,遍历下一个
if (!segment) {
continue;
}
// 如果该路径元素是*号 且是最后一个元素 则增加可变路径结点
if (hasSplat && i === lastIndex) {
// 同一路径下如果已经有了可变路径,维持之前的可变路径不变, 否则增加可变结点
hash = (
hash.variablePaths ||
(hash.variablePaths = new RouteNode(hash, segment, true))
);

// 如果该可变路径不是* 而是:,则抛出冲突的异常,即不可以同时在同一路径下分别设置*和:两种通配符可变路径
if (!hash.isSplat) {
throw RouteConflictError(pathname, hash);
}
} else if (segment.indexOf(':') === 0) { // 如果是:通配符,增加可变路径结点
segment = segment.slice(1); // 获取:后的元素
// 同一路径下如果已经有了可变路径,维持之前的可变路径不变, 否则增加可变结点
hash = (
hash.variablePaths ||
(hash.variablePaths = new RouteNode(hash, segment))
);
// 如果设置的可变路径元素跟之前的不一致 或者之前设置是*可变路径,则抛出冲突异常
if (hash.segment !== segment || hash.isSplat) {
throw RouteConflictError(pathname, hash);
}
} else if (segment === '__proto__') {
hash = (
(
hash.hasOwnProperty('proto') &&
hash.proto
) ||
(hash.proto = new RouteNode(hash, segment))
);
} else { // 设置静态路径 查找当前has下的静态路径是否已经存在该路径元素 如果存在,直接复制结点,否则创建新的结点
hash = (
(
hash.staticPaths.hasOwnProperty(segment) &&
hash.staticPaths[segment]
) ||
(hash.staticPaths[segment] = new RouteNode(hash, segment))
);
}
}
// 判断是否设置了重复路径,重复则抛出冲突异常
if (!hash.handler) {
hash.src = pathname;
hash.handler = handler;
} else {
throwRouteConflictError(pathname, hash);
}
}

插入结点逻辑:

  • 将整个路径以/分割成一个数组,遍历每层路径元素,通过判断*和:[key]插入可变路径结点,否则插入静态路径结点
  • 同一层路径结点的静态路径结点,即兄弟结点可以有多个
  • 同一层路径结点的可变路径结点只有一个

抛出冲突异常的几个地方

  • 重复set了相同的路径
  • 同一层路径下set了两个不一样的通配符(如:一个是*,一个是:[key]),避免同时出现/api/*/api/:name

get(pathname)

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
function get(pathname) {
var pathSegments = pathname.split('/');
// 当前路由hash结点树
var hash = this._hash;
var splat = null;
var params = {};
var variablePaths;

for (var i = 0; i < pathSegments.length; i++) {
var segment = pathSegments[i];

if (!segment && !hash.isSplat) {
continue;
} else if (
segment === '__proto__' &&
hash.hasOwnProperty('proto')
) {
hash = hash.proto;
} else if (hash.staticPaths.hasOwnProperty(segment)) {
hash = hash.staticPaths[segment];
} else if ((variablePaths = hash.variablePaths)) {
if (variablePaths.isSplat) {
splat = pathSegments.slice(i).join('/');
hash = variablePaths;
break;
} else {
params[variablePaths.segment] = segment;
hash = variablePaths;
}
} else {
hash = null;
break;
}
}

return new RouteResult(hash, params, splat);
}

查找逻辑:

  • 优先从静态路径中查找,__proto__ > staticPaths > variablePaths 因此可以通过设置静态路由覆盖可变路径,如 api/review覆盖api/:name
  • splat保存指代*的路径内容,params保存:[key]的路径内容

总结

通过对http-hash-router源码阅读,以后碰到类似路径冲突的异常,能快速定位到问题,在set路由也会更留心,避免此类错误发生。此外,有了对http-hash-router在查找路由过程,优先于先找静态路径的认识,在解决路由冲突的问题上,提供了新的解决思路:通过set静态路径覆盖可变路径的方案