1. 程式人生 > >[AngularJS面面觀] 13. Angular工具庫 --- 異常物件建立方法minErr

[AngularJS面面觀] 13. Angular工具庫 --- 異常物件建立方法minErr

本系列文章會討論Angular框架除了提供scope等核心功能外,還提供了哪些功能。

作為Angular工具庫這一系列文章的開篇,首先來看看但凡程式都繞不開的一個話題 - 異常。
那麼Angular在異常處理方面又提供了哪些工具呢?

引子 - scope中是如何丟擲異常的?

首先,讓我們看看在定義$rootScope的過程中,哪些程式碼和異常有關:

// 定義異常物件
function $RootScopeProvider() {
  var $rootScopeMinErr = minErr('$rootScope');
  // ......
}

// 下面是丟擲異常的2個場景
// 1. 當Digest Cycle正在進行,不要重複啟動DC
function beginPhase(phase) { if ($rootScope.$$phase) { throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase); } $rootScope.$$phase = phase; } // 2. 當DC的次數超過閾值(TTL預設值為10)時,丟擲我們耳熟能詳的infdig異常 if ((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw
$rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', TTL, watchLog); }

下面看看在真實情況下,丟擲的異常在控制檯中是什麼樣子的。這裡需要注意的是,當引用的angular是經過壓縮處理後的angular.min.js時,產生的輸出和引用未經壓縮處理的angular.js時是不同的。(這一點也讓我在對比原始碼和實際輸出的時候有些詫異,發現很多原始碼中的輸出在真實的瀏覽器控制檯環境下不見了。後來才發現經過壓縮的原始碼會將部分輸出省略)

// 引用angular.min.js
angular.min.js:117Error: [$rootScope:infdig] http://errors.angularjs.org/1.5.7/$rootScope/infdig?p0=10&p1=%5B%5B%7B%22ms…urn%20scope.b%3B%20%7D%22%2C%22newVal%22%3A13%2C%22oldVal%22%3A12%7D%5D%5D
    at Error (native)
    at http://localhost:10001/angular.min.js:6:412
    at m.$digest (http://localhost:10001/angular.min.js:143:281)
    at m.$apply (http://localhost:10001/angular.min.js:145:401)
    // ......

// 引用angular.js
angular.js:13708 Error: [$rootScope:infdig] 10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: [[{"msg":"fn: function (scope) { return scope.a; }","newVal":7,"oldVal":6},{"msg":"fn: function (scope) { return scope.b; }","newVal":9,"oldVal":8}],[{"msg":"fn: function (scope) { return scope.a; }","newVal":8,"oldVal":7},{"msg":"fn: function (scope) { return scope.b; }","newVal":10,"oldVal":9}],[{"msg":"fn: function (scope) { return scope.a; }","newVal":9,"oldVal":8},{"msg":"fn: function (scope) { return scope.b; }","newVal":11,"oldVal":10}],[{"msg":"fn: function (scope) { return scope.a; }","newVal":10,"oldVal":9},{"msg":"fn: function (scope) { return scope.b; }","newVal":12,"oldVal":11}],[{"msg":"fn: function (scope) { return scope.a; }","newVal":11,"oldVal":10},{"msg":"fn: function (scope) { return scope.b; }","newVal":13,"oldVal":12}]]
http://errors.angularjs.org/1.5.7/$rootScope/infdig?p0=10&p1=%5B%5B%7B%22ms…urn%20scope.b%3B%20%7D%22%2C%22newVal%22%3A13%2C%22oldVal%22%3A12%7D%5D%5D
    at angular.js:68
    at Scope.$digest (angular.js:17324)
    at Scope.$apply (angular.js:17552)
    // ......

可見當使用angular.js時,異常的說明(也就是上述呼叫$rootScopeMinErr時傳入的第二個引數)也會被打印出來。因此,建議在學習angular的時候使用未經壓縮的angular.js。裡面會保留你在原始碼中看到的所有輸出。

瞭解了minErr的使用場景,下面看看它是如何定義與實現的。

minErr方法

1. 異常訊息的構成

首先還是從文件來直觀地感受一下:

/**
 *
 * 該物件用於在Angular內部輸出詳盡的錯誤資訊。可以按下面的方法進行呼叫:
 *
 * var exampleMinErr = minErr('example');
 * throw exampleMinErr('one', 'This {0} is {1}', foo, bar);
 * ......
 */
function minErr(module, ErrorConstructor){
  // ......
}

minErr方法本身是接受兩個引數的。第一個引數用來定義一個模組名稱,比如上述的example就可以看作是一個模組的名稱。它的作用是為某類異常提供一個名稱空間。第二個引數是一個可能需要的自定義Error建構函式。當預設的JavaScript Error型別無法滿足需求時,就可以傳入一個繼承自Error型別的自定義型別作為錯誤型別。

而minErr方法的返回也很有意思,返回的是一個函式。該函式沒有定義引數列表,但是對於引數出現的順序有它自己的規範:
1. 第一個引數表示一個code,該code和上面的模組名稱進行拼接後得到某個錯誤的具體字串表示,比如上述例子中的example.one
2. 第二個引數是一個訊息模版,它會在使用未經壓縮的angular.js時顯示在丟擲的異常資訊中。如果使用壓縮的angular.min.js則不顯示。
3. 第三個引數及後續引數用於替換訊息模版中的佔位符:throw exampleMinErr('one', 'This {0} is {1}', foo, bar),其中的{0}和{1}會被分別替換成foo和bar。

該函式的返回:

function minErr(module, ErrorConstructor) {
  ErrorConstructor = ErrorConstructor || Error;
  return function() {
    // message構建過程
    return new ErrorConstructor(message);
  };
}

由此可見該函式重要的功能就是構造用於建立錯誤物件的message,它的構建過程如下:

return function() {
  var SKIP_INDEXES = 2;

  var templateArgs = arguments,
    code = templateArgs[0],
    message = '[' + (module ? module + ':' : '') + code + '] ',
    template = templateArgs[1],
    paramPrefix, i;

  message += template.replace(/\{\d+\}/g, function(match) {
    var index = +match.slice(1, -1),
      shiftedIndex = index + SKIP_INDEXES;

    if (shiftedIndex < templateArgs.length) {
      return toDebugString(templateArgs[shiftedIndex]);
    }

    return match;
  });

  message += '\nhttp://errors.angularjs.org/"NG_VERSION_FULL"/' +
    (module ? module + '/' : '') + code;

  for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
    message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' +
      encodeURIComponent(toDebugString(templateArgs[i]));
  }

  return new ErrorConstructor(message);
};
});

