我們現(xiàn)在有了實時的投票、評分和排名。然而,由于帖子在首頁上跳來跳去,導(dǎo)致了跳動不穩(wěn)的用戶體驗。我們用動畫來平滑這種過渡。
_uihooks
相對較新,Blaze 文檔也未包含該特性。正如其名稱所示,它提供了每當(dāng)插入、刪除或動畫元素時可以被觸發(fā)的 hooks。
Hooks 的全部清單如下:
insertElement
: 當(dāng)新元素被插入時調(diào)用。moveElement
: 當(dāng)元素被移動時調(diào)用。removeElement
: 當(dāng)元素被刪除時調(diào)用。一旦定義,這些 hooks 就會替代 Meteor 的默認(rèn)行為。換句話說,Meteor 會用我們規(guī)定的行為來替代默認(rèn)的插入、移動或刪除元素的行為 ———— 這由我們來確定這行為會真正地工作!
在我們開始有趣部分(使東西移動)之前,我們需要理解 Meteor 如何與 DOM(Document Object Model————組成頁面內(nèi)容的 HTML 元素集合)交互的。
要記住的最關(guān)鍵的一點是,DOM 元素不能真正被“移動”;但是,它們可以被刪除,被創(chuàng)建(注意,這是 DOM 本身的限制,而不是 Meteor 的)。所以要給元素 A 和 B 互換位置的錯覺,Meteor 實際上會刪除元素 B,并在元素 A 前插入一個全新的副本(B')。
這使得動畫有點麻煩,因為我們不能只是把 B 動畫移動到新位置,因為 B 在 Meteor 重新渲染頁面時就會消失(由于響應(yīng)性,這瞬間發(fā)生)。但請不要擔(dān)心,我們會找到一個解決辦法。
不過首先,讓我們講個故事。
在 1980 年,正值冷戰(zhàn)。奧運會正在莫斯科舉行,蘇聯(lián)決心不惜任何代價要贏得 100 米短跑的金牌。所以,一群聰明的蘇聯(lián)科學(xué)家為其中一名運動員裝備了一臺傳送器,只要槍聲一響,那名運動員就會瞬間消失,通過時空連續(xù)的作用直接出現(xiàn)在終點線上。
還好,賽事官員立刻注意到了這個違規(guī)行為,這名運動員沒有辦法只好又瞬時移動回到起跑器上,才能被允許像其他選手一樣賽跑參賽。
我的歷史資料沒有那么可靠,所以你應(yīng)該對這個故事半信半疑。但是,盡量嘗試記住“有傳送器的蘇聯(lián)賽跑者”這個比喻,我們要在這一章中用到這一點。
當(dāng) Meteor 接收到更新并實時地更改 DOM 時,我們的帖子會立即傳送到它的終點位置,就像蘇聯(lián)賽跑者一樣。但是不論是在奧運會還是在我們的應(yīng)用中,我們不能瞬移任何東西。所以我們需要把元件傳送回到“起跑器”上,使它“跑”(換句話說,“動畫”它)到終點。
所以交換帖子 A 和 B (分別位于 p1 和 p2 位置),我們會經(jīng)過如下步驟:
下面圖表詳細(xì)解釋上述步驟:
再次說明,第 3 、4 步中,我們沒有動畫 A 和 B' 到它們的位置,而是瞬間“傳送”了它們。因為這是瞬間發(fā)生的,這會產(chǎn)生 B 沒有被刪除的幻覺,并且兩個元素被動畫到了它們的新位置。
默認(rèn)情況下,Meteor 負(fù)責(zé)步驟 1 和 2,我們自己很容易重新實施它們。在步驟 5 和 6 中所有我們在做的事情是移動元素到正確的位置。因此,唯一我們真正需要擔(dān)心的部分是步驟 3 和 4,即,發(fā)送元素到動畫的起點。
為了在頁面中動畫渲染的帖子,我們必須用到 CSS 樣式。讓我們按順序快速瀏覽 CSS 定位。
頁面元素默認(rèn)使用靜態(tài)定位。靜態(tài)定位的元素適應(yīng)頁面內(nèi)容流,它們在屏幕上的坐標(biāo)不能更改或動畫。
另一方面,相對定位是說元素也同樣適應(yīng)頁面內(nèi)容流,但是可以相對于原始位置進(jìn)行定位。
絕對定位更進(jìn)一步,允許你規(guī)定元素的 x/y 坐標(biāo),坐標(biāo)相對于文檔或第一個絕對或相對定位的父元素。
我們使用相對定位來動畫我們的帖子。我們已經(jīng)為你準(zhǔn)備好了 CSS,你需要做的就是將代碼添加到你的樣式表中:
.post{
position:relative;
}
.post.animate{
transition:all 300ms 0ms ease-in;
}
注意我們只動畫有 .animate
CSS 的帖子。通過添加或刪除 CSS 名來控制是否添加動畫效果。
這使步驟 5 和 6 變得簡單:我們需要做的是重置 top
坐標(biāo)值為 0px
(默認(rèn)值),帖子就會回到它們“正常的”位置。
基本上,我們僅有的挑戰(zhàn)是搞明白元素要從相對于它們新位置的哪里開始動畫(步驟 3 和 4),換句話說,它們要偏移多少。但這也不難:正確的偏移量就是帖子的原來位置減去它的新位置。
既然我們了解了為帖子列表添加動畫的各種因素,我們算是準(zhǔn)備好開始添加動畫了。我們首先需要把帖子列表放入一個新的 .wrapper
容器元素中:
<template name="postsList">
<div class="posts page">
<div class="wrapper">
{{#each posts}}
{{> postItem}}
{{/each}}
</div>
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{else}}
{{#unless ready}}
{{> spinner}}
{{/unless}}
{{/if}}
</div>
</template>
現(xiàn)在讓我們加入 _uihooks
。在模板 onRendered
回調(diào)函數(shù)中,選擇 .wrapper
div,并定義一個 moveElement
的 hook。
Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
moveElement: function (node, next) {
// 現(xiàn)在不做任何事情
}
}
});
剛剛定義的 moveElement
會在元素位置改變時被調(diào)用,從而取代 Blaze 的默認(rèn)行為。由于現(xiàn)在這個函數(shù)還是空的,意味著什么都不會發(fā)生。
去試一下:打開“Best”最佳帖子頁面,給一些帖子投票:帖子排序不會發(fā)生變化,除非強(qiáng)制刷新(刷新頁面或改變路徑)。
我們已經(jīng)驗證 _uihooks
可以工作,現(xiàn)在讓我們來動畫它!
moveElement
hook 接受兩個參數(shù):node
和 next
。
node
是當(dāng)前正在移動到新位置的 DOM 元素next
是 node
移動的新位置之后的元素了解這些之后,我們可以逐一實現(xiàn)如下動畫過程(如果你需要刷新一下你的記憶,可參考之前“蘇聯(lián)賽跑者”的例子)。當(dāng)一個新的位置改變發(fā)生時,我們將:
next
前插入 node
(換句話說,如果我們沒有指定任何 moveElement
hook 的話,默認(rèn)行為就會發(fā)生)。node
回到它的起始位置。node
和 next
之間的每個元素,為 node
騰出空間。我們通過 jQuery 的魔力來做這些事情,這也是迄今為止最好的操作 DOM 的 JavaScript 庫。jQuery 已經(jīng)超出本書范圍,但是讓我們快速瀏覽一下我們即將用到的 jQuery 方法:
$()
:使任何一個 DOM 元素成為 jQuery 對象。offset()
:取得元素相對于文檔的當(dāng)前位置,返回包含 top
和 left
屬性的對象。outerHeight()
:取得“outer”元素的高度(包括 padding 和可選的 margin)。nextUntil(selector)
:取得所有目標(biāo)元素之后到(但不包含)匹配 selector
的元素。insertBefore(selector)
:在匹配 selector
的元素之前插入另一個元素。removeClass(class)
:如果該元素有 class
CSS 類,刪除它。css(propertyName, propertyValue)
:設(shè)置 CSS propertyName
屬性為 propertyValue
。height()
:取得該元素的高度。addClass(class)
:為元素添加 class
CSS 類。Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
moveElement: function (node, next) {
var $node = $(node), $next = $(next);
var oldTop = $node.offset().top;
var height = $node.outerHeight(true);
// 找出 next 與 node 之間所有的元素
var $inBetween = $next.nextUntil(node);
if ($inBetween.length === 0)
$inBetween = $node.nextUntil(next);
// 把 node 放在預(yù)訂位置
$node.insertBefore(next);
// 測量新 top 偏移坐標(biāo)
var newTop = $node.offset().top;
// 將 node *移回*至原始所在位置
$node
.removeClass('animate')
.css('top', oldTop - newTop);
// push every other element down (or up) to put them back
$inBetween
.removeClass('animate')
.css('top', oldTop < newTop ? height : -1 * height);
// 強(qiáng)制重繪
$node.offset();
// 動畫,重置所有元素的 top 坐標(biāo)為 0
$node.addClass('animate').css('top', 0);
$inBetween.addClass('animate').css('top', 0);
}
}
});
注解:
$node
的高度,便于知道要偏移 $inBetween
的元素多少距離。我們使用 outerHeight(true)
使 margin 和 padding 加入計算中。next
是在 node
之前還是之后,所以我們在定義 $inBetween
時同時考慮這兩種情況。animate
CSS 類(在 CSS 樣式表中定義了實際動畫)。top
屬性值為 0 來把元素歸位到應(yīng)在位置。你也許在想 $node.offset()
這行代碼。為什么我們不打算移動 $node
,而去關(guān)心它的位置呢?
要這么想:如果你告訴一臺有完美邏輯的機(jī)器人向北奔跑 5 千米,跑完后再跑回起點,它也許認(rèn)為既然又回到起點,那么何不節(jié)省能量而待在原地。
所以為了確保機(jī)器人能跑完 10 千米,我們會告訴它在跑到 5 千米時記錄它的坐標(biāo)才能轉(zhuǎn)向。
瀏覽器以相似的方式工作:如果我們在同一時間只給出 css('top', oldTop - newTop)
和 css('top', 0)
的話,新坐標(biāo)就會簡單地替換舊坐標(biāo),什么也不會發(fā)生。如果我們想真正地看到動畫,就需要強(qiáng)制瀏覽器去在元素改變位置后重新繪制它。
一個簡單的強(qiáng)制重繪的方法是讓瀏覽器檢查元素的 offset
屬性————再次重繪元素才能讓瀏覽器識別它。
讓我們再試一次。回到“Best”最佳帖子頁面,給帖子投票:現(xiàn)在應(yīng)該可以看到帖子如芭蕾舞般優(yōu)雅地上下滑動。
既然我們已經(jīng)搞定比較難的重新排序,那么插入和刪除帖子的動畫就是小菜一碟了!
首先,我們漸入新帖子(注意為了簡單,我們在此用 JavaScript 動畫):
Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
insertElement: function (node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
moveElement: function (node, next) {
//...
}
}
});
為了更好看到效果,我們通過控制臺插入新帖子,來測試動畫:
Meteor.call('postInsert', {url: 'http://apple.com', title: 'Testing Animations'})
其次,我們動畫淡出刪除的帖子:
Template.postsList.onRendered(function () {
this.find('.wrapper')._uihooks = {
insertElement: function (node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
moveElement: function (node, next) {
//...
},
removeElement: function(node) {
$(node).fadeOut(function() {
$(this).remove();
});
}
}
});
再次,在控制臺(用 Posts.remove('somePostId')
)刪除一個帖子來測試動畫效果。
到目前為止,我們已經(jīng)在頁面內(nèi)動畫了元素。但是如果我們想添加頁面之間的過渡動畫呢?
頁面過渡是 Iron Router 的任務(wù)。點擊一個鏈接,{{> yield}}
helper 的內(nèi)容自動地更換。
就像我們?yōu)樘恿斜砀淖?Blaze 默認(rèn)行為一樣,我們也可以為 {{> yield}}
做同樣的事情,在不同路由之間添加漸隱過渡動畫效果!
如果我們想漸入漸隱頁面,我們必須要確保它們在各自上方顯示。我們用添加了 position:absolute
屬性的 .page
container div 來包裹每個頁面模板。
但不能相對于窗口來絕對定位我們的頁面,因為這樣頁面會覆蓋應(yīng)用的 header。所以我們給 #main
div 添加 position:relative
以便 .page
div 的 position:absolute
會得到其正確位置。
為了節(jié)省時間,我們已經(jīng)在 sytle.css
中添加了必要的 CSS 代碼:
//...
#main{
position: relative;
}
.page{
position: absolute;
top: 0px;
width: 100%;
}
//...
是時候添加頁面過渡代碼了。代碼看起來很熟悉,因為這和我們添加和刪除帖子時的代碼完全一致:
Template.layout.onRendered(function() {
this.find('#main')._uihooks = {
insertElement: function(node, next) {
$(node)
.hide()
.insertBefore(next)
.fadeIn();
},
removeElement: function(node) {
$(node).fadeOut(function() {
$(this).remove();
});
}
}
});
我們剛剛看了一些為 Meteor 應(yīng)用添加動畫元素的模式。雖然這不是一個詳盡的清單,但是希望這會提供一個基礎(chǔ),在其上去構(gòu)建更復(fù)雜的過渡動畫。
更多建議: