不止是UI:React的使用場景探索
React不僅是一個強大的交互式UI渲染類庫,而且還提供了一個用于處理數據和用戶輸入的***方法。它倡導可重用并且易于測試的輕量級組件。不僅在Web應用中,這些重要的特性同樣適用于其他的技術場景。
在這一部分內容中,我們將會看到如何在下面的場景中使用React:
- 桌面應用
- 游戲
- 電子郵件
- 繪圖
桌面應用
借助 atom-shell 或者 node-webkit 這類項目,我們可以在桌面上運行一個 Web應用。來自 Github 的 Atom Editor 就是使用 atom-shell 以及 React創(chuàng)建的。
下面將 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 完成所有初始化工作并準備創(chuàng)建瀏覽器窗口時,會調用下面的方法。
- app.on('ready', function() {
- // 創(chuàng)建瀏覽器窗口。
- mainWindow = new BrowserWindow({
- width: 800,
- height: 600
- });
- // 加載應用的 index.html 文件。
- // mainWindow.loadUrl('file://' + __dirname + '/index.html');
- mainWindow.loadUrl('http://localhost:8080/');
- // 在窗口關閉時觸發(fā)。
- mainWindow.on('closed', function() {
- // 直接引用 window 對象,如果你的應用支持多個窗口,通常需要把 window 存儲到
- // 一個數組中。此時,你需要刪除相關聯(lián)的元素。
- mainWindow = null;
- });
- });
借助 atom-shell 或者 node-webkit 這類項目,我們可以將創(chuàng)建 web的技術應用于創(chuàng)建桌面應用。就像開發(fā) web 應用一樣,React同樣可以幫助你構建強大的交互式桌面應用。
游戲
通常,游戲對用戶交互有很高的要求,玩家需要及時地對游戲狀態(tài)的改變做出響應。相比之下,在絕大多數web應用中,用戶不是在消費資源就是在產生資源。本質上,游戲就是一個狀態(tài)機,包括兩個基本要素:
- 更新視圖
- 響應事件
在本書概覽部分,你應該已經注意到:React關注的范疇比較窄,僅僅包括兩件事:
- 更新 DOM
- 響應事件
React 和游戲之間的相似點遠不止這些。React 的虛擬 DOM 架構成就了高性能的3D 游戲引擎,對于每一個想要達到的視圖狀態(tài),渲染引擎都保證了對視圖或者DOM 的一次有效更新。
2048這個游戲的實現(xiàn)就是將 React 應用于游戲中的一個示例。這個游戲的目的是把桌面上相匹配的數字結合在一起,直到2048。
下面,深入地看一下實現(xiàn)過程。源碼被分為兩部分。***部分是用于實現(xiàn)游戲邏輯的全局函數,第二部分是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中定義的虛擬網格位置直接相關。繼初始化數據結構后,你將會看到一系列的函數對該給定數據結構進行操作。這些函數都按照固定的方式執(zhí)行,返回一個新的桌面并且不會改變輸入值。這使得游戲邏輯更清晰,因為可以將在數字方塊移動前后的桌面數據結構進行比較,并且在不改變游戲狀態(tài)的情況下推測出下一步。
關于數據結構,另一個有趣的屬性是數字方塊之間在結構上共享。所有的桌面共享了對桌面上未改變過的數字方塊的引用。這使得創(chuàng)建一個新桌面非???,并且可以通過判斷引用是否相同來比較桌面。
這個游戲由兩個 React 組件構成,GameBoard 和Tiles。
Tiles是一個簡單的 React 組件。每當給它的 props 指定一個board,它總會渲染出完整的 Tiles。這給了我們利用 CSS3 transition實現(xiàn)動畫的機會。
- 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是一個狀態(tài)機,用于響應按下方向鍵這一用戶事件,并與游戲的邏輯功能進行交互,然后用一個新的桌面來更新狀態(tài)。
- 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組件中,我們初始化了用于和桌面交互的鍵盤監(jiān)聽器。每一次按下方向鍵,我們都會去調用setBoard,該方法的參數是游戲邏輯中新創(chuàng)建的桌面。如果新桌面和原來的不同,我們會更新GameBoard 組件的狀態(tài)。這避免了不必要的函數執(zhí)行,同時提升了性能。
在 render 方法中,我們渲染了當前桌面上的所有 Tile組件。通過計算游戲邏輯中的桌面并渲染出得分。
每當我們按下方向鍵時,addTile方法會保證在桌面上添加新的數字方塊。直到桌面已經滿了,沒有新的數字可以結合時,游戲結束。
基于以上的實現(xiàn),為這個游戲添加一個撤銷功能就很容易了。我們可以把所有桌面的變化歷史保存在GameBoard 組件的狀態(tài)中,并且在當前桌面上新增一個撤銷按鈕(代碼)。
這個游戲實現(xiàn)起來非常簡單。借助React,開發(fā)者僅聚焦在游戲邏輯和用戶交互上即可,不必去關心如何保證視圖上的同步。
#p#
電子郵件
盡管 React 在創(chuàng)建 web 交互式 UI 上做了優(yōu)化,但它的核心還是渲染HTML。這意味著,我們在編寫 React應用時的諸多優(yōu)勢,同樣可以用來編寫令人頭疼的 HTML 電子郵件。
創(chuàng)建 HTML 電子郵件需要將許多的 table在每個客戶端上進行精準地渲染。想要編寫電子郵件,你可能要回溯到幾年以前,就像是回到1999 年編寫 HTML 一樣。
在多終端下成功地渲染郵件并不是一件簡單的事。在我們使用 React來完成設計的過程中,可能會碰到若干挑戰(zhàn),不過這些挑戰(zhàn)與是否使用React 無關。
用 React 為電子郵件渲染 HTML 的核心是React.renderToStaticMarkup。這個函數返回了一個包含了完整組件樹的HTML 字符串,指定了最外層的組件。React.renderToStaticMarkup 和React.renderToString 之間唯一的區(qū)別就是前者不會創(chuàng)建額外的 DOM屬性,比如 React 用于在客戶端索引 DOM 的 data-react-id屬性。因為電子郵件客戶端并不在瀏覽器中運行——我們也就不需要那些屬性了。
使用 React 創(chuàng)建一個電子郵件,下圖中的設計應該分別應用于 PC 端和移動端:
為了渲染出電子郵件,我寫了一小段腳本,輸出用于發(fā)送電子郵件的 HTML 結構:
- // render_email.js
- var React = require('react');
- var SurveyEmail = require('survey_email');
- var survey = {};
- console.log(
- React.renderToStaticMarkup(<SurveyEmail survey={survey}/>)
- );
我們看一下 SurveyEmail 的核心結構。首先,創(chuàng)建一個 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>
- );
- }
- });
這里實現(xiàn)了將 KPI上下堆放的排版,但是在 PC 端我們的設計是左右相鄰排版?,F(xiàn)在的挑戰(zhàn)是,讓它既能在 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也不支持這些屬性。不過,他們已經提供了一個類似的實現(xiàn)可用于浮動兩個div。而事實上,我們使用了兩個分組,通過響應式的樣式表,依據屏幕尺寸的大小來控制顯示或隱藏。
盡管我們使用了表格,但有一點很明確,使用 React渲染電子郵件和編寫瀏覽器端的響應式 UI有著同樣的優(yōu)勢:組件的重用性、可組合性以及可測試性。
繪圖
在我們的 Survey Builder示例應用中,我們想要繪制出在公共關系活動日當天,某次調查的完成數量的圖表。我們想把完成數量在我們的調查表中表現(xiàn)成一個簡單的走勢圖,一眼就可以看出調查的完成情況。
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創(chuàng)建一個簡單的 SVG。
有趣的部分是,在 generatePath函數中計算每個坐標應該在哪里渲染并返回一個 SVG 路徑的描述。
它返回了一個像“M0,30 L10,20 L20,50”一樣的字符串。 SVG路徑將它翻譯為繪制指令。指令間通過空格分開。“M0,30”意味著將指針移動到x0 和 y30。同理,“L10,20”意味著從當前指針位置畫一條指向 x10 和 y20的線,以此類推。
以同樣的方式為大型的圖表編寫 scale 函數可能有一點枯燥。但是,如果使用 D3這樣的類庫編寫就會變得非常簡單,并且 D3 提供的 scale函數可用于取代手動地創(chuàng)建路徑,就像這樣:
- 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>
- );
- }
- });
總結
在這一章里我們學到了:
- React 不只局限于瀏覽器,還可被用于創(chuàng)建桌面應用以及電子郵件。
- React 如何輔助游戲開發(fā)。
- 使用 React 創(chuàng)建圖表是一個非??尚械姆绞剑浜?D3這樣的類庫會表現(xiàn)得更出色。