fix(router): sort possible routes by cost

This commit is contained in:
Brian Ford 2015-05-12 14:53:13 -07:00
parent 8b6fa1cf19
commit 17392f663f
6 changed files with 78 additions and 33 deletions

View File

@ -20,15 +20,18 @@ export class Instruction {
matchedUrl:string;
params:Map<string, string>;
reuse:boolean;
cost:number;
constructor({params, component, children, matchedUrl}:{params:StringMap, component:any, children:Map, matchedUrl:string} = {}) {
constructor({params, component, children, matchedUrl, parentCost}:{params:StringMap, component:any, children:Map, matchedUrl:string, cost:int} = {}) {
this.reuse = false;
this.matchedUrl = matchedUrl;
this.cost = parentCost;
if (isPresent(children)) {
this._children = children;
var childUrl;
StringMapWrapper.forEach(this._children, (child, _) => {
childUrl = child.matchedUrl;
this.cost += child.cost;
});
if (isPresent(childUrl)) {
this.matchedUrl += childUrl;

View File

@ -52,7 +52,7 @@ class StarSegment {
var paramMatcher = RegExpWrapper.create("^:([^\/]+)$");
var wildcardMatcher = RegExpWrapper.create("^\\*([^\/]+)$");
function parsePathString(route:string):List {
function parsePathString(route:string) {
// normalize route as not starting with a "/". Recognition will
// also normalize.
if (route[0] === "/") {
@ -61,21 +61,25 @@ function parsePathString(route:string):List {
var segments = splitBySlash(route);
var results = ListWrapper.create();
var cost = 0;
for (var i=0; i<segments.length; i++) {
var segment = segments[i],
match;
match;
if (isPresent(match = RegExpWrapper.firstMatch(paramMatcher, segment))) {
ListWrapper.push(results, new DynamicSegment(match[1]));
cost += 100;
} else if (isPresent(match = RegExpWrapper.firstMatch(wildcardMatcher, segment))) {
ListWrapper.push(results, new StarSegment(match[1]));
cost += 10000;
} else if (segment.length > 0) {
ListWrapper.push(results, new StaticSegment(segment));
cost += 1;
}
}
return results;
return {segments: results, cost};
}
var SLASH_RE = RegExpWrapper.create('/');
@ -89,12 +93,17 @@ export class PathRecognizer {
segments:List;
regex:RegExp;
handler:any;
cost:number;
constructor(path:string, handler:any) {
this.handler = handler;
this.segments = ListWrapper.create();
var segments = parsePathString(path);
// TODO: use destructuring assignment
// see https://github.com/angular/ts2dart/issues/158
var parsed = parsePathString(path);
var cost = parsed['cost'];
var segments = parsed['segments'];
var regexString = '^';
ListWrapper.forEach(segments, (segment) => {
@ -103,6 +112,7 @@ export class PathRecognizer {
this.regex = RegExpWrapper.create(regexString);
this.segments = segments;
this.cost = cost;
}
parseParams(url:string):StringMap {

View File

@ -39,6 +39,7 @@ export class RouteRecognizer {
var match;
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
var solution = StringMapWrapper.create();
StringMapWrapper.set(solution, 'cost', pathRecognizer.cost);
StringMapWrapper.set(solution, 'handler', pathRecognizer.handler);
StringMapWrapper.set(solution, 'params', pathRecognizer.parseParams(url));

View File

@ -77,39 +77,46 @@ export class RouteRegistry {
return null;
}
var solutions = componentRecognizer.recognize(url);
var componentSolutions = componentRecognizer.recognize(url);
var fullSolutions = ListWrapper.create();
for(var i = 0; i < solutions.length; i++) {
var candidate = solutions[i];
for(var i = 0; i < componentSolutions.length; i++) {
var candidate = componentSolutions[i];
if (candidate['unmatchedUrl'].length == 0) {
return handlerToLeafInstructions(candidate, parentComponent);
}
ListWrapper.push(fullSolutions, handlerToLeafInstructions(candidate, parentComponent));
} else {
var children = StringMapWrapper.create(),
allMapped = true;
var children = StringMapWrapper.create(),
allMapped = true;
StringMapWrapper.forEach(candidate['handler']['components'], (component, name) => {
if (!allMapped) {
return;
}
var childInstruction = this.recognize(candidate['unmatchedUrl'], component);
if (isPresent(childInstruction)) {
childInstruction.params = candidate['params'];
children[name] = childInstruction;
} else {
allMapped = false;
}
});
if (allMapped) {
return new Instruction({
component: parentComponent,
children: children,
matchedUrl: candidate['matchedUrl']
StringMapWrapper.forEach(candidate['handler']['components'], (component, name) => {
if (!allMapped) {
return;
}
var childInstruction = this.recognize(candidate['unmatchedUrl'], component);
if (isPresent(childInstruction)) {
childInstruction.params = candidate['params'];
children[name] = childInstruction;
} else {
allMapped = false;
}
});
if (allMapped) {
ListWrapper.push(fullSolutions, new Instruction({
component: parentComponent,
children: children,
matchedUrl: candidate['matchedUrl'],
parentCost: candidate['cost']
}));
}
}
}
if (fullSolutions.length > 0) {
ListWrapper.sort(fullSolutions, (a, b) => a.cost < b.cost ? -1 : 1);
return fullSolutions[0];
}
return null;
}
@ -127,13 +134,15 @@ function handlerToLeafInstructions(context, parentComponent) {
StringMapWrapper.forEach(context['handler']['components'], (component, outletName) => {
children[outletName] = new Instruction({
component: component,
params: context['params']
params: context['params'],
parentCost: 0
});
});
return new Instruction({
component: parentComponent,
children: children,
matchedUrl: context['matchedUrl']
matchedUrl: context['matchedUrl'],
parentCost: context['cost']
});
}

View File

@ -23,6 +23,7 @@ export function main() {
recognizer.addConfig('/test', handler);
expect(recognizer.recognize('/test')[0]).toEqual({
'cost' : 1,
'handler': { 'components': { 'a': 'b' } },
'params': {},
'matchedUrl': '/test',
@ -34,6 +35,7 @@ export function main() {
recognizer.addConfig('/', handler);
expect(recognizer.recognize('/')[0]).toEqual({
'cost': 0,
'handler': { 'components': { 'a': 'b' } },
'params': {},
'matchedUrl': '/',
@ -44,6 +46,7 @@ export function main() {
it('should work with a dynamic segment', () => {
recognizer.addConfig('/user/:name', handler);
expect(recognizer.recognize('/user/brian')[0]).toEqual({
'cost': 101,
'handler': handler,
'params': { 'name': 'brian' },
'matchedUrl': '/user/brian',
@ -57,6 +60,7 @@ export function main() {
var solutions = recognizer.recognize('/a');
expect(solutions.length).toBe(1);
expect(solutions[0]).toEqual({
'cost': 1,
'handler': handler,
'params': {},
'matchedUrl': '/b',

View File

@ -27,6 +27,24 @@ export function main() {
expect(instruction.getChildInstruction('default').component).toBe(DummyCompB);
});
it('should prefer static segments to dynamic', () => {
registry.config(rootHostComponent, {'path': '/:site', 'component': DummyCompB});
registry.config(rootHostComponent, {'path': '/home', 'component': DummyCompA});
var instruction = registry.recognize('/home', rootHostComponent);
expect(instruction.getChildInstruction('default').component).toBe(DummyCompA);
});
it('should prefer dynamic segments to star', () => {
registry.config(rootHostComponent, {'path': '/:site', 'component': DummyCompA});
registry.config(rootHostComponent, {'path': '/*site', 'component': DummyCompB});
var instruction = registry.recognize('/home', rootHostComponent);
expect(instruction.getChildInstruction('default').component).toBe(DummyCompA);
});
it('should match the full URL recursively', () => {
registry.config(rootHostComponent, {'path': '/first', 'component': DummyParentComp});