我最近在試玩 Angular 2。剛開始感覺很奇怪,和我們鍾愛的第 1 版完全不同。第 1 版是用 ES5 標準的純 javascript 編寫,而第 2 版採用了 typescript 和 es 2015。不過,你已經可以採取一些步驟,讓你的 Angular 1 程式碼(或用 Angular 1 建立的新專案)更加接近 Angular 2。
我為什麼要為 Angular 1 遷移到 Angular 2 做準備
首先,當時機成熟了,你打算用 Angular 2 作為框架時,肯定想讓程式碼遷移更容易些。目前,Angular 小組已經提供了一些遷移策略,你可以混合使用 Angular 1 和 Angular 2 元件,但目標是要將程式碼庫統一,最終只使用一個框架。
其次,在 Angular 2 中更多的是寫純 javascript,然後才是使用專有的框架程式碼。
再次,社群和瀏覽器廠商將逐步擁抱 Ecmascript 的最新標準,所以,堅持使用標準編碼,儘可能讓程式碼庫可複用,而不管選擇的框架是什麼。
遷移到 Angular 2 的步驟
採取這些策略可以讓你的程式碼更加接近 Angular 2,使轉換變得容易。
1. 開始用 Ecmascript 2015
Angular 2 使用 Typescript 編寫,Typescript 是 Ecmascript 2015 的超集,帶有更多的特性。不過,如果你不喜歡 Typescript, 也可以只用 Ecmascript 2015 編寫 angular 2。 目前,程式碼最終都會編譯成 Ecmascript 5。所以實際上你也可以用 Ecmascript 5 來編寫 Angular 2。
但是,在我看來,使用 Ecmascript 2015 的新特性,可以減少程式碼量(有些時候…)、增強程式碼可讀性、用上令人興奮的特性,如解構。
如果想使用 Ecmascript 2015 的特性,你需要一個轉換器來編譯程式碼。目前最流行的轉換器是 babel。babel 在很多流行的構建指令碼中都可以配置,如 gulp、webpack、browserify 及其它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 物件屬性增強 var exports = { search: search, setType: setType, setDuration: setDuration }; // 可以寫成這樣 var exports = { search, setType, setDuration }; ///////////// // 使用“胖箭頭” => 可以簡化程式碼並增強可讀性 var videoIds = response.data.items.map(function(video){ return video.id[idPropertyName[activeType]]; }).join(','); // 使用了胖箭頭符號 var videoIds = response.data.items.map((video) => { return video.id[idPropertyName[activeType]]; }).join(','); |
2. 使用 “angular.service” 替換 “angular.factory”
使用 Ecmascript 2015 意味著我們可以用新的 “class” 關鍵字來建立新物件甚至擴充套件其它物件。我曾經寫過,比起繼承我更熱衷於組合,所以我看不出用 “extend” 實現繼承有什麼用處,不過通過 class 的特性的確可以為建立物件增加好用的語法糖(簡化程式碼)。
在 angular 1 中的 “service” 和 “factory” 的區別是例項化方法:
“service” 使用 “new” 關鍵字呼叫(僅一次)
“factory” 使用普通函式呼叫 — 不需要 “new” 關鍵字。
在 Angular 2 中,Services 使用了 Ecmascript 2015 類編寫。這會導致你需要將 Angular 1 程式碼中的 factories 轉化成 services,並且使用 “class” 替代 function。
例如在我的開源專案-Echoes Player 中,我使用了“class” 和 Angular“service” 編寫 youtube api 服務(我以前用的是 factory):
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 26 27 28 29 30 31 32 33 34 35 36 37 |
(function() { 'use strict'; /* @ngInject */ class YoutubePlayerApi { /* @ngInject */ constructor ($window, $q) { /*jshint validthis: true */ this.deferred = $q.defer(); //當 API 準備好時,Youtube 回撥 $window.onYouTubeIframeAPIReady = () => { this.deferred.resolve() }; } // 注入 YouTube 的 iFrame API load () { let validProtocols = ['http:', 'https:']; let url = '//www.youtube.com/iframe_api'; // 我們願意使用相關的 url 協議,但為避免協議不可用,還是回退到 ‘http:’ if (validProtocols.indexOf(window.location.protocol) < 0) { url = 'http:' + url; } let tag = document.createElement('script'); tag.src = url; let firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); return this.deferred.promise; } } angular .module('youtube.player') .service('YoutubePlayerApi', YoutubePlayerApi); })(); |
3. 編寫 Controllers 時使用 “Class” 替換 “function”
這一步和上一步多少有些相似。Angular 1 中的 Controllers 總是不停被重建(或 “新建”)- 因為它們不是單例。
Angular 2 幾乎不用 controllers。
反之,Angular 2 是基於元件的。每個元件都有一個簡單的類(包含少量 es7 註解)來控制。如果你的 Angular 1 程式碼是用 web 元件方式來編寫的,那麼很可能每個指令(directive)都對應一個 controller 函式來控制。
有個很重的點必須意識到 - 指令的概念在 Angular 2 中更加簡單:
- 使用了元素選擇器的指令都是元件。
- 剩下的都是指令。
Angular 2 仍然會在內部初始化 services 和 controllers,不要自己去初始化,因為那樣會導致程式碼很難測試。不過 Angular 2 還是易於測試並對 TDD (測試驅動開發) 和 BDD(行為驅動開發)友好。我還寫過一篇文章,內容是講為什麼應當封裝 “new” 關鍵字,從而寫出更容易測試的程式碼。
例如把 controller 寫成類,會使程式碼遷移到 angular 2 元件變得非常容易:
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 26 27 28 |
class DurationCtrl { /* @ngInject */ constructor (YoutubeSearch) { this.YoutubeSearch = YoutubeSearch; this.durations = [ 'Any', 'Short (less then 4 minutes)', 'Medium (4-20 minutes)', 'Long (longer than 20 minutes)' ]; this.durationsMap = [ '', 'short', 'medium', 'long' ]; } onDurationChange (duration, index) { this.YoutubeSearch.setType(this.YoutubeSearch.types.VIDEO); this.YoutubeSearch.setDuration(this.durationsMap[index]); this.YoutubeSearch.search(); } } angular .module('echoes') .controller('DurationCtrl', DurationCtrl); |
4. 使用指令封裝程式碼
在這一步,你需要重新思考程式碼,並且使用更好的架構。從 元件(components)/指令(directives) 的角度開始思考。千萬不要在 index.html 或任何未關聯指令的模板中編寫任何 Angular 程式碼。例如:如果你在一段描述個人資料卡片的程式碼中使用了 ng-repeat
, 你可以建立一個指令,“<person-profile-card>” 或者 “<profile-cards>” (作為一個列表)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<div ng-repeat="person in vm.persons"> <img ng-src="person.thumb"> <h3>{{:: person.name }}</h3> <aside>{{:: person.moto }}</aside> <p> {{:: person.description }} </p> </div> <!-- 可以轉換成一個元件 --> <div ng-repeat="person in vm.persons"> <person-profile-card model="person"></person-profile-card> </div> <!-- 可以成為另一個列表元件 --> <profile-cards items="vm.persons"></profile-cards> |
在即將推出的 Angular 1.5 版本里,你可以使用 ”angular.component“ 定義來建立元件,使得語法( 出自 todd motto 之手)比指令(directive) 更優美。
記住,元件搭配元件(或指令) 就是 Angular 2 的全部,從這個角度思考,將有助於你更好的重新組織程式碼,也更容易使用 Angular 2。
5. 使用 Angular2to1,ng-upgrade 或其它方法
Angular 2 採用了一個簡單漂亮的語法來定義元件(指令)。為了在 es5 程式碼中體驗 Angular 2 的元件語法,我建立了一個 npm 模組 “angular2to1”。示例,你可以在 Angular 1 應用中使用 Angular 2 的 es5 標準語法來定義一個指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var myApp = ng .Component({ selector: 'youtube-videos' providers: [ 'core.services' ], bindings: { videos: '@' } }) .View({ templateUrl: 'app/youtube-videos/youtube-videos.tpl.html' }) .Class({ constructor: 'YoutubeVideosCtrl' }) |
這和定義一個 Angular 1 指令等效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
angular .module('youtube-videos', [ 'core.services' ]) .directive('youtubeVideos', youtubeVideos); /* @ngInject */ function youtubeVideos () { var directive = { controller: 'YoutubeVideosCtrl', controllerAs: 'vm', restrict: 'E', replace: true, template: 'app/youtube-videos/youtube-videos.tpl.html', bindToController: true, scope: { videos: '@' } }; return directive; } |
有大量的選項可供使用。
ng-upgraders 是一個程式碼倉庫,包含了 Angular 2 升級策略的資源連結。裡面有一些有趣的專案,有的專案提供了在 Angular 1 中使用 Angular 2 的 typescript 註解及 Ecmascript 2015 的可能性,這樣幾乎可以完全用 Angular 2 的語法來寫 Angular 1 了。
另外,有很多在 Angular 1 中使用 Ecmascript 2015 的解決方案,無論是從軟體架構還是 Angular 推薦架構的角度來看,都堅持了最佳實踐和嚴格準則。
我比較喜歡的一個專案是: NG6-Starter , 專案包含使用 Ecmascrpipt 2015 編寫 Angular 1 應用的骨架程式碼,元件生成器,測試配置和更多內容。
6. (彩蛋)使用模組載入器 system.js、webpack、browserify 或其它工具
所有 Angular 2 例子都依賴於 System.js 庫的元件懶載入機制。System.js 既允許我們使用懶載入,也可以編譯成壓縮好的單一檔案用於生產環境。這樣你就可以在不同的檔案中編寫元件(components)和服務(services),不管構建還是開發,都使用構建指令碼來解決依賴關係。
In addition, if you rather use gulp.js, webpack or browserify – that’s a no brainer and can be easily configured and integrated.
另外,如果你就是喜歡用 gulp.js、webpack 或 browserify - 完全沒理由。沒關係,配置和移植都很簡單。
總結
我傾向於遵守標準。我認為 Ecmascript 2015 最終會成為了 javascript 語言下一代正式標準,所以,有理由去使用它(我之前也寫過)並且擁抱變化。
如果其它的框架、平臺和庫擁抱了 Ecmascript 2015 標準 ,所有人都會受益。大家可以用更靈活的方式編碼,同時在不同的庫和專案中共享程式碼。