在智能手機和平板電腦的黎明時期, Flipboard 推出「移動先行」的體驗,使咱們能夠從新思考頁面中內容佈局的原則,以及與觸摸屏相關的,如何得到更好的用戶體驗的因素。css
爲了創建完整的體驗,咱們將 Flipboard 帶到 web 端。咱們在 Flipboard 所作的,在每臺用戶使用的設備上都擁有獨立的價值:整理那些來自你最關心的主題,來源以及人的最好的故事。所以把咱們的服務帶到web端,也是一個合乎邏輯的延伸。html
當咱們開始這個項目後,認識到咱們須要把源自移動體驗的思考搬到 web 端,試圖提高 web 端的內容佈局和交互。咱們想達到原生應用般的精緻和性能,且仍能感知到真實的瀏覽器。html5
早些時間,通過測試大量的產品原型後,咱們決定使用滾動的方式做爲 web 端體驗。咱們的移動應用被你們所熟知的是相似翻書般的體驗,在觸摸屏上這很直觀。但一系列的緣由代表,滾動在 web 端的體驗更加天然。react
爲了優化滾動的性能, 咱們知道咱們須要保證頁面渲染的頻率低於16ms,同時限制迴流(reflow)和重繪(repaints)。這在動畫中尤爲重要。爲了不動畫中從新渲 染,有兩個屬性你能夠安全地做用於動畫上: CSS transform 和 opacity。但這樣選擇餘地過小了。ios
當你想實現元素上寬度動畫效果怎麼辦?css3
一幀幀的滾動動畫如何處理?git
注意,在上圖中,頂部的圖標從白色到黑色。這裏使用了兩個單獨的元素相互覆蓋,根據下面的內容來互相裁剪)。 這些類型的動畫一直在網上遭受詬病,特別是在移動設備上,只由於一個簡單的緣由:DOM 太慢了。github
<canvas>
大多數現代移動設備都擁有硬件加速的 canvas,咱們爲何不利用起來呢?HTML5 遊戲已經作到了。咱們能真正在 canvas 上開發應用界面麼?web
Canvas 是一種當即模式的繪圖 API,這意味着繪製時不保留所繪製對象的信息。與其相反的是保留模式,這是一種聲明性的 API,維護所繪製對象的層次結構。算法
保留模式api的優勢是,對於你的應用程序,他們一般更容易構建複雜的場景,例如 DOM。一般這都會帶來性能成本,須要額外的內存來保存場景和更新場景,這可能會很慢。
Canvas 受益於當即模式,容許直接發送繪圖命令到 GPU。但若用它來構建用戶界面,須要進行一個更高層次的抽象。例如一些簡單的處理,好比當繪製一個異步加載的資源到一個元素上時會出現問題,如在圖片上 繪製文本。在HTML中,因爲元素存在順序,以及 CSS 中存在 z-index
,所以是很容易實現的。
<canvas>
元素中創建UI相比 HTML+CSS,canvas 則有些先天不足,缺乏很是多在 HTML + CSS 中理所固然的特性。
canvas有一個很簡單的 API 用於繪製文字:fillText(text, x, y [, maxWidth])
這個函數接受三個參數:文字自己以及繪製起點的x
,y
座標。但 canvas 只能一次繪製一行文字。若是你須要讓文字換行,須要本身寫函數。
你可使用drawImage()
函數在 canvas 上繪製圖片。這是個可變參數函數,你能夠指定更多參數,從而控制定位和裁切。可是canvas不在意圖像是否加載,或不能肯定只在圖像加載事件後調用函數。
經過 DOM 元素的順序或使用 CSS 的 z-index屬性,在 HTML 和 CSS 指定一個元素是否在另外一個上應該很容易。但請記住,canvas 是當即模式的繪圖 API。當元素重疊或者其中一個須要重繪時,都必須以同一順序從新繪製(或至少局部重繪)。
譯者注 關於局部重繪提升性能的文章你們能夠參考:《提升HTML5 canvas性能的幾種方法(轉)》
須要使用一個自定義 web 字體嗎? canvas 的文本 API 並不在意字體是否加載。你須要一種方法來知道一個字體是否加載,並繪製任何依賴此字體的區域。幸運的是,現代瀏覽器有一個基於promise的API。不幸的是, iOS WebKit (iOS 8 時)不支持它。
<canvas>
的優勢鑑於全部這些缺點,人們開始質疑 canvas 來代替 DOM 這一選擇。最終,咱們的討論由一個很簡單的真理來決定:你不能基於 dom 創建一個60 fps的滾動列表視圖。
許多人(包括咱們)已經嘗試過,但都失敗了。可滾動的元素能夠在純 HTML 和 CSS 中經過 overflow:scroll
實現:(結合 IOS 上的 -webkit-overflow-scrolling:touch
),但這些不能在滾動動畫中給予你逐幀控制,同時移動瀏覽器很難處理又長又複雜的內容。
爲了構建一個內含至關複雜的內容的無限滾動列表,咱們須要在 web 端實現一個UITableView
與 DOM 相比,今天的大多數設備都有基於硬件加速的 canvas 實現,能夠直接發送繪圖命令到 GPU。這意味着咱們能夠很是快的渲染元素;在許多狀況下,咱們所說的是毫秒級的範圍。
相比 HTML + CSS , canvas 也是一個很是「苗條」的 API ,這減小了界面上的 bug 或瀏覽器之間的不一致性。有一個理由更加直接,canvas 沒有 Can I Use?。
譯者注 UITableView 是 IOS 控件
如前所述,爲了有點效果,咱們須要一個更高層次的抽象,而不是簡單地繪製矩形、文本和圖像。咱們構建了一個很是小的抽象,容許開發人員處理一個節點樹,而不是處理一個嚴格的繪圖命令序列。
渲染層( RenderLayer )是基本節點,其餘節點創建在其上。常見的屬性如 top
,left
,width
,height
,backgroundColor
和 zIndex
在這個層展示。 RenderLayer 只不過是一個普通的 JavaScript 對象,包含這些屬性和一個子元素數組。
咱們使用圖像層的附加屬性來指定圖像 URL 和信息。你沒必要擔憂圖像加載事件的監聽,圖像層會處理後將一個信號發送到繪圖引擎來表示圖片須要更新。
文本層能夠顯示多行文本截斷,這在 DOM 裏處理成本很是高。文本層還支持自定義字體,也會處理當字體加載完畢後更新的動做。
這些層能夠合成起來以便構建複雜的界面。下面是一個渲染層樹
{
frame: [0, 0, 320, 480],
backgroundColor: '#fff',
children: [
{
type: 'image',
frame: [0, 0, 320, 200],
imageUrl: 'http://lorempixel.com/360/420/cats/1/'
},
{
type: 'text',
frame: [10, 210, 300, 260],
text: 'Lorem ipsum...',
fontSize: 18,
lineHeight: 24
}
]
}
當一個層須要重繪,例如一個圖像加載後,它發送一個信號到繪圖引擎表示其框架是髒( dirty )的。這些修改使用 requestAnimationFrame來批量處理,避免佈局抖動,以後在下一幀畫布重繪。
對於 web 端,也許咱們最理所固然關注的,是瀏覽器如何來滾動網頁。瀏覽器廠商已經不遺餘力提升滾動性能。
這實際上是一個妥協。爲了達到60 fps 的滾動指標,移動瀏覽器在執行滾動期間,中止 JavaScript 的執行,這是怕 DOM 修改致使迴流。最近, IOS 和 Android 暴露了 onScroll 事件,他們的工做過程更像桌面瀏覽器了,但若是你試圖在滾動時保持 DOM 元素的位置同步,具體的實現可能會有差異。
幸運的是,瀏覽器廠商已經意識到這個問題。特別是 Chrome 團隊已經開放了爲了改善手機端這種狀況所作的工做。
回到 canvas ,簡短的回答是你必須用 JavaScript 實現滾動。
你首先須要的是一種計算滾動程度的算法。若是你不想研究數學,Zynga 開源的純滾動實現,適合任何相似此佈局的狀況。
咱們使用一個 canvas 元素來完成滾動。在每個觸摸事件時,根據當前的滾動程度去更新渲染樹。以後,整個渲染樹使用新的座標來從新渲染。
這聽起來使人難以置信的慢,在 canvas 上可使用一個重要的優化技術--畫布上繪圖操做的結果能夠在離屏層(off-screen)canvas 被緩存。離屏層(off-screen)以後能夠用來從新繪製層。
這種技術不只能夠用於圖像層,文本和圖形也適用。兩個成本最高的繪圖操做是填充文本和圖像。可是一旦這些層繪製一次之後,接下來使用離屏層從新繪製他們是很是快的。
在上面的演示中,每一個頁面的內容分爲兩層:圖像層和文本層。文本層包含多個元素組合在一塊兒。每一幀滾動動畫中,這兩層都使用位圖緩存來重繪。
在一個無限列表的滾動過程當中,大量的 RenderLayers 會被創建和銷燬。這會在內存中建立大量的垃圾,當進行垃圾收集時將中止主線程。
爲了不產生大量垃圾, RenderLayers 與相關對象聚集到一個池中。這意味着只有相對較少的層對象被建立。當再也不須要時,它會被釋放回池中,以後能夠重用。
緩存複合層的特性能夠帶來另外一個優點:可以將渲染的部分結構做爲一個位圖。你有沒有創建部分 DOM 結構快照的需求?當你將這些結構渲染在 canvas 時,速度會快得使人難以置信。這個將一個項目放入一本雜誌的界面,利用了這種特性來執行一個時間軸維度的平穩過渡。快照包含去掉頂部和底部的整個項目。
如今咱們已經擁有了構建一個應用程序的磚塊。然而,經過命令來構建 RenderLayers 多是乏味的。若是咱們有個相似於DOM工做方式的聲明式 API 不是很好麼?
咱們是 React 框架的忠實粉絲。它的單必定向的數據流和聲明式API已經改變了人們構建應用程序的方式。react最引人注目的特徵就是虛擬 DOM (virtual DOM)。呈現爲HTML容器只是它在瀏覽器中的一個簡單實現。最近引入的 React Native 證實了這一點。
若是咱們將咱們的 canvas 佈局引擎與 react 組件結合起來會咋樣?
React Canvas 使React組件擁有了渲染到canvas的能力。
第一個版本的 canvas 佈局引擎看上去很像命令式的代碼。若是你作過 JavaScript DOM 操做你可能會運行過這樣的代碼:
// Create the parent layer
var root = RenderLayer.getPooled();
root.frame = [0, 0, 320, 480];
// Add an image
var image = RenderLayer.getPooled('image');
image.frame = [0, 0, 320, 200];
image.imageUrl = 'http://lorempixel.com/360/420/cats/1/';
root.addChild(image);
// Add some text
var label = RenderLayer.getPooled('text');
label.frame = [10, 210, 300, 260];
label.text = 'Lorem ipsum...';
label.fontSize = 18;
label.lineHeight = 24;
root.addChild(label);
固然,這樣能完成效果,可是誰想這樣寫代碼?除了容易出錯,也很難想象出渲染結果
使用React Canvas則變成下面這樣:
var MyComponent = React.createClass({
render: function () {
return (
<Group style={styles.group}>
<Image style={styles.image} src='http://...' />
<Text style={styles.text}>
Lorem ipsum...
</Text>
</Group>
);
}
});
var styles = {
group: {
left: 0,
top: 0,
width: 320,
height: 480
},
image: {
left: 0,
top: 0,
width: 320,
height: 200
},
text: {
left: 10,
top: 210,
width: 300,
height: 260,
fontSize: 18,
lineHeight: 24
}
};
您可能會注意到,一切都彷佛是絕對定位實現的。確實是這樣。咱們的canvas渲染引擎的誕生,就伴隨着驅動像素級佈局,實現多行文本過長省略的使命。傳統的CSS不能作到這一點,因此絕對定位的方式對咱們來講很適合。然而,這種方法並不適合於全部應用程序。
Facebook 最近開源的CSS的JavaScript實現。他支持 CSS 的一些子集,包括 margin
、padding
,position
和最重要的 flexbox
。
將 css 佈局整合到 React Canvas 只是一個時間問題。看看這個例子,看看咱們是如何改變組件樣式的。
你如何在 React Canvas 中建立一個達到60 fps ,無限,分頁的滾動列表?事實證實這實現起來很是容易,由於 react 會作虛擬 DOM 的 diff 。在render()
函數中只有當前可見的元素被返回,React負責更新滾動期間所需的虛擬 DOM 樹。
var ListView = React.createClass({
getInitialState: function () {
return {
scrollTop: 0
};
},
render: function () {
var items = this.getVisibleItemIndexes().map(this.renderItem);
return (
<Group
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
onTouchCancel={this.handleTouchEnd}>
{items}
</Group>
);
},
renderItem: function (itemIndex) {
// Wrap each item in a <Group> which is translated up/down based on
// the current scroll offset.
var translateY = (itemIndex * itemHeight) - this.state.scrollTop;
var style = { translateY: translateY };
return (
<Group style={style} key={itemIndex}>
<Item />
</Group>
);
},
getVisibleItemIndexes: function () {
// Compute the visible item indexes based on `this.state.scrollTop`.
}
});
爲了勾住(捕獲)滾動,咱們在列表視圖組件中,調用Scroller(滾動組件)的 setState()
方法。
...
// Create the Scroller instance on mount.
componentDidMount: function () {
this.scroller = new Scroller(this.handleScroll);
},
// This is called by the Scroller at each scroll event.
handleScroll: function (left, top) {
this.setState({ scrollTop: top });
},
handleTouchStart: function (e) {
this.scroller.doTouchStart(e.touches, e.timeStamp);
},
handleTouchMove: function (e) {
e.preventDefault();
this.scroller.doTouchMove(e.touches, e.timeStamp, e.scale);
},
handleTouchEnd: function (e) {
this.scroller.doTouchEnd(e.timeStamp);
}
...
儘管這是一個簡化版本,但展現了 React 一些最優秀的特性。觸摸事件被聲明式綁定在 render()函數中。每一個 touchmove 事件被轉發到 Scroller 中來計算當前滾動的偏移。每一個 Scroller 發出的滾動事件則用於更新狀態列表視圖組件,只對當前屏幕可見的元素進行渲染。全部這一切發生在16ms如下,由於 react 的 diff 算法很是快。
你能夠查看這個滾動列表完整實現的源代碼。
React Canvas 並不能徹底取代 DOM。咱們在咱們的移動 web app 中,性能要求最關鍵的地方去使用,主要是滾動時間軸視圖這部分。
當渲染性能不是問題的時候, Dom 多是一個更好的方法。事實上,對某些元素輸入字段和音頻/視頻等,這是惟一的方法
從某種意義上說, Flipboard 的移動 web 是一個混合( hybird )的應用程序。相比傳統的原生應用和網絡技術結合的方式, Flipboard 的內容所有是 web 內容。它的 UI 基於 dom 實現,並在適當的地方使用 canvas 渲染。
這個領域須要進一步探索。使用降級內容( canvas 的 DOM 子樹)應該容許 VoiceOver 這樣的屏幕閱讀器與內容交互。咱們在測試的設備上看到了不一樣的結果。另外,關於焦點的管理也有標準,但目前暫時不被瀏覽器支持。
Bespin在2009年提出的一種方法,是元素渲染到 canvas 時,同時維護一個平行Dom,用於元素同步。咱們正在繼續研究實現可訪問性的正確方法。
在追求60 fps 的過程當中,咱們有時會採起極端措施。 Flipboard 爲研究移動網絡的侷限性提供了一個案例。雖然這種方法可能並不適用於全部應用程序,咱們將應用的交互和性能水平提高到能夠與本地應用相競爭。咱們但願經過開放咱們在 React Canvas 中所作的工做,可讓其餘引人注目的例子出現。
用手機訪問flipboard.com,體驗一下。或者若是你沒有 Flipboard 帳戶,體驗一下 Flipboard 上一系列的雜誌。請讓咱們得到你的想法。
特別感謝 Charles, Eugene 和 Anh 的編輯和建議。
http://jcodecraeer.com/a/qianduankaifa/css3/2015/0305/2549.html