feat(WebWorker): Add Router Support for WebWorker Apps

Closes #3563.
This commit is contained in:
Jason Teplitz
2016-01-21 09:58:28 -08:00
committed by Rado Kirov
parent 800c8f196f
commit 8bea667a0b
43 changed files with 839 additions and 153 deletions

View File

@ -36,6 +36,7 @@ import {BrowserDomAdapter} from './browser/browser_adapter';
import {wtfInit} from 'angular2/src/core/profile/wtf_init';
import {MessageBasedRenderer} from 'angular2/src/web_workers/ui/renderer';
import {MessageBasedXHRImpl} from 'angular2/src/web_workers/ui/xhr_impl';
import {BrowserPlatformLocation} from 'angular2/src/router/browser_platform_location';
import {
ServiceMessageBrokerFactory,
ServiceMessageBrokerFactory_
@ -59,6 +60,13 @@ export const WORKER_RENDER_PLATFORM: Array<any /*Type | Provider | any[]*/> = CO
new Provider(PLATFORM_INITIALIZER, {useValue: initWebWorkerRenderPlatform, multi: true})
]);
/**
* A list of {@link Provider}s. To use the router in a Worker enabled application you must
* include these providers when setting up the render thread.
*/
export const WORKER_RENDER_ROUTER: Array<any /*Type | Provider | any[]*/> =
CONST_EXPR([BrowserPlatformLocation]);
export const WORKER_RENDER_APPLICATION_COMMON: Array<any /*Type | Provider | any[]*/> = CONST_EXPR([
APPLICATION_COMMON_PROVIDERS,
WORKER_RENDER_MESSAGING_PROVIDERS,

View File

@ -0,0 +1,58 @@
import {Injectable} from 'angular2/core';
import {History, Location} from 'angular2/src/facade/browser';
import {UrlChangeListener} from './platform_location';
import {PlatformLocation} from './platform_location';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
/**
* `PlatformLocation` encapsulates all of the direct calls to platform APIs.
* This class should not be used directly by an application developer. Instead, use
* {@link Location}.
*/
@Injectable()
export class BrowserPlatformLocation extends PlatformLocation {
private _location: Location;
private _history: History;
constructor() {
super();
this._init();
}
// This is moved to its own method so that `MockPlatformLocationStrategy` can overwrite it
/** @internal */
_init() {
this._location = DOM.getLocation();
this._history = DOM.getHistory();
}
/** @internal */
get location(): Location { return this._location; }
getBaseHrefFromDOM(): string { return DOM.getBaseHref(); }
onPopState(fn: UrlChangeListener): void {
DOM.getGlobalEventTarget('window').addEventListener('popstate', fn, false);
}
onHashChange(fn: UrlChangeListener): void {
DOM.getGlobalEventTarget('window').addEventListener('hashchange', fn, false);
}
get pathname(): string { return this._location.pathname; }
get search(): string { return this._location.search; }
get hash(): string { return this._location.hash; }
set pathname(newPath: string) { this._location.pathname = newPath; }
pushState(state: any, title: string, url: string): void {
this._history.pushState(state, title, url);
}
replaceState(state: any, title: string, url: string): void {
this._history.replaceState(state, title, url);
}
forward(): void { this._history.forward(); }
back(): void { this._history.back(); }
}

View File

@ -5,7 +5,7 @@ import {
APP_BASE_HREF,
normalizeQueryParams
} from './location_strategy';
import {EventListener} from 'angular2/src/facade/browser';
import {UrlChangeListener} from './platform_location';
import {isPresent} from 'angular2/src/facade/lang';
import {PlatformLocation} from './platform_location';
@ -58,7 +58,7 @@ export class HashLocationStrategy extends LocationStrategy {
}
}
onPopState(fn: EventListener): void {
onPopState(fn: UrlChangeListener): void {
this._platformLocation.onPopState(fn);
this._platformLocation.onHashChange(fn);
}

View File

@ -1,5 +1,6 @@
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {OpaqueToken} from 'angular2/core';
import {UrlChangeListener} from './platform_location';
/**
* `LocationStrategy` is responsible for representing and reading route state
@ -24,7 +25,7 @@ export abstract class LocationStrategy {
abstract replaceState(state: any, title: string, url: string, queryParams: string): void;
abstract forward(): void;
abstract back(): void;
abstract onPopState(fn: (_: any) => any): void;
abstract onPopState(fn: UrlChangeListener): void;
abstract getBaseHref(): string;
}

View File

@ -1,5 +1,4 @@
import {Injectable, Inject, Optional} from 'angular2/core';
import {EventListener, History, Location} from 'angular2/src/facade/browser';
import {isBlank} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {
@ -8,7 +7,7 @@ import {
normalizeQueryParams,
joinWithSlash
} from './location_strategy';
import {PlatformLocation} from './platform_location';
import {PlatformLocation, UrlChangeListener} from './platform_location';
/**
* `PathLocationStrategy` is a {@link LocationStrategy} used to configure the
@ -75,7 +74,7 @@ export class PathLocationStrategy extends LocationStrategy {
this._baseHref = href;
}
onPopState(fn: EventListener): void {
onPopState(fn: UrlChangeListener): void {
this._platformLocation.onPopState(fn);
this._platformLocation.onHashChange(fn);
}

View File

@ -1,50 +1,48 @@
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {Injectable} from 'angular2/core';
import {EventListener, History, Location} from 'angular2/src/facade/browser';
/**
* `PlatformLocation` encapsulates all of the direct calls to platform APIs.
* This class should not be used directly by an application developer. Instead, use
* {@link Location}.
*
* `PlatformLocation` encapsulates all calls to DOM apis, which allows the Router to be platform
* agnostic.
* This means that we can have different implementation of `PlatformLocation` for the different
* platforms
* that angular supports. For example, the default `PlatformLocation` is {@link
* BrowserPlatformLocation},
* however when you run your app in a WebWorker you use {@link WebWorkerPlatformLocation}.
*
* The `PlatformLocation` class is used directly by all implementations of {@link LocationStrategy}
* when
* they need to interact with the DOM apis like pushState, popState, etc...
*
* {@link LocationStrategy} in turn is used by the {@link Location} service which is used directly
* by
* the {@link Router} in order to navigate between routes. Since all interactions between {@link
* Router} /
* {@link Location} / {@link LocationStrategy} and DOM apis flow through the `PlatformLocation`
* class
* they are all platform independent.
*/
@Injectable()
export class PlatformLocation {
private _location: Location;
private _history: History;
export abstract class PlatformLocation {
abstract getBaseHrefFromDOM(): string;
abstract onPopState(fn: UrlChangeListener): void;
abstract onHashChange(fn: UrlChangeListener): void;
constructor() { this._init(); }
pathname: string;
search: string;
hash: string;
// This is moved to its own method so that `MockPlatformLocationStrategy` can overwrite it
/** @internal */
_init() {
this._location = DOM.getLocation();
this._history = DOM.getHistory();
}
abstract replaceState(state: any, title: string, url: string): void;
getBaseHrefFromDOM(): string { return DOM.getBaseHref(); }
abstract pushState(state: any, title: string, url: string): void;
onPopState(fn: EventListener): void {
DOM.getGlobalEventTarget('window').addEventListener('popstate', fn, false);
}
abstract forward(): void;
onHashChange(fn: EventListener): void {
DOM.getGlobalEventTarget('window').addEventListener('hashchange', fn, false);
}
get pathname(): string { return this._location.pathname; }
get search(): string { return this._location.search; }
get hash(): string { return this._location.hash; }
set pathname(newPath: string) { this._location.pathname = newPath; }
pushState(state: any, title: string, url: string): void {
this._history.pushState(state, title, url);
}
replaceState(state: any, title: string, url: string): void {
this._history.replaceState(state, title, url);
}
forward(): void { this._history.forward(); }
back(): void { this._history.back(); }
abstract back(): void;
}
/**
* A serializable version of the event from onPopState or onHashChange
*/
export interface UrlChangeEvent { type: string; }
export interface UrlChangeListener { (e: UrlChangeEvent): any; }

View File

@ -0,0 +1,42 @@
// import {ROUTER_PROVIDERS_COMMON} from './router_providers_common';
import {ROUTER_PROVIDERS_COMMON} from 'angular2/router';
import {Provider} from 'angular2/core';
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {BrowserPlatformLocation} from './browser_platform_location';
import {PlatformLocation} from './platform_location';
/**
* A list of {@link Provider}s. To use the router, you must add this to your application.
*
* ### Example ([live demo](http://plnkr.co/edit/iRUP8B5OUbxCWQ3AcIDm))
*
* ```
* import {Component} from 'angular2/core';
* import {
* ROUTER_DIRECTIVES,
* ROUTER_PROVIDERS,
* RouteConfig
* } from 'angular2/router';
*
* @Component({directives: [ROUTER_DIRECTIVES]})
* @RouteConfig([
* {...},
* ])
* class AppCmp {
* // ...
* }
*
* bootstrap(AppCmp, [ROUTER_PROVIDERS]);
* ```
*/
export const ROUTER_PROVIDERS: any[] = CONST_EXPR([
ROUTER_PROVIDERS_COMMON,
CONST_EXPR(new Provider(PlatformLocation, {useClass: BrowserPlatformLocation})),
]);
/**
* Use {@link ROUTER_PROVIDERS} instead.
*
* @deprecated
*/
export const ROUTER_BINDINGS = ROUTER_PROVIDERS;

View File

@ -0,0 +1,40 @@
import {LocationStrategy} from 'angular2/src/router/location_strategy';
import {PathLocationStrategy} from 'angular2/src/router/path_location_strategy';
import {Router, RootRouter} from 'angular2/src/router/router';
import {RouteRegistry, ROUTER_PRIMARY_COMPONENT} from 'angular2/src/router/route_registry';
import {Location} from 'angular2/src/router/location';
import {CONST_EXPR, Type} from 'angular2/src/facade/lang';
import {ApplicationRef, OpaqueToken, Provider} from 'angular2/core';
import {BaseException} from 'angular2/src/facade/exceptions';
/**
* The Platform agnostic ROUTER PROVIDERS
*/
export const ROUTER_PROVIDERS_COMMON: any[] = CONST_EXPR([
RouteRegistry,
CONST_EXPR(new Provider(LocationStrategy, {useClass: PathLocationStrategy})),
Location,
CONST_EXPR(new Provider(
Router,
{
useFactory: routerFactory,
deps: CONST_EXPR([RouteRegistry, Location, ROUTER_PRIMARY_COMPONENT, ApplicationRef])
})),
CONST_EXPR(new Provider(
ROUTER_PRIMARY_COMPONENT,
{useFactory: routerPrimaryComponentFactory, deps: CONST_EXPR([ApplicationRef])}))
]);
function routerFactory(registry: RouteRegistry, location: Location, primaryComponent: Type,
appRef: ApplicationRef): RootRouter {
var rootRouter = new RootRouter(registry, location, primaryComponent);
appRef.registerDisposeListener(() => rootRouter.dispose());
return rootRouter;
}
function routerPrimaryComponentFactory(app: ApplicationRef): Type {
if (app.componentTypes.length == 0) {
throw new BaseException("Bootstrap at least one component before injecting Router.");
}
return app.componentTypes[0];
}

View File

@ -4,4 +4,5 @@
*/
export const RENDERER_CHANNEL = "ng-Renderer";
export const XHR_CHANNEL = "ng-XHR";
export const EVENT_CHANNEL = "ng-events";
export const EVENT_CHANNEL = "ng-Events";
export const ROUTER_CHANNEL = "ng-Router";

View File

@ -0,0 +1,7 @@
// This file contains interface versions of browser types that can be serialized to Plain Old
// JavaScript Objects
export class LocationType {
constructor(public href: string, public protocol: string, public host: string,
public hostname: string, public port: string, public pathname: string,
public search: string, public hash: string, public origin: string) {}
}

View File

@ -6,6 +6,7 @@ import {RenderComponentType} from "angular2/src/core/render/api";
import {Injectable} from "angular2/src/core/di";
import {RenderStore} from 'angular2/src/web_workers/shared/render_store';
import {ViewEncapsulation, VIEW_ENCAPSULATION_VALUES} from 'angular2/src/core/metadata/view';
import {LocationType} from './serialized_types';
// PRIMITIVE is any type that does not need to be serialized (string, number, boolean)
// We set it to String so that it is considered a Type.
@ -31,6 +32,8 @@ export class Serializer {
return this._serializeRenderComponentType(obj);
} else if (type === ViewEncapsulation) {
return serializeEnum(obj);
} else if (type === LocationType) {
return this._serializeLocation(obj);
} else {
throw new BaseException("No serializer for " + type.toString());
}
@ -55,6 +58,8 @@ export class Serializer {
return this._deserializeRenderComponentType(map);
} else if (type === ViewEncapsulation) {
return VIEW_ENCAPSULATION_VALUES[map];
} else if (type === LocationType) {
return this._deserializeLocation(map);
} else {
throw new BaseException("No deserializer for " + type.toString());
}
@ -90,6 +95,25 @@ export class Serializer {
}
}
private _serializeLocation(loc: LocationType): Object {
return {
'href': loc.href,
'protocol': loc.protocol,
'host': loc.host,
'hostname': loc.hostname,
'port': loc.port,
'pathname': loc.pathname,
'search': loc.search,
'hash': loc.hash,
'origin': loc.origin
};
}
private _deserializeLocation(loc: {[key: string]: any}): LocationType {
return new LocationType(loc['href'], loc['protocol'], loc['host'], loc['hostname'], loc['port'],
loc['pathname'], loc['search'], loc['hash'], loc['origin']);
}
private _serializeRenderComponentType(obj: RenderComponentType): Object {
return {
'id': obj.id,
@ -106,4 +130,4 @@ export class Serializer {
}
export class RenderStoreObject {}
export class RenderStoreObject {}

View File

@ -50,11 +50,13 @@ export class ServiceMessageBroker_ extends ServiceMessageBroker {
ObservableWrapper.subscribe(source, (message) => this._handleMessage(message));
}
registerMethod(methodName: string, signature: Type[], method: Function, returnType?: Type): void {
registerMethod(methodName: string, signature: Type[], method: (..._: any[]) => Promise<any>| void,
returnType?: Type): void {
this._methods.set(methodName, (message: ReceivedMessage) => {
var serializedArgs = message.args;
var deserializedArgs: any[] = ListWrapper.createFixedSize(signature.length);
for (var i = 0; i < signature.length; i++) {
let numArgs = signature === null ? 0 : signature.length;
var deserializedArgs: any[] = ListWrapper.createFixedSize(numArgs);
for (var i = 0; i < numArgs; i++) {
var serializedArg = serializedArgs[i];
deserializedArgs[i] = this._serializer.deserialize(serializedArg, signature[i]);
}

View File

@ -0,0 +1,54 @@
import {BrowserPlatformLocation} from 'angular2/src/router/browser_platform_location';
import {Injectable} from 'angular2/src/core/di';
import {ROUTER_CHANNEL} from 'angular2/src/web_workers/shared/messaging_api';
import {
ServiceMessageBrokerFactory,
ServiceMessageBroker
} from 'angular2/src/web_workers/shared/service_message_broker';
import {PRIMITIVE, Serializer} from 'angular2/src/web_workers/shared/serializer';
import {bind} from './bind';
import {LocationType} from 'angular2/src/web_workers/shared/serialized_types';
import {MessageBus} from 'angular2/src/web_workers/shared/message_bus';
import {Promise, EventEmitter, ObservableWrapper, PromiseWrapper} from 'angular2/src/facade/async';
import {UrlChangeListener} from 'angular2/src/router/platform_location';
@Injectable()
export class MessageBasedPlatformLocation {
private _channelSink: EventEmitter<Object>;
private _broker: ServiceMessageBroker;
constructor(private _brokerFactory: ServiceMessageBrokerFactory,
private _platformLocation: BrowserPlatformLocation, bus: MessageBus,
private _serializer: Serializer) {
this._platformLocation.onPopState(<UrlChangeListener>bind(this._sendUrlChangeEvent, this));
this._platformLocation.onHashChange(<UrlChangeListener>bind(this._sendUrlChangeEvent, this));
this._broker = this._brokerFactory.createMessageBroker(ROUTER_CHANNEL);
this._channelSink = bus.to(ROUTER_CHANNEL);
}
start(): void {
this._broker.registerMethod("getLocation", null, bind(this._getLocation, this), LocationType);
this._broker.registerMethod("setPathname", [PRIMITIVE], bind(this._setPathname, this));
this._broker.registerMethod("pushState", [PRIMITIVE, PRIMITIVE, PRIMITIVE],
bind(this._platformLocation.pushState, this._platformLocation));
this._broker.registerMethod("replaceState", [PRIMITIVE, PRIMITIVE, PRIMITIVE],
bind(this._platformLocation.replaceState, this._platformLocation));
this._broker.registerMethod("forward", null,
bind(this._platformLocation.forward, this._platformLocation));
this._broker.registerMethod("back", null,
bind(this._platformLocation.back, this._platformLocation));
}
private _getLocation(): Promise<Location> {
return PromiseWrapper.resolve(this._platformLocation.location);
}
private _sendUrlChangeEvent(e: Event): void {
let loc = this._serializer.serialize(this._platformLocation.location, LocationType);
let serializedEvent = {'type': e.type};
ObservableWrapper.callEmit(this._channelSink, {'event': serializedEvent, 'location': loc});
}
private _setPathname(pathname: string): void { this._platformLocation.pathname = pathname; }
}

View File

@ -0,0 +1,20 @@
import {MessageBasedPlatformLocation} from './platform_location';
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {BrowserPlatformLocation} from 'angular2/src/router/browser_platform_location';
import {APP_INITIALIZER, Provider, Injector, NgZone} from 'angular2/core';
export const WORKER_RENDER_ROUTER = CONST_EXPR([
MessageBasedPlatformLocation,
BrowserPlatformLocation,
CONST_EXPR(
new Provider(APP_INITIALIZER,
{useFactory: initRouterListeners, multi: true, deps: CONST_EXPR([Injector])}))
]);
function initRouterListeners(injector: Injector): () => void {
return () => {
let zone = injector.get(NgZone);
zone.run(() => injector.get(MessageBasedPlatformLocation).start());
};
}

View File

@ -0,0 +1,136 @@
import {Injectable} from 'angular2/src/core/di';
import {
PlatformLocation,
UrlChangeEvent,
UrlChangeListener
} from 'angular2/src/router/platform_location';
import {
FnArg,
UiArguments,
ClientMessageBroker,
ClientMessageBrokerFactory
} from 'angular2/src/web_workers/shared/client_message_broker';
import {ROUTER_CHANNEL} from 'angular2/src/web_workers/shared/messaging_api';
import {LocationType} from 'angular2/src/web_workers/shared/serialized_types';
import {Promise, PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {BaseException} from 'angular2/src/facade/exceptions';
import {PRIMITIVE, Serializer} from 'angular2/src/web_workers/shared/serializer';
import {MessageBus} from 'angular2/src/web_workers/shared/message_bus';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {StringWrapper} from 'angular2/src/facade/lang';
import {deserializeGenericEvent} from './event_deserializer';
@Injectable()
export class WebWorkerPlatformLocation extends PlatformLocation {
private _broker: ClientMessageBroker;
private _popStateListeners: Array<Function> = [];
private _hashChangeListeners: Array<Function> = [];
private _location: LocationType = null;
private _channelSource: EventEmitter<Object>;
constructor(brokerFactory: ClientMessageBrokerFactory, bus: MessageBus,
private _serializer: Serializer) {
super();
this._broker = brokerFactory.createMessageBroker(ROUTER_CHANNEL);
this._channelSource = bus.from(ROUTER_CHANNEL);
ObservableWrapper.subscribe(this._channelSource, (msg: {[key: string]: any}) => {
var listeners: Array<Function> = null;
if (StringMapWrapper.contains(msg, 'event')) {
let type: string = msg['event']['type'];
if (StringWrapper.equals(type, "popstate")) {
listeners = this._popStateListeners;
} else if (StringWrapper.equals(type, "hashchange")) {
listeners = this._hashChangeListeners;
}
if (listeners !== null) {
let e = deserializeGenericEvent(msg['event']);
// There was a popState or hashChange event, so the location object thas been updated
this._location = this._serializer.deserialize(msg['location'], LocationType);
listeners.forEach((fn: Function) => fn(e));
}
}
});
}
/** @internal **/
init(): Promise<boolean> {
var args: UiArguments = new UiArguments("getLocation");
var locationPromise: Promise<LocationType> = this._broker.runOnService(args, LocationType);
return PromiseWrapper.then(locationPromise, (val: LocationType): boolean => {
this._location = val;
return true;
}, (err): boolean => { throw new BaseException(err); });
}
getBaseHrefFromDOM(): string {
throw new BaseException(
"Attempt to get base href from DOM from WebWorker. You must either provide a value for the APP_BASE_HREF token through DI or use the hash location strategy.");
}
onPopState(fn: UrlChangeListener): void { this._popStateListeners.push(fn); }
onHashChange(fn: UrlChangeListener): void { this._hashChangeListeners.push(fn); }
get pathname(): string {
if (this._location === null) {
return null;
}
return this._location.pathname;
}
get search(): string {
if (this._location === null) {
return null;
}
return this._location.search;
}
get hash(): string {
if (this._location === null) {
return null;
}
return this._location.hash;
}
set pathname(newPath: string) {
if (this._location === null) {
throw new BaseException("Attempt to set pathname before value is obtained from UI");
}
this._location.pathname = newPath;
var fnArgs = [new FnArg(newPath, PRIMITIVE)];
var args = new UiArguments("setPathname", fnArgs);
this._broker.runOnService(args, null);
}
pushState(state: any, title: string, url: string): void {
var fnArgs =
[new FnArg(state, PRIMITIVE), new FnArg(title, PRIMITIVE), new FnArg(url, PRIMITIVE)];
var args = new UiArguments("pushState", fnArgs);
this._broker.runOnService(args, null);
}
replaceState(state: any, title: string, url: string): void {
var fnArgs =
[new FnArg(state, PRIMITIVE), new FnArg(title, PRIMITIVE), new FnArg(url, PRIMITIVE)];
var args = new UiArguments("replaceState", fnArgs);
this._broker.runOnService(args, null);
}
forward(): void {
var args = new UiArguments("forward");
this._broker.runOnService(args, null);
}
back(): void {
var args = new UiArguments("back");
this._broker.runOnService(args, null);
}
}

View File

@ -0,0 +1,21 @@
import {ApplicationRef, Provider, NgZone, APP_INITIALIZER} from 'angular2/core';
import {PlatformLocation} from 'angular2/src/router/platform_location';
import {WebWorkerPlatformLocation} from './platform_location';
import {ROUTER_PROVIDERS_COMMON} from 'angular2/src/router/router_providers_common';
import {Promise} from 'angular2/src/facade/async';
export var WORKER_APP_ROUTER = [
ROUTER_PROVIDERS_COMMON,
new Provider(PlatformLocation, {useClass: WebWorkerPlatformLocation}),
new Provider(APP_INITIALIZER,
{
useFactory: (platformLocation: WebWorkerPlatformLocation, zone: NgZone) => () =>
initRouter(platformLocation, zone),
multi: true,
deps: [PlatformLocation, NgZone]
})
];
function initRouter(platformLocation: WebWorkerPlatformLocation, zone: NgZone): Promise<boolean> {
return zone.run(() => { return platformLocation.init(); });
}