feat(core): support for bootstrap with custom zone (#17672)

PR Close #17672
This commit is contained in:
Miško Hevery 2017-06-01 14:45:49 -07:00 committed by Igor Minar
parent 6e1896b333
commit 344a5ca545
7 changed files with 123 additions and 35 deletions

View File

@ -28,7 +28,7 @@ import {InternalViewRef, ViewRef} from './linker/view_ref';
import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile';
import {Testability, TestabilityRegistry} from './testability/testability';
import {Type} from './type';
import {NgZone} from './zone/ng_zone';
import {NgZone, NoopNgZone} from './zone/ng_zone';
let _devMode: boolean = true;
let _runModeLocked: boolean = false;
@ -158,6 +158,22 @@ export function getPlatform(): PlatformRef|null {
return _platform && !_platform.destroyed ? _platform : null;
}
/**
* Provides additional options to the bootstraping process.
*
* @stable
*/
export interface BootstrapOptions {
/**
* Optionally specify which `NgZone` should be used.
*
* - Provide your own `NgZone` instance.
* - `zone.js` - Use default `NgZone` which requires `Zone.js`.
* - `noop` - Use `NoopNgZone` which does nothing.
*/
ngZone?: NgZone|'zone.js'|'noop';
}
/**
* The Angular platform is the entry point for Angular on a web page. Each page
* has exactly one platform, and services (such as reflection) which are common
@ -168,6 +184,7 @@ export function getPlatform(): PlatformRef|null {
*
* @stable
*/
@Injectable()
export class PlatformRef {
private _modules: NgModuleRef<any>[] = [];
private _destroyListeners: Function[] = [];
@ -199,17 +216,14 @@ export class PlatformRef {
*
* @experimental APIs related to application bootstrap are currently under review.
*/
bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>): Promise<NgModuleRef<M>> {
return this._bootstrapModuleFactoryWithZone(moduleFactory);
}
private _bootstrapModuleFactoryWithZone<M>(moduleFactory: NgModuleFactory<M>, ngZone?: NgZone):
bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions):
Promise<NgModuleRef<M>> {
// Note: We need to create the NgZone _before_ we instantiate the module,
// as instantiating the module creates some providers eagerly.
// So we create a mini parent injector that just contains the new NgZone and
// pass that as parent to the NgModuleFactory.
if (!ngZone) ngZone = new NgZone({enableLongStackTrace: isDevMode()});
const ngZoneOption = options ? options.ngZone : undefined;
const ngZone = getNgZone(ngZoneOption);
// Attention: Don't use ApplicationRef.run here,
// as we want to be sure that all possible constructor calls are inside `ngZone.run`!
return ngZone.run(() => {
@ -249,20 +263,15 @@ export class PlatformRef {
* ```
* @stable
*/
bootstrapModule<M>(moduleType: Type<M>, compilerOptions: CompilerOptions|CompilerOptions[] = []):
Promise<NgModuleRef<M>> {
return this._bootstrapModuleWithZone(moduleType, compilerOptions);
}
private _bootstrapModuleWithZone<M>(
moduleType: Type<M>, compilerOptions: CompilerOptions|CompilerOptions[] = [],
ngZone?: NgZone): Promise<NgModuleRef<M>> {
bootstrapModule<M>(
moduleType: Type<M>, compilerOptions: (CompilerOptions&BootstrapOptions)|
Array<CompilerOptions&BootstrapOptions> = []): Promise<NgModuleRef<M>> {
const compilerFactory: CompilerFactory = this.injector.get(CompilerFactory);
const compiler = compilerFactory.createCompiler(
Array.isArray(compilerOptions) ? compilerOptions : [compilerOptions]);
const options = optionsReducer({}, compilerOptions);
const compiler = compilerFactory.createCompiler([options]);
return compiler.compileModuleAsync(moduleType)
.then((moduleFactory) => this._bootstrapModuleFactoryWithZone(moduleFactory, ngZone));
.then((moduleFactory) => this.bootstrapModuleFactory(moduleFactory, options));
}
private _moduleDoBootstrap(moduleRef: InternalNgModuleRef<any>): void {
@ -305,6 +314,18 @@ export class PlatformRef {
get destroyed() { return this._destroyed; }
}
function getNgZone(ngZoneOption?: NgZone | 'zone.js' | 'noop'): NgZone {
let ngZone: NgZone;
if (ngZoneOption === 'noop') {
ngZone = new NoopNgZone();
} else {
ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) ||
new NgZone({enableLongStackTrace: isDevMode()});
}
return ngZone;
}
function _callAndReportToErrorHandler(
errorHandler: ErrorHandler, ngZone: NgZone, callback: () => any): any {
try {
@ -325,6 +346,15 @@ function _callAndReportToErrorHandler(
}
}
function optionsReducer<T extends Object>(dst: any, objs: T | T[]): T {
if (Array.isArray(objs)) {
dst = objs.reduce(optionsReducer, dst);
} else {
dst = {...dst, ...(objs as any)};
}
return dst;
}
/**
* A reference to an Angular application running on a page.
*

View File

@ -98,7 +98,7 @@ export class NgZone {
readonly onUnstable: EventEmitter<any> = new EventEmitter(false);
/**
* Notifies when there is no more microtasks enqueue in the current VM Turn.
* Notifies when there is no more microtasks enqueued in the current VM Turn.
* This is a hint for Angular to do change detection, which may enqueue more microtasks.
* For this reason this event can fire multiple times per VM Turn.
*/
@ -216,7 +216,7 @@ export class NgZone {
}
}
function noop(){};
function noop() {}
const EMPTY_PAYLOAD = {};
@ -308,3 +308,27 @@ function onLeave(zone: NgZonePrivate) {
zone._nesting--;
checkStable(zone);
}
/**
* Provides a noop implementation of `NgZone` which does nothing. This zone requires explicit calls
* to framework to perform rendering.
*
* @internal
*/
export class NoopNgZone implements NgZone {
readonly hasPendingMicrotasks: boolean = false;
readonly hasPendingMacrotasks: boolean = false;
readonly isStable: boolean = true;
readonly onUnstable: EventEmitter<any> = new EventEmitter();
readonly onMicrotaskEmpty: EventEmitter<any> = new EventEmitter();
readonly onStable: EventEmitter<any> = new EventEmitter();
readonly onError: EventEmitter<any> = new EventEmitter();
run(fn: () => any): any { return fn(); }
runGuarded(fn: () => any): any { return fn(); }
runOutsideAngular(fn: () => any): any { return fn(); }
runTask<T>(fn: () => any): any { return fn(); }
}

View File

@ -16,8 +16,8 @@ import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ServerModule} from '@angular/platform-server';
import {ComponentFixture, ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';
import {NoopNgZone} from '../src/zone/ng_zone';
import {ComponentFixtureNoNgZone, TestBed, async, inject, withModule} from '../testing';
@Component({selector: 'bootstrap-app', template: 'hello'})
class SomeComponent {
@ -287,6 +287,15 @@ export function main() {
defaultPlatform.bootstrapModule(createModule({bootstrap: [SomeComponent]}))
.then(module => expect((<any>defaultPlatform)._modules).toContain(module));
}));
it('should bootstrap with NoopNgZone', async(() => {
defaultPlatform
.bootstrapModule(createModule({bootstrap: [SomeComponent]}), {ngZone: 'noop'})
.then((module) => {
const ngZone = module.injector.get(NgZone);
expect(ngZone instanceof NoopNgZone).toBe(true);
});
}));
});
describe('bootstrapModuleFactory', () => {

View File

@ -6,11 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
import {NgZone} from '@angular/core/src/zone/ng_zone';
import {EventEmitter, NgZone} from '@angular/core';
import {async, fakeAsync, flushMicrotasks} from '@angular/core/testing';
import {AsyncTestCompleter, Log, beforeEach, describe, expect, inject, it, xit} from '@angular/core/testing/src/testing_internal';
import {browserDetection} from '@angular/platform-browser/testing/src/browser_util';
import {scheduleMicroTask} from '../../src/util';
import {NoopNgZone} from '../../src/zone/ng_zone';
const needsLongerTimers = browserDetection.isSlow || browserDetection.isEdge;
const resultTimer = 1000;
@ -170,6 +171,25 @@ export function main() {
}), testTimeout);
});
});
describe('NoopNgZone', () => {
const ngZone = new NoopNgZone();
it('should run', () => {
let runs = false;
ngZone.run(() => {
ngZone.runGuarded(() => { ngZone.runOutsideAngular(() => { runs = true; }); });
});
expect(runs).toBe(true);
});
it('should have EventEmitter instances', () => {
expect(ngZone.onError instanceof EventEmitter).toBe(true);
expect(ngZone.onStable instanceof EventEmitter).toBe(true);
expect(ngZone.onUnstable instanceof EventEmitter).toBe(true);
expect(ngZone.onMicrotaskEmpty instanceof EventEmitter).toBe(true);
});
});
}
function commonTests() {

View File

@ -572,9 +572,9 @@ export class UpgradeAdapter {
constructor() {}
ngDoBootstrap() {}
}
(platformRef as any)
._bootstrapModuleWithZone(
DynamicNgUpgradeModule, this.compilerOptions, this.ngZone)
platformRef
.bootstrapModule(
DynamicNgUpgradeModule, [this.compilerOptions !, {ngZone: this.ngZone}])
.then((ref: NgModuleRef<any>) => {
this.moduleRef = ref;
this.ngZone.run(() => {

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectorRef, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgZone, OnChanges, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core';
import {ChangeDetectorRef, Component, EventEmitter, Input, NO_ERRORS_SCHEMA, NgModule, NgModuleFactory, NgZone, OnChanges, SimpleChange, SimpleChanges, Testability, destroyPlatform, forwardRef} from '@angular/core';
import {async, fakeAsync, flushMicrotasks, tick} from '@angular/core/testing';
import {BrowserModule} from '@angular/platform-browser';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
@ -56,7 +56,7 @@ export function main() {
template: `{{ 'ng2(' }}<ng1>{{'transclude'}}</ng1>{{ ')' }}`,
})
class Ng2 {
};
}
@NgModule({
declarations: [adapter.upgradeNg1Component('ng1'), Ng2],
@ -80,7 +80,8 @@ export function main() {
it('supports the compilerOptions argument', async(() => {
const platformRef = platformBrowserDynamic();
spyOn(platformRef, '_bootstrapModuleWithZone').and.callThrough();
spyOn(platformRef, 'bootstrapModule').and.callThrough();
spyOn(platformRef, 'bootstrapModuleFactory').and.callThrough();
const ng1Module = angular.module('ng1', []);
@Component({selector: 'ng2', template: `{{ 'NG2' }}(<ng-content></ng-content>)`})
@ -96,13 +97,17 @@ export function main() {
})
class Ng2AppModule {
ngDoBootstrap() {}
};
}
const adapter: UpgradeAdapter = new UpgradeAdapter(Ng2AppModule, {providers: []});
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2));
adapter.bootstrap(element, ['ng1']).ready((ref) => {
expect((platformRef as any)._bootstrapModuleWithZone)
.toHaveBeenCalledWith(jasmine.any(Function), {providers: []}, jasmine.any(Object));
expect(platformRef.bootstrapModule).toHaveBeenCalledWith(jasmine.any(Function), [
{providers: []}, jasmine.any(Object)
]);
expect(platformRef.bootstrapModuleFactory)
.toHaveBeenCalledWith(
jasmine.any(NgModuleFactory), {providers: [], ngZone: jasmine.any(NgZone)});
ref.dispose();
});
}));
@ -395,7 +400,7 @@ export function main() {
imports: [BrowserModule],
})
class Ng2Module {
};
}
const element = html(`<div>
<ng2 literal="Text" interpolate="Hello {{name}}"

View File

@ -698,8 +698,8 @@ export declare const platformCore: (extraProviders?: StaticProvider[] | undefine
export declare class PlatformRef {
readonly destroyed: boolean;
readonly injector: Injector;
/** @stable */ bootstrapModule<M>(moduleType: Type<M>, compilerOptions?: CompilerOptions | CompilerOptions[]): Promise<NgModuleRef<M>>;
/** @experimental */ bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>): Promise<NgModuleRef<M>>;
/** @stable */ bootstrapModule<M>(moduleType: Type<M>, compilerOptions?: (CompilerOptions & BootstrapOptions) | Array<CompilerOptions & BootstrapOptions>): Promise<NgModuleRef<M>>;
/** @experimental */ bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options?: BootstrapOptions): Promise<NgModuleRef<M>>;
destroy(): void;
onDestroy(callback: () => void): void;
}