升級 AngularJS 至 Angular


Victor Savkin 大神撰寫了一系列文章詳細介紹如何升級 AngularJS 應用:

深入理解 NgUpgrade

原理

Angular 應用由組件樹構成,每個組件都擁有一個 注入器,並且應用的每個 NgModule 也有注入器。框架在解析組件依賴時,會首先試圖從組件樹獲取,找不到后才去 NgModule 的注入器中查找依賴。

借助 NgUpgrade 我們可以在 Angular 應用中啟動一個已經加載好的 AngularJS 應用,通常我們稱其為 * 混合應用*( HybridApplication,不是移動端的那個 Hybrid)。因此我們便能混合使用兩個框架的組件和 DI 系統。

啟動

最簡單的啟動方式就是在根模塊(一般是 AppModule)的 ngDoBootstrap 方法執行:

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {}
// Access global AngularJS 1.x object
const m = angular.module('AngularJsModule', []);
m.directive('appRoot', downgradeComponent({component: AppComponent}));

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    UpgradeModule
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) {}

  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['AngularJsModule']);
  }
}

默認情況下這是一個很好的方式,確保了升級的組件能夠訪問到 AngularJS 原組件。但在懶加載啟動 AngularJS 應用的場景下就行不通了,因為只有用戶導航到指定的路由時才能獲取 AngularJS 相關的引用。

UpgradeModule.bootstrapangular.bootstrap 方法簽名相同,如果我們查看它的實現,會發現本質上也是調用了 angular.bootstrap,不過做了如下變動:

  • 確保 angular.bootstrap 運行在正確的區域

  • 添加額外的模塊配置來確保 Angular 與 AngularJS 能相互引用

  • 適配 API 使用 Protractor 確保混合應用的可測試性

有一點需要注意, @angular/upgrade/static 獲取的是全局的 window.angular,所以我們需要在導入升級模塊前導入 AngularJS 框架:

import 'angular';
import { UpgradeModule } from '@angular/upgrade/static';

 否則會看到 AngularJSv1.xisnotloaded 字樣的報錯。

在懶加載 AngularJS 時,需要我們手動設置全局 angular 對象:

import * as angular from "angular";
// ...
setAngularJSGlobal(angular);

讀者在別的地方可能看到升級使用的是 @angular/upgrade,那到底用哪個?

由於歷史原因, NgUpgrade 模塊有兩個入口,如上的 @angular/upgrade/static@angular/upgrade,而我們應該使用前者,它提供了更完善的報錯並且支持 AOT 模式。

依賴注入

現在我們知道了如何啟動一個混合應用,現在來看如何橋接 AngularJS 和 Angular 依賴注入系統。

在升級過程中,升級 服務(在 Angular 中稱為 Injectable,可注入對象)是最重要的工作之一。通常不需要對可注入對象本身做額外的更改,只需在 DI 中正確地設置好它們即可。

假設我們的 AngularJS 應用有一個如下的可注入對象:

const m = angular.module('AngularJsModule', []);
m.value('angularJsInjectable', 'angularJsInjectable-value');

在 Angular 部分,可以通過 UpgradeModule 提供的 $injector 訪問到它。

const m = angular.module('AngularJsModule', []);
m.value('angularJsInjectable', 'angularJsInjectable-value');

function needsAngularJsInjectableFactory($injector) {
  return `needsAngularJsInjectable got ${$injector.get('angularJsInjectable')}`;
}

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  providers: [
    {
      provide: 'needsAngularJsInjectable',
      useFactory: needsAngularJsInjectableFactory,
      deps: ['$injector'] // $injector is provided by UpgradeModule
    }
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) {}

  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['AngularJsModule']);
    console.log(this.upgrade.injector.get('needsAngularJsInjectable'));
  }
}

UpgradeModule 引入了 $injector(保存了AngularJS 應用的注入器),所以我們可以在 AppModule 或它的子模塊中訪問它。

