龙空技术网

如何把Angular1.X的项目升级到Angular5.0

源码情报局 28

前言:

而今姐妹们对“jsangularjs”可能比较讲究,姐妹们都想要剖析一些“jsangularjs”的相关文章。那么小编同时在网络上搜集了一些关于“jsangularjs””的相关资讯,希望大家能喜欢,各位老铁们快快来了解一下吧!

前两天 Angular 核心团队决定在 7月1日发布 AngularJS 1.7版本,这将会是一个为期三年的长期支持版本(LTS),此发行版将只包含非破坏性变更和 Bug 修复,至此 AngularJS 真的落下帷幕了。

至今许多企业仍然在使用 AngularJS 1.x 构建复杂的 Web 应用,通常这些项目都历史悠久,代码庞大,经历过许多不同开发人员的维护。尽管 AngularJS 曾经那么流行,但它毕竟是十年前的框架,现在的前端开发是属于 React、Vue 和 Angular 等现代框架的时代,虽然通过一些复杂的构建工具配置,也能享受到 TypeScript(ES6)、Webpack 等好处,但仍然是戴着镣铐跳舞,力不从心。再加上 Angular 团队决定不再为它开发新功能,社区许多库也停止更新后,所有的重心都转移到了新版的 Angular。那么升级项目原框架也就愈发迫切了。

本文借助于NgUpgrade 模块成功升级了公司的 AngularJS 项目至最新版 Angular 5。我们需要首先引入模块加载器,比如 Webpack,然后再将开发语言迁移到 TypeScript,这个 GitHub 上还挺多 boilerplate,自己造一个也很简单,比如笔者改造的公司项目就是基于 CRA 暴改而来。

一、深入理解 NgUpgrade

1. 原理

Angular 应用由组件树构成,每个组件都拥有一个注入器,并且应用的每个 NgModule 也有注入器。框架在解析组件依赖时,会首先试图从组件树获取,找不到后才去 NgModule 的注入器中查找依赖。

借助 NgUpgrade 我们可以在 Angular 应用中启动一个已经加载好的 AngularJS 应用,通常我们称其为 混合应用(Hybrid Application,不是移动端的那个 Hybrid)。因此我们便能混合使用两个框架的组件和 DI 系统。

1. 启动

最简单的启动方式就是在根模块(一般是 AppModule)的 ngDoBootstrap 方法执行:

@Component({

selector: 'app-root',

templateUrl: './app.component.html'

})

export class AppComponent {}

// 访问全局的 AngularJS 1.x 对象

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.bootstrap 与 angular.bootstrap 方法签名相同,如果我们查看它的实现,会发现本质上也是调用了 angular.bootstrap,不过做了如下变动:

确保 angular.bootstrap 运行在正确的区域

添加额外的模块配置来确保 Angular 与 AngularJS 能相互引用

适配 API 使用 Protractor 确保混合应用的可测试性

有一点需要注意,@angular/upgrade/static 获取的是全局的 window.angular,所以我们需要在导入升级模块前导入 AngularJS 框架:

import 'angular';

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

否则会看到 AngularJS v1.x is not loaded 字样的报错。

在懒加载 AngularJS 时,需要我们手动设置全局 angular 对象:

import * as angular from 'angular';

// ...

setAngularJSGlobal(angular);

读者在别的地方可能看到升级使用的是 @angular/upgrade,那到底用哪个?

由于历史原因,NgUpgrade 模块有两个入口,如上的 @angular/upgrade/static 和 @angular/upgrade,而我们应该使用前者,它提供了更完善的报错并且支持 AOT 模式。

2. 依赖注入

现在我们知道了如何启动一个混合应用,现在来看如何桥接 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 来自于 UpgradeModule

}

]

})

