最近在研究從 Angular 1.x 切換到 Angular 2,因為目前 Angular 2 正處于 RC 階段還會有比較多的變化谬泌,暫時還是使用 1.x 。 為了以后能比較方便的遷移殉摔,所以雖然用的是 1.x 但是在寫法上還是希望更接近于 2.0 的寫法浊仆,也就有了以下我想要實現(xiàn)的功能:使用 decorator 來處理 angular 1.x 的依賴注入。
在設計芳杏、實現(xiàn)的時候我盡量只使用 ES6 和 ES7 的新特性矩屁,因為在試用 Angular 2 的時候發(fā)現(xiàn) Typescript 編譯確實是太慢了,而且 Typescript 的靜態(tài)類型檢查雖然是我喜歡的但是使用起來對開發(fā)效率影響比較大蚜锨,綜合平衡后決定未來還是基于 ES 標準開發(fā)就好了档插,不打算切換到 Typescript 或者其它 XType。
Angular 1.x 依賴注入的現(xiàn)狀和問題
使用過 Angular 的朋友都知道依賴注入是 Angular 中非常重要的特性亚再,在 1.x 時代有3種方式可以實現(xiàn)依賴注入:
方式一:
angular.module('app', [])
.controller('mainCtrl', [
'$scope',
function($scope) { ... }
])
方式二:
function mainCtrl($scope) { ... }
mainCtrl.$inject = ['$scope'];
angular.module('app', [])
.controller('mainCtrl', mainCtrl);
方式三:
angular.module('app', [])
.controller(
'mainCtrl',
function($scope) { ... }
);
方式1郭膛、2都需要寫兩次需要注入的服務并且配置服務名稱和編寫入?yún)r的順序必需一致,比較影響開發(fā)效率而且容易出錯氛悬。而方式3在框架內(nèi)部是通過分析函數(shù)的入?yún)⒚Q來確定需要注入的服務则剃,使用起來非常方便但是如果對JS代碼進行壓縮處理后耘柱,框架就無法得知真正需要注入的服務了。
所以棍现,目前這三種依賴注入的方式都存在一些問題调煎。不過基本方式三和 ng-annotate 還是可以比較完美的解決依賴注入的問題,簡單來說 ng-annotate 就是通過自動構(gòu)建工具在壓縮JS代碼前將方式三轉(zhuǎn)換成方式二己肮,而這一切都是由構(gòu)建工具自動完成我們在開發(fā)的時候并不用關(guān)心士袄。
廢話了這么多只是為了說明本文希望解決的問題:其實很簡單,就是希望在依賴注入的時候只寫一次需要注入的服務谎僻!雖然使用 ng-annotate 已經(jīng)可以實現(xiàn)娄柳,但是我希望用最新的標準來處理這個問題,以便未來更方便的向 Angular2 過渡艘绍,這種方式就是使用 Decorator赤拒。
使用 Decorator 解決 angular 1.x 的依賴注入問題
Decorator 是 ES7 的一個提案,目前通過 Babel 或者 Typescropt 已經(jīng)可以提前享用诱鞠。嘗試過 Angular2 的朋友應該知道在 Angular2 中就引入了很多的 Decorator挎挖,例如:
import { Component } from '@angular/core';
@Component({
...
})
export class MainComponent {
...
}
這里的 Component 就是一個 Decorator, 使用過 Python 的朋友應該也非常熟悉這種語法。簡單來說 Decorator 就是一個模板航夺,可以在編譯階段對被裝飾的類或類成員進行修改(目前通過Babel使用的時候貌似還是在Runtime階段起使用)蕉朵。
在使用 Decorator 解決依賴注入問題的時候我首先想到的就是 ng-annotate 的方案,在 Decorator 里去分析被裝飾類構(gòu)造函數(shù)的入?yún)⒘斜矸蟠妫缓鬄楸谎b飾類添加$inject
屬性墓造,代碼如下:
function inject() {
return function(target) {
let services = angular.injector.$$annotate(target);
target.$inject = services;
}
}
@inject()
class MainComponent {
constructor($scope) {
...
}
}
其中angular.injector.$$annotate(target)
就是 angular 中解析入?yún)⒚Q列表的函數(shù),之前還花了好長時間自己去實現(xiàn)锚烦,后來一想 angular 既然可以自動發(fā)現(xiàn)需要注入的服務肯定有完整的入?yún)⒎治雒倜觯痪驮谠创a的 src/auto/injector.js
里找到了。這種方式在沒有壓縮JS代碼的情況下測試運行沒有問題涮俄,$inject
屬性正確添加蛉拙。但是,在壓縮JS代碼后就出現(xiàn)沒有找到aProvider
這樣的錯誤了彻亲,閱讀 Babel 轉(zhuǎn)譯后的代碼也沒有發(fā)現(xiàn) MainComponent.$inject = [...]
這行代碼孕锄,所以我認為在 Babel 中 Decorator 其實是在 Runtime 中執(zhí)行而不是在編譯的時候執(zhí)行(難道我對編譯和運行時的理解有錯?還是提案里不是說的在編譯時執(zhí)行苞尝?)畸肆。
其實在 Github 中已經(jīng)有人為 Angular 1.x 的依賴注入提供了 Decorator 的實現(xiàn),大概的實現(xiàn)方式是:
function inject(...services) {
return function(target) {
target.$inject = services;
}
}
@inject('$scope', '$http')
class MainComponent {
constructor($scope, $http) {
...
}
}
但是這種實現(xiàn)方式還是需要寫兩次要注入的服務并且入?yún)⒌捻樞蚺c配置的順序還必須一致宙址,無非是將MainComponent.$inject = []
改成了@inject(...)
轴脐,并不是我希望能達到的目的。但是基于這個思路我想到了第二種實現(xiàn)方案,通過 decorator 的入?yún)@取到需要注入的服務大咱,然后在 decorator 中去修改被裝飾類的構(gòu)造函數(shù)恬涧,在實例化被裝飾類時自動將注入的服務添加為實例的屬性。
如何在 decorator 中去修改被裝飾類的 constructor 呢碴巾?這就需要使用到 ES6 的一個新 API Proxy 了溯捆,具體實現(xiàn)如下:
function inject(...services) {
return function(target, property, descriptor) {
var p = new Proxy(target, {
construct: function(target, args) {
for(let i in services) {
target.prototype[services[i]] = args[i];
}
return new target(...args);
}
});
p.$inject = services;
return p;
}
}
@inject('$scope', '$http')
class MainComponent {
constructor() {
this.$http.get(url).then(data => {
this.$scope.data = data;
})
}
}
在 decorator 中我創(chuàng)建了一個 Proxy 實例來攔截被裝飾類的實例化行為,將需要注入的服務自動添加為被裝飾類的實例屬性厦瓢,從而在被裝飾類的實例中可以直接使用而不用關(guān)心依賴注入的細節(jié)(在 constructor 中不用再關(guān)心第一個入?yún)氖悄莻€服務提揍,第二個參數(shù)對應的是那個服務...)。當然旷痕,這只是一個最基礎的實現(xiàn)還可以更完善一些碳锈,例如:添加 $scope as scope
語法修改服務實例屬性名稱等。
而 Proxy 在 ES6 的文檔中有詳細說明欺抗,它可以提供一種攔截機制從而去改變被攔截對象的一些行為,配套的還有一個 Reflect 可以在修改被攔截對象后獲取對象原始的行為强重。
以上就是本文的全部绞呈,使用 decorator 可以實現(xiàn)很多有意思的功能,而從 Angular 1.x 向 2.0 遷移在 Angular 團隊內(nèi)部也在做很多的努力间景。不過我的真正目的還是脫離框架去實現(xiàn)一些常用功能模塊佃声,以便將來不管是用什么框架都能使用的到,最近在開發(fā)前后端的時候都在往這個方向努力倘要。在當前各個語言都擁有完善的包管理器的情況下圾亏,個人覺得一個大而全的框架已經(jīng)不是我需要的了!