注意在 upgrade.bootstrap 調用之后, $injector 才會被定義,如果在啟動前訪問則會拋出錯誤。

借助 UpgradeModule 的 downgradeInjectable 方法,我們可以在 AngularJS 應用中訪問到 Angular 的可注入對象:

import { downgradeInjectable, UpgradeModule } from '@angular/upgrade/static';

export class AngularInjectable {
  get value() { return 'angularInjectable-value'; }
}

const m = angular.module('AngularJsModule', []);
m.factory('angularInjectable', downgradeInjectable(AngularInjectable));
m.factory('needsAngularInjectable', (angularInjectable: AngularInjectable) => `needsAngularInjectable [got ${angularInjectable.value}]`);

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule
  ],
  providers: [
    AngularInjectable
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) {}

  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['AngularJsModule']);
    console.log(this.upgrade.$injector.get('needsAngularInjectable')); // 'angularInjectable-value'
  }
}

注意 Angular 的依賴注入運行使用任何類型的 Token 標識可注入對象的依賴,但 AngularJS 只能使用字符串,所以上述代碼中的 m.factory('angularInjectable',downgradeInjectable(AngularInjectable)) 會將 AngularInjectable 映射成 angularInjectable 字符串。

組件

NgUpgrade 模塊提供的另一重要功能是 AngularJS 和 Angular 組件的混合使用。

借助 downgradeComponent 方法,我們可以降級 Angular 組件給 AngularJS 上下文使用:

const m = angular.module('AngularJsModule', []);

// The root component of the application, downgraded to AngularJS.
@Component({
  selector: 'app-root',
  template: `
    AppComponent written in Angular and downgraded to AngularJS'  
    <angularjs-component></angularjs-component>
  `
})
export class AppComponent {}
m.directive('appRoot', downgradeComponent({component: AppComponent}));

所有降級的組件都需要聲明在 Angular 的入口組件列表里:

@NgModule({
  declarations: [
    AppComponent,
    AngularJSComponent,
    AngularComponent
  ],
  // All downgraded components have to be listed here.
  entryComponents: [ 
    AppComponent,
    AngularComponent
  ],
  imports: [
    BrowserModule,
    UpgradeModule
  ]
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) {}

  ngDoBootstrap() {
    this.upgrade.bootstrap(document.body, ['AngularJsModule']);
  }
}

具體來看, m.directive('appRoot',downgradeComponent({component:AppComponent}));,將會創建一個選擇器為 appRoot 的 AngularJS 指令,而該指令將會使用 AppComponent 來渲染它的模板。由於這層間接關系,我們需要把 AppComponent 注冊為入口組件。

<app-root> 元素由 AngularJS 所有,意味着我們可以給它應用其它 AngularJS 指令。不過,它的模板,依然是由 Angular 渲染。

downgradeComponent 方法會設置好所有的 AngularJS 綁定關系,即 AppComponent 的輸入輸出。

升級 AngularJS 組件給 Angular 使用則需將它們聲明為指令,並繼承 UpgradeComponent

// An AngularJS component upgraded to Angular. 
// Note that this is @Directive and not a @Component.
@Directive({selector: 'angularjs-component'}) 
export class AngularJSComponent extends UpgradeComponent {
  constructor(ref: ElementRef, inj: Injector) {
    super('angularjsComponent', ref, inj);
  }
}
m.component('angularjsComponent', {
  template: `
    angularjsComponent written in AngularJS and upgraded to Angular
    <angular-component></angular-component>
  `
});

假設我們的組件之間有如下輸入輸出配置:

const m = angular.module('AngularJsModule', []);

@Component({
  selector: 'app-root',
  template: `
    AppComponent written in Angular and downgraded to AngularJS:
    counter {{counter}}
    <angularjs-component [counterTimes2]="counter * 2" (multiply)="multiplyCounter($event)">
    </angularjs-component>
  `
})
export class AppComponent {
  counter = 1;

  multiplyCounter(n: number): void {
    this.counter *= n;
  }
}
m.directive('appRoot', downgradeComponent({component: AppComponent}));


