refactor(navigation): async component loading (aka lazy loading)

async component loading (aka lazy loading)
This commit is contained in:
Dan Bucholtz
2017-03-02 15:05:35 -06:00
parent e40590d68c
commit 96657535f1
6 changed files with 204 additions and 140 deletions

View File

@ -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<any, ComponentFactoryResolver>();
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<any> {
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<any> {
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(<any>nav);
tabSelector = this._getTabSelector(<any>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;
if (isArray(segment.defaultHistory)) {
return convertToViews(this, segment.defaultHistory).then(views => {
views.push(view);
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;
}

View File

@ -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<any> {
return convertToView(this._linker, page, params).then(viewController => {
return this._queueTrns({
insertStart: -1,
insertViews: [convertToView(this._linker, page, params)],
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<any> {
return convertToView(this._linker, page, params).then(viewController => {
return this._queueTrns({
insertStart: insertIndex,
insertViews: [convertToView(this._linker, page, params)],
insertViews: [viewController],
opts: opts,
}, done);
});
}
insertPages(insertIndex: number, insertPages: any[], opts?: NavOptions, done?: Function): Promise<any> {
return convertToViews(this._linker, insertPages).then(viewControllers => {
return this._queueTrns({
insertStart: insertIndex,
insertViews: convertToViews(this._linker, insertPages),
insertViews: viewControllers,
opts: opts,
}, done);
});
}
pop(opts?: NavOptions, done?: Function): Promise<any> {
@ -152,13 +161,15 @@ export class NavControllerBase extends Ion implements NavController {
}
setRoot(pageOrViewCtrl: any, params?: any, opts?: NavOptions, done?: Function): Promise<any> {
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<any> {
const viewControllers = convertToViews(this._linker, pages);
return convertToViews(this._linker, pages).then(viewControllers => {
return this._setPages(viewControllers, opts, done);
});
}
_setPages(viewControllers: ViewController[], opts?: NavOptions, done?: Function): Promise<any> {
@ -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<any>, 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

View File

@ -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(<ViewController>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<ViewController>[] = [];
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;

View File

@ -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
})
});

View File

@ -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
};

View File

@ -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<any>;
_nav: NavController;
_zIndex: number;
_state: ViewState;
_state: number;
_cssClass: string;
/**