速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

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

  • 2015-07-28
  • 本文字数:7150 字

    阅读完需:约 23 分钟

编者按: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>
);
}
});

组件中嵌套了

复制代码
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>
);
}
});

把它们添加到 组件中:

复制代码
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 文件的方式美化

复制代码
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>
);
}
});

完成后的 如下:

复制代码
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 就变得很容易。

为了渲染出走势图,我们还需要一个带有一组指令的

完成后的示例如下:

复制代码
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 的方方面面,适合广大前端开发者、设计人员,及所有对未来技术趋势感兴趣者阅读。

2015-07-28 21:028122

评论

发布
暂无评论
发现更多内容

医疗数字化:区块链或成最强辅助

CECBC

医疗

Flink的广播变量

大数据技术指南

大数据 flink 5月日更

线程与线程池的那些事之线程篇

秦怀杂货店

Java 线程 多线程 进程 并发

数据预处理&特征工程

Qien Z.

Python 5月日更

IPFS与 Filecoin的区别是什么?

meio

IPFS Filecoin

夺魁!亚马逊云科技荣膺中国数据管理解决方案领导者!

亚马逊云科技 (Amazon Web Services)

数据挖掘从入门到放弃(七):TensorFlow和keras实现线性回归LinearRegression

数据社

机器学习 5月日更

ArrayList vs LinkedList

ltc

ArrayList linkedlist

亚马逊云科技宣布在上海设立生命健康行业数字化赋能中心

亚马逊云科技 (Amazon Web Services)

API网关才是大势所趋?SpringCloud Gateway保姆级入门教程

Zhendong

Java spring API 网关

【epoll】epoll多路复用和Reactor设计思想

Linux服务器开发

后端 网络编程 C/C++ epoll Linux服务器开发

产品思维

lenka

5月日更

父母为什么催婚,而你又在逃避什么

小天同学

婚姻 爱情 5月日更

博睿数据多点开花,数据链DNA加速走进金融、医疗、政企行业

博睿数据

数据链DNA

全面隔绝虚拟货币暴涨暴跌风险

CECBC

区块链技术在数字农业领域的应用挑战

CECBC

破茧成蝶,如何用数据之力重塑您的业务?| 云途专栏

亚马逊云科技 (Amazon Web Services)

【偶尔一道ctf】xctf adword mobile easy-apk

Thrash

为什么“放弃”?

Nydia

学习

【签约计划】试用期规则

InfoQ写作社区官方

签约计划 热门活动

SWOT分析模型

石云升

创业 战略 职场经验 5月日更

架构学习笔记:复杂度来源

风翱

5月日更 架构复杂度

Dubbo 延迟与粘滞连接

青年IT男

dubbo

发布两小时,Github访问量破百万,阿里内部首次公布的 Java10W 字面经有多强?

Java 程序员 架构 面试

5分钟速读之Rust权威指南(六)

wzx

rust

520,珍惜眼前人

数据社

520 520单身福利 520 单身福利

写给大神

小梨蕊

情人节 520单身福利 520 单身福利

NetCore配置文件使用全过程

happlyfox

520单身福利

造车潮

ES_her0

5月日更

一款开源小工具,提升K8S资源管理幸福感!

Rancher

从外包跳槽到大厂,我用了55天成功逆袭。

Java 程序员 架构 面试

不止是UI:React的使用场景探索_语言 & 开发_Tom Hallett_InfoQ精选文章