@Directive({selector: 'angularjs-component'})
export class AngularJSComponent extends UpgradeComponent {
  @Input() counterTimes2: number;
  @Output() multiply: EventEmitter<number>;

  constructor(ref: ElementRef, inj: Injector) {
    super('angularjsComponent', ref, inj);
  }
}
m.component('angularjsComponent', {
  bindings: {
    counterTimes2: `<`,
    multiply: '&'
  },
  template: `
    angularjsComponent written in AngularJS and upgraded to Angular
    counterTimes2: {{$ctrl.counterTimes2}}
    <button ng-click="$ctrl.multiply(2)">Double</button>
    <angular-component [counter-times-4]="$ctrl.counterTimes2 * 2" (multiply)="$ctrl.multiply($event)">
    </angular-component>
   `
});

@Component({
  selector: 'angular-component',
  template: `
    AngularComponent written in Angular and downgraded to AngularJS:
    counterTimes4: {{counterTimes4}}
    <button (click)="multiply.next(3)">Triple</button>
  `
})
export class AngularComponent {
  @Input() counterTimes4: number;
  @Output() multiply = new EventEmitter();
}
m.directive('angularComponent', downgradeComponent({ component: AngularComponent }));

為降級組件添加輸入輸出無需額外的配置, downgradeComponent 都為我們自動做了,只需在原組件中聲明好即可。

export class AngularComponent {
  @Input() counterTimes4: number;
  @Output() multiply = new EventEmitter();
}

然后在 AngularJS 上下文中即可使用綁定關系:

<angular-component [counter-times-4]="$ctrl.counterTimes2 * 2" (multiply)="$ctrl.multiply($event)">
</angular-component>

注意在 Angular 模板中,我們需要使用中括號和小括號分別標識輸入和輸出。

在升級組件中,我們需要分別在兩處列出輸入輸出綁定:

@Directive({selector: 'angularjs-component'})
export class AngularJSComponent extends UpgradeComponent {
  @Input() counterTimes2: number;
  @Output() multiply: EventEmitter<number>; // Do not create an instance of EventEmitter here
  constructor(ref: ElementRef, inj: Injector) {
    super('angularjsComponent', ref, inj);
  }
}

m.component('angularjsComponent', {
  bindings: {
    counterTimes2: '<', // < corresponds to @Input
    multiply: '&' // & corresponds to @Output
  },
  template: `
    ...
  `
});

AngularJS 與 Angular 實現雙向綁定的方式完全不同,AngularJS 擁有特殊的雙向綁定機制,而 Angular 則是簡單地利用了輸入/輸出對。NgUpgrade 負責橋接它們。

@Component({
  selector: 'app-root',
  template: `
    AppComponent written in Angular and downgraded to AngularJS:
    counter {{counter}}
    <angularjs-component [(twoWay)]="counter">
    </angularjs-component>
  `
})
export class AppComponent {
}
m.directive('appRoot', downgradeComponent({component: AppComponent}));


@Directive({selector: 'angularjs-component'})
export class AngularJSComponent extends UpgradeComponent {

  // We need to declare these two properties.
  // [(twoWay)]="counter" is the same as [twoWay]="counter" (twoWayChange)="counter=$event"
  @Input() twoWay: number;
  @Output() twoWayChange: EventEmitter<number>;

  constructor(ref: ElementRef, inj: Injector) {
    super('angularjsComponent', ref, inj);
  }
}
m.component('angularjsComponent', {
  bindings: {
    twoWay: '='
  },
  template: `
    angularjsComponent written in AngularJS and upgraded to Angular
    Bound via a two-way binding: <input ng-model="$ctrl.twoWay">
  `
});

AngularJS 與 Angular 的變更檢測機制也完全不同。AngularJS 中需要借助 $scope.apply 來觸發一次變更檢測循環,亦稱為 digest循環。在 Angular 中,不再使用 $scope.apply,而是依賴於 Zone.js,每一個瀏覽器事件都會觸發一次變更檢測。

