第一次聽說到 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,畢竟沒有特別做處理。
資源分享
- Firebase 官方文件
- Stackoverflow
angularfire
tag - angularFire github repo - 老實說 angularFire 方面的官方文件寫得跟 code 很不一致(其實是 code 寫得跟文件不一致),非常建議有碰到什麼奇怪的問題可以直接去查 source code。
- 我整理的小 gist module
fireutil
- ng-fire-alarm - 另一個 Firebase + AngularJS module,採用跟官方版不一樣的 approach,值得試試看
後記
雖然困難重重,但我還是覺得做 real-time webapp 很酷啊!!! 另外真心希望官方 github repo 的維護人員能多一點… 現在好像只有兩個,而且有一個不太靠譜(!?)。
額外抱怨一下官方在事件 callback 的時候都用 $timeout
包起來… 這樣對嗎!!!!!