限时!亚马逊云科技云从业者认证考试五折,未过免费补考!更有好礼相送! 了解详情
写点什么

不止是 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:028016

评论

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

架构实战营模块一作业

😊😊😊

作业一

专业开发人员需要低代码开发平台吗

力软低代码开发平台

面试官:小伙子你说一下分布分表

Java全栈架构师

程序员 程序人生 后端 架构师 分布分表

架构实战 1 - 微信业务架构图-学生管理系统技术选型

架构实战营 「架构实战营」

面试常问的高并发系统设计,我们到底该怎么学?

程序员小毕

程序员 面试 后端 高并发 架构设计

百亿节点、毫秒级延迟,携程金融基于 NebulaGraph 的大规模图应用实践

NebulaGraph

风控 风控模型 电商风控

SCRM是什么,SCRM和CRM区别?

优秀

CRM scrm

结合RocketMQ源码说说并发编程的三大神器

程序员小毕

源码 程序员 RocketMQ 后端 架构师

文盘Rust -- r2d2 实现redis连接池

京东科技开发者

redis 后端 封装 连接池 rust语言

深圳前端培训学习编程的就业方向有哪些?

小谷哥

看场景、重实操,实时数仓不是“纸上谈兵”

阿里云大数据AI技术

大数据 实时数仓 12 月 PK 榜

【中国信通院|低代码·无代码应用沙龙】低代码平台在云智慧的实践探索

云智慧AIOps社区

大前端 低代码 数据可视化 无代码 可视化大屏

Flink架构与应用漫聊

数新网络官方账号

数读甘肃数字经济 | 2022年甘肃14市州数字经济发展活跃度指数发布

易观分析

数字经济 甘肃

AI技术实践|用腾讯云录音文件识别让无字幕视频自动生成字幕

牵着蜗牛去散步

人工智能 腾讯云 腾讯 语音识别

极客时间-架构训练营-作业一

devilyaos

架构训练营 架构训练营10期

重写事件抛发接收机制原理

千锋IT教育

无刷直流电机最强科普(收藏版)

元器件秋姐

发展史 电机 BLDC应用 电子工程师 无刷直流电机

架构训练营模块1作业

gigifrog

架构训练营

java架构师培训学习需要多久?

小谷哥

大数据培训学习后程序员就业前景如何

小谷哥

微信业务架构图 & 学生系统技术架构图

白杨

flex弹性盒子中flex-grow与flex的区别

千锋IT教育

阿里云祝顺民:生而为云,连接增长

云布道师

阿里云 云网络

【PCB技能】脑瓜疼的PCB反复评审难题,解决方法在这!

华秋PCB

工具 PCB PCB设计

北京前端培训程序员学习方法有哪些

小谷哥

掌握高并发系统设计,面试弯道超车

小小怪下士

Java 面试 系统设计 后端 高并发

架构训练模块1

KING

主数据管理实施四部曲概论

用友BIP

大数据培训学习需要注重哪些知识呢

小谷哥

数据库到底应该如何存储密码?

Java永远的神

数据库 程序员 程序人生 后端 架构师

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