refactor(router): cleanup & simplifications

This commit is contained in:
Victor Berchet 2017-04-02 17:21:56 -07:00 committed by Hans
parent 46ce3317c3
commit 0ab04bd62c
5 changed files with 151 additions and 173 deletions

View File

@ -54,6 +54,11 @@ function canLoadFails(route: Route): Observable<LoadedRouterConfig> {
`Cannot load children because the guard of the route "path: '${route.path}'" returned false`)));
}
/**
* Returns the `UrlTree` with the redirection applied.
*
* Lazy modules are loaded along the way.
*/
export function applyRedirects(
moduleInjector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer,
urlTree: UrlTree, config: Routes): Observable<UrlTree> {
@ -131,6 +136,7 @@ class ApplyRedirects {
return this.expandSegment(ngModule, segmentGroup, routes, segmentGroup.segments, outlet, true);
}
// Recursively expand segment groups for all the child outlets
private expandChildren(
ngModule: NgModuleRef<any>, routes: Route[],
segmentGroup: UrlSegmentGroup): Observable<{[name: string]: UrlSegmentGroup}> {
@ -182,18 +188,18 @@ class ApplyRedirects {
return noMatch(segmentGroup);
}
if (route.redirectTo !== undefined && !(allowRedirects && this.allowRedirects)) {
return noMatch(segmentGroup);
}
if (route.redirectTo === undefined) {
return this.matchSegmentAgainstRoute(ngModule, segmentGroup, route, paths);
}
if (allowRedirects && this.allowRedirects) {
return this.expandSegmentAgainstRouteUsingRedirect(
ngModule, segmentGroup, routes, route, paths, outlet);
}
return noMatch(segmentGroup);
}
private expandSegmentAgainstRouteUsingRedirect(
ngModule: NgModuleRef<any>, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
@ -294,8 +300,9 @@ class ApplyRedirects {
}
if (route.loadChildren) {
if ((<any>route)._loadedConfig !== void 0) {
return of ((<any>route)._loadedConfig);
// lazy children belong to the loaded module
if (route._loadedConfig !== undefined) {
return of (route._loadedConfig);
}
return mergeMap.call(runCanLoadGuard(ngModule.injector, route), (shouldLoad: boolean) => {
@ -417,8 +424,6 @@ function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment
lastChild: number,
positionalParamSegments: {[k: string]: UrlSegment}
} {
const noMatch =
{matched: false, consumedSegments: <any[]>[], lastChild: 0, positionalParamSegments: {}};
if (route.path === '') {
if ((route.pathMatch === 'full') && (segmentGroup.hasChildren() || segments.length > 0)) {
return {matched: false, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
@ -429,13 +434,18 @@ function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment
const matcher = route.matcher || defaultUrlMatcher;
const res = matcher(segments, segmentGroup, route);
if (!res) return noMatch;
if (!res) {
return {
matched: false, consumedSegments: <any[]>[], lastChild: 0, positionalParamSegments: {},
}
}
return {
matched: true,
consumedSegments: res.consumed,
lastChild: res.consumed.length,
positionalParamSegments: res.posParams
positionalParamSegments: res.posParams,
};
}
@ -475,7 +485,7 @@ function addEmptySegmentsToChildrenIfNeeded(
children: {[name: string]: UrlSegmentGroup}): {[name: string]: UrlSegmentGroup} {
const res: {[name: string]: UrlSegmentGroup} = {};
for (const r of routes) {
if (emptyPathRedirect(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) {
if (isEmptyPathRedirect(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) {
res[getOutlet(r)] = new UrlSegmentGroup([], {});
}
}
@ -495,22 +505,19 @@ function createChildrenForEmptySegments(
}
function containsEmptyPathRedirectsWithNamedOutlets(
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
return routes
.filter(
r => emptyPathRedirect(segmentGroup, slicedSegments, r) &&
getOutlet(r) !== PRIMARY_OUTLET)
.length > 0;
segmentGroup: UrlSegmentGroup, segments: UrlSegment[], routes: Route[]): boolean {
return routes.some(
r => isEmptyPathRedirect(segmentGroup, segments, r) && getOutlet(r) !== PRIMARY_OUTLET);
}
function containsEmptyPathRedirects(
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
return routes.filter(r => emptyPathRedirect(segmentGroup, slicedSegments, r)).length > 0;
segmentGroup: UrlSegmentGroup, segments: UrlSegment[], routes: Route[]): boolean {
return routes.some(r => isEmptyPathRedirect(segmentGroup, segments, r));
}
function emptyPathRedirect(
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], r: Route): boolean {
if ((segmentGroup.hasChildren() || slicedSegments.length > 0) && r.pathMatch === 'full') {
function isEmptyPathRedirect(
segmentGroup: UrlSegmentGroup, segments: UrlSegment[], r: Route): boolean {
if ((segmentGroup.hasChildren() || segments.length > 0) && r.pathMatch === 'full') {
return false;
}
@ -518,5 +525,5 @@ function emptyPathRedirect(
}
function getOutlet(route: Route): string {
return route.outlet ? route.outlet : PRIMARY_OUTLET;
return route.outlet || PRIMARY_OUTLET;
}

View File

@ -38,8 +38,7 @@ import {Tree, TreeNode} from './utils/tree';
*
* @description
* RouterState is a tree of activated routes. Every node in this tree knows about the "consumed" URL
* segments,
* the extracted parameters, and the resolved data.
* segments, the extracted parameters, and the resolved data.
*
* See {@link ActivatedRoute} for more information.
*

View File

@ -108,34 +108,36 @@ export function isNavigationCancelingError(error: Error) {
return (error as any)[NAVIGATION_CANCELING_ERROR];
}
// Matches the route configuration (`route`) against the actual URL (`segments`).
export function defaultUrlMatcher(
segments: UrlSegment[], segmentGroup: UrlSegmentGroup, route: Route): UrlMatchResult {
const path = route.path;
const parts = path.split('/');
const posParams: {[key: string]: UrlSegment} = {};
const consumed: UrlSegment[] = [];
segments: UrlSegment[], segmentGroup: UrlSegmentGroup, route: Route): UrlMatchResult|null {
const parts = route.path.split('/');
let currentIndex = 0;
for (let i = 0; i < parts.length; ++i) {
if (currentIndex >= segments.length) return null;
const current = segments[currentIndex];
const p = parts[i];
const isPosParam = p.startsWith(':');
if (!isPosParam && p !== current.path) return null;
if (isPosParam) {
posParams[p.substring(1)] = current;
}
consumed.push(current);
currentIndex++;
if (parts.length > segments.length) {
// The actual URL is shorter than the config, no match
return null;
}
if (route.pathMatch === 'full' &&
(segmentGroup.hasChildren() || currentIndex < segments.length)) {
(segmentGroup.hasChildren() || parts.length < segments.length)) {
// The config is longer than the actual URL but we are looking for a full match, return null
return null;
} else {
return {consumed, posParams};
}
const posParams: {[key: string]: UrlSegment} = {};
// Check each config part against the actual URL
for (let index = 0; index < parts.length; index++) {
const part = parts[index];
const segment = segments[index];
const isParameter = part.startsWith(':');
if (isParameter) {
posParams[part.substring(1)] = segment;
} else if (part !== segment.path) {
// The actual URL part does not match the config, no match
return null;
}
}
return {consumed: segments.slice(0, parts.length), posParams};
}

View File

@ -207,21 +207,13 @@ export class UrlSegment {
toString(): string { return serializePath(this); }
}
export function equalSegments(a: UrlSegment[], b: UrlSegment[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; ++i) {
if (a[i].path !== b[i].path) return false;
if (!shallowEqual(a[i].parameters, b[i].parameters)) return false;
}
return true;
export function equalSegments(as: UrlSegment[], bs: UrlSegment[]): boolean {
return equalPath(as, bs) && as.every((a, i) => shallowEqual(a.parameters, bs[i].parameters));
}
export function equalPath(a: UrlSegment[], b: UrlSegment[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; ++i) {
if (a[i].path !== b[i].path) return false;
}
return true;
export function equalPath(as: UrlSegment[], bs: UrlSegment[]): boolean {
if (as.length !== bs.length) return false;
return as.every((a, i) => a.path === bs[i].path);
}
export function mapChildrenIntoArray<T>(
@ -288,8 +280,8 @@ export class DefaultUrlSerializer implements UrlSerializer {
serialize(tree: UrlTree): string {
const segment = `/${serializeSegment(tree.root, true)}`;
const query = serializeQueryParams(tree.queryParams);
const fragment =
tree.fragment !== null && tree.fragment !== undefined ? `#${encodeURI(tree.fragment)}` : '';
const fragment = typeof tree.fragment === `string` ? `#${encodeURI(tree.fragment)}` : '';
return `${segment}${query}${fragment}`;
}
}
@ -301,34 +293,35 @@ export function serializePaths(segment: UrlSegmentGroup): string {
}
function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
if (segment.hasChildren() && root) {
if (!segment.hasChildren()) {
return serializePaths(segment);
}
if (root) {
const primary = segment.children[PRIMARY_OUTLET] ?
serializeSegment(segment.children[PRIMARY_OUTLET], false) :
'';
const children: string[] = [];
forEach(segment.children, (v: UrlSegmentGroup, k: string) => {
if (k !== PRIMARY_OUTLET) {
children.push(`${k}:${serializeSegment(v, false)}`);
}
});
if (children.length > 0) {
return `${primary}(${children.join('//')})`;
} else {
return `${primary}`;
}
} else if (segment.hasChildren() && !root) {
return children.length > 0 ? `${primary}(${children.join('//')})` : primary;
} else {
const children = mapChildrenIntoArray(segment, (v: UrlSegmentGroup, k: string) => {
if (k === PRIMARY_OUTLET) {
return [serializeSegment(segment.children[PRIMARY_OUTLET], false)];
} else {
return [`${k}:${serializeSegment(v, false)}`];
}
});
return `${serializePaths(segment)}/(${children.join('//')})`;
} else {
return serializePaths(segment);
return [`${k}:${serializeSegment(v, false)}`];
});
return `${serializePaths(segment)}/(${children.join('//')})`;
}
}
@ -360,7 +353,6 @@ function serializeQueryParams(params: {[key: string]: any}): string {
const SEGMENT_RE = /^[^\/()?;=&#]+/;
function matchSegments(str: string): string {
SEGMENT_RE.lastIndex = 0;
const match = str.match(SEGMENT_RE);
return match ? match[0] : '';
}
@ -368,7 +360,6 @@ function matchSegments(str: string): string {
const QUERY_PARAM_RE = /^[^=?&#]+/;
// Return the name of the query param at the start of the string or an empty string
function matchQueryParams(str: string): string {
QUERY_PARAM_RE.lastIndex = 0;
const match = str.match(SEGMENT_RE);
return match ? match[0] : '';
}
@ -376,126 +367,101 @@ function matchQueryParams(str: string): string {
const QUERY_PARAM_VALUE_RE = /^[^?&#]+/;
// Return the value of the query param at the start of the string or an empty string
function matchUrlQueryParamValue(str: string): string {
QUERY_PARAM_VALUE_RE.lastIndex = 0;
const match = str.match(QUERY_PARAM_VALUE_RE);
return match ? match[0] : '';
}
class UrlParser {
private remaining: string;
constructor(private url: string) { this.remaining = url; }
peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); }
capture(str: string): void {
if (!this.remaining.startsWith(str)) {
throw new Error(`Expected "${str}".`);
}
this.remaining = this.remaining.substring(str.length);
}
parseRootSegment(): UrlSegmentGroup {
if (this.remaining.startsWith('/')) {
this.capture('/');
}
this.consumeOptional('/');
if (this.remaining === '' || this.remaining.startsWith('?') || this.remaining.startsWith('#')) {
if (this.remaining === '' || this.peekStartsWith('?') || this.peekStartsWith('#')) {
return new UrlSegmentGroup([], {});
}
// The root segment group never has segments
return new UrlSegmentGroup([], this.parseChildren());
}
parseChildren(): {[key: string]: UrlSegmentGroup} {
if (this.remaining.length == 0) {
parseQueryParams(): {[key: string]: any} {
const params: {[key: string]: any} = {};
if (this.consumeOptional('?')) {
do {
this.parseQueryParam(params);
} while (this.consumeOptional('&'));
}
return params;
}
parseFragment(): string { return this.consumeOptional('#') ? decodeURI(this.remaining) : null; }
private parseChildren(): {[outlet: string]: UrlSegmentGroup} {
if (this.remaining === '') {
return {};
}
if (this.peekStartsWith('/')) {
this.capture('/');
}
this.consumeOptional('/');
const paths: any[] = [];
const segments: UrlSegment[] = [];
if (!this.peekStartsWith('(')) {
paths.push(this.parseSegments());
segments.push(this.parseSegment());
}
while (this.peekStartsWith('/') && !this.peekStartsWith('//') && !this.peekStartsWith('/(')) {
this.capture('/');
paths.push(this.parseSegments());
segments.push(this.parseSegment());
}
let children: {[key: string]: UrlSegmentGroup} = {};
let children: {[outlet: string]: UrlSegmentGroup} = {};
if (this.peekStartsWith('/(')) {
this.capture('/');
children = this.parseParens(true);
}
let res: {[key: string]: UrlSegmentGroup} = {};
let res: {[outlet: string]: UrlSegmentGroup} = {};
if (this.peekStartsWith('(')) {
res = this.parseParens(false);
}
if (paths.length > 0 || Object.keys(children).length > 0) {
res[PRIMARY_OUTLET] = new UrlSegmentGroup(paths, children);
if (segments.length > 0 || Object.keys(children).length > 0) {
res[PRIMARY_OUTLET] = new UrlSegmentGroup(segments, children);
}
return res;
}
parseSegments(): UrlSegment {
// parse a segment with its matrix parameters
// ie `name;k1=v1;k2`
private parseSegment(): UrlSegment {
const path = matchSegments(this.remaining);
if (path === '' && this.peekStartsWith(';')) {
throw new Error(`Empty path url segment cannot have parameters: '${this.remaining}'.`);
}
this.capture(path);
let matrixParams: {[key: string]: any} = {};
if (this.peekStartsWith(';')) {
matrixParams = this.parseMatrixParams();
}
return new UrlSegment(decode(path), matrixParams);
return new UrlSegment(decode(path), this.parseMatrixParams());
}
parseQueryParams(): {[key: string]: any} {
private parseMatrixParams(): {[key: string]: any} {
const params: {[key: string]: any} = {};
if (this.peekStartsWith('?')) {
this.capture('?');
this.parseQueryParam(params);
while (this.remaining.length > 0 && this.peekStartsWith('&')) {
this.capture('&');
this.parseQueryParam(params);
}
}
return params;
}
parseFragment(): string {
if (this.peekStartsWith('#')) {
return decodeURI(this.remaining.substring(1));
}
return null;
}
parseMatrixParams(): {[key: string]: any} {
const params: {[key: string]: any} = {};
while (this.remaining.length > 0 && this.peekStartsWith(';')) {
this.capture(';');
while (this.consumeOptional(';')) {
this.parseParam(params);
}
return params;
}
parseParam(params: {[key: string]: any}): void {
private parseParam(params: {[key: string]: any}): void {
const key = matchSegments(this.remaining);
if (!key) {
return;
}
this.capture(key);
let value: any = '';
if (this.peekStartsWith('=')) {
this.capture('=');
if (this.consumeOptional('=')) {
const valueMatch = matchSegments(this.remaining);
if (valueMatch) {
value = valueMatch;
@ -507,15 +473,14 @@ class UrlParser {
}
// Parse a single query parameter `name[=value]`
parseQueryParam(params: {[key: string]: any}): void {
private parseQueryParam(params: {[key: string]: any}): void {
const key = matchQueryParams(this.remaining);
if (!key) {
return;
}
this.capture(key);
let value: any = '';
if (this.peekStartsWith('=')) {
this.capture('=');
if (this.consumeOptional('=')) {
const valueMatch = matchUrlQueryParamValue(this.remaining);
if (valueMatch) {
value = valueMatch;
@ -540,10 +505,12 @@ class UrlParser {
}
}
parseParens(allowPrimary: boolean): {[key: string]: UrlSegmentGroup} {
// parse `(a/b//outlet_name:c/d)`
private parseParens(allowPrimary: boolean): {[outlet: string]: UrlSegmentGroup} {
const segments: {[key: string]: UrlSegmentGroup} = {};
this.capture('(');
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
while (!this.consumeOptional(')') && this.remaining.length > 0) {
const path = matchSegments(this.remaining);
const next = this.remaining[path.length];
@ -566,11 +533,26 @@ class UrlParser {
const children = this.parseChildren();
segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] :
new UrlSegmentGroup([], children);
if (this.peekStartsWith('//')) {
this.capture('//');
this.consumeOptional('//');
}
}
this.capture(')');
return segments;
}
private peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); }
// Consumes the prefix when it is present and returns whether it has been consumed
private consumeOptional(str: string): boolean {
if (this.peekStartsWith(str)) {
this.remaining = this.remaining.substring(str.length);
return true;
}
return false;
}
private capture(str: string): void {
if (!this.consumeOptional(str)) {
throw new Error(`Expected "${str}".`);
}
}
}

View File

@ -45,10 +45,6 @@ export function flatten<T>(arr: T[][]): T[] {
return Array.prototype.concat.apply([], arr);
}
export function first<T>(a: T[]): T {
return a.length > 0 ? a[0] : null;
}
export function last<T>(a: T[]): T {
return a.length > 0 ? a[a.length - 1] : null;
}
@ -67,34 +63,26 @@ export function forEach<K, V>(map: {[key: string]: V}, callback: (v: V, k: strin
export function waitForMap<A, B>(
obj: {[k: string]: A}, fn: (k: string, a: A) => Observable<B>): Observable<{[k: string]: B}> {
const waitFor: Observable<B>[] = [];
if (Object.keys(obj).length === 0) {
return of ({})
}
const waitHead: Observable<B>[] = [];
const waitTail: Observable<B>[] = [];
const res: {[k: string]: B} = {};
forEach(obj, (a: A, k: string) => {
const mapped = map.call(fn(k, a), (r: B) => res[k] = r);
if (k === PRIMARY_OUTLET) {
waitFor.push(map.call(fn(k, a), (_: B) => {
res[k] = _;
return _;
}));
waitHead.push(mapped);
} else {
waitTail.push(mapped);
}
});
forEach(obj, (a: A, k: string) => {
if (k !== PRIMARY_OUTLET) {
waitFor.push(map.call(fn(k, a), (_: B) => {
res[k] = _;
return _;
}));
}
});
if (waitFor.length > 0) {
const concatted$ = concatAll.call(of (...waitFor));
const last$ = l.last.call(concatted$);
const concat$ = concatAll.call(of (...waitHead, ...waitTail));
const last$ = l.last.call(concat$);
return map.call(last$, () => res);
}
return of (res);
}
export function andObservables(observables: Observable<Observable<any>>): Observable<boolean> {