fix(router): sort possible routes by cost
This commit is contained in:
parent
8b6fa1cf19
commit
17392f663f
|
@ -20,15 +20,18 @@ export class Instruction {
|
||||||
matchedUrl:string;
|
matchedUrl:string;
|
||||||
params:Map<string, string>;
|
params:Map<string, string>;
|
||||||
reuse:boolean;
|
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.reuse = false;
|
||||||
this.matchedUrl = matchedUrl;
|
this.matchedUrl = matchedUrl;
|
||||||
|
this.cost = parentCost;
|
||||||
if (isPresent(children)) {
|
if (isPresent(children)) {
|
||||||
this._children = children;
|
this._children = children;
|
||||||
var childUrl;
|
var childUrl;
|
||||||
StringMapWrapper.forEach(this._children, (child, _) => {
|
StringMapWrapper.forEach(this._children, (child, _) => {
|
||||||
childUrl = child.matchedUrl;
|
childUrl = child.matchedUrl;
|
||||||
|
this.cost += child.cost;
|
||||||
});
|
});
|
||||||
if (isPresent(childUrl)) {
|
if (isPresent(childUrl)) {
|
||||||
this.matchedUrl += childUrl;
|
this.matchedUrl += childUrl;
|
||||||
|
|
|
@ -52,7 +52,7 @@ class StarSegment {
|
||||||
var paramMatcher = RegExpWrapper.create("^:([^\/]+)$");
|
var paramMatcher = RegExpWrapper.create("^:([^\/]+)$");
|
||||||
var wildcardMatcher = RegExpWrapper.create("^\\*([^\/]+)$");
|
var wildcardMatcher = RegExpWrapper.create("^\\*([^\/]+)$");
|
||||||
|
|
||||||
function parsePathString(route:string):List {
|
function parsePathString(route:string) {
|
||||||
// normalize route as not starting with a "/". Recognition will
|
// normalize route as not starting with a "/". Recognition will
|
||||||
// also normalize.
|
// also normalize.
|
||||||
if (route[0] === "/") {
|
if (route[0] === "/") {
|
||||||
|
@ -61,6 +61,7 @@ function parsePathString(route:string):List {
|
||||||
|
|
||||||
var segments = splitBySlash(route);
|
var segments = splitBySlash(route);
|
||||||
var results = ListWrapper.create();
|
var results = ListWrapper.create();
|
||||||
|
var cost = 0;
|
||||||
|
|
||||||
for (var i=0; i<segments.length; i++) {
|
for (var i=0; i<segments.length; i++) {
|
||||||
var segment = segments[i],
|
var segment = segments[i],
|
||||||
|
@ -68,14 +69,17 @@ function parsePathString(route:string):List {
|
||||||
|
|
||||||
if (isPresent(match = RegExpWrapper.firstMatch(paramMatcher, segment))) {
|
if (isPresent(match = RegExpWrapper.firstMatch(paramMatcher, segment))) {
|
||||||
ListWrapper.push(results, new DynamicSegment(match[1]));
|
ListWrapper.push(results, new DynamicSegment(match[1]));
|
||||||
|
cost += 100;
|
||||||
} else if (isPresent(match = RegExpWrapper.firstMatch(wildcardMatcher, segment))) {
|
} else if (isPresent(match = RegExpWrapper.firstMatch(wildcardMatcher, segment))) {
|
||||||
ListWrapper.push(results, new StarSegment(match[1]));
|
ListWrapper.push(results, new StarSegment(match[1]));
|
||||||
|
cost += 10000;
|
||||||
} else if (segment.length > 0) {
|
} else if (segment.length > 0) {
|
||||||
ListWrapper.push(results, new StaticSegment(segment));
|
ListWrapper.push(results, new StaticSegment(segment));
|
||||||
|
cost += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return {segments: results, cost};
|
||||||
}
|
}
|
||||||
|
|
||||||
var SLASH_RE = RegExpWrapper.create('/');
|
var SLASH_RE = RegExpWrapper.create('/');
|
||||||
|
@ -89,12 +93,17 @@ export class PathRecognizer {
|
||||||
segments:List;
|
segments:List;
|
||||||
regex:RegExp;
|
regex:RegExp;
|
||||||
handler:any;
|
handler:any;
|
||||||
|
cost:number;
|
||||||
|
|
||||||
constructor(path:string, handler:any) {
|
constructor(path:string, handler:any) {
|
||||||
this.handler = handler;
|
this.handler = handler;
|
||||||
this.segments = ListWrapper.create();
|
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 = '^';
|
var regexString = '^';
|
||||||
|
|
||||||
ListWrapper.forEach(segments, (segment) => {
|
ListWrapper.forEach(segments, (segment) => {
|
||||||
|
@ -103,6 +112,7 @@ export class PathRecognizer {
|
||||||
|
|
||||||
this.regex = RegExpWrapper.create(regexString);
|
this.regex = RegExpWrapper.create(regexString);
|
||||||
this.segments = segments;
|
this.segments = segments;
|
||||||
|
this.cost = cost;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseParams(url:string):StringMap {
|
parseParams(url:string):StringMap {
|
||||||
|
|
|
@ -39,6 +39,7 @@ export class RouteRecognizer {
|
||||||
var match;
|
var match;
|
||||||
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
|
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
|
||||||
var solution = StringMapWrapper.create();
|
var solution = StringMapWrapper.create();
|
||||||
|
StringMapWrapper.set(solution, 'cost', pathRecognizer.cost);
|
||||||
StringMapWrapper.set(solution, 'handler', pathRecognizer.handler);
|
StringMapWrapper.set(solution, 'handler', pathRecognizer.handler);
|
||||||
StringMapWrapper.set(solution, 'params', pathRecognizer.parseParams(url));
|
StringMapWrapper.set(solution, 'params', pathRecognizer.parseParams(url));
|
||||||
|
|
||||||
|
|
|
@ -77,14 +77,14 @@ export class RouteRegistry {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var solutions = componentRecognizer.recognize(url);
|
var componentSolutions = componentRecognizer.recognize(url);
|
||||||
|
var fullSolutions = ListWrapper.create();
|
||||||
|
|
||||||
for(var i = 0; i < solutions.length; i++) {
|
for(var i = 0; i < componentSolutions.length; i++) {
|
||||||
var candidate = solutions[i];
|
var candidate = componentSolutions[i];
|
||||||
if (candidate['unmatchedUrl'].length == 0) {
|
if (candidate['unmatchedUrl'].length == 0) {
|
||||||
return handlerToLeafInstructions(candidate, parentComponent);
|
ListWrapper.push(fullSolutions, handlerToLeafInstructions(candidate, parentComponent));
|
||||||
}
|
} else {
|
||||||
|
|
||||||
var children = StringMapWrapper.create(),
|
var children = StringMapWrapper.create(),
|
||||||
allMapped = true;
|
allMapped = true;
|
||||||
|
|
||||||
|
@ -102,13 +102,20 @@ export class RouteRegistry {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (allMapped) {
|
if (allMapped) {
|
||||||
return new Instruction({
|
ListWrapper.push(fullSolutions, new Instruction({
|
||||||
component: parentComponent,
|
component: parentComponent,
|
||||||
children: children,
|
children: children,
|
||||||
matchedUrl: candidate['matchedUrl']
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -127,13 +134,15 @@ function handlerToLeafInstructions(context, parentComponent) {
|
||||||
StringMapWrapper.forEach(context['handler']['components'], (component, outletName) => {
|
StringMapWrapper.forEach(context['handler']['components'], (component, outletName) => {
|
||||||
children[outletName] = new Instruction({
|
children[outletName] = new Instruction({
|
||||||
component: component,
|
component: component,
|
||||||
params: context['params']
|
params: context['params'],
|
||||||
|
parentCost: 0
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return new Instruction({
|
return new Instruction({
|
||||||
component: parentComponent,
|
component: parentComponent,
|
||||||
children: children,
|
children: children,
|
||||||
matchedUrl: context['matchedUrl']
|
matchedUrl: context['matchedUrl'],
|
||||||
|
parentCost: context['cost']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ export function main() {
|
||||||
recognizer.addConfig('/test', handler);
|
recognizer.addConfig('/test', handler);
|
||||||
|
|
||||||
expect(recognizer.recognize('/test')[0]).toEqual({
|
expect(recognizer.recognize('/test')[0]).toEqual({
|
||||||
|
'cost' : 1,
|
||||||
'handler': { 'components': { 'a': 'b' } },
|
'handler': { 'components': { 'a': 'b' } },
|
||||||
'params': {},
|
'params': {},
|
||||||
'matchedUrl': '/test',
|
'matchedUrl': '/test',
|
||||||
|
@ -34,6 +35,7 @@ export function main() {
|
||||||
recognizer.addConfig('/', handler);
|
recognizer.addConfig('/', handler);
|
||||||
|
|
||||||
expect(recognizer.recognize('/')[0]).toEqual({
|
expect(recognizer.recognize('/')[0]).toEqual({
|
||||||
|
'cost': 0,
|
||||||
'handler': { 'components': { 'a': 'b' } },
|
'handler': { 'components': { 'a': 'b' } },
|
||||||
'params': {},
|
'params': {},
|
||||||
'matchedUrl': '/',
|
'matchedUrl': '/',
|
||||||
|
@ -44,6 +46,7 @@ export function main() {
|
||||||
it('should work with a dynamic segment', () => {
|
it('should work with a dynamic segment', () => {
|
||||||
recognizer.addConfig('/user/:name', handler);
|
recognizer.addConfig('/user/:name', handler);
|
||||||
expect(recognizer.recognize('/user/brian')[0]).toEqual({
|
expect(recognizer.recognize('/user/brian')[0]).toEqual({
|
||||||
|
'cost': 101,
|
||||||
'handler': handler,
|
'handler': handler,
|
||||||
'params': { 'name': 'brian' },
|
'params': { 'name': 'brian' },
|
||||||
'matchedUrl': '/user/brian',
|
'matchedUrl': '/user/brian',
|
||||||
|
@ -57,6 +60,7 @@ export function main() {
|
||||||
var solutions = recognizer.recognize('/a');
|
var solutions = recognizer.recognize('/a');
|
||||||
expect(solutions.length).toBe(1);
|
expect(solutions.length).toBe(1);
|
||||||
expect(solutions[0]).toEqual({
|
expect(solutions[0]).toEqual({
|
||||||
|
'cost': 1,
|
||||||
'handler': handler,
|
'handler': handler,
|
||||||
'params': {},
|
'params': {},
|
||||||
'matchedUrl': '/b',
|
'matchedUrl': '/b',
|
||||||
|
|
|
@ -27,6 +27,24 @@ export function main() {
|
||||||
expect(instruction.getChildInstruction('default').component).toBe(DummyCompB);
|
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', () => {
|
it('should match the full URL recursively', () => {
|
||||||
registry.config(rootHostComponent, {'path': '/first', 'component': DummyParentComp});
|
registry.config(rootHostComponent, {'path': '/first', 'component': DummyParentComp});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue