写点什么

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

评论

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

企业电子文档管理需要注意的点及解决措施

小炮

文档管理

java培训SpringBoot 中的各种参数校验

@零度

JAVA开发 springboot

墨天轮访谈 | 阿里云捷熙:AnalyticDB,人人可用的数据分析服务

墨天轮

数据库 阿里云 国产数据库

LR.Net低代码开发平台 快速设计权限管理模块

力软低代码开发平台

王者荣耀商城异地多活架构设计

小虾米

架构师实战营

纪念左晖:敢为天下先(修订版)

IT民工大叔

产业互联网 数字化转型 企业家精神 左晖

技术分享| 快对讲调度系统设计概要

anyRTC开发者

音视频 语音通话 调度 视频通话 快对讲

eBPF 简介

申屠鹏会

ebpf

CC2530 ADC配置步骤

DS小龙哥

5月月更

探密"一学就会,一用就废"的OKR

Bruce Talk

OKR 敏捷 Agile

性能优化手记上篇之【原则】&【方法】

鲸品堂

详解GaussDB(DWS)的CPU资源隔离管控能力

华为云开发者联盟

数据库 cpu GaussDB 资源管控

服务端技术进阶(三)从架构到监控报警,支付系统设计如何步步为营

No Silver Bullet

架构 支付系统 架构设计 5月月更 监控报警

web前端培训Vue3 TypeScript 如何实现useRequest

@零度

Vue 前端开发

【linux运维】linux运维会被淘汰吗?会消失在云计算中吗?

行云管家

云计算 IT运维 云运维

虚拟机是什么?跟堡垒机有哪些区别?

行云管家

虚拟机 堡垒机

Docker容器:将带UI的程序直接转为Web应用,so easy

华为云开发者联盟

云计算 后端 Docker容器 Web应用

拯救工程师,远程开发C++的四大秘笈|视频教程

OneFlow

c++ 教程分享

浅析数字化转型与产业互联网

IT民工大叔

产业互联网 数字化转型

架构的尽头是架构师

IT民工大叔

架构师

SREWorks持续交付云原生化: 镜像构建

阿里云大数据AI技术

大数据 运维 云原生

CopyOnWriteArrayList 源码分析-其它方法

zarmnosaj

5月月更

大数据培训实时数仓实践以及架构的演进

@零度

数仓 大数据开发

OpenYurt v0.7.0 版本解读:无侵入的跨网络域解决方案 Raven

阿里巴巴云原生

阿里云 开源 云原生 边缘计算 openyurt

没有JDK和Maven,用Docker也能构建Maven工程

程序员欣宸

Java Docker maven 5月月更

SAP UI5 框架的 manifest.json

汪子熙

前端开发 SAP Fiori SAP UI5 5月月更

校企融合,打造金融科技应用人才高地

非凸科技

校企合作 非凸

“技术商人”溯源考

IT民工大叔

华为 工程师 任正非讲话 商业意识 复合型人才

如视技术副总裁杨永林:当传统产业遇到“数字空间”

阿里云弹性计算

视觉计算 数字空间 VR看房

快速上手vs2019

Loken

音视频 5月月更

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