【黑科技】React-canvas助力HTML5

TNFE發表於2019-05-17

圖片描述

1、什麼是流暢的使用者體驗?

遊戲的開發界有一個理論,就是當動畫或者互動響應達到60FPS(60幀每秒)的時候,就可以定義為流暢,按此理論,那麼每幀裡所有操作必須在16ms完成。要想提高頁面的使用者體驗,必須在效能上下功夫。最早做動畫都是用 setTimeout來實現的,而 setTimeout的處理回撥的時間精度都在 16ms 左右。所以,可以想象正常用頁面這兩個函式就已經 16 ms了,再加上reflow/repaint/compositing 卡頓或跳幀就是家常便飯了。不過還好的是w3c 標準和各瀏覽器廠商較早就支援了動畫介面 RAF(RequestAnimationFrame 函式)來處理動畫幀回撥。解決了上述 setTimeout不足的問題。但是,另一個問題仍然沒解決,當瀏覽器開啟網頁時,需要解析文件,在記憶體中生成DOM結構,如果遇到複雜的文件,這個過程是很慢的。如果遇上低端的手機瀏覽器,可以想象一下,如果網頁上有上萬個形狀(不管是圖片或CSS),生成DOM需要多久?更不要提與其中某一個形狀互動了。

2、React是什麼?

使用者與瀏覽器互動,從技術上看就是使用者在操作DOM,所有的DOM操作都是同步的,會堵塞瀏覽器。JavaScript操作DOM時,必須等前一個操作結束,才能執行後一個操作。只要一個操作有卡頓,整個網頁就會短暫失去響應。瀏覽器重繪網頁的頻率是60FPS,JavaScript做不到在16毫秒內完成DOM操作,因此產生了跳幀。使用者體驗上的不流暢、不連貫就源於此。JavaScript語言執行效率本身很快,但是DOM太慢了,DOM拖慢JavaScript。為了解決這個問題,React出現了,React是 Facebook 推出的一個用來構建使用者介面的 JavaScript 開源框架,React的最引人注目的特徵就是引入了虛擬DOM(Virtual DOM)這個概念,在瀏覽器端用JavaScript實現了一套DOM API。基於React進行開發時所有的DOM構造都是通過虛擬DOM進行,每當使用者介面需要變化時,React都會重新構建整個DOM樹,然後React將當前整個DOM樹和上一次的DOM樹進行對比,得到DOM結構的區別,然後僅僅將需要變化的部分進行實際的瀏覽器DOM更新。React實現了程式碼最小化參與DOM操作的方法,大大提升了瀏覽器的效能。

3、Canvas是什麼?

Canvas 是 HTML5 的畫布元素,也一個原生的DOM 元素。它相當於一個“白板”,我們可以通過javascript在這塊白板上增加文字與影像,“繪製”一些可視內容。目前大多數H5遊戲和動畫特效都是用canvas實現的。很多在微信裡傳播的小遊戲和小應用,也是用canvas實現的。用canvas的話整個頁面只用一個DOM 元素,並且瀏覽器只需要繪製一次形成一幅圖。這大大降低了DOM 數量與渲染的複雜度。更好的是,canvas預設支援GPU硬體加速的,可以將原來 CPU 密集型操作變成 GPU 操作。提高了動畫的流暢度。值得一提的是,微信瀏覽器的核心,也即是QQ瀏覽器 X5 核心已經內建了很多遊戲引擎(比如白鷺遊戲引擎與cocos2dx),供開發者開發canvas遊戲,所以長時間來看,微信瀏覽器的畫布效能將會越來越強大。

4、新的方法

大多數現代移動裝置都擁有硬體加速的 canvas,我們為什麼不利用起來呢?HTML5 遊戲已經做到了。我們為什麼不採用遊戲的思路設計介面,在 canvas 上開發應用介面麼,用canvas來渲染頁面呢?相信你已經想到了,但是有人已經做到了,那就是Flipboard公司的React-canvas。React-canvas是什麼呢?光看名字就知道這是跟react和canvas相關的。React-canvas,可以使我們用react技術渲染canvas。

5、React-canvas入門

React Canvas 是依賴於React的一個元件,它擁有了渲染到canvas的能力,它可以讓我們脫離繁瑣的canvas命令式繪圖,使用簡單的css佈局(Layout)。接下來給大家演示一個簡單的圖文實現。

5.1.安裝node

新時代的前端開發離不開node環境,所以,react-canvas也不例外,node安裝的具體步驟不再贅述。記住,Node版本不低於4.0。

圖片描述

5.2建立專案空間

在D:nodejsreactdemo建立資料夾,此為開發的根目錄
開啟到此目錄,切換到命令列,執行 npm init,預設回車,初始化package.json

5.3安裝框架和外掛

需要在node環境上安裝一系列框架
切換到命令列,
執行

npm install react

安裝基礎框架react(注:安裝v0.13.0,新版本我沒試驗,不知是否可行),
執行

npm install jsx-loader

安裝編譯react用的jsx-loader外掛,
執行

npm install react-canvas

安裝核心框架react-canvas
執行