message += '\nhttp://errors.angularjs.org/"NG_VERSION_FULL"/' +
  (module ? module + '/' : '') + code;

for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
  message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' +
    encodeURIComponent(toDebugString(templateArgs[i]));
}

程式碼比較長,但是邏輯主線非常清晰。完全圍繞著message的構建:
1. 異常程式碼。由module以及code組成:'[' + (module ? module + ':' : '') + code + '] '
2. 通過訊息模版生成異常描述。這一部分是否顯示根據引用的angular.js是否壓縮來決定。
3. 生成異常參考連結。點選連結後會進入到angular的官網,會給一些關於異常的解釋。生成連結的過程中會拼接上呼叫引數。

2. 簡單訊息模板的實現

上面構建message的第一步和第三步都很清晰,重點來看看第二步是如何實現的,相關程式碼如下:

message += template.replace(/\{\d+\}/g, function(match) {
  var index = +match.slice(1, -1),
    shiftedIndex = index + SKIP_INDEXES;

  if (shiftedIndex < templateArgs.length) {
    return toDebugString(templateArgs[shiftedIndex]);
  }

  return match;
});

這裡運用到了replace方法的第二個引數-接受一個function表示每個match的替換邏輯,這種用法並不是很常見。一般我們會直接使用一個字串作為第二個引數,來表示替換字串。相關的文件可以參考這裡

就拿這個例子而言:
'This {0} is {1}', foo, bar

replace第二個function引數被呼叫時,傳入的match實際上是’{0}’和’{1}’。那麼通過slice(1, -1)得到的index就分別為0和1。緊接著通過index來得到arguments引數類陣列中對應的引數,呼叫toDebugString進行替換。關於toDebugString的實現:

function toDebugString(obj) {
  if (typeof obj === 'function') {
    return obj.toString().replace(/ \{[\s\S]*$/, '');
  } else if (isUndefined(obj)) {
    return 'undefined';
  } else if (typeof obj !== 'string') {
    return serializeObject(obj);
  }
  return obj;
}

以上,就是message的構建過程。其中實現了一個簡單的模板替換演算法。當需要在應用中實現類似邏輯,又不希望為這麼一點功能引用一些第三方庫比如mustache,那麼不妨考慮一下上述方法。另外值得一提的是,如果你的應用中已經引用了諸如underscore或者lodash這樣的庫,這些庫也提供了模版替換的方法,以lodash中的template方法為例:

// Use the "interpolate" delimiter to create a compiled template.
var compiled = _.template('hello <%= user %>!');
compiled({ 'user': 'fred' });
// → 'hello fred!'

// Use the HTML "escape" delimiter to escape data property values.
var compiled = _.template('<b><%- value %></b>');
compiled({ 'value': '<script>' });
// → '<b>&lt;script&gt;</b>'

除此之外,lodash的template方法還提供了更多的替換方法,詳情可以參考上面的連結。

結語

以上就是angular中用於封裝異常的方法,通過minErr首先構建一個某個特定模組下的異常建立方法。然後傳入具體的錯誤程式碼(code),訊息模版(template)以及具體引數來完成異常物件的建立。

如果你想在你的應用中使用minErr來完成異常的定義,可以通過angular.$$minErr來得到該函式。但是從命名前面的兩個$符號可以知道,angular並不鼓勵在angular框架之外來使用它。因為隨著版本的變更,作為內部實現的minErr函式也可能會發生變化。如果應用程式程式碼直接依賴於它的話,在升級angular版本的時候或許會出現一些問題(取決於minErr的實現和使用方式是否發生了變化)。

所以擺在我們面前有兩個選項:
1. 定義屬於應用程式自己的minErr。經過以上的討論,可以發現它的實現原理也十分的簡單清晰,因此在中大型應用中根據需要對minErr進行模仿和定製,建立一個應用程式版的minErr,以此來將日益繁雜的異常型別組織地井井有條也是一種非常不錯的選擇。
2. 仍然使用angular.$$minErr。對於中小型的應用是不錯的選擇,因為這類應用異常種類不會很多,使用angular框架提供的功能足矣。而且有了對minErr原理性的瞭解,只要未來的版本中不發生什麼重大的變更,升級到新的版本是沒有什麼困難的。