angularFire v0.7.0 閒聊

afire-logo.png

第一次聽說到 Firebase 的時候,angularFire 已經是 v0.3.0 版了。基於 Firebase 的特性,與 AngularJS 結合之後撰寫起來會有非常的 Magic! 的感覺,也因此 angularFire 的出現,讓當時剛開始學習 AngularJS 不久的我也感到躍躍欲試。

大概四個月以前,我需要製作某個神秘的小 project,而這個 project 需要 backend 的支持來存一些資料,於是我決定來試一下 Firebase + AngularJS 這組合。經過一番努力完成小專案之後,深深覺得 angularFire 雖然看來神奇,但神奇得不是很好用。

之後大概又過了一個月,angularFire 迎來了首次的重大 API 更動(看來官方應該也是有聽到大家的聲音),這次更動讓 Firebase 需要完全重寫 angularFire 的文件,根本是個不一樣的 module 了。

而我自從聽說大改版之後一直沒有抽時間出來試一下新的 API,我非常期待它是好用的。 好不容易上週終於百忙之中(是有多忙)抽出時間來升級之前小專案使用的 module 到最新版(v0.3.0 直上 v0.7.0),感覺新版確實是有了長足的進步,比之前好用了很多,但還是有些有點雞樂的地方需要注意與克服。

嶄新的 $firebase

新的 data binding 部分的 service 完全整合到了 $firebase 裡,寫出來的 code 看似非常乾淨。 與 v0.3.0 的時候的 angularFire service 主打的 3-way data binding 完全不同,3-way data binding 的功能被移到 .$bind() 上去了 (應該是終於發現這雖然很酷,其實不是這麼常用 lol)

自動與 Firebase backend sync 的 $firebase object

$firebase 產出的 binding 雖然不到 3-way,它至少是 2.5-way 的狀態;後端有更動的資料會即時自動更新。 為了達到這個效果,也需要付出一些代價 - 不管 $firebase 對應的的 Firebase URL 是不是一個 primitive type,拿到的東西都會是 [object Object]。

var url = 'https://xxx.firebaseio.com/foo';  // 存的是 primitive value "bar"
$scope.foo = $firebase(new Firebase(url));
$scope.foo.$on('change', function () {
  expect($scope.foo.$value).toEqual('bar');
});

順帶一提,其實對於 Firebase 來說沒有 array 這種東西,只有 list of data;因此 $firebase 的產出目前沒有 [object Array]。

崩潰的事件順序

在 angularFire 的 .$on method 有多出兩個原本的 Firebase API 沒有的事件 - loaded 與 change,讓原本就已經不少的事件數注入了新的能量(在講啥)。 值得注意的是,親自去看一下 angularfire.js 的 source code 會發現只有 child_* 系列的事件是直接轉接上原本 Firebase 的事件,其它都是另外挑時機再做 broadcast 的。

註:於是就出現了忘記 re-broadcast value 事件的神妙 issue

於是造成了現在的事件順序有點奇怪,以 $scope.foo(對應 object type 的 Firebase URL) 為例,這邊整理條列如下:

事件$scope.foo特殊狀態補充說明
loaded{}首次載入已經拿到資料,不過偏偏還不 apply 到目標上
child_added{}首次載入已經拿到資料,不過偏偏還不 apply 到目標上
value{}已經拿到資料,不過偏偏還不 apply 到目標上
change{data}目標上終於有值
child_added{data}不是首次載入這個是對應原本 Firebase 的事件
child_*{data}這邊都是直接對應原本 Firebase 的事件

列完真是令人心寒 XD 當然有些也有被開成 issue,不過我想他們可能人手蠻不足的…

舉例來說,$getIndex() does not return value in $on(‘loaded’) callback 0.7.0 這個 issue 從他 close 的 commit 看起來根本沒修到 report 的人的問題點。 關鍵應該是在 v0.7.0 把 loaded 事件觸發的時間提前了「幾行」,來到了 _updatePrimitive() 跟 _updateModel() 之前,而實際上是這兩個函式跑完才會更新 $firebase object 上的值。

Array panic

其實在 AngularJS 的世界裡,array 扮演著相當重要的角色。 ng-repeat的時候大部分用的都是 array,因為大部分的 filter 是為 array 設計的;像 stackoverflow 上的這個問題,回答者第一句就說到:

I would change my data structure to an array.

前面有提到過「$firebase 的產出目前沒有 [object Array]」,要硬轉當然也是可以,但這樣會破壞掉原本 $firebase object 建立的同步機制。

對,這樣真的是超難用的! 我的小專案甚至做出在 每次更新的時候視需要再將資料重建成 array 這種可怕的事…

Overlooked orderByPriority filter

在 angularFire 的文件裡有一個很不起眼的名字 - orderByPriority。乍看之下它應該是某種讓資料可以照著 Firebase priority 做排序的東西。它的介紹是這樣寫:

The orderByPriority filter is provided by AngularFire to convert an object returned by $firebase into an array. The objects in the array are ordered by priority (as defined in Firebase).

What!!!!!! 我都已經使用了各種奇技淫巧好不容易處理好 array 轉換的問題,你才跟我說 orderByPriority 就是我要的 filter !?

為什麼這個 filter 不叫 arraylize 或 toArray,甚至 iLoveArray 都好啊!!!!

排序方式在它的敘述裡也是次要的句子,結果硬是變成了命名 filter 的主角…

看看它在 HTML 裡的英姿呢?

<div ng-repeat="msg in msgs | orderByPriority | filter:{$:search} | orderBy:('-' + order)">
	...
</div>

Makes lots of sense!!!! (這是反話)

希望之光 $firebaseArr

有 array panic 的人當然不只我一個。

Add get data as array option 裡除了希望有這個 feature 之外,也提供了他現在正在用的一個 factory function。我一看真是驚為天人,單手打趴了我之前寫了上百行的另一種 implementation。

於是當然收來使用,稍微加上一些修改之後變成這樣:

// Ref: https://github.com/firebase/angularFire/issues/200#issuecomment-31905575
.factory('$firebaseArr', ['$firebase', '$filter', '$log', function($firebase, $filter, $log) {
  return $firebaseArr;

  function $firebaseArr(ref) {
    var dataObj = $firebase(ref);
    var dataArr = angular.extend([], dataObj);

    dataObj.$on('change', function() {
      dataArr.length = 0;
      angular.extend(dataArr, $filter('orderByPriority')(dataObj));
    });

    /**
     * Destroy the $firebaseArr object
     */
    dataArr.$destroy = function () {
      dataObj.$off();
    };

    /**
     * Create $firebaseArr object from a child
     * @param  {string}             name                   child's name
     * @return {$firebaseArr object}
     */
    dataArr.$childArr = function(name) {
      var ref = dataObj.$getRef().child(name);
      return $firebaseArr(ref);
    };

    return dataArr;
  }
}])

基本上使用上跟原本的 $firebase 是一模一樣,除了 return 值是 array。 而使用 .$bind() 的目標出來還是 object,畢竟沒有特別做處理。

資源分享

後記

雖然困難重重,但我還是覺得做 real-time webapp 很酷啊!!! 另外真心希望官方 github repo 的維護人員能多一點… 現在好像只有兩個,而且有一個不太靠譜(!?)。

額外抱怨一下官方在事件 callback 的時候都用 $timeout 包起來… 這樣對嗎!!!!!

這樣對嗎