BT

你的观点很重要! 快来参与InfoQ调研吧!

不止是UI:React的使用场景探索

| 作者 Tom Hallett等 关注 0 他的粉丝 发布于 2015年7月29日. 估计阅读时间: 36 分钟 | ArchSummit社交架构图谱:Facebook、Snapchat、Tumblr等背后的核心技术

A note to our readers: As per your request we have developed a set of features that allow you to reduce the noise, while not losing sight of anything that is important. Get email and web notifications by choosing the topics you are interested in.

编者按:InfoQ开设新栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自卓越开发者联盟著《React:引领未来的用户界面开发框架》中的章节“其它应用场景”,探索了React在UI之外的其它用法。

React不仅是一个强大的交互式UI渲染类库,而且还提供了一个用于处理数据和用户输入的绝佳方法。它倡导可重用并且易于测试的轻量级组件。不仅在Web应用中,这些重要的特性同样适用于其他的技术场景。

在这一章,我们将会看到如何在下面的场景中使用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这样的类库会表现得更出色。

书籍简介

2014年横空出世的由Facebook推出的开源框架React.js,基于Virtual DOM重新定义了用户界面的开发方式,彻底革新了大家对前端框架的认识。《React:引领未来的用户界面开发框架》是这一领域的首本技术书籍,由多位一线专家精心撰写,采用一个全程实例全面介绍和剖析了React.js 的方方面面,适合广大前端开发者、设计人员,及所有对未来技术趋势感兴趣者阅读。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

React很先进 但是理念貌似跟Web component相悖? 我该如何选择 by n yuxiao

React很先进 但是理念貌似跟Web component相悖? 我该如何选择

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT