From 96657535f1fc68c3ce941adf53ef68aaf96a6388 Mon Sep 17 00:00:00 2001 From: Dan Bucholtz Date: Thu, 2 Mar 2017 15:05:35 -0600 Subject: [PATCH] refactor(navigation): async component loading (aka lazy loading) async component loading (aka lazy loading) --- src/navigation/deep-linker.ts | 188 +++++++++++++++----------- src/navigation/nav-controller-base.ts | 63 +++++---- src/navigation/nav-util.ts | 60 ++++---- src/navigation/swipe-back.ts | 4 +- src/navigation/url-serializer.ts | 22 ++- src/navigation/view-controller.ts | 7 +- 6 files changed, 204 insertions(+), 140 deletions(-) diff --git a/src/navigation/deep-linker.ts b/src/navigation/deep-linker.ts index 30d4f35b61..3adbf47191 100644 --- a/src/navigation/deep-linker.ts +++ b/src/navigation/deep-linker.ts @@ -1,7 +1,9 @@ +import { ComponentFactory, ComponentFactoryResolver } from '@angular/core'; import { Location } from '@angular/common'; import { App } from '../components/app/app'; -import { convertToViews, isNav, isTab, isTabs, NavSegment, DIRECTION_BACK } from './nav-util'; +import { convertToViews, isNav, isTab, isTabs, NavLink, NavSegment, DIRECTION_BACK } from './nav-util'; +import { ModuleLoader } from '../util/module-loader'; import { isArray, isPresent } from '../util/util'; import { Nav } from '../components/nav/nav'; import { NavController } from './nav-controller'; @@ -117,20 +119,23 @@ import { ViewController } from './view-controller'; */ export class DeepLinker { - /** - * @internal - */ - segments: NavSegment[] = []; - /** - * @internal - */ - history: string[] = []; - /** - * @internal - */ - indexAliasUrl: string; + /** @internal */ + _segments: NavSegment[] = []; + /** @internal */ + _history: string[] = []; + /** @internal */ + _indexAliasUrl: string; + /** @internal */ + _cfrMap = new Map(); - constructor(public _app: App, public _serializer: UrlSerializer, public _location: Location) { } + + constructor( + public _app: App, + public _serializer: UrlSerializer, + public _location: Location, + public _moduleLoader: ModuleLoader, + public _baseCfr: ComponentFactoryResolver + ) {} /** * @internal @@ -141,14 +146,14 @@ export class DeepLinker { console.debug(`DeepLinker, init load: ${browserUrl}`); // update the Path from the browser URL - this.segments = this._serializer.parse(browserUrl); + this._segments = this._serializer.parse(browserUrl); // remember this URL in our internal history stack - this.historyPush(browserUrl); + this._historyPush(browserUrl); // listen for browser URL changes this._location.subscribe((locationChg: { url: string }) => { - this.urlChange(normalizeUrl(locationChg.url)); + this._urlChange(normalizeUrl(locationChg.url)); }); } @@ -156,23 +161,23 @@ export class DeepLinker { * The browser's location has been updated somehow. * @internal */ - urlChange(browserUrl: string) { + _urlChange(browserUrl: string) { // do nothing if this url is the same as the current one - if (!this.isCurrentUrl(browserUrl)) { + if (!this._isCurrentUrl(browserUrl)) { - if (this.isBackUrl(browserUrl)) { + if (this._isBackUrl(browserUrl)) { // scenario 2: user clicked the browser back button // scenario 4: user changed the browser URL to what was the back url was // scenario 5: user clicked a link href that was the back url console.debug(`DeepLinker, browser urlChange, back to: ${browserUrl}`); - this.historyPop(); + this._historyPop(); } else { // scenario 3: user click forward button // scenario 4: user changed browser URL that wasn't the back url // scenario 5: user clicked a link href that wasn't the back url console.debug(`DeepLinker, browser urlChange, forward to: ${browserUrl}`); - this.historyPush(browserUrl); + this._historyPush(browserUrl); } // get the app's root nav @@ -180,10 +185,10 @@ export class DeepLinker { if (appRootNav) { if (browserUrl === '/') { // a url change to the index url - if (isPresent(this.indexAliasUrl)) { + if (isPresent(this._indexAliasUrl)) { // we already know the indexAliasUrl // update the url to use the know alias - browserUrl = this.indexAliasUrl; + browserUrl = this._indexAliasUrl; } else { // the url change is to the root but we don't @@ -198,8 +203,8 @@ export class DeepLinker { } // normal url - this.segments = this._serializer.parse(browserUrl); - this.loadNavFromPath(appRootNav); + this._segments = this._serializer.parse(browserUrl); + this._loadNavFromPath(appRootNav); } } } @@ -216,13 +221,13 @@ export class DeepLinker { if (activeNav) { // build up the segments of all the navs from the lowest level - this.segments = this.pathFromNavs(activeNav); + this._segments = this._pathFromNavs(activeNav); // build a string URL out of the Path - const browserUrl = this._serializer.serialize(this.segments); + const browserUrl = this._serializer.serialize(this._segments); // update the browser's location - this.updateLocation(browserUrl, direction); + this._updateLocation(browserUrl, direction); } } } @@ -230,35 +235,70 @@ export class DeepLinker { /** * @internal */ - updateLocation(browserUrl: string, direction: string) { - if (this.indexAliasUrl === browserUrl) { + _updateLocation(browserUrl: string, direction: string) { + if (this._indexAliasUrl === browserUrl) { browserUrl = '/'; } - if (direction === DIRECTION_BACK && this.isBackUrl(browserUrl)) { + if (direction === DIRECTION_BACK && this._isBackUrl(browserUrl)) { // this URL is exactly the same as the back URL // it's safe to use the browser's location.back() console.debug(`DeepLinker, location.back(), url: '${browserUrl}'`); - this.historyPop(); + this._historyPop(); this._location.back(); - } else if (!this.isCurrentUrl(browserUrl)) { + } else if (!this._isCurrentUrl(browserUrl)) { // probably navigating forward console.debug(`DeepLinker, location.go('${browserUrl}')`); - this.historyPush(browserUrl); + this._historyPush(browserUrl); this._location.go(browserUrl); } } + + getComponentFromName(componentName: string): Promise { + const link = this._serializer.getLinkFromName(componentName); + if (link) { + // cool, we found the right link for this component name + return this.getNavLinkComponent(link); + } + + // umm, idk + return Promise.reject(`invalid link: ${componentName}`); + } + + + getNavLinkComponent(link: NavLink) { + if (link.component) { + // sweet, we're already got a component loaded for this link + return Promise.resolve(link.component); + } + + if (link.loadChildren) { + // awesome, looks like we'll lazy load this component + // using loadChildren as the URL to request + return this._moduleLoader.load(link.loadChildren).then(loadedModule => { + // kerpow!! we just lazy loaded a component!! + // update the existing link with the loaded component + link.component = loadedModule.component; + this._cfrMap.set(link.component, loadedModule.componentFactoryResolver); + return link.component; + }); + } + + return Promise.reject(`invalid link component: ${link.name}`); + } + + /** * @internal */ - getComponentFromName(componentName: any): any { - const segment = this._serializer.createSegmentFromName(componentName); - if (segment && segment.component) { - return segment.component; + resolveComponent(component: any): ComponentFactory { + let cfr = this._cfrMap.get(component); + if (!cfr) { + cfr = this._baseCfr; } - return null; + return cfr.resolveComponentFactory(component); } /** @@ -268,7 +308,7 @@ export class DeepLinker { // create a segment out of just the passed in name const segment = this._serializer.createSegmentFromName(nameOrComponent); if (segment) { - const path = this.pathFromNavs(nav, segment.component, data); + const path = this._pathFromNavs(nav, segment.component, data); // serialize the segments into a browser URL // and prepare the URL with the location and return const url = this._serializer.serialize(path); @@ -284,7 +324,7 @@ export class DeepLinker { * * @internal */ - pathFromNavs(nav: NavController, component?: any, data?: any): NavSegment[] { + _pathFromNavs(nav: NavController, component?: any, data?: any): NavSegment[] { const segments: NavSegment[] = []; let view: ViewController; let segment: NavSegment; @@ -321,7 +361,7 @@ export class DeepLinker { if (isTab(nav)) { // this nav is a Tab, which is a child of Tabs // add a segment to represent which Tab is the selected one - tabSelector = this.getTabSelector(nav); + tabSelector = this._getTabSelector(nav); segments.push({ id: tabSelector, name: tabSelector, @@ -347,7 +387,7 @@ export class DeepLinker { /** * @internal */ - getTabSelector(tab: Tab): string { + _getTabSelector(tab: Tab): string { if (isPresent(tab.tabUrlPath)) { return tab.tabUrlPath; } @@ -385,7 +425,7 @@ export class DeepLinker { * @internal */ initNav(nav: any): NavSegment { - const path = this.segments; + const path = this._segments; if (nav && path.length) { if (!nav.parent) { @@ -408,22 +448,18 @@ export class DeepLinker { /** * @internal */ - initViews(segment: NavSegment): ViewController[] { - let views: ViewController[]; - - if (isArray(segment.defaultHistory)) { - views = convertToViews(this, segment.defaultHistory); - - } else { - views = []; - } - + initViews(segment: NavSegment) { const view = new ViewController(segment.component, segment.data); view.id = segment.id; - views.push(view); + if (isArray(segment.defaultHistory)) { + return convertToViews(this, segment.defaultHistory).then(views => { + views.push(view); + return views; + }); + } - return views; + return Promise.resolve([view]); } /** @@ -436,13 +472,13 @@ export class DeepLinker { * * @internal */ - loadNavFromPath(nav: NavController, done?: Function) { + _loadNavFromPath(nav: NavController, done?: Function) { if (!nav) { done && done(); } else { - this.loadViewFromSegment(nav, () => { - this.loadNavFromPath(nav.getActiveChildNav(), done); + this._loadViewFromSegment(nav, () => { + this._loadNavFromPath(nav.getActiveChildNav(), done); }); } } @@ -450,7 +486,7 @@ export class DeepLinker { /** * @internal */ - loadViewFromSegment(navInstance: any, done: Function) { + _loadViewFromSegment(navInstance: any, done: Function) { // load up which nav ids belong to its nav segment let segment = this.initNav(navInstance); if (!segment) { @@ -509,25 +545,25 @@ export class DeepLinker { /** * @internal */ - isBackUrl(browserUrl: string) { - return (browserUrl === this.history[this.history.length - 2]); + _isBackUrl(browserUrl: string) { + return (browserUrl === this._history[this._history.length - 2]); } /** * @internal */ - isCurrentUrl(browserUrl: string) { - return (browserUrl === this.history[this.history.length - 1]); + _isCurrentUrl(browserUrl: string) { + return (browserUrl === this._history[this._history.length - 1]); } /** * @internal */ - historyPush(browserUrl: string) { - if (!this.isCurrentUrl(browserUrl)) { - this.history.push(browserUrl); - if (this.history.length > 30) { - this.history.shift(); + _historyPush(browserUrl: string) { + if (!this._isCurrentUrl(browserUrl)) { + this._history.push(browserUrl); + if (this._history.length > 30) { + this._history.shift(); } } } @@ -535,18 +571,18 @@ export class DeepLinker { /** * @internal */ - historyPop() { - this.history.pop(); - if (!this.history.length) { - this.historyPush(this._location.path()); + _historyPop() { + this._history.pop(); + if (!this._history.length) { + this._historyPush(this._location.path()); } } } -export function setupDeepLinker(app: App, serializer: UrlSerializer, location: Location) { - const deepLinker = new DeepLinker(app, serializer, location); +export function setupDeepLinker(app: App, serializer: UrlSerializer, location: Location, moduleLoader: ModuleLoader, cfr: ComponentFactoryResolver) { + const deepLinker = new DeepLinker(app, serializer, location, moduleLoader, cfr); deepLinker.init(); return deepLinker; } diff --git a/src/navigation/nav-controller-base.ts b/src/navigation/nav-controller-base.ts index 125cfb49e7..5408bcae35 100644 --- a/src/navigation/nav-controller-base.ts +++ b/src/navigation/nav-controller-base.ts @@ -4,7 +4,7 @@ import { AnimationOptions } from '../animations/animation'; import { App } from '../components/app/app'; import { Config } from '../config/config'; import { convertToView, convertToViews, NavOptions, DIRECTION_BACK, DIRECTION_FORWARD, INIT_ZINDEX, - TransitionResolveFn, TransitionInstruction, ViewState } from './nav-util'; + TransitionResolveFn, TransitionInstruction, STATE_INITIALIZED, STATE_LOADED, STATE_PRE_RENDERED } from './nav-util'; import { setZIndex } from './nav-util'; import { DeepLinker } from './deep-linker'; import { DomController } from '../platform/dom-controller'; @@ -72,27 +72,36 @@ export class NavControllerBase extends Ion implements NavController { } push(page: any, params?: any, opts?: NavOptions, done?: Function): Promise { - return this._queueTrns({ - insertStart: -1, - insertViews: [convertToView(this._linker, page, params)], - opts: opts, - }, done); + return convertToView(this._linker, page, params).then(viewController => { + return this._queueTrns({ + insertStart: -1, + insertViews: [viewController], + opts: opts, + }, done); + }).catch((err: Error) => { + console.error('Failed to navigate: ', err.message); + throw err; + }); } insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: Function): Promise { - return this._queueTrns({ - insertStart: insertIndex, - insertViews: [convertToView(this._linker, page, params)], - opts: opts, - }, done); + return convertToView(this._linker, page, params).then(viewController => { + return this._queueTrns({ + insertStart: insertIndex, + insertViews: [viewController], + opts: opts, + }, done); + }); } insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions, done?: Function): Promise { - return this._queueTrns({ - insertStart: insertIndex, - insertViews: convertToViews(this._linker, insertPages), - opts: opts, - }, done); + return convertToViews(this._linker, insertPages).then(viewControllers => { + return this._queueTrns({ + insertStart: insertIndex, + insertViews: viewControllers, + opts: opts, + }, done); + }); } pop(opts?: NavOptions, done?: Function): Promise { @@ -152,13 +161,15 @@ export class NavControllerBase extends Ion implements NavController { } setRoot(pageOrViewCtrl: any, params?: any, opts?: NavOptions, done?: Function): Promise { - const viewControllers = [convertToView(this._linker, pageOrViewCtrl, params)]; - return this._setPages(viewControllers, opts, done); + return convertToView(this._linker, pageOrViewCtrl, params).then((viewController) => { + return this._setPages([viewController], opts, done); + }); } setPages(pages: any[], opts?: NavOptions, done?: Function): Promise { - const viewControllers = convertToViews(this._linker, pages); - return this._setPages(viewControllers, opts, done); + return convertToViews(this._linker, pages).then(viewControllers => { + return this._setPages(viewControllers, opts, done); + }); } _setPages(viewControllers: ViewController[], opts?: NavOptions, done?: Function): Promise { @@ -220,7 +231,7 @@ export class NavControllerBase extends Ion implements NavController { this._queue.length = 0; while (trns) { - if (trns.enteringView && (trns.enteringView._state !== ViewState.LOADED)) { + if (trns.enteringView && (trns.enteringView._state !== STATE_LOADED)) { // destroy the entering views and all of their hopes and dreams this._destroyView(trns.enteringView); } @@ -483,17 +494,17 @@ export class NavControllerBase extends Ion implements NavController { { provide: ViewController, useValue: enteringView }, { provide: NavParams, useValue: enteringView.getNavParams() } ]); - const componentFactory = this._cfr.resolveComponentFactory(enteringView.component); + const componentFactory = this._linker.resolveComponent(enteringView.component); const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, this._viewport.parentInjector); // create ComponentRef and set it to the entering view enteringView.init(componentFactory.create(childInjector, [])); - enteringView._state = ViewState.INITIALIZED; + enteringView._state = STATE_INITIALIZED; this._preLoad(enteringView); } _viewAttachToDOM(view: ViewController, componentRef: ComponentRef, viewport: ViewContainerRef) { - assert(view._state === ViewState.INITIALIZED, 'view state must be INITIALIZED'); + assert(view._state === STATE_INITIALIZED, 'view state must be INITIALIZED'); // fire willLoad before change detection runs this._willLoad(view); @@ -501,7 +512,7 @@ export class NavControllerBase extends Ion implements NavController { // render the component ref instance to the DOM // ******** DOM WRITE **************** viewport.insert(componentRef.hostView, viewport.length); - view._state = ViewState.PRE_RENDERED; + view._state = STATE_PRE_RENDERED; if (view._cssClass) { // the ElementRef of the actual ion-page created @@ -604,7 +615,7 @@ export class NavControllerBase extends Ion implements NavController { } }); - if (enteringView && enteringView._state === ViewState.INITIALIZED) { + if (enteringView && enteringView._state === STATE_INITIALIZED) { // render the entering component in the DOM // this would also render new child navs/views // which may have their very own async canEnter/Leave tests diff --git a/src/navigation/nav-util.ts b/src/navigation/nav-util.ts index d5e42b2ff2..15cc518092 100644 --- a/src/navigation/nav-util.ts +++ b/src/navigation/nav-util.ts @@ -7,33 +7,38 @@ import { NavControllerBase } from './nav-controller-base'; import { Transition } from '../transitions/transition'; -export function getComponent(linker: DeepLinker, nameOrPageOrView: any): any { +export function getComponent(linker: DeepLinker, nameOrPageOrView: any, params?: any) { if (typeof nameOrPageOrView === 'function') { - return nameOrPageOrView; + return Promise.resolve( + new ViewController(nameOrPageOrView, params) + ); } + if (typeof nameOrPageOrView === 'string') { - return linker.getComponentFromName(nameOrPageOrView); + return linker.getComponentFromName(nameOrPageOrView).then((component) => { + return new ViewController(component, params); + }); } - return null; + + return Promise.resolve(null); } -export function convertToView(linker: DeepLinker, nameOrPageOrView: any, params: any): ViewController { +export function convertToView(linker: DeepLinker, nameOrPageOrView: any, params: any) { if (nameOrPageOrView) { if (isViewController(nameOrPageOrView)) { // is already a ViewController - return nameOrPageOrView; - } - let component = getComponent(linker, nameOrPageOrView); - if (component) { - return new ViewController(component, params); + return Promise.resolve(nameOrPageOrView); } + + return getComponent(linker, nameOrPageOrView, params); } + console.error(`invalid page component: ${nameOrPageOrView}`); - return null; + return Promise.resolve(null); } -export function convertToViews(linker: DeepLinker, pages: any[]): ViewController[] { - const views: ViewController[] = []; +export function convertToViews(linker: DeepLinker, pages: any[]) { + const views: Promise[] = []; if (isArray(pages)) { for (var i = 0; i < pages.length; i++) { var page = pages[i]; @@ -50,7 +55,7 @@ export function convertToViews(linker: DeepLinker, pages: any[]): ViewController } } } - return views; + return Promise.all(views); } let portalZindex = 9999; @@ -98,19 +103,21 @@ export function isNav(nav: any): boolean { // public link interface export interface DeepLinkMetadataType { - name: string; + name?: string; segment?: string; - defaultHistory?: any[]; + defaultHistory?: string[]; } /** * @private */ export class DeepLinkMetadata implements DeepLinkMetadataType { - component: any; - name: string; + component?: any; + viewFactoryFunction?: string; + loadChildren?: string; + name?: string; segment?: string; - defaultHistory?: any[]; + defaultHistory?: string[]; } export interface DeepLinkDecorator extends TypeDecorator {} @@ -134,7 +141,8 @@ export interface DeepLinkConfig { // internal link interface, not exposed publicly export interface NavLink { - component: any; + component?: any; + loadChildren?: string; name?: string; segment?: string; parts?: string[]; @@ -148,7 +156,8 @@ export interface NavLink { export interface NavSegment { id: string; name: string; - component: any; + component?: any; + loadChildren?: string; data: any; navId?: string; defaultHistory?: NavSegment[]; @@ -192,11 +201,10 @@ export interface TransitionInstruction { requiresTransition?: boolean; } -export enum ViewState { - INITIALIZED, - PRE_RENDERED, - LOADED, -} + +export const STATE_INITIALIZED = 1; +export const STATE_PRE_RENDERED = 2; +export const STATE_LOADED = 3; export const INIT_ZINDEX = 100; diff --git a/src/navigation/swipe-back.ts b/src/navigation/swipe-back.ts index ba84f3afd0..aef0911c85 100644 --- a/src/navigation/swipe-back.ts +++ b/src/navigation/swipe-back.ts @@ -1,6 +1,6 @@ import { swipeShouldReset } from '../util/util'; import { DomController } from '../platform/dom-controller'; -import { GestureController, GesturePriority, GESTURE_GO_BACK_SWIPE } from '../gestures/gesture-controller'; +import { GestureController, GESTURE_PRIORITY_GO_BACK_SWIPE, GESTURE_GO_BACK_SWIPE } from '../gestures/gesture-controller'; import { NavControllerBase } from './nav-controller-base'; import { Platform } from '../platform/platform'; import { SlideData } from '../gestures/slide-gesture'; @@ -26,7 +26,7 @@ export class SwipeBackGesture extends SlideEdgeGesture { domController: domCtrl, gesture: gestureCtlr.createGesture({ name: GESTURE_GO_BACK_SWIPE, - priority: GesturePriority.GoBackSwipe, + priority: GESTURE_PRIORITY_GO_BACK_SWIPE, disableScroll: true }) }); diff --git a/src/navigation/url-serializer.ts b/src/navigation/url-serializer.ts index f3af4755de..bd4ab53bfb 100644 --- a/src/navigation/url-serializer.ts +++ b/src/navigation/url-serializer.ts @@ -35,21 +35,25 @@ export class UrlSerializer { } createSegmentFromName(nameOrComponent: any): NavSegment { - const configLink = this.links.find((link: NavLink) => { - return (link.component === nameOrComponent) || - (link.name === nameOrComponent) || - (link.component.name === nameOrComponent); - }); + const configLink = this.getLinkFromName(nameOrComponent); return configLink ? { id: configLink.name, name: configLink.name, component: configLink.component, + loadChildren: configLink.loadChildren, data: null, defaultHistory: configLink.defaultHistory } : null; } + getLinkFromName(nameOrComponent: any) { + return this.links.find(link => { + return (link.component === nameOrComponent) || + (link.name === nameOrComponent); + }); + } + /** * Serialize a path, which is made up of multiple NavSegments, * into a URL string. Turn each segment into a string and concat them to a URL. @@ -65,13 +69,14 @@ export class UrlSerializer { if (component) { const link = findLinkByComponentData(this.links, component, data); if (link) { - return this.createSegment(link, data); + return this._createSegment(link, data); } } return null; } - createSegment(configLink: NavLink, data: any): NavSegment { + /** @internal */ + _createSegment(configLink: NavLink, data: any): NavSegment { let urlParts = configLink.parts; if (isPresent(data)) { @@ -101,6 +106,7 @@ export class UrlSerializer { id: urlParts.join('/'), name: configLink.name, component: configLink.component, + loadChildren: configLink.loadChildren, data: data, defaultHistory: configLink.defaultHistory }; @@ -151,6 +157,7 @@ export const parseUrlParts = (urlParts: string[], configLinks: NavLink[]): NavSe id: urlParts[i], name: urlParts[i], component: null, + loadChildren: null, data: null }; } @@ -181,6 +188,7 @@ export const fillMatchedUrlParts = (segments: NavSegment[], urlParts: string[], id: matchedUrlParts.join('/'), name: configLink.name, component: configLink.component, + loadChildren: configLink.loadChildren, data: createMatchedData(matchedUrlParts, configLink), defaultHistory: configLink.defaultHistory }; diff --git a/src/navigation/view-controller.ts b/src/navigation/view-controller.ts index 49a94746cc..5bd3ca5090 100644 --- a/src/navigation/view-controller.ts +++ b/src/navigation/view-controller.ts @@ -1,10 +1,11 @@ import { ComponentRef, ElementRef, EventEmitter, Output, Renderer } from '@angular/core'; -import { Footer, Header } from '../components/toolbar/toolbar'; +import { Footer } from '../components/toolbar/toolbar-footer'; +import { Header } from '../components/toolbar/toolbar-header'; import { isPresent } from '../util/util'; import { Navbar } from '../components/navbar/navbar'; import { NavController } from './nav-controller'; -import { NavOptions, ViewState } from './nav-util'; +import { NavOptions } from './nav-util'; import { NavParams } from './nav-params'; import { Content } from '../components/content/content'; @@ -46,7 +47,7 @@ export class ViewController { _cmp: ComponentRef; _nav: NavController; _zIndex: number; - _state: ViewState; + _state: number; _cssClass: string; /**