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 沒有的事件 - loadedchange,讓原本就已經不少的事件數注入了新的能量(在講啥)。 值得注意的是,親自去看一下 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 不叫 arraylizetoArray,甚至 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 包起來… 這樣對嗎!!!!!

這樣對嗎