export class AppModule {

constructor(private upgrade: UpgradeModule) {}

ngDoBootstrap() {

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

// AngularJS 启动后才能通过 UpgradeModule 拿到注入器

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 字符串。

3. 组件

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

借助 downgradeComponent 方法,我们可以降级 Angular 组件给 AngularJS 上下文使用:

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

@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

],

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:

@Directive({selector: 'angularjs-component'})

export class AngularJSComponent extends UpgradeComponent {

constructor(ref: ElementRef, inj: Injector) {

// 第一个参数是我们想要升级的 AngularJS 组件

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>; // 这里千万不要创建 EventEmitter 实例

constructor(ref: ElementRef, inj: Injector) {

super('angularjsComponent', ref, inj);

}

}

m.component('angularjsComponent', {

bindings: {

counterTimes2: '<', // < 对应 @Input

multiply: '&' // & 对应 @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 {

// 我们需要分别声明这两个属性

// [(twoWay)]="counter" 等同于 [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)可能还有些问题,希望不久能修复。

二、升级 Angular 应用的两种方式

在我们把应用包裹进上述的外壳后,剩余部分的升级方式分为垂直切片(Vertical Slicing)和水平切片(Horizontal Slicin)两种。

1. 垂直切片

垂直切片的意义在于,尽管一次性重写整个应用不太实际,但按路由、按功能来重写通常是可行的。此种场景下,路由页面可能是 AngularJS 写的,也可能是 Angular 写的。

换句话说,我们看到的页面上的所有东西,要么是 AngularJS 写的,要么是 Angular 写的。

此策略可视化如下:

它的劣势之一就是某段时间内不得不为某些公共组件编写两个不同的版本,一个使用 AngularJS,另一个使用 Angular。

2. 水平切片

水平切片与之相反。先从公共组件开始升级,比如输入框、日期选择器等,然后升级使用它们的组件,最后一步步直至升级完根组件。

此种方式的主要特点是无论你打开哪个页面,都同时运行着两个框架。垂直切片的主要优势是同一时刻我们的应用只会运行单个框架,意味着代码更容易 debug,也更容易理解。其次,使用垂直切片可以使我们的升级过程抽象为单个路由,这对于某些多人维护的大型项目来说尤为重要,因为少了很多相互协作调试的成本。最后,垂直切片允许我们在导航到遗留路由时才懒加载 NgUpgrade 和 AngularJS,对于应用的体积和加载速度能够有所改善。

水平切片的最大优势是更加细粒度,开发人员可以升级某个组件并立即发布到生产环境,而升级路由可能花费数月。

三、管理路由和 URL

绝大多数的 AngularJS 应用都有使用路由,Angular 应用亦然,那我们在升级过程中就不得不同时处理两个路由系统。

URL,具体来说是 window.location,是一个全局的、可变的状态,要管理好它并不是一件容易的事儿,同时使用不同的框架和路由系统时尤甚。升级过程中多个路由器都会被激活,我们需要知道怎样做才不会出错。

升级时有两种 URL 管理设置可选:单一所有权(Single Ownership) 和 混合所有权(mixed ownership)。

1. 单一所有权

设我们的混合应用有四个路由。使用垂直切片升级部分路由后:

在单一所有权设置中,升级到 Angular 的功能由 Angular 路由器管理,其他遗留的功能由 AngularJS 路由器(或是 UI-router)管理。也就是说,每一个路由都有一个唯一的所有者,要么是新的 Angular Router,要么是 AngularJS Router。

2. 混合所有权

在混合所有权设置中,URL 能够同时被 Angular Router 和 AngularJS Router 所管理,其中可能一部分是 AngularJS,而另一部分是 Angular。通常可能发生在我们想要展示某个使用 AngularJS 编写的对话框,而其它部分已经升级到 Angular 这种场景下。那到底选哪个?

尽量可能的话,我们应该使用单一所有权设置,它能够使我们的应用在新老部分之间的过渡更加清晰。同一时刻只升级一个路由,可以避免某些相关衍生问题。

3. 相邻出口

相邻出口(Sibling Outlets) 是升级使用多个路由的应用最有用的策略之一,最简单的实现方式是由 Angular 的 router-outlet 指令和 AngularJS 的 ng-view 指令组成,也就是说有两个相邻路由出口,一个给 Angular,另一个给 AngularJS:

@Component({

selector: 'app-component',

template: `

<router-outlet></router-outlet>

<div></div>

`

})

class AppComponent { }

在单一所有权设置中,同一时刻只有一个出口是激活的,另一个为空。而在混合所有权中,它俩能同时激活。然后为已经升级的功能定义 Angular 路由配置,而且我们要限定路由只处理已升级的功能。主要有两种实现方式,覆盖 UrlHandlingStrategy 和空路径凹槽路由(Sink Route)。

4. 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 {}

5. 凹槽路由

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 来阻止应用导航到已升级的部分。

本文章由源码时代H5前端学科讲师原创!

转载须注明出处()!感谢大家的配合!

标签: #jsangularjs #jsangularui