由於混合應用是 Angular 應用,使用 Zone.js,所以我們不再需要關注 $scope.apply

Angular 還提供了嚴格的機制來確保變更的順序是可預測的,混合應用也保留了這些機制。

AngularJS 和 Angular 都提供了投射內容 DOM 到視圖 DOM 的方式,AngularJS 中稱為 transclusion,Angular 中稱為 reprojection

@Component({
  selector: 'app-root',
  template: `
    AppComponent written in Angular and downgraded to AngularJS
    <angularjs-component>
      Projected from parent
    </angularjs-component>
  `
})
export class AppComponent {}
m.directive('appRoot', downgradeComponent({component: AppComponent}));


@Directive({selector: 'angularjs-component'})
export class AngularJSComponent extends UpgradeComponent {
  constructor(ref: ElementRef, inj: Injector) {
    super('angularjsComponent', ref, inj);
  }
}
m.component('angularjsComponent', {
  template: `
    angularjsComponent written in AngularJS and upgraded to Angular
    <ng-transclude></ng-transclude>
    <angular-component>
      Projected from parent
    </angular-component>
  `
});


@Component({
  selector: 'angular-component',
  template: `
    AngularComponent written in Angular and downgraded to AngularJS:
    <ng-content></ng-content>
  `
})
export class AngularComponent {
}
m.directive('angularComponent', downgradeComponent({ component: AngularComponent }));

就像例子中的那樣,一切正常。AngularJS 中使用 <ng-transclude></ng-transclude>,Angular 中使用 <ng-content></ng-content>。多插槽投射(Multi-slot reprojection)可能還有些問題,希望不久能修復。

外殼升級

外殼升級(Upgrade Shell)是大多數應用所采用的升級策略。在此策略下,我們替換或引入 AngularJS 應用的根組件為 Angular 組件。

假設我們的 AngularJS 應用如下:

const m = angular.module('AngularJSAppModule', [deps]);
m.component(...);
m.service(...);
angular.bootstrap(document, ['AngularJSAppModule']);

首先移除啟動調用:

const m = angular.module('AngularJSAppModule', [deps]);
m.component(...);
m.service(...);
// angular.bootstrap(document, ['AngularJSAppModule']); - No longer needed

接着定義一個只渲染 ng-view 的根組件:

@Component({
  selector: 'app-component',
  template: `<div class="ng-view"></div>`,
})
class AppComponent {}

然后降級注冊到 AngularJS 模塊:

m.directive('appRoot', downgradeComponent({component: AppComponent}));

最后我們定義一個 Angular 模塊,導入 UpgradeModule

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule,
  ],
  declarations: [AppComponent],
  entryComponents: [AppComponent]
})
class AppModule {
  constructor(private upgrade: UpgradeModule) {}

  ngDoBootstrap() {
    this.upgrade.bootstrap(document, ['AngularJsAppModule']);
  }
}

這里我們使用注入的 UpgradeModulengDoBootstrap 方法內啟動已加載的 AngularJS 應用。為了使升級模塊工作正常,只能在 Angular 范圍內執行 upgrade.bootstrap

設置完成后,應用啟動執行順序將會是:

  • Angular 應用啟動

  • AngularJS 應用啟動

  • AppComponent 被創建

  • AngularJS 路由介入並插入視圖到 ng-view

作為升級應用的第一步,我們通過此策略,不到5分鍾,便擁有了一個新的 Angular 應用,盡管內部實現都是 AngularJS。

升級 Angular 應用的兩種方式

在我們把應用包裹進上述的外殼后,剩余部分的升級方式分為垂直切片(Vertical Slicing)和水平切片(Horizontal Slicin)兩種。

垂直切片

垂直切片的意義在於,盡管一次性重寫整個應用不太實際,但按路由、按功能來重寫通常是可行的。此種場景下,路由頁面可能是 AngularJS 寫的,也可能是 Angular 寫的。

