“AI 技术+人才”如何成为企业增长新引擎?戳此了解>>> 了解详情
写点什么

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

评论

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

不降功能只降资源,六个应用场景带你了解OCP Express

OceanBase 数据库

数据库 oceanbase

ChatGPT 开源了第一款插件,都来学习一下源码吧!

Python猫

魔法诗~~~一套基于Vue开发的实用、高端、炫酷的响应式前端网页!!!

京茶吉鹿

Vue 前端 网页设计

机器学习实战系列[一]:工业蒸汽量预测(最新版本上篇)含数据探索特征工程等

汀丶人工智能

数据挖掘 机器学习 决策树 LightGBM

Swift之struct二进制大小分析

京东科技开发者

swift 数据结构 struct 二进制 企业号 3 月 PK 榜

揭秘网页性能监控|如何从多个角度分析监控结果

云智慧AIOps社区

监控 监控宝 网站监控 网页性能优化 监控产品

Java开发新手必读:PO、VO、DAO、BO、DTO、POJO,区别在哪儿?

Java你猿哥

Java 后端 ssm Java工程师 Java基础知识点

mysqldump 详解

GreatSQL

MySQL greatsql greatsql社区

Nautilus Chain 首个生态基础设施 Poseiswap,公布空投规则

EOSdreamer111

pytest学习和使用5-Pytest和Unittest中的断言如何使用?

Python 自动化测试 pytest 测试报告 Allure

软件工程高效学 | 实践工具:Microsoft Office Visio

TiAmo

开发工具 Visio绘图注释工具

AI训练性能提升30%,阿里云发布GPU计算裸金属实例ebmgn7ex

云布道师

弹性计算

履约核心引擎低代码化原理与实践

京东科技开发者

低代码 规则引擎 企业号 3 月 PK 榜 履约中心

数仓安全测试之SSRF漏洞

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 3 月 PK 榜

即时通讯技术文集(第11期):IM通信格式的选型及Protobuf专题 [共16篇]

JackJiang

网络编程 即时通讯 IM

kafka Log存储解析以及索引机制

石臻臻的杂货铺

kafka

c4d超强三维动画设计:CINEMA 4D Studio R2023.2.0 中文激活版

真大的脸盆

Mac Mac 软件 C4D

java实现布隆过滤器

小小怪下士

Java 程序员 布隆过滤器

一种自平衡解决数据倾斜的分表方法

京东科技开发者

数据倾斜 分布分表 企业号 3 月 PK 榜 B 端产品 数据分表

Swift之struct二进制大小分析

京东科技开发者

swift App struct 移动开发 企业号 3 月 PK 榜

百度内容理解推理服务FaaS实战——Punica系统

百度Geek说

云原生 Faas 成本优化 企业号 3 月 PK 榜 AI推理

钉钉协作Tab前端进化之路

阿里技术

前端 钉钉

【4月8日】Elastic 中国开发者大会 2023 议程预告

极限实验室

大数据 elasticsearch elastic 开源 开发者大会

IntelliJ IDEA 2023.1 版本可以安装了

HoneyMoose

ShareSDK Android SDK API

MobTech袤博科技

OceanBase 信息技术服务管理体系通过 ISO20000 认证和 ITSS 认证

OceanBase 数据库

数据库 oceanbase

为什么 MySQL 不推荐使用 join?

Java你猿哥

Java MySQL sql 后端 ssm

Nautilus Chain 首个生态基础设施 Poseiswap,公布空投规则

股市老人

避免使用CSS @import 影响页面加载速度

南城FE

CSS css3 前端

ShareSDK Android端权限说明

MobTech袤博科技

首届OceanBase开发者大会|NineData首席架构师谭宇受邀参会,并发表了主题演讲

NineData

多云架构 数据管理 oceanbase 开发者大会 NineData

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