X Tutup
Skip to content

Commit 5db8907

Browse files
committed
fix(router): improve route matching priorities
1 parent c29ab86 commit 5db8907

File tree

7 files changed

+239
-126
lines changed

7 files changed

+239
-126
lines changed

modules/angular2/src/router/instruction.js

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export class RouteParams {
1414
}
1515
}
1616

17+
/**
18+
* An `Instruction` represents the component hierarchy of the application based on a given route
19+
*/
1720
export class Instruction {
1821
component:any;
1922
_children:Map<string, Instruction>;
@@ -24,21 +27,21 @@ export class Instruction {
2427
// the part of the URL captured by this instruction and all children
2528
accumulatedUrl:string;
2629

27-
params:Map<string, string>;
30+
params:StringMap<string, string>;
2831
reuse:boolean;
29-
cost:number;
32+
specificity:number;
3033

31-
constructor({params, component, children, matchedUrl, parentCost}:{params:StringMap, component:any, children:StringMap, matchedUrl:string, cost:number} = {}) {
34+
constructor({params, component, children, matchedUrl, parentSpecificity}:{params:StringMap, component:any, children:Map, matchedUrl:string, parentSpecificity:number} = {}) {
3235
this.reuse = false;
3336
this.capturedUrl = matchedUrl;
3437
this.accumulatedUrl = matchedUrl;
35-
this.cost = parentCost;
38+
this.specificity = parentSpecificity;
3639
if (isPresent(children)) {
3740
this._children = children;
3841
var childUrl;
3942
StringMapWrapper.forEach(this._children, (child, _) => {
4043
childUrl = child.accumulatedUrl;
41-
this.cost += child.cost;
44+
this.specificity += child.specificity;
4245
});
4346
if (isPresent(childUrl)) {
4447
this.accumulatedUrl += childUrl;
@@ -50,22 +53,28 @@ export class Instruction {
5053
this.params = params;
5154
}
5255

53-
hasChild(outletName:string):Instruction {
56+
hasChild(outletName:string):boolean {
5457
return StringMapWrapper.contains(this._children, outletName);
5558
}
5659

60+
/**
61+
* Returns the child instruction with the given outlet name
62+
*/
5763
getChild(outletName:string):Instruction {
5864
return StringMapWrapper.get(this._children, outletName);
5965
}
6066

67+
/**
68+
* (child:Instruction, outletName:string) => {}
69+
*/
6170
forEachChild(fn:Function): void {
6271
StringMapWrapper.forEach(this._children, fn);
6372
}
6473

6574
/**
6675
* Does a synchronous, breadth-first traversal of the graph of instructions.
6776
* Takes a function with signature:
68-
* (parent:Instruction, child:Instruction) => {}
77+
* (child:Instruction, outletName:string) => {}
6978
*/
7079
traverseSync(fn:Function): void {
7180
this.forEachChild(fn);
@@ -74,7 +83,7 @@ export class Instruction {
7483

7584

7685
/**
77-
* Takes a currently active instruction and sets a reuse flag on this instruction
86+
* Takes a currently active instruction and sets a reuse flag on each of this instruction's children
7887
*/
7988
reuseComponentsFrom(oldInstruction:Instruction): void {
8089
this.traverseSync((childInstruction, outletName) => {

modules/angular2/src/router/path_recognizer.js

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,25 +62,34 @@ function parsePathString(route:string) {
6262

6363
var segments = splitBySlash(route);
6464
var results = ListWrapper.create();
65-
var cost = 0;
65+
var specificity = 0;
66+
67+
// The "specificity" of a path is used to determine which route is used when multiple routes match a URL.
68+
// Static segments (like "/foo") are the most specific, followed by dynamic segments (like "/:id"). Star segments
69+
// add no specificity. Segments at the start of the path are more specific than proceeding ones.
70+
// The code below uses place values to combine the different types of segments into a single integer that we can
71+
// sort later. Each static segment is worth hundreds of points of specificity (10000, 9900, ..., 200), and each
72+
// dynamic segment is worth single points of specificity (100, 99, ... 2).
73+
if (segments.length > 98) {
74+
throw new BaseException(`'${route}' has more than the maximum supported number of segments.`);
75+
}
6676

6777
for (var i=0; i<segments.length; i++) {
6878
var segment = segments[i],
6979
match;
7080

7181
if (isPresent(match = RegExpWrapper.firstMatch(paramMatcher, segment))) {
7282
ListWrapper.push(results, new DynamicSegment(match[1]));
73-
cost += 100;
83+
specificity += (100 - i);
7484
} else if (isPresent(match = RegExpWrapper.firstMatch(wildcardMatcher, segment))) {
7585
ListWrapper.push(results, new StarSegment(match[1]));
76-
cost += 10000;
7786
} else if (segment.length > 0) {
7887
ListWrapper.push(results, new StaticSegment(segment));
79-
cost += 1;
88+
specificity += 100 * (100 - i);
8089
}
8190
}
8291

83-
return {segments: results, cost};
92+
return {segments: results, specificity};
8493
}
8594

8695
function splitBySlash (url:string):List<string> {
@@ -93,16 +102,18 @@ export class PathRecognizer {
93102
segments:List;
94103
regex:RegExp;
95104
handler:any;
96-
cost:number;
105+
specificity:number;
106+
path:string;
97107

98108
constructor(path:string, handler:any) {
109+
this.path = path;
99110
this.handler = handler;
100111
this.segments = [];
101112

102113
// TODO: use destructuring assignment
103114
// see https://github.com/angular/ts2dart/issues/158
104115
var parsed = parsePathString(path);
105-
var cost = parsed['cost'];
116+
var specificity = parsed['specificity'];
106117
var segments = parsed['segments'];
107118
var regexString = '^';
108119

@@ -112,7 +123,7 @@ export class PathRecognizer {
112123

113124
this.regex = RegExpWrapper.create(regexString);
114125
this.segments = segments;
115-
this.cost = cost;
126+
this.specificity = specificity;
116127
}
117128

118129
parseParams(url:string):StringMap<string, string> {

modules/angular2/src/router/route_recognizer.js

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import {RegExp, RegExpWrapper, StringWrapper, isPresent} from 'angular2/src/facade/lang';
1+
import {RegExp, RegExpWrapper, StringWrapper, isPresent, BaseException} from 'angular2/src/facade/lang';
22
import {Map, MapWrapper, List, ListWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
33

44
import {PathRecognizer} from './path_recognizer';
55

6+
/**
7+
* `RouteRecognizer` is responsible for recognizing routes for a single component.
8+
* It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of components.
9+
*/
610
export class RouteRecognizer {
711
names:Map<string, PathRecognizer>;
812
redirects:Map<string, string>;
@@ -20,14 +24,25 @@ export class RouteRecognizer {
2024

2125
addConfig(path:string, handler:any, alias:string = null): void {
2226
var recognizer = new PathRecognizer(path, handler);
27+
MapWrapper.forEach(this.matchers, (matcher, _) => {
28+
if (recognizer.regex.toString() == matcher.regex.toString()) {
29+
throw new BaseException(`Configuration '${path}' conflicts with existing route '${matcher.path}'`);
30+
}
31+
});
2332
MapWrapper.set(this.matchers, recognizer.regex, recognizer);
2433
if (isPresent(alias)) {
2534
MapWrapper.set(this.names, alias, recognizer);
2635
}
2736
}
2837

29-
recognize(url:string):List<StringMap> {
30-
var solutions = [];
38+
39+
/**
40+
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
41+
*
42+
*/
43+
recognize(url:string):List<RouteMatch> {
44+
var solutions = ListWrapper.create();
45+
3146
MapWrapper.forEach(this.redirects, (target, path) => {
3247
//TODO: "/" redirect case
3348
if (StringWrapper.startsWith(url, path)) {
@@ -38,21 +53,20 @@ export class RouteRecognizer {
3853
MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => {
3954
var match;
4055
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
41-
var solution = StringMapWrapper.create();
42-
StringMapWrapper.set(solution, 'cost', pathRecognizer.cost);
43-
StringMapWrapper.set(solution, 'handler', pathRecognizer.handler);
44-
StringMapWrapper.set(solution, 'params', pathRecognizer.parseParams(url));
45-
4656
//TODO(btford): determine a good generic way to deal with terminal matches
47-
if (url == '/') {
48-
StringMapWrapper.set(solution, 'matchedUrl', '/');
49-
StringMapWrapper.set(solution, 'unmatchedUrl', '');
50-
} else {
51-
StringMapWrapper.set(solution, 'matchedUrl', match[0]);
52-
var unmatchedUrl = StringWrapper.substring(url, match[0].length);
53-
StringMapWrapper.set(solution, 'unmatchedUrl', unmatchedUrl);
57+
var matchedUrl = '/';
58+
var unmatchedUrl = '';
59+
if (url != '/') {
60+
matchedUrl = match[0];
61+
unmatchedUrl = StringWrapper.substring(url, match[0].length);
5462
}
55-
ListWrapper.push(solutions, solution);
63+
ListWrapper.push(solutions, new RouteMatch({
64+
specificity: pathRecognizer.specificity,
65+
handler: pathRecognizer.handler,
66+
params: pathRecognizer.parseParams(url),
67+
matchedUrl: matchedUrl,
68+
unmatchedUrl: unmatchedUrl
69+
}));
5670
}
5771
});
5872

@@ -68,3 +82,20 @@ export class RouteRecognizer {
6882
return isPresent(pathRecognizer) ? pathRecognizer.generate(params) : null;
6983
}
7084
}
85+
86+
export class RouteMatch {
87+
specificity:number;
88+
handler:StringMap<string, any>;
89+
params:StringMap<string, string>;
90+
matchedUrl:string;
91+
unmatchedUrl:string;
92+
constructor({specificity, handler, params, matchedUrl, unmatchedUrl}:
93+
{specificity:number, handler:StringMap, params:StringMap, matchedUrl:string, unmatchedUrl:string} = {}) {
94+
95+
this.specificity = specificity;
96+
this.handler = handler;
97+
this.params = params;
98+
this.matchedUrl = matchedUrl;
99+
this.unmatchedUrl = unmatchedUrl;
100+
}
101+
}

0 commit comments

Comments
 (0)
X Tutup