AngularJS 平滑升级指南

分离 controller 的 function

1
2
3
app.controller('myController', ['$scoppe', function($scope){
// AngularJS 中定义controller
}])

写成两个文件:
负责注册所有controller的文件

1
2
3
4
// app-controller.js
app.controller('firstController', ['$scope', firstCtrl])
.controller('secondController', ['$scope', secondCtrl])
.controller(...)

与其余单个controller文件

1
2
3
4
// first.controller.js
var firstCtrl = function($scope) {
$scope.info = 'hello world';
};

这样代码就会变得清晰,一个注册 conrtroller 的文件与其他负责完成业务逻辑与交互逻辑的单个 controller 文件。

这里需要注意的一点是,app-controller.js 用到了 first.controller.js 中的 firstCtrl 方法,所以需要在调用注册函数之前定义这些方法。

通过 as 语法不再使用 $scope

在 Angular 中是没有 $scope 概念的。AngularJS 中提供了一种避免使用 $scope 的方法,就是 as 语法。

1
2
3
<div ng-app="app" ng-controller="firstCtrl as $ctrl">
<div ng-bind="$ctrl.info"></div>
</div>

1
2
3
4
5
6
7
8
9
// first.controller.js 现在看起来只是一个普通的方法了
var firstCtrl = function() {
this.info = 'hello world';
};
// app-controller.js
app.controller('firstController', [firstCtrl])
.controller('secondController', [secondCtrl])
.controller(...)

使用 as 语法,相当于在 $scope 中定义了一个属性 $ctrl ,并将这个属性指向 firstCtrl 方法作用域的 this 上。
因此,使用 as 语法后, controller 方法就可以使用 this 来操作变量。这样几乎与 Angular 中 Component 类的操作方法一致,在注册 controller 时也不再用注入 $scope 依赖了。

这里使用 $ctrl 属性可以根据需要调整名称,但统一使用 $ctrl 是最佳实践。一方面可以在所有的 controller 中统一使用相同的属性名调用 controller 中的变量,另一方面使用 $ 前缀可以作为 AngularJS 的标记,避免变量名冲突。

在使用 $watch$on 等方法时还是需要注入 $scope

使用 TypeScript

TypeScript 是 JavaScript 的超集

使用 class 来定义 firstCtrl:

1
2
3
4
5
6
7
8
9
10
// firstCtrl.controller.ts
export default class FirstCtrl {
info : string = 'hello world';
constructor () {
console.log(this.info);
}
hasInfo () : boolean {
return this.info.length > 0;
}
}

在其他文件中也全部可以做优化:

1
2
// app.ts
export default angular.module('app', []);

1
2
3
4
5
6
7
8
// app-controller.ts
import app from './app';
import FirstCtrl from './firstCtrl';
import SecondCtrl from './secondCtrl';
app.controller('FirstCtrl', [firstCtrl])
.controller('SecondCtrl', [secondCtrl])
.controller(...)

代码在 TypeScript 下编译完成后,仍可以生成兼容 AngularJS 的 ES3 版本 JS 代码。

根据最佳实践进一步优化

我们建议在 constructor 中,只对一些变量的值进行初始化,不执行具体的业务逻辑,而将涉及到业务逻辑的初始化,则放到单独的初始化方法中完成。
这也符合 Angular 的理念,在 Angular 中,每个 component 都有一系列生命周期方法,并通过 Angular 框架自动调用运行,我们涉及到业务逻辑的初始化工作在这里拆分到单独的初始化方法中,将更方便我们的代码升级到 Angular 。

最佳实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// firstCtrl.controller.ts
export default class FirstCtrl {
info : string = 'hello world';
constructor () {
this._onInit();
}
// TS 中,类的方法默认为 public 类型,因此不需要显示声明
hasInfo (): boolean {
return this.info.length > 0;
}
// 这里用 TS 中的 private 关键字将方法声明为私有方法
private _onInit () : void {
console.log(this.info);
}
}

初始化方法并不需要在网页中或其他模块中调用,因此我们将其设定为私有方法(以 _ 开头),并写在所有公有方法后面。

升级到 Angular

到此为止,我们对项目的修改已经十分巨大,但它依然可以兼容 AngularJS (在 TypeScript Compiler 帮忙编译的情况下),而代码已经变得整洁而先进,并且对现有的项目运行完全没有任何影响。

Angular 中没有 controller 概念,我们将 controller 改造为 component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// first.component.ts
import { Component, OnInit } from '@angular/core';
@Component ({
selector: 'first-ctrl',
template: '<span *ngIf="hasInfo()">{{ info }}</span>'
})
export class FirstComponent implements OnInit {
info : string = 'hello world';
constructor () {
// 初始化方法为生命周期方法,由 Angular 框架自动调用,这里不需要手动调用
}
// TS 中,类的方法默认为 public 类型,因此不需要显示声明
hasInfo (): boolean {
return this.info.length > 0;
}
// 这里用 Angular 中的生命周期方法
ngOnInit () : void {
console.log(this.info);
}
}

改到这里, first.component.ts 的 Angular 升级已经完成,我们可以看到,除了添加一些 annotation(装饰器) 以外, component 的核心代码机会没有变动。
这说明经过优化的、完全兼容 AngularJS 的 TypeScript 版本核心代码,可以直接升级到 Angular ,只要添加一些必要的 annotation 就可以了。