From 344a5ca5452bc01a1e5ff8f04aa7a283ce4134cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mis=CC=8Cko=20Hevery?= Date: Thu, 1 Jun 2017 14:45:49 -0700 Subject: [PATCH] feat(core): support for bootstrap with custom zone (#17672) PR Close #17672 --- packages/core/src/application_ref.ts | 66 ++++++++++++++----- packages/core/src/zone/ng_zone.ts | 28 +++++++- packages/core/test/application_ref_spec.ts | 13 +++- packages/core/test/zone/ng_zone_spec.ts | 22 ++++++- .../upgrade/src/dynamic/upgrade_adapter.ts | 6 +- packages/upgrade/test/dynamic/upgrade_spec.ts | 19 ++++-- tools/public_api_guard/core/core.d.ts | 4 +- 7 files changed, 123 insertions(+), 35 deletions(-) diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts index b024bfb626..95f7ebbe97 100644 --- a/packages/core/src/application_ref.ts +++ b/packages/core/src/application_ref.ts @@ -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[] = []; private _destroyListeners: Function[] = []; @@ -199,17 +216,14 @@ export class PlatformRef { * * @experimental APIs related to application bootstrap are currently under review. */ - bootstrapModuleFactory(moduleFactory: NgModuleFactory): Promise> { - return this._bootstrapModuleFactoryWithZone(moduleFactory); - } - - private _bootstrapModuleFactoryWithZone(moduleFactory: NgModuleFactory, ngZone?: NgZone): + bootstrapModuleFactory(moduleFactory: NgModuleFactory, options?: BootstrapOptions): Promise> { // 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(moduleType: Type, compilerOptions: CompilerOptions|CompilerOptions[] = []): - Promise> { - return this._bootstrapModuleWithZone(moduleType, compilerOptions); - } - - private _bootstrapModuleWithZone( - moduleType: Type, compilerOptions: CompilerOptions|CompilerOptions[] = [], - ngZone?: NgZone): Promise> { + bootstrapModule( + moduleType: Type, compilerOptions: (CompilerOptions&BootstrapOptions)| + Array = []): Promise> { 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): 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(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. * diff --git a/packages/core/src/zone/ng_zone.ts b/packages/core/src/zone/ng_zone.ts index 1f16d2e3ca..843468e843 100644 --- a/packages/core/src/zone/ng_zone.ts +++ b/packages/core/src/zone/ng_zone.ts @@ -98,7 +98,7 @@ export class NgZone { readonly onUnstable: EventEmitter = 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 = new EventEmitter(); + readonly onMicrotaskEmpty: EventEmitter = new EventEmitter(); + readonly onStable: EventEmitter = new EventEmitter(); + readonly onError: EventEmitter = new EventEmitter(); + + run(fn: () => any): any { return fn(); } + + runGuarded(fn: () => any): any { return fn(); } + + runOutsideAngular(fn: () => any): any { return fn(); } + + runTask(fn: () => any): any { return fn(); } +} diff --git a/packages/core/test/application_ref_spec.ts b/packages/core/test/application_ref_spec.ts index d9c18fdb0a..2e06b5ac5b 100644 --- a/packages/core/test/application_ref_spec.ts +++ b/packages/core/test/application_ref_spec.ts @@ -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((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', () => { diff --git a/packages/core/test/zone/ng_zone_spec.ts b/packages/core/test/zone/ng_zone_spec.ts index f2c89b621a..ccc5189cfc 100644 --- a/packages/core/test/zone/ng_zone_spec.ts +++ b/packages/core/test/zone/ng_zone_spec.ts @@ -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() { diff --git a/packages/upgrade/src/dynamic/upgrade_adapter.ts b/packages/upgrade/src/dynamic/upgrade_adapter.ts index d404dbada8..c4d3e44b18 100644 --- a/packages/upgrade/src/dynamic/upgrade_adapter.ts +++ b/packages/upgrade/src/dynamic/upgrade_adapter.ts @@ -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) => { this.moduleRef = ref; this.ngZone.run(() => { diff --git a/packages/upgrade/test/dynamic/upgrade_spec.ts b/packages/upgrade/test/dynamic/upgrade_spec.ts index 9a32de8acd..f29b1079a0 100644 --- a/packages/upgrade/test/dynamic/upgrade_spec.ts +++ b/packages/upgrade/test/dynamic/upgrade_spec.ts @@ -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(' }}{{'transclude'}}{{ ')' }}`, }) 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' }}()`}) @@ -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(`
(moduleType: Type, compilerOptions?: CompilerOptions | CompilerOptions[]): Promise>; - /** @experimental */ bootstrapModuleFactory(moduleFactory: NgModuleFactory): Promise>; + /** @stable */ bootstrapModule(moduleType: Type, compilerOptions?: (CompilerOptions & BootstrapOptions) | Array): Promise>; + /** @experimental */ bootstrapModuleFactory(moduleFactory: NgModuleFactory, options?: BootstrapOptions): Promise>; destroy(): void; onDestroy(callback: () => void): void; }