換句話說,我們看到的頁面上的所有東西,要么是 AngularJS 寫的,要么是 Angular 寫的。

此策略可視化如下:

它的劣勢之一就是 某段時間內不得不為某些公共組件編寫兩個不同的版本,一個使用AngularJS,另一個使用Angular

水平切片

水平切片與之相反。

先從公共組件開始升級,比如輸入框、日期選擇器等,然后升級使用它們的組件,最后一步步直至升級完根組件。

此種方式的主要特點是無論你打開哪個頁面,都同時運行着兩個框架。

垂直切片的主要優勢是同一時刻我們的應用只會運行單個框架,意味着代碼更容易 debug,也更容易理解。其次,使用垂直切片可以使我們的升級過程抽象為單個路由,這對於某些多人維護的大型項目來說尤為重要,因為少了很多相互協作調試的成本。最后,垂直切片允許我們在導航到遺留路由時才懶加載 NgUpgrade 和 AngularJS,對於應用的體積和加載速度能夠有所改善。

水平切片的最大優勢是更加細粒度,開發人員可以升級某個組件並立即發布到生產環境,而升級路由可能花費數月。

管理路由和 URL

絕大多數的 AngularJS 應用都有使用路由,Angular 應用亦然,那我們在升級過程中就不得不同時處理兩個路由系統。

URL,具體來說是 window.location,是一個全局的、可變的狀態,要管理好它並不是一件容易的事兒,同時使用不同的框架和路由系統時尤甚。升級過程中多個路由器都會被激活,我們需要知道怎樣做才不會出錯。

升級時有兩種 URL 管理設置可選: 單一所有權(SingleOwnership混合所有權(mixed ownership

單一所有權

假設我們的混合應用有四個路由。

使用垂直切片升級部分路由后:

在單一所有權設置中,升級到 Angular 的功能由 Angular 路由器管理,其他遺留的功能由 AngularJS 路由器(或是 UI-router)管理。也就是說,每一個路由都有一個唯一的所有者,要么是新的 Angular Router,要么是 AngularJS Router

混合所有權

在混合所有權設置中,URL 能夠同時被 Angular Router 和 AngularJS Router 所管理,其中可能一部分是 AngularJS,而另一部分是 Angular。

通常可能發生在我們想要展示某個使用 AngularJS 編寫的對話框,而其它部分已經升級到 Angular 這種場景下。

那到底選哪個?

盡量可能的話,我們應該使用單一所有權設置,它能夠使我們的應用在新老部分之間的過渡更加清晰。同一時刻只升級一個路由,可以避免某些相關衍生問題。

相鄰出口

相鄰出口(SiblingOutlets 是升級使用多個路由的應用最有用的策略之一,最簡單的實現方式是由 Angular 的 router-outlet 指令和 AngularJS 的 ng-view 指令組成,也就是說有兩個相鄰路由出口,一個給 Angular,另一個給 AngularJS:

@Component({
  selector: 'app-component',
  template: `
    <router-outlet></router-outlet>
    <div class="ng-view"></div>
  `
})
class AppComponent { }

在單一所有權設置中,同一時刻只有一個出口是激活的,另一個為空。而在混合所有權中,它倆能同時激活。

然后為已經升級的功能定義 Angular 路由配置,而且我們要限定路由只處理已升級的功能。主要有兩種實現方式,覆蓋 UrlHandlingStrategy 和空路徑 凹槽路由(SinkRoute

UrlHandlingStrategy

我們可以提供一個自定義的 URL 控制策略來告訴 Angular 路由應該處理哪些 URL,對於不符合規則的 URL,它將卸載所有組件並把根路由出口置空。

class CustomHandlingStrategy implements UrlHandlingStrategy {
  shouldProcessUrl(url) { return url.toString().startsWith("/feature1") || url.toString() === "/"; }
  extract(url) { return url; }
  merge(url, whole) { return url; }
}

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule,
    RouterModule.forRoot([
      { path: '', pathMatch: 'full', component: HomeComponent },
      { path: 'feature1/sub1', component: Feature1Sub1Component },
      { path: 'feature1/sub2', component: Feature1Sub2Component }
    ])
  ],
  providers: [
    { provide: UrlHandlingStrategy, useClass: CustomHandlingStrategy }
  ],
  bootstrap: [AppComponent],
  declarations: [AppComponent, HomeComponent, Feature1Sub1Component, Feature1Sub2Component]
})
class AppModule {}

凹槽路由

Angular 處理路由是按照順序來的,如果我們在配置列表末尾放一個空路徑路由,那匹配不到任何路由后就會匹配任意 URL,此時讓它渲染一個空組件,就實現了同樣的效果。

@Component({selector: 'empty', template: ''})
class EmptyComponent {}

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule,
    RouterModule.forRoot([
      { path: '', pathMatch: 'full', component: HomeComponent },
      { path: 'feature1/sub1', component: Feature1Sub1Component },
      { path: 'feature1/sub2', component: Feature1Sub2Component },
      { path: '', component: EmptyComponent }
    ])
  ],
  bootstrap: [AppComponent],
  declarations: [AppComponent, HomeComponent, Feature1Sub1Component, Feature1Sub2Component, EmptyComponent]
})
class AppModule {}

