QCon 演讲火热征集中,快来分享技术实践与洞见! 了解详情
写点什么

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

评论

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

GTC 2024 火线评论:GPU 的高效存储利用

XSKY星辰天合

分布式存储 软件定义存储 GTC2024

美国对苹果提起反垄断诉讼;周鸿祎:不转向 AI 手机的厂商会成下一个「诺基亚」丨 RTE 开发者日报 Vol.170

声网

低代码助力企业数智转型

不在线第一只蜗牛

低代码 数智转型

从零开始学Spring Boot系列-集成Kafka

快乐非自愿限量之名

kafka Spring Boot 后端

【教程】高效数据加密混淆方法及实现简介

雪奈椰子

Beyond Compare 4 for Mac(好用的文件对比工具) 4.4.7(28397)中文版

iMac小白

新零售SaaS架构:线上商城系统架构设计

快乐非自愿限量之名

架构 零售 SaaS

jackson对象带时间的转换报错,附带fastjson封装工具就填

光进

java‘

实时数据采集:选品利器,API接口一网打尽

技术冰糖葫芦

API 接口

策略分析轻松搞定!10款免费可视化模板助你提升分析效率!

彭宏豪95

在线白板 办公软件 效率软件 SWOT boardmix

众安保险AI代码助手DevPilot获奖,全面开源构筑技术新生态

ZA技术社区

海信电视U8N Pro,用AI算尽 “每一粒沙”

脑极体

AI

PowerPhotos for Mac v2.5.7b3 直装版 mac专用图片管理工具

iMac小白

深度解析大模型的关键特性与优势

木南曌

#大模型

选择美国高防服务器出租,提升网络防御力

一只扑棱蛾子

美国高防服务器 高防服务器

热点!浅谈低代码到底是什么?

互联网工科生

【教程】深入探究 JS代码混淆与加密技术

SideNotes v1.4.14 for Mac激活版 让你随时在 Mac 屏幕上管理笔记

iMac小白

绿色节能|AIRIOT智慧建材能耗管理解决方案

AIRIOT

物联网平台 智慧系统 智慧建材能耗管理

AI大模型运维开发探索第三篇:深入浅出运维智能体

阿里云大数据AI技术

运维 智能体 大数据运维

Prompt进阶系列4:LangGPT(构建高性能Prompt实践指南)--结构化Prompt

汀丶人工智能

人工智能 Prompt工程

.NET Core 服务实现监控可观测性最佳实践

观测云

.net core

Java22重磅发布!!!!卷不动了,真的卷不动了。。。。

不在线第一只蜗牛

Java 前端 前端开发

低代码与AI金融时代:重塑金融行业的未来

EquatorCoco

人工智能 AI 低代码 金融

一个案例,看懂AI Agent厂商的商业落地路径

王吉伟频道

LLM 大语言模型 AI Agent AI智能体 RPA Agent

头部银行 AI 落地实践|数据应用赋能经营管理闭环

Kyligence

游戏直播APP平台开发多少钱成本:定制与成品源码差距这么大

软件开发-梦幻运营部

有赞畅捷通T+对接无忧,业务体验双升级!

聚道云软件连接器

案例分享

Go 语法糖 for range 中的 copy 问题

蓬蒿

golang

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