孵了八個月,AngularJS 1.3版終於在前幾天破殼而出

一直很期待的ngModelOptions updateOn功能隨著1.3版問市,未來繫結到<input>可指定移開焦點才觸發,比每敲一個字母重算一次有效率,另外也可選擇debouncing累積多次變化只重算一次(相當於KO的throttle擴充方法),這些都是之前在寫KO MVVM慣用的做法,1.3起正式支援。

另外還看到一些亮點:

有這些新武器,Angular就更好用了。

迫不及待地把進行中專案的Angular從1.2.25升到1.3。薑!薑!薑!薑~ 換裝新引擎的網站… 整個爛掉了! 無法建立Controller,大小繫結隨之全軍覆沒… orz

才一升級就被迫觀摩新版Source Code,莫非是程式魔人的宿命。使用消去法逐一拔掉專案模組縮小範圍,最後鎖定一段參考stackoverflow討論加入的$controller Decorator邏輯,如下所示:Demo

<!DOCTYPE html>
<html ng-app="app">
<head>
  <meta charset="utf-8">
  <title>Controller Decorator</title>
</head>
<body ng-controller="myCtrl">
  <div>{{test}}</div>
  <script src="//code.angularjs.org/1.3.0/angular.js"></script>
  <script>
    //http://stackoverflow.com/questions/23382734/angularjs-get-controller-name-from-scope
    angular.module("app",[])
    .config(["$provide", function($provider) {
      $provider.decorator("$controller", [
        "$delegate", function($delegate) {
          return function(constructor, locals) {
            //do nothing, just to test decorator
            return $delegate(constructor, locals);
          }
        }
      ])
    }])
    .controller("myCtrl", function ($scope) {
        $scope.test = "TEST";
    });    
  </script>
</body>
</html>

程式執行會出現以下錯誤,Controller建立失敗,後面全都不用玩:

"TypeError: object is not a function
    at https://code.angularjs.org/1.3.0/angular.js:7594:13
    at forEach (https://code.angularjs.org/1.3.0/angular.js:343:20)
    at nodeLinkFn (https://code.angularjs.org/1.3.0/angular.js:7593:11)
    at compositeLinkFn (https://code.angularjs.org/1.3.0/angular.js:6991:13)
    at compositeLinkFn (https://code.angularjs.org/1.3.0/angular.js:6994:13)
    at publicLinkFn (https://code.angularjs.org/1.3.0/angular.js:6870:30)
    at https://code.angularjs.org/1.3.0/angular.js:1489:27
    at Scope.$eval (https://code.angularjs.org/1.3.0/angular.js:14123:28)
    at Scope.$apply (https://code.angularjs.org/1.3.0/angular.js:14221:23)
    at bootstrapApply (https://code.angularjs.org/1.3.0/angular.js:1487:15)"

將angular換成1.2.25或將$provider.decorator()片段,問題就會消失。迫不得已,開始Line By Line Debug追蹤,比對加上及移除$provider.decorator("$controller", …)的行為差異,最後找到一處關鍵:

加入decorator時,later = false;未加時,later = true。再往源頭找,真相大白:

angular.js 1.2.25版

    /**
     * @ngdoc service
     * @name $controller
     * @requires $injector
     *
     * @param {Function|string} constructor If called with a function then it's considered to be the
     *    controller constructor function. Otherwise it's considered to be a string which is used
     *    to retrieve the controller constructor using the following steps:
     *
     *    * check if a controller with given name is registered via `$controllerProvider`
     *    * check if evaluating the string on the current scope returns a constructor
     *    * check `window[constructor]` on the global `window` object
     *
     * @param {Object} locals Injection locals for Controller.
     * @return {Object} Instance of given controller.
     *
     * @description
     * `$controller` service is responsible for instantiating controllers.
     *
     * It's just a simple call to {@link auto.$injector $injector}, but extracted into
     * a service, so that one can override this service with [BC version](https://gist.github.com/1649788).
     */
    return function(expression, locals) {
      var instance, match, constructor, identifier;

angular.js 1.3.0版

    /**
     * @ngdoc service
     * @name $controller
     * @requires $injector
     *
     * @param {Function|string} constructor If called with a function then it's considered to be the
     *    controller constructor function. Otherwise it's considered to be a string which is used
     *    to retrieve the controller constructor using the following steps:
     *
     *    * check if a controller with given name is registered via `$controllerProvider`
     *    * check if evaluating the string on the current scope returns a constructor
     *    * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
     *      `window` object (not recommended)
     *
     * @param {Object} locals Injection locals for Controller.
     * @return {Object} Instance of given controller.
     *
     * @description
     * `$controller` service is responsible for instantiating controllers.
     *
     * It's just a simple call to {@link auto.$injector $injector}, but extracted into
     * a service, so that one can override this service with [BC version](https://gist.github.com/1649788).
     */
    return function(expression, locals, later, ident) {
      // PRIVATE API:
      //   param `later` --- indicates that the controller's constructor is invoked at a later time.
      //                     If true, $controller will allocate the object with the correct
      //                     prototype chain, but will not invoke the controller until a returned
      //                     callback is invoked.
      //   param `ident` --- An optional label which overrides the label parsed from the controller
      //                     expression, if any.
      var instance, match, constructor, identifier;

1.3.0版的$controller()增加兩個新參數later及ident,而先前decorator()程式針對1.2.X撰寫,只有expression與locals,傳遞到$delegate時漏了later及ident,導致跑錯邏輯。$controller被定義成Private API,故Angluar自由調整無可厚非,但decorator()無可避免與其產生相依,因而被波及。

知道原因,稍加修改補上later及ident,裝妥Angular 1.3新引擎的專案就順利起飛囉~

  • 心得1:追蹤及修改3rd Party Source Code好像已經變成前端攻城獅的必要技能
  • 心得2:某些追求程式省時省工便於修改擴充的巧妙解法,在Library更動時挺易碎裂,不如Copy & Paste之類愚公移山法強韌。但我仍傾向前者,只要遇到問題能修正,對照平時省下的工程及對架構的簡化,仍然很值得。

Comments

# by Chris

You save my day, thanks

Post a comment