對於 AngularJS 部分,我們仍使用 $routeProvider 配置遺留路由,同樣需要設置凹槽路由。

angular.config(($routeProvider) => {
  $routeProvider
    .when('/feature2', {template : '<feature2></feature2>'})
    .otherwise({template : ''});
});

在 AngularJS 中要實現自定義的 URL 控制策略,可以訂閱 UI-router 的 $stateChangeStart 事件后調用 preventDefault 來阻止應用導航到已升級的部分。

懶加載 AngularJS 應用

Angular 路由最棒的一點是支持懶加載,它可以讓我們在渲染應用首屏時所需的靜態資源打包體積盡可能小。

上述混合應用的一大特點就是同時給客戶端打包了兩個框架,但這在初始包中其實是沒必要的。

我們可以設置在只有用戶導航到遺留路由,才去加載 AngularJS 框架、NgUpgrade 模塊和 AngularJS 相關的業務代碼。

假設我們的混合應用有四個路由, /angular_a/angular_b 由 Angular 控制,遺留的 AngularJS 應用通過 UI-router 控制着 /angularjs_a/angularjs_b,入口是 /angular_a

相鄰路由出口

@Component({
  selector: 'app-root',
  template: `
    <router-outlet></router-outlet>
    <div ui-view></div>
  `
})
export class AppComponent {}
 
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      //...
    ])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

根組件配置了相鄰路由出口策略,其中 <divui-view> 只有在 AngularJS 框架加載后才會激活,在那之前只是一個普通的 DOM 元素。

在 AngularJS 路由配置列表中設置凹槽路由:

export const module = angular.module('AngularJSApp', ['ui.router']);
 
module.config(($locationProvider, $stateProvider) => {
  //...
 
  $stateProvider.state('sink', {
    url: '/*path',
    template: ''
  });
});

Angular 中也定義一個凹槽路由來捕獲不匹配的 URL。