npm install webpack

安裝打包程式碼用的工具webpack。
然後建立文字檔案index.html
建立配置檔案webpack.config.js
建立js資料夾存放程式碼檔案。
至此,你的工作環境下應該有

圖片描述

其中node_modules資料夾下,應該至少有自動生成的react、react-canvas、webpack三個資料夾。

5.4讓程式碼跑起來

開啟index.html檔案,程式碼如下:

<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="Content-type" content="text/html; charset=utf-8">
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <title>react canvas第一步</title>
</head>
<body>
  <div id="main"></div>
  <script src="bundle.js"></script>
</body>
</html>

主頁面只有一個id=maindiv標籤,並外鏈一個js檔案。
開啟js資料夾,建立index.js,程式碼如下

var React = require('react');
var ReactCanvas = require('react-canvas');

var Surface = ReactCanvas.Surface;
var Image = ReactCanvas.Image;
var Text = ReactCanvas.Text;

var MyComponent = React.createClass({

// 介面渲染
  render: function () {
    var surfaceWidth = window.innerWidth;
    var surfaceHeight = window.innerHeight;
    var imageStyle = this.getImageStyle();
    var textStyle = this.getTextStyle();

    return (
      <Surface width={surfaceWidth} height={surfaceHeight} left={0} top={0}>
        <Image style={imageStyle} src='http://img1.gtimg.com/joke/pics/hv1/193/44/1996/129801313.png' />
        <Text style={textStyle}>
          哈哈,你來打我呀
        </Text>
      </Surface>
    );
  },
// 計算居中
  getImageHeight: function () {
    return Math.round(window.innerHeight / 2);
  },
   getImageWidth: function () {
    return Math.round(window.innerWidth / 2);
  },

//  圖片樣式
  getImageStyle: function () {
    return {
      top: this.getImageHeight() -32,
      left: this.getImageWidth() -32,
      width: 64,
      height: 64
    };
  },

// 文字樣式
  getTextStyle: function () {
    return {
      top: this.getImageHeight() + 64,
      left: 0,
      width: window.innerWidth,
      height: 20,
      lineHeight: 20,
      fontSize: 12,
      textAlign : 'center'
    };
  }

});
React.render(<MyComponent />, document.getElementById('main'));

配置webpack.config.js

module.exports = {
    //入口檔案
  entry: './js/main.jsx',
    //輸出檔案
  output: {
    path: __dirname,
    filename: 'bundle.js'
  },
module: {
    loaders: [
// 凡是遇到jsx、js結尾的,都用jsx-loader這個外掛來載入,
 // 且啟用harmony模式
      { test: /\.jsx$/, loader: 'jsx-loader?harmony'},
      { test: /\.js$/, loader: 'jsx-loader?harmony'},
    ]
  },
 // 表示這個依賴項是外部lib,遇到require它不需要編譯,
 // 且在瀏覽器端對應window.React
  //externals: {
    //'react': 'window.React'
  //},
// 現在可以寫 require('file') 代替 require('file.jsx')
  resolve: {
    root: __dirname,
    extensions: ['', '.js', '.jsx']
  }
};

配置完畢後,切換到命令列執行
webpack -p
命令,打包,編譯
打包後在根目錄下生成一個bundle.js,直接用瀏覽器開啟,就可以看到效果了。

圖片描述

線上demo地址:點我

6、語法及元素(標籤、元件)

react-canvas的語法和react一樣,如果你熟悉react,react-canvas很容易上手。react-canvas自定義了幾個標籤。這些標籤也都是標準的React components

<Surface>

Surface是一個頂級標籤,是一個容器,你可以把任何元素放在上面,可以把它看成canvas元素,整個專案要被套在一個surface裡面。在上述示例中已經使用。

<Layer>

Layer 層級僅次於surface,可以放其他元素。 基本樣式和屬性例如top, width, left, height, backgroundColor and zIndex可以在這一層設定。

<Group>

Group是一個容器,因為react渲染元件時必須要有一個總的標籤包含所有的雜湊標籤,在react-canvas中,Group扮演了div的角色,可以用來做零散標籤的父標籤。把一系列相關聯標籤用Group封裝起來,一方面可以提高程式碼的內聚,更加模組化,另一方面可以提高頁面滾動時的效能。

<Text>

Text 是用來存放文字的,是一個彈性的標籤,canvas不支援自動截斷換行,而Text標籤支援。

<Image>

Image 跟你想象的一樣,用來放圖片的。 但是它支援載入完畢後才顯示,並且可以隨意的隱藏。

<ListView>

ListView是一個列表,可以認為它相當於HTML頁面裡的ul或者native app中的UITableView ,它可以提高頁面的滾動效能。
同時react-canvas給各個標籤以事件支援,有touchstart,move,end,click等事件。

7、圖文滾動列表

接下來介紹如何在 React Canvas 中建立一個達到60 fps ,分頁的滾動列表。事實證明這實現起來非常容易,
修改main.js程式碼為

/** @jsx React.DOM */

'use strict';

var React = require('react');
var ReactCanvas = require('react-canvas');
// page檔案負責渲染單個頁面
var Page = require('./components/Page');
// data檔案存放json格式的圖文
var articles = require('./common/data');

var Surface = ReactCanvas.Surface;
var ListView = ReactCanvas.ListView;

var App = React.createClass({
// 渲染整個列表
  render: function () {
    var size = this.getSize();
    return (
      <Surface top={0} left={0} width={size.width} height={size.height}>
        <ListView
          style={this.getListViewStyle()}
          snapping={true}
          scrollingDeceleration={0.92}
          scrollingPenetrationAcceleration={0.13}
          numberOfItemsGetter={this.getNumberOfPages}
          itemHeightGetter={this.getPageHeight}
          itemGetter={this.renderPage} />
      </Surface>
    );
  },
// 渲染單頁
  renderPage: function (pageIndex, scrollTop) {
    var size = this.getSize();
    var article = articles[pageIndex % articles.length];
    var pageScrollTop = pageIndex * this.getPageHeight() - scrollTop;
    return (
      <Page
        width={size.width}
        height={size.height}
        article={article}
        pageIndex={pageIndex}
        scrollTop={pageScrollTop} />
    );
  },
// 瀏覽器大小
  getSize: function () {
    return document.getElementById('main').getBoundingClientRect();
  },
// 整個列表的外觀
  getListViewStyle: function () {
    var size = this.getSize();
    return {
      top: 0,
      left: 0,
      width: size.width,
      height: size.height,
    };
  },
 // 設定頁面的可滾動的次數,若超過頁面的數量,迴圈滾動
  getNumberOfPages: function () {
    return 9;
  },
// 計算單頁的高度
  getPageHeight: function () {
    return this.getSize().height;
  }

});

React.render(<App />, document.getElementById('main'));

整個頁面程式碼由一個surface,一個listview,9個page組成,可以上下屏滾動。react-canvas將網頁變成了一個canvas,使用者就等於在跟圖片互動,這樣就繞開了DOM,降低了操作時滯。而且,canvas可以被硬體加速,這樣就提高了效能,體驗非常流暢。

你可以檢視這個線上的Demo點我,pc使用者記得用chrome模擬手機瀏覽器。文章末尾附件裡有完整實現的原始碼。

8、React-canvas的優缺點

8.1優點

React-canvas使用一個 canvas 元素來繪製介面,完成滾動。在每一個觸控事件時,根據當前的滾動程度去更新渲染樹。之後,整個渲染樹使用新的座標來重新渲染。在 canvas 上有個重要的技術叫離屏canvas(off-screen),可以現在記憶體中完成繪製,之後可以一次性複製到使用者介面,並且使用離屏層重新繪製也是非常快的。React在介面更新之前會做虛擬 DOM 的 diff 。在render() 函式中只更新有變動的介面,React進一步提升了React-canvas的效能。流暢是React-canvas的主要優點。
程式碼基於react,因為react現在比較火,很多前端已經熟悉react的書寫,react也有很多相關的元件,所以react-canvas比較應景,讓一部分人很快的可以上手。另外相容性也比較好,繼承react和canvas的相容性和跨平臺優點。

8.2缺點

當前不成熟和不穩定,react一直在變動,所以React-canvas也會隨之一併升級。react-canvas還不完善,很多DOM中的標籤特性在目前react-canvas裡面未能實現,比如無法對文字進行復制,這讓使用React-canvas的時候會有一些限制。這個專案已經在Github上開源,作者也在對專案進行重構,期望下次更新的時候是一個功能強大的版本。另外,react-canvas的學習成本也比較高,使用react-canvas需要對react和canvas有一定的瞭解。

9、總結

React Canvas並不能完全取代DOM。個人覺得只適用在移動web(或者webview)上,手機的硬體資源相對有限,使用者互動又相對頻繁,我們可以在我們的頁面中效能要求最關鍵的地方去使用它,尤其是在微信瀏覽器中很常見滾動檢視這部分。當渲染效能不是問題的時候, DOM 可能是一個更好的方法。事實上,對於某些元素比如輸入欄位,和音訊/視訊標籤等,DOM是唯一的方法。從某種意義上說,react-canvas也算是一個混合( hybird )的應用程式。相比傳統的原生應用,react-canvas內容全部是 web 。我們在開發中可以把介面基於 dom 實現,並在適當的地方使用 canvas 渲染。DOM和canvas各取所長,優勢互補。React-canvas能將頁面的互動和效能水平提升到可以與本地應用相競爭,這就是它引人注意的地方。


10、安利時間

我們在web開發過程中,都見過或者使用過一些奇技淫巧,這種技術我們統稱為黑魔法,這些黑魔法散落在各個角落,為了方便大家查閱和學習,我們做了收集、整理和歸類,並在github上做了一個專案——awesome-blackmargic,希望各位愛鑽研的開發者能夠喜歡,也希望大家可以把自己的獨門絕技分享出來,如果有興趣可以給我們發pr。

如果你對React感興趣,想進一步瞭解React,加入我們的QQ群(784383520)吧!

相關文章