1. 程式人生 > >React Native探索(六)不止是UI:React的使用場景探索

React Native探索(六)不止是UI:React的使用場景探索

藉助 atom-shell 或者 node-webkit 這類專案,我們可以在桌面上執行一個 Web應用。來自 Github 的 Atom Editor 就是使用 atom-shell 以及 React建立的。

下面將 atom-shell 應用於我們的SurveyBuilder

首先,從這裡下載並且安裝 atom-shell。使用下面的 desktop 指令碼執行 atom-shell,就可以在視窗中開啟該應用。

// desktop.js
var app = require('app');
var BrowserWindow = require('browser-window');
// 載入 SurveyBuilder 服務,然後啟動它。
var server = require('./server/server');
server.listen('8080');
// 向我們的服務提供崩潰報告。
require('crash-reporter').start();
// 保留 window 物件的一個全域性引用。
// 當 javascript 物件被當作垃圾回收時,視窗將會自動關閉。
var mainWindow = null;
// 當所有視窗都關閉時退出。
app.on('window-all-closed', function() {
  if (process.platform != 'darwin')
    app.quit();
});
// 當 atom-shell 完成所有初始化工作並準備建立瀏覽器視窗時,會呼叫下面的方法。
app.on('ready', function() {
  // 建立瀏覽器視窗。
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600
  });
  // 載入應用的 index.html 檔案。
  // mainWindow.loadUrl('file://' + __dirname + '/index.html');
  mainWindow.loadUrl('
http://localhost:8080/
'); // 在視窗關閉時觸發。 mainWindow.on('closed', function() { // 直接引用 window 物件,如果你的應用支援多個視窗,通常需要把 window 儲存到 // 一個數組中。此時,你需要刪除相關聯的元素。 mainWindow = null; }); });

藉助 atom-shell 或者 node-webkit 這類專案,我們可以將建立 web的技術應用於建立桌面應用。就像開發 web 應用一樣,React同樣可以幫助你構建強大的互動式桌面應用。

遊戲

通常,遊戲對使用者互動有很高的要求,玩家需要及時地對遊戲狀態的改變做出響應。相比之下,在絕大多數web應用中,使用者不是在消費資源就是在產生資源。本質上,遊戲就是一個狀態機,包括兩個基本要素:

  1. 更新檢視
  2. 響應事件

在本書概覽部分,你應該已經注意到:React關注的範疇比較窄,僅僅包括兩件事:

  1. 更新 DOM
  2. 響應事件

React 和遊戲之間的相似點遠不止這些。React 的虛擬 DOM 架構成就了高效能的3D 遊戲引擎,對於每一個想要達到的檢視狀態,渲染引擎都保證了對檢視或者DOM 的一次有效更新。

2048這個遊戲的實現就是將 React 應用於遊戲中的一個示例。這個遊戲的目的是把桌面上相匹配的數字結合在一起,直到2048。

下面,深入地看一下實現過程。原始碼被分為兩部分。第一部分是用於實現遊戲邏輯的全域性函式,第二部分是React 元件。你馬上會看到遊戲桌面的初始資料結構。

var initial_board = {
  a1:null, a2:null, a3:null, a4:null,
  b1:null, b2:null, b3:null, b4:null,
  c1:null, c2:null, c3:null, c4:null,
  d1:null, d2:null, d3:null, d4:null
};

桌面的資料結構是一個物件,它的 key 與 CSS中定義的虛擬網格位置直接相關。繼初始化資料結構後,你將會看到一系列的函式對該給定資料結構進行操作。這些函式都按照固定的方式執行,返回一個新 的桌面並且不會改變輸入值。這使得遊戲邏輯更清晰,因為可以將在數字方塊移動前後的桌面資料結構進行比較,並且在不改變遊戲狀態的情況下推測出下一步。

關於資料結構,另一個有趣的屬性是數字方塊之間在結構上共享。所有的桌面共享了對桌面上未改變過的數字方塊的引用。這使得建立一個新桌面非常快,並且可以通過判斷引用是否相同來比較桌面。

這個遊戲由兩個 React 元件構成,GameBoard 和Tiles。

Tiles是一個簡單的 React 元件。每當給它的 props 指定一個board,它總會渲染出完整的 Tiles。這給了我們利用 CSS3 transition實現動畫的機會。

var Tiles = React.createClass({
  render: function(){
    var board = this.props.board;
    // 首先,將桌面的 key 排序,停止 DOM 元素的重組。
    var tiles = used_spaces(board).sort(function(a, b) {
      return board[a].id - board[b].id;
    });
    return (
      <div className="board">
        {tiles.map(function(key){
          var tile = board[key];
          var val = tile_value(tile);
          return (
            <span key={tile.id} className={key + " value" + val}>
              {val}
            </span>
          );
        })}
      </div>
    );
  }
});
<!-- 渲染數字方塊後的輸出示例 -->
<div class="board" data-reactid=".0.1">
  <span class="d2 value64" data-reactid=".0.1.$2">64</span>
  <span class="d1 value8" data-reactid=".0.1.$27">8</span>
  <span class="c1 value8" data-reactid=".0.1.$28">8</span>
  <span class="d3 value8" data-reactid=".0.1.$32">8</span>
</div>
/* 將 CSS transistion 應用於數字方塊上的動畫 */
.board span{
  /* ... */
  transition: all 100ms linear;
}

GameBoard是一個狀態機,用於響應按下方向鍵這一使用者事件,並與遊戲的邏輯功能進行互動,然後用一個新的桌面來更新狀態。

var GameBoard = React.createClass({
  getInitialState: function() {
    return this.addTile(this.addTile(initial_board));
  },
  keyHandler: function(e) {
    var directions = {
      37 : left,
      38 : up,
      39 : right,
      40 : down
    };
    if (directions[e.keyCode]
    && this.setBoard(fold_board(this.state, directions[e.keyCode]))
    && Math.floor(Math.random() * 30, 0) > 0) {
      setTimeout(function() {
        this.setBoard(this.addTile(this.state));
      }.bind(this), 100);
    }
  },
  setBoard: function(new_board) {
    if (!same_board(this.state, new_board)) {
      this.setState(new_board);
      return true;
    }
    return false;
  },
  addTile: function(board) {
    var location = available_spaces(board).sort(function() {
      return.5 - Math.random();
    }).pop();
    if (location) {
      var two_or_four = Math.floor(Math.random() * 2, 0) ? 2 : 4;
      return set_tile(board, location, new_tile(two_or_four));
    }
    return board;
  },
  newGame: function() {
    this.setState(this.getInitialState());
  },
  componentDidMount: function() {
    window.addEventListener("keydown", this.keyHandler, false);
  },
  render: function() {
    var status = !can_move(this.state) ? " - Game Over!": "";
    return (
      <div className = "app" >
        <span className = "score" >
          Score: {score_board(this.state)} {status}
        </span>
        <Tiles board={this.state}/ >
        <button onClick={this.newGame}> New Game </button>
      </div >
    );
  }
});

在 GameBoard元件中,我們初始化了用於和桌面互動的鍵盤監聽器。每一次按下方向鍵,我們都會去呼叫setBoard,該方法的引數是遊戲邏輯中新創 建的桌面。如果新桌面和原來的不同,我們會更新GameBoard 元件的狀態。這避免了不必要的函式執行,同時提升了效能。

在 render 方法中,我們渲染了當前桌面上的所有 Tile元件。通過計算遊戲邏輯中的桌面並渲染出得分。

每當我們按下方向鍵時,addTile方法會保證在桌面上新增新的數字方塊。直到桌面已經滿了,沒有新的數字可以結合時,遊戲結束。

基於以上的實現,為這個遊戲新增一個撤銷功能就很容易了。我們可以把所有桌面的變化歷史儲存在GameBoard 元件的狀態中,並且在當前桌面上新增一個撤銷按鈕(程式碼)。

這個遊戲實現起來非常簡單。藉助React,開發者僅聚焦在遊戲邏輯和使用者互動上即可,不必去關心如何保證檢視上的同步。

電子郵件

儘管 React 在建立 web 互動式 UI 上做了優化,但它的核心還是渲染HTML。這意味著,我們在編寫 React應用時的諸多優勢,同樣可以用來編寫令人頭疼的 HTML 電子郵件。

建立 HTML 電子郵件需要將許多的 table在每個客戶端上進行精準地渲染。想要編寫電子郵件,你可能要回溯到幾年以前,就像是回到1999 年編寫 HTML 一樣。

在多終端下成功地渲染郵件並不是一件簡單的事。在我們使用 React來完成設計的過程中,可能會碰到若干挑戰,不過這些挑戰與是否使用React 無關。

用 React 為電子郵件渲染 HTML 的核心是React.renderToStaticMarkup。這個函式返回了一個包含了完整元件樹的HTML 字串,指定了最外層的元件。React.renderToStaticMarkup 和React.renderToString 之間唯一的區別就是前者不會建立額外的 DOM屬性,比如 React 用於在客戶端索引 DOM 的 data-react-id屬性。因為電子郵件客戶端並不在瀏覽器中執行——我們也就不需要那些屬性了。

使用 React 建立一個電子郵件,下圖中的設計應該分別應用於 PC 端和移動端:

為了渲染出電子郵件,我寫了一小段指令碼,輸出用於傳送電子郵件的 HTML 結構:

// render_email.js
var React = require('react');
var SurveyEmail = require('survey_email');
var survey = {};
console.log(
  React.renderToStaticMarkup(<SurveyEmail survey={survey}/>)
);

我們看一下 SurveyEmail 的核心結構。首先,建立一個 Email 元件:

var Email = React.createClass({
  render: function () {
    return (
      <html>
        <body>
          {this.prop.children}
        </body>
      </html>
    );
  }
});

<SurveyEmail/>元件中嵌套了<Email/>。

var SurveyEmail = React.createClass({
  propTypes: {
    survey: React.PropTypes.object.isRequired
  },
  render: function () {
    var survey = this.props.survey;
    return (
      <Email>
        <h2>{survey.title}</h2>
      </Email>
    );
  }
});

接下來,按照給定的兩種設計分別渲染出這兩個KPI,在 PC 端上左右相鄰排版,在移動裝置中上下堆放排版。每一個 KPI在結構上相似,所以他們可以共享同一個元件:

var SurveyEmail = React.createClass({
  render: function () {
    return (
      <table className='kpi'>
        <tr>
          <td>{this.props.kpi}</td>
        </tr>
        <tr>
          <td>{this.props.label}</td>
        </tr>
      </table>
    );
  }
});

把它們新增到 <SurveryEmail/>元件中:

var SurveyEmail = React.createClass({
  propTypes: {
    survey: React.PropTypes.object.isRequired
  },
  render: function () {
    var survey = this.props.survey;
    var completions = survey.activity.reduce(function (memo,ac){
      return memo + a;
    }, 0);
    var daysRunning = survey.activity.length;
    return (
      <Email>
        <h2>{survey.title}</h2>
        <KPI kpi={completions} label='Completions'/>
        <KPI kpi={daysRunning} label='Days running'/>
      </Email>
    );
  }
});

這裡實現了將 KPI上下堆放的排版,但是在 PC 端我們的設計是左右相鄰排版。現在的挑戰是,讓它既能在 PC 又能在移動裝置上工作。首先我們應解決下面幾個問題。

通過新增 CSS 檔案的方式美化 <Email/>:

var fs = require('fs');
var Email = React.createClass({
  propTypes: {
    responsiveCSSFile: React.PropTypes.string
  },
  render: function () {
    var responsiveCSSFile = this.props.responsiveCSSFile;
    var styles;
      if (responsiveCSSFile) {
        styles = <style>{fs.readFileSync(responsiveCSSFile)}</style>;
      }
      return (
        <html>
          <body>
            {styles}
            {this.prop.children}
          </body>
        </html>
      );
  }
});

完成後的 <SurveyEmail/> 如下:

var SurveyEmail = React.createClass({
  propTypes: {
    survey: React.PropTypes.object.isRequired
  },
  render: function () {
    var survey = this.props.survey;
    var completions = survey.activity.reduce(function (memo, ac) {
      return memo + a;
    }, 0);
    var daysRunning = survey.activity.length;
    return (
      <Email responsiveCSS='path/to/mobile.css'>
        <h2>{survey.title}</h2>
        <table className='for-desktop'>
          <tr>
            <td>
              <KPI kpi={completions} label='Completions'/>
            </td>
            <td>
              <KPI kpi={daysRunning} label='Days running'/>
            </td>
          </tr>
        </table>
        <div className='for-mobile'>
          <KPI kpi={completions} label='Completions'/>
          <KPI kpi={daysRunning} label='Days running'/>
        </div>
      </Email>
    );
  }
});


我們把電子郵件按照 PC 端和移動端進行了分組。不幸的是,在電子郵件中我們無法使用float: left,因為大多數的瀏覽器並不支援它。還有 HTML標籤中的 align 和 valign 屬性已經被廢棄,因而 React也不支援這些屬性。不過,他們已經提供了一個類似的實現可用於浮動兩個div。而事實上,我們使用了兩個分組,通過響應式的樣式表,依據螢幕尺 寸的大小來控制顯示或隱藏。

儘管我們使用了表格,但有一點很明確,使用 React渲染電子郵件和編寫瀏覽器端的響應式 UI有著同樣的優勢:元件的重用性、可組合性以及可測試性。

繪圖

在我們的 Survey Builder示例應用中,我們想要繪製出在公共關係活動日當天,某次調查的完成數量的圖表。我們想把完成數量在我們的調查表中表現成一個簡單的走勢圖,一眼就可以看出調查的完成情況。

React 支援 SVG 標籤,因而製作簡單的 SVG 就變得很容易。

為了渲染出走勢圖,我們還需要一個帶有一組指令的<Path/>。

完成後的示例如下:

var Sparkline = React.createClass({
  propTypes: {
    points: React.PropTypes.arrayOf(React.PropTypes.number).isRequired
  },
  render: function () {
    var width = 200;
    var height = 20;
    var path = this.generatePath(width, height, this.props.points);
    return (
      <svg width={width} height={height}>
        <path d={path} stroke='#7ED321' strokeWidth='2' fill='none'/>
      </svg>
    );
  },
  generatePath: function (width, height, points){
    var maxHeight = arrMax(points);
    var maxWidth = points.length;
    return points.map(function (p, i) {
      var xPct = i / maxWidth * 100;
      var x = (width / 100) * xPct;
      var yPct = 100 - (p / maxHeight * 100);
      var y = (height / 100) * yPct;
      if (i === 0) {
        return 'M0,' + y;
      } else {
        return 'L' + x + ',' + y;
      }
    }).join(' ');
  }
});

上面的 Sparkline 元件需要一組表示座標的數字。然後,使用 path建立一個簡單的 SVG。

有趣的部分是,在 generatePath函式中計算每個座標應該在哪裡渲染並返回一個 SVG 路徑的描述。

它返回了一個像“M0,30 L10,20 L20,50”一樣的字串。 SVG路徑將它翻譯為繪製指令。指令間通過空格分開。“M0,30”意味著將指標移動到x0 和 y30。同理,“L10,20”意味著從當前指標位置畫一條指向 x10 和 y20的線,以此類推。

以同樣的方式為大型的圖表編寫 scale 函式可能有一點枯燥。但是,如果使用 D3這樣的類庫編寫就會變得非常簡單,並且 D3 提供的 scale函式可用於取代手動地建立路徑,就像這樣:

var Sparkline = React.createClass({
  propTypes: {
    points: React.PropTypes.arrayOf(React.PropTypes.number).isRequired
  },
  render: function () {
    var width = 200;
    var height = 20;
    var points = this.props.points.map(function (p, i) {
      return { y: p, x: i };
    });
    var xScale = d3.scale.linear()
      .domain([0, points.length])
      .range([0, width]);
    var yScale = d3.scale.linear()
      .domain([0, arrMax(this.props.points)])
      .range([height, 0]);
    var line = d3.svg.line()
      .x(function (d) { return xScale(d.x) })
      .y(function (d) { return yScale(d.y) })
      .interpolate('linear');
    return (
      <svg width={width} height={height}>
        <path d={line(points)} stroke='#7ED321' strokeWidth='2' fill='none'/>
      </svg>
    );
  }
});

總結

在這一章裡我們學到了:

  1. React 不只侷限於瀏覽器,還可被用於建立桌面應用以及電子郵件。
  2. React 如何輔助遊戲開發。
  3. 使用 React 建立圖表是一個非常可行的方式,配合 D3這樣的類庫會表現得更出色。

書籍簡介