專案再遇到Angular JavaScript原本執行正常啟用壓縮(Minification)後冒出找不到nPrivder、bProvider等錯誤訊息,這是Angular Dependency Injection機制因壓縮變數更名崩壞的典型案例,但訊息詭異(nProvider、bProvider是啥鬼?)常讓新手迷惑,特寫篇筆記補充。

用一個簡單的ViewModel及Service範例來說明。

開始前,先花一點時間說明Class化(物件化)的概念。雖然在許多Angular教學中,寫個function就能做出Controller、Service,實務上我都建議將ViewModel、Service寫成Class(JavaScript的Class寫法跟Function相似,Function本身可視為為建構式,內部用this.*定義屬性及方法),並將每個Class獨立成單一JavaScript方便管理。更進一步,Class還可改用TypeScript撰寫,享受編譯檢查、繼承、介面等強型別專屬的好處。(但在此先以純JavaScript為例,避免失焦)

Class化還有另一項好處:未來在Angular 2.0,Directive、Service、ViewModel等將全面物件化(搭配TypeScript或ES6的Class語法),現階段避用Controller接入$scope加工掛屬性的傳統寫法,改成將ViewModel物件化並直接繫結物件屬性,較容易與Angular 2.0接軌。

如下例,ng-controller寫成ctrl as vm,vm.writeLog()即對應到ViewModel物件的wirteLog()方法,而不是$scope.writeLog()。

<!DOCTYPE html>
<html ng-app="app">
<body ng-controller="ctrl as vm">
    <button ng-click="vm.writeLog('Test')">Test</button>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.3.2/angular.min.js"></script>
    <script src="app.js"></script>
</body>
</html>

在app.js宣告myViewModel類別,以controller("ctrl", myViewModel)將其註冊成Controller,注意動作邏輯被寫在this.writeLog而非$scope.writeLog,但ViewModel仍需取得$scope,以便應用$watch、$digest等Angular機制 。

//將Service跟ViewModel寫成Class,實務上還會獨立一個Class一個js檔
//而用TypeScript來寫Service及ViewModel類別是更好的選擇
function myLoggerService($log) {
    this.log = function (msg) {
        $log.log(msg);
    }
}
 
function myViewModel($scope, myLogger) {
    this.writeLog = function (msg) {
        myLogger.log(msg);
    };
}
 
angular.module("app", [])
    //註冊服務或Controller時,直接給函式名稱當參數
    //Angular神奇的DI機制會自動找到$log、$scope、myLogger
    //傳給myLoggerService及myViewModel
    .service("myLogger", myLoggerService)
    .controller("ctrl", myViewModel);

回到壓縮問題(跳一下)。myLoggerService建構時需要$log參數,myViewModel建構時需要$scope、myLogger(服務),但下方只寫了.service("myLogger", myLoggerService)及.controller("ctrl", myViewModel),建構函式卻能正確接收$logger或myLogger,這都歸功於Angular的依賴注入機制,它能解析參數名稱取得對應的物件、服務。然而,JavaScript壓縮機制會將區域變數更名為單一字母以節省空間,上述的程式碼在壓縮後,myLoggerService的$log被改成n,myViewModel的$scope跟myLogger被改成n及t:

function myLoggerService(n){this.log=function(t){n.log(t)}}function myViewModel(n,t){this.writeLog=function(n){t.log(n)}}angular.module("app",[]).service("myLogger",myLoggerService).controller("ctrl",myViewModel);
//# sourceMappingURL=app.min.js.map

如此一來,Angular靠變數名稱取得對應物件的做法當場破功!

針對這個問題,Angular提供兩種解法:

function myLoggerService($log) {
    this.log = function (msg) {
        $log.log(msg);
    }
}
 
function myViewModel($scope, myLogger) {
    this.writeLog = function (msg) {
        myLogger.log(msg);
    };
}
myViewModel.$inject = ["$scope", "myLogger"];
 
angular.module("app", [])
    //克服JS壓縮DI失效問題
    //方法一:使用陣列式宣告,依序放入參數名稱字串,函式殿後
    .service("myLogger", ["$log", myLoggerService])
    //方法二:使用$inject宣告參數名稱陣列
    .controller("ctrl", myViewModel);

方法一,傳入函式的地方改寫成陣列,依序放入參數名稱字串,函式當成陣列最後一個元素。

方法二,為函式加上$inject屬性,指定參數名稱字串陣列。(可讀性更高,推薦使用)

由於參數名稱以字串常數存在,壓縮時會完整保留,而Angular能識別這兩種特殊寫法,DI機制就又活起來了!撰寫NG程式時,記得養成習慣。

function myLoggerService(n){this.log=function(t){n.log(t)}}function myViewModel(n,t){this.writeLog=function(n){t.log(n)}}myViewModel.$inject=["$scope","myLogger"];angular.module("app",[]).service("myLogger",["$log",myLoggerService]).controller("ctrl",myViewModel);
//# sourceMappingURL=appFixed.min.js.map

[NG系列]

http://www.darkthread.net/kolab/labs/default.aspx?m=post&t=angularjs

Comments

Be the first to post a comment

Post a comment