@NgModule({
  declarations: [
    AppComponent,
    AngularAComponent,
    AngularBComponent
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {path: '', redirectTo: 'angular_a', pathMatch: 'full'},
      {path: 'angular_a', component: AngularAComponent},
      {path: 'angular_b', component: AngularBComponent},
      {path: '', loadChildren: './angularjs.module#AngularJSModule'}
    ])
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Angular 應用會假設沒匹配到的路由交由 AngularJS 處理,所以凹槽路由這里會去加載 AngularJS 應用的代碼。

加載 AngularJS

AngularJSModule 是 AngularJS 應用的一層簡易 Angular 包裝器。

//angularjs.module.ts
import {Component, NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
 
import {module} from './angularjsapp';
import {UpgradeModule} from '@angular/upgrade/static';
import {setUpLocationSync} from '@angular/router/upgrade';
 
@Component({template: ``})
export class EmptyComponent {}
 
@NgModule({
  declarations: [
    EmptyComponent
  ],
  imports: [
    UpgradeModule,
    RouterModule.forChild([
      {path: '**', component: EmptyComponent}
    ])
  ]
})
export class AngularJSModule {
  // The constructor is called only once, so we bootstrap the application
  // only once, when we first navigate to the legacy part of the app.
  constructor(upgrade: UpgradeModule) {
    upgrade.bootstrap(document.body, [module.name]);
    setUpLocationSync(upgrade);
  }
}

為了使 Angular 路由成功運行,我們需要渲染一個空組件。然后在模塊的構造函數里啟動 AngularJS 應用,並設置地址同步,監聽 window.location 變更。

概覽

至此整個混合應用搭建完畢,我們來看下應用具體加載過程。

當用戶打開網頁時,Angular 應用啟動,路由重定向到 /angular_a,實例化 AngularAComponent 組件,放入 AppComponet 定義的路由出口 <router-outlet>

此時我們尚未加載 AngularJS、NgUpgrade 和其他 AngularJS 應用代碼,可以隨意在 /angular_a/angular_b 路由間切換。

當用戶導航到 /angularjs_a,Angular 路由器將會匹配到 {path:'',loadChildren:'./angularjs.module#AngularJSModule'} 路由,執行加載 AngularJSModule

這個 chunk 包中的模塊包含了 AngularJS、NgUpgrade 和其他 AngularJS 應用代碼。

一旦該模塊加載,將會調用 upgrade.bootstrap 方法啟動 AngularJS 應用,觸發 UI-router,匹配到 /angularjs_a,放入 <divui-view>。與此同時,Angular 將會把 EmptyComponent 放入 <router-outlet>

當用戶從 /angularjs_a 導航到 /angular_a,UI-router 將會匹配到凹槽路由並將空模板放入 <divui-view>setUpLocationSync 幫助方法將會通知 Angular 路由器 URL 發生變更,那 Angular 路由器就會把 AngularAComponent 放入 <router-outlet>

此時 AngularJS 應用仍在運行,並沒有被卸載。卸載一個真實的 AngularJS 應用幾乎是不可能的,所以我們把它繼續留在內存中。

當用戶從 /angular_a 導航到 /angular_b 時,UI-router 仍然匹配到凹槽路由,Angular 路由器則更新 <router-outlet>

最后,當用戶再導航到 /angularjs_a,Angular 路由器匹配到凹槽路由,而正在運行的 UI-router,將會匹配到相應的狀態。此時我們沒必要再去加載啟動 AngularJS 應用,因為已經執行過一次。

大功告成,現在只有在用戶導航到受 AngularJS 控制的路由時,我們才會加載 AngularJS,這使得首次加載變得很快,但會使用戶初次導航到 /angularjs_a 時加載變慢,我們可以開啟預加載來修復這個場景。

//app,module.ts
import {PreloadAllModules, RouterModule} from '@angular/router';
 
@NgModule({
  declarations: [
    AppComponent,
    AngularAComponent,
    AngularBComponent
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot([
      {path: '', redirectTo: 'angular_a', pathMatch: 'full'},
      {path: 'angular_a', component: AngularAComponent},
      {path: 'angular_b', component: AngularBComponent},
      {path: '', loadChildren: './angularjs.module#AngularJSModule'}
    ], {
      enableTracing: true,
      preloadingStrategy: PreloadAllModules // ADD THIS!
    })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

通過設置預加載策略,當用戶在使用 Angular 應用時,路由器會在背后預加載 AngularJSModule

更有趣的是,路由器將會實例化 AngularJSModule,致使 AngularJS 應用啟動,這意味着整個啟動過程也是發生在背后。

真是兩全其美,我們既擁有了體積更小的初始化包,隨后又擁有了更快的路由切換。

完整代碼托管在 GitHub

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM