如果你想了解如何將現(xiàn)有的AngularJs 1.X(簡稱ng1)的項(xiàng)目平滑升級至Angular 2.X(簡稱ng2),這篇文章或許會(huì)對你有幫助。
本文假設(shè)你原有的ng1項(xiàng)目有如下屬性:
使用的ng1版本為1.2.X+;
使用的Javascript版本為es3或少量es5特性;
暫時(shí)不考慮其他外部依賴,例如第三方ng1插件等;
如果你有一些typescript的知識(shí),閱讀本文將會(huì)更加輕松。
本文的目標(biāo)是:
將ng1升級至ng2,為了實(shí)現(xiàn)平滑升級,我們還將盡量在升級過程中把步子邁小,同時(shí)使升級后的代碼兼容ng1,從而在升級過程中不影響當(dāng)前項(xiàng)目的正常開發(fā)和發(fā)布;
將Javascript升級至Typescript,平滑升級過程中還可以生成兼容ng1的typescript版本代碼;
盡量嘗試使用最佳實(shí)踐。
讓我們開始吧!
我們先來定義一個(gè)在本文中一直會(huì)使用到的ng1項(xiàng)目基礎(chǔ)代碼,后面的代碼實(shí)例都基于這個(gè)基礎(chǔ)代碼開發(fā):
// app.js/* 定義一個(gè)ng1 module,并假設(shè)該module為ng1項(xiàng)目的啟動(dòng)module,
例如在html標(biāo)簽中添加 ng-app="app"
*/var app = angular.module('app', []);
這個(gè)項(xiàng)目至少還要有一個(gè)controller:
// index.controller.js
/* 一個(gè)簡單的controller */
app.controller('IndexCtrl', ['$scope', function ($scope) {
$scope.info = 'hello world';
console.log($scope.info);
}]);
我們還要有一個(gè)html頁面:
...
...
這樣,一個(gè)最簡單的ng1項(xiàng)目就完成了。
我們從最常使用的controller開始下手。首先回顧一下剛才定義的controller:
// index.controller.js
/* 一個(gè)簡單的controller */
app.controller('IndexCtrl', ['$scope', function ($scope) {
$scope.info = 'hello world';
console.log($scope.info);
}]);
仔細(xì)查看代碼,我們可以發(fā)現(xiàn),我們定義controller的方式使用的是angular.Module.controller方法,這樣的定義方式與ng1完全耦合在一起,無法脫離ng1的依賴,因此,第一步我們現(xiàn)在將controller實(shí)體function從該方法中剝離出來。
1. 分離controller的實(shí)體function
分離的方式很簡單,我們定義一個(gè)function變量,再將該function變量寫在angular.Module.controller方法中:
// index.controller.js
/* 一個(gè)簡單的controller */
app.controller('IndexCtrl', ['$scope', indexCtrl]);
var indexCtrl = function ($scope) {
$scope.info = 'hello world';
console.log($scope.info);
};
但是這樣做還是不夠,因?yàn)镮ndex.controller.js文件中還是調(diào)用了app.controller方法,這個(gè)方法是ng1專有的,因此這個(gè)文件還是在依賴ng1,因此我們可以將所有使用app.controller方法注冊controller的代碼集中到一個(gè)app-controller.js中。
這樣,我們將index.controller.js文件拆分成兩個(gè)文件:
// app-controller.js
app.controller('IndexCtrl', ['$scope', indexCtrl])
.controller('LoginCtrl', ['$scope', loginCtrl])
.controller(...);
// index.controller.js
var indexCtrl = function ($scope) {
$scope.info = 'hello world';
console.log($scope.info);
};
修改之后的代碼就清晰多了,app-controller.js專門負(fù)責(zé)注冊項(xiàng)目中所有用到的controller,index.controller.js以及其他的類似login.controller.js等專門負(fù)責(zé)完成controller中的業(yè)務(wù)邏輯和交互邏輯。
這里有一個(gè)問題要注意,因?yàn)閍pp-controller.js中要調(diào)用indexCtrl這個(gè)變量,因此一定要在調(diào)用之前定義這個(gè)變量,否則調(diào)用時(shí)indexCtrl為undefined。避免這個(gè)問題的方法,一種是在html中引用js文件時(shí)首先引用定義indexCtrl的js文件,最后引用app-controller.js文件;第二種是使用typescript中的模塊管理功能管理相互之間的依賴。本文最終會(huì)形成typescript版本的代碼,所以這個(gè)問題僅在改動(dòng)過程中出現(xiàn)。
2. 不再使用丑陋的$scope
index.controller.js中的代碼使用了$scope,而$scope是使用DI的方式注入到方法中,因此這里并沒有對ng1產(chǎn)生直接的依賴,這也是依賴注入的一大好處。
但是controller方法中到處充滿了$scope,不得不說代碼會(huì)顯得很混亂和丑陋。如果你對ng2有一些了解,那么應(yīng)該知道ng2中是沒有$scope概念的。為了升級至ng2,我們從現(xiàn)在開始對$scope說NO!
ng1中提供了一種可以避免使用$scope的方式,那就是as語法,我們先來看一下如何使用:
...
...
// index.controller.js
var indexCtrl = function () {
this.info = 'hello world';
console.log(this.info);
};
// app-controller.js
app.controller('IndexCtrl', [indexCtrl])
.controller('LoginCtrl', [loginCtrl])
.controller(...);
使用as語法,相當(dāng)于在$scope中定義了一個(gè)屬性$ctrl,并將這個(gè)屬性指向indexCtrl方法作用域的this上,具體可以參見官方文檔(https://docs.angularjs.org/api/ng/directive/ngController)。
因此,使用as語法后,controller方法就可以使用this來操作其中的變量,這樣幾乎與ng2中Component類的操作方法一致,在注冊controller時(shí)也不用注入$scope依賴了。
同時(shí)在html中,調(diào)用controller方法中的變量時(shí),需要使用$ctrl.foobar來實(shí)現(xiàn)(因?yàn)?ctrl為$scope的一個(gè)屬性)。
這里使用的$ctrl屬性可以根據(jù)需要調(diào)整名稱,如ctrl、indexCtrl等,但統(tǒng)一使用$ctrl是最佳實(shí)踐,一方面可以在所有html的controller中統(tǒng)一使用相同的屬性名調(diào)用controller中的變量,另一方面使用$前綴可以作為ng1的一個(gè)標(biāo)記,避免出現(xiàn)變量名沖突的情況。
這時(shí)我們再重新觀察一下index.controller.js文件,它已經(jīng)完全純凈,只是一個(gè)非常普通的方法,一點(diǎn)ng1的影子都看不出來了:
// index.controller.js
var indexCtrl = function () {
this.info = 'hello world';
console.log(this.info);
};
3. 向typescript進(jìn)發(fā)
我們現(xiàn)在的js代碼還停留在es3階段,要靠近ng2,我們還需要使用typescript。
升級至typescript后,需要使用typescript compiler(http://www.typescriptlang.org/)編譯為es3或es5版本。
注意:雖然代碼升級為typescript,但仍然可以在編譯后兼容ng1。
這里,我們使用typescript中的class來定義indexCtrl:
// index.controller.ts
// 為變量增加類型聲明,同時(shí)也就不用再判斷變量類型了
export default class IndexCtrl {
info : string = 'hello world';
constructor () {
console.log(this.info);
}
// 這里我們?yōu)閏ontroller添加一個(gè)可以在html中調(diào)用的方法
hasInfo () : boolean {
// 使用了typescript,這里就可以省略類型判斷了
return this.info.length > 0;
}
}
我們使用以下幾種方式實(shí)現(xiàn)IndexCtrl:
之前使用this定義的變量,我們在class的最頂端使用foo = 'bar'的方式聲明為類屬性;
初始化工作在class中的constructor中完成;
需要在html中調(diào)用的方法使用foo () {}的方式生命為類方法。
代碼中export default使用了typescript中的模塊管理功能,將類導(dǎo)出,并可以在其他文件中調(diào)用。
類似的,我們將其他文件也做相應(yīng)的優(yōu)化:
// app.ts
export default angular.module('app', []);
// app-controller.ts
// 這里使用import調(diào)用其他模塊,從此就不用擔(dān)心變量是否已定義的問題啦
import app from './app';
import IndexCtrl from './index.controller';
import LoginCtrl from './login.controller';
...
app.controller('IndexCtrl', [indexCtrl])
.controller('LoginCtrl', [loginCtrl])
.controller(...);
// 這個(gè)文件不需要其他文件調(diào)用,因此不需要export模塊
...
ng-if="$ctrl.hasInfo()"> ?...
雖然到此為止,代碼在typescript下編譯完成后,仍然可以生成兼容ng1的es3版本js代碼。
4. 根據(jù)最佳實(shí)踐進(jìn)一步優(yōu)化
這里有一些最佳實(shí)踐,我們可以用來參考。
最佳實(shí)踐:我們建議在constructor中,只對一些變量的值進(jìn)行初始化,不執(zhí)行具體的業(yè)務(wù)邏輯(例如本文中的console.log(this.info)),而將涉及到業(yè)務(wù)邏輯的初始化,則放到單獨(dú)的初始化方法中完成。
這個(gè)最佳實(shí)踐也正符合ng2的理念,在ng2中,每個(gè)component都有一系列生命周期方法,并通過ng2框架自動(dòng)調(diào)用運(yùn)行,我們涉及到業(yè)務(wù)邏輯的初始化工作在這里拆分到單獨(dú)的初始化方法中,將更方便我們的代碼升級至ng2:
// index.controller.ts
// 接下來優(yōu)化typescript版本
export default class IndexCtrl {
info : string = 'hello world';
constructor () {
this._onInit();
}
// typescript中,類的方法默認(rèn)為public類型,因此不需要顯式聲明
hasInfo () : boolean {
return this.info.length > 0;
}
// 這里我們將初始化方法同意命名為_onInit
// 這里用typescript中的private關(guān)鍵字將方法聲明為私有方法
private _onInit () : void {
console.log(this.info);
}
}
最佳實(shí)踐:初始化方法并不需要在網(wǎng)頁中或其他模塊中調(diào)用,因此我們將其設(shè)定為私有方法(以_開頭),并寫在所有公開方法后面。
5. 升級為ng2
到此為止,雖然我們對項(xiàng)目的修改已經(jīng)如此巨大,但它依然可以兼容ng1(在typescript compiler幫忙編譯的情況下),而代碼卻已經(jīng)變得整潔而先進(jìn),對你現(xiàn)有的項(xiàng)目的運(yùn)行完全沒有任何影響。
準(zhǔn)備工作做的這么充分,升級至ng2就指日可待啦:
// index.component.ts
// ng2中沒有controller概念,我們將controller改造為component
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'index-ctrl',
template: '{{ info }}'
})
export class IndexComponent implements OnInit {
info : string = 'hello world';
constructor () {
// 初始化方法為生命周期方法,由ng2框架自動(dòng)調(diào)用,這里不需要手動(dòng)調(diào)用
}
hasInfo () : boolean {
return this.info.length > 0;
}
// 這里用ng2中的生命周期方法
ngOnInit () : void {
console.log(this.info);
}
}
改到這里,index.component.ts的ng2升級就完成了,我們可以看到,除了添加一些annotation以外,component的核心代碼幾乎沒有變動(dòng)。
這說明經(jīng)過優(yōu)化的、完全兼容ng1的typescript版本核心代碼(也就是此步驟之前的所有重構(gòu)工作),可以直接升級至ng2,之要添加一些必要的annotation就可以了。有木有很平滑!
ngModule的ng2升級就留給你做練習(xí)吧。
原文:http://mp.weixin.qq.com/s?__biz=MzI5MDM2NjY5Nw==&mid=2247483843&idx=1&sn=90a1ac29272a0791464c934857ba0ee7&chksm=ec21b445db563d539ce014a1309cca36ed9c4c4d964fa4003a01f23396ea3da915f53107b883&mpshare=1&scene=1&srcid=1112L0HzskOj2DolrEDLs0vI#rd
(未完待續(xù))