React 蓝图(二)
原文:
zh.annas-archive.org/md5/a2bffca9c62012ab857fb3d1f735ef00译者:飞龙
第三章:使用 ReactJS 进行响应式 Web 开发
几年前,构建 Web 应用相对容易。您的 Web 应用在具有大致相同屏幕尺寸的台式机和笔记本电脑上查看,并且可以创建一个轻量级的移动版本来服务访问您网站的少量移动用户。如今,情况已经逆转,移动设备同样重要,有时甚至比台式机和笔记本电脑更重要。今天的屏幕尺寸可以从 4 英寸智能手机到 9 英寸平板电脑,以及任何介于两者之间的尺寸。
在本章中,我们将探讨构建适用于任何设备(无论大小或应用是否在桌面或移动浏览器上查看)的 Web 应用的实践。目标是创建一个能够适应用户设置并为每个人提供愉悦体验的应用环境。
“响应式开发”这个术语是一个涵盖一系列设计技术(如自适应、流体、液体或弹性布局,以及混合或移动开发)的通用术语。它可以分为两个主要组件:灵活布局和灵活媒体内容。
在这些主题中,我们将涵盖创建响应式 ReactJS 应用所需的所有内容:
-
创建灵活布局
-
选择合适的框架
-
使用 Bootstrap 设置响应式应用
-
创建灵活的网格
-
创建响应式菜单和导航
-
创建响应式井
-
创建响应式面板
-
创建响应式警报
-
嵌入媒体和视频内容
-
创建响应式按钮
-
创建动态进度条
-
创建流体轮播
-
与流体图片和图片元素一起工作
-
创建响应式表单字段
-
使用图标和字体图标
-
创建响应式着陆页
创建灵活布局
灵活布局的宽度会根据用户视口的尺寸而变化。视口是用户设备可查看区域的通用术语。它比“窗口”或“浏览器大小”等术语更受欢迎,因为并非所有设备都使用 Windows。您可以设计布局以使用用户宽度的百分比,或者根本不指定任何宽度,让布局无论大小都填满视口。
在我们讨论灵活布局的所有优点之前,让我们简要地看看它的对立面,即固定宽度布局。
固定宽度意味着将页面的整体宽度设置为预定的像素值,然后考虑到这个限制来设计应用元素。在可联网移动设备爆炸性增长之前,这是开发网络应用的主要设计技术。
固定宽度设计具有一定的优势。主要优势是它让设计师对外观拥有完全的控制权。基本上,用户看到的就是设计师设计的。它也更容易进行结构化,并且与固定宽度的元素(如图片和表单)一起工作,不那么麻烦。
这种设计类型的明显缺点是,你最终得到的是一个僵化的布局,它不会根据用户环境的任何变化而改变。你经常会遇到对于大视口设备来说白空间过多,这会违背某些设计原则,或者对于小视口设备来说设计过宽的情况。
采用固定宽度设计可能适用于某些用例,但它取决于你猜测哪种布局约束对大多数应用用户来说效果最佳的决定,你很可能会排除一个可能非常大的用户群体使用你的应用。
因此,一个响应式应用通常应该设计成一个灵活的布局,以便为你的应用的所有用户保持可用性。
一个自适应应用通常指的是当发生变化时易于修改的应用,而响应式意味着快速对变化做出反应。这两个术语可以互换使用,当我们使用“响应式”这个术语时,通常意味着它也应该具有自适应的特性。弹性和流体大致意思相同,通常描述的是基于百分比的布局设计,能够适应浏览器或视口大小的变化。
另一方面,移动端开发意味着创建一个专门版本的应用,该版本旨在仅在手机浏览器上运行。这种方法偶尔是可行的,但它伴随着一些权衡,例如维护一个独立的代码库,依赖浏览器嗅探将用户引导到移动版本,以及搜索引擎优化(SEO)方面的问题,因为你必须为移动版和桌面版维护不同的 URL。
混合应用指的是以这种方式开发的移动应用,它们可以托管在利用移动平台WebView的本地应用中。你可以将 WebView 视为一个专有的、全屏的浏览器,它被钩在移动平台的本地环境中。这种方法的优点是,你可以使用标准的 Web 开发实践,此外,你还可以访问通常仅限于从移动浏览器内部访问的本地功能。另一个优点是,你可以将你的应用发布到原生应用商店。
使用 ReactJS 开发原生应用是一个有吸引力的提议,而 React Native 项目也提供了一个可行的选择。使用 React Native,你可以将你关于 ReactJS 所学的所有内容应用到开发可以在苹果和安卓设备上运行的应用,并且可以发布到苹果的 App Store 和谷歌的 Play 商店。
选择正确的框架
虽然当然可以自己设置一个灵活的布局,但使用响应式框架有很多意义。例如,你可以节省大量时间,使用已经经过多年战斗测试并由一支熟练的设计师团队维护的框架。你还可以利用这样一个广泛使用的响应式框架在网络上拥有大量有用资源的优势。缺点是,你可能需要学习框架期望你如何布局你的页面,有时,你可能不完全同意框架强加给你的设计决策。
考虑到这些因素,让我们来看看一些可供你选择的重大框架:
-
Bootstrap: 毫无疑问,Bootstrap 是这个领域的领导者。它非常受欢迎,有大量的资源和扩展可用。与 React-Bootstrap 项目的结合也使得在 ReactJS 中开发网络应用时,这是一个非常明显的选择。
-
Zurb Foundation: 基础框架是继 Bootstrap 之后第二大玩家,如果你认为 Bootstrap 不适合你,它是一个自然的选择。这是一个成熟的框架,只需付出很少的努力就能提供很多复杂性。
-
Pure: 由 Yahoo! 提供的 Pure 是一个轻量级且模块化的框架。如果你担心其他框架的字节大小(这个大约有 4 KB,而 Bootstrap 大约是 150 KB,Foundation 是 350 KB),它非常合适。
-
Material Design: 由 Google 提供的 Material Design 是一个非常有力的竞争者。它带来了很多新的想法,是 Bootstrap 和 Foundation 的一个令人兴奋的替代品。还有一个名为 Material UI 的 ReactJS 实现,它将 Material Design 和 ReactJS 结合起来,这使得它成为 Bootstrap 和 React-Bootstrap 的一个有吸引力的替代品。Material Design 在其提供的 UX 元素应该如何表现和交互方面非常具有意见性,而 Bootstrap 和其他框架则给你在设置交互方面提供了更多的自由度。
显然,选择一个适合每个项目的框架并不容易。我们之前没有提到的另一个选择是独自完成,也就是说,完全自己创建网格和灵活布局。这绝对是一个可行的策略,但它也带来了一些缺点。
主要的缺点是,你将无法从多年的调整和测试中受益。尽管大多数现代浏览器都相当强大,但你的代码将在众多浏览器和设备上运行。很可能你的用户会遇到你不知道的问题,因为你没有相同的硬件配置。
最后,你必须决定你是想设计应用程序,还是想创建一个新的灵活 CSS 框架。这个选择应该清楚地说明为什么在本章中我们选择了一个特定的框架来关注,而这个框架就是 Bootstrap。
毫无疑问,Bootstrap 是前面提到的框架中最成熟和最受欢迎的,并且在社区中拥有出色的支持。网络景观仍在以快速的速度发展,您可以确信 Bootstrap 会随着它一起发展。
使用 Bootstrap 设置您的应用
我们已经在上一章中查看了一个 Bootstrap 和 React-Bootstrap 的实现,但我们只是略过了您可以做什么。让我们更深入地看看 React-Bootstrap 能为我们提供什么。
通过复制第一章中的脚手架来开始这个项目,深入 ReactJS,然后向您的项目中添加 React-Bootstrap。打开终端,转到您的项目根目录,然后使用您喜欢的dependencies或devDependencies( whichever you prefer)替换以下列表,然后从命令行运行npm install命令:
"devDependencies": {
"babel-preset-es2015": "6.9.0",
"babel-preset-react": "6.11.1",
"babelify": "7.3.0",
"browser-sync": "2.13.0",
"browserify": "13.0.1",
"browserify-middleware": "7.0.0",
"history": "3.0.0",
"jsxstyle": "0.0.18",
"react": "15.1.0",
"react-bootstrap": "0.29.5",
"react-dom": "15.1.0",
"react-router": "2.5.2",
"reactify": "1.1.1",
"serve-favicon": "2.3.0",
"superagent": "2.1.0",
"uglifyjs": "2.4.10",
"watchify": "3.7.0"
},
此外,您需要下载 Bootstrap CSS 或使用 CDN 将其包含在您的index.html文件中。然后,将以下内容添加到index.html的<head>部分:
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css" />
<link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
创建一个灵活的网格
在 Bootstrap 等 CSS 框架的核心中,存在着网格的概念。网格是一种结构,允许您以一致的方式水平垂直堆叠内容。它提供了一个可预测的布局框架,当您编码时易于可视化。
网格由两个主要组件组成:行和列。在每一行中,您可以添加一定数量的列,从一列到最多 12 列,具体取决于框架。一些框架,如 Bootstrap,还会添加一个容器,您可以将它包裹在行和列周围。
使用网格非常适合响应式设计。您可以轻松地制作出在大桌面浏览器和小型移动浏览器上看起来都很棒的网络站。
这一切都取决于您如何构建您的列。例如,您可以将列设置为当浏览器宽度小于或等于 320 像素(典型的移动浏览器宽度)时为全宽,当浏览器宽度大于给定的像素大小时为三分之一宽度。您在浏览器尺寸上切换类的方法称为媒体查询。所有网格框架都内置了基于媒体查询切换大小的类;您很少需要自己编写媒体查询。
Bootstrap 中的网格系统使用 12 列,并且可以通过初始化为<Grid fluid={true}>来选择性地设置为流体。默认情况下是非流体,但值得注意的是,这两种设置都会返回一个响应式网格。主要区别在于流体网格始终具有 100%的宽度,并且会在每次宽度变化时不断调整。非流体网格由媒体查询控制,当视口宽度超过某些阈值时,会改变宽度。
网格列可以通过以下属性来区分:
-
xs:这是用于额外小型的设备,如手机(<768 px)
-
sm:这是用于小型设备,如平板电脑(≥768 px)的。
-
md:这是用于中等设备,如桌面(≥992 px)的。
-
lg:这是用于大型设备,如桌面(≥1200 px)的。
你也可以将push和offset与前面的属性结合使用,例如,你可以使用xsOffset来偏移超小设备上可见的列,依此类推。offset和push之间的区别在于,offset将强制其他列移动,而push将与其他列重叠。
尺寸向上膨胀。如果你定义了xs属性但没有sm、md或lg属性,所有列都将使用xs设置。如果你定义了xs和sm属性,超小视口将使用xs属性,而其他所有视口将使用sm属性。
让我们看看一个实际例子。在你的source/examples文件夹中创建一个文件(如果不存在,请创建该文件夹),命名为grid.jsx,并添加以下代码:
'use strict';
import React from 'react';
import {Grid,Row,Col} from "react-bootstrap";
在我们的脚本中,我们只导入我们当前需要的部分。在这个例子中,我们需要Grid、Row和Col,所以我们将这些命名并确保它们以这些名称导入并可用。
虽然不指定每个组件的名称而导入所有组件会更方便,但具体指定导入可以使你更容易理解你正在工作的文件中需要什么。这也可能在打包你的 JavaScript 代码以部署时减少体积,因为打包器可以移除所有可用但从未使用过的组件。请注意,这并不适用于当前版本的Browserify或Webpack(我们将在第六章 Advanced React 中讨论),但 Webpack 至少已经在路上了。
小贴士
当你从一个较大的库中导入单个组件时,请使用这种方法导入:
const Row = require('react-bootstrap').Row;
这将只导入所需的组件,而忽略库中的其余部分。如果你这样做是一致的,你的包大小将会减小。
让我们看看下面的代码:
const GridExample = React.createClass ({
render: function () {
return (
<div>
<h2>The grid</h2>
<Grid fluid={true}>
<Row>
<Col xs = { 1 }> 1 </Col>
<Col xs = { 1 }> 1 </Col>
<Col xs = { 1 }> 1 </Col>
<Col xs = { 1 }> 1 </Col>
<Col xs = { 1 }> 1 </Col>
<Col xs = { 1 }> 1 </Col>
<Col xs = { 1 }> 1 </Col>
<Col xs = { 1 }> 1 </Col>
<Col xs = { 1 }> 1 </Col>
<Col xs = { 1 }> 1 </Col>
<Col xs = { 1 }> 1 </Col>
<Col xs = { 1 }> 1 </Col>
</Row>
<Row>
<Col xs = { 2 } sm = { 4 }> xs2 sm4 </Col>
<Col xs = { 4 } sm = { 4 }> xs4 sm4 </Col>
<Col xs = { 6 } sm = { 4 }> xs6 sm4 </Col>
</Row>
这一行将为超小设备和从小到大的设备显示不同的列尺寸:
注意
记住,尺寸是向上膨胀,而不是向下。
<Row>
<Col
xs = { 6 }
sm = { 4 }
md = { 8 }
lg = { 2 }>
xs6 sm4 md8 lg2
</Col>
<Col
xs = { 6 }
sm = { 8 }
md = { 4 }>
xs6 sm8 md4 lg10
</Col>
</Row>
这一行显示了两个列,它们的宽度根据视口的不同而有很大变化。在智能手机上,列的宽度相等。在小视口中,左侧列覆盖行的一三分之一,而右侧列覆盖剩余部分。在中视口中,左侧列突然成为主导列,但在非常大的视口中,左侧列再次减少到一个更小的比例。
这显然是一个为了展示网格功能而人为设置的例子。对于实际应用来说,这是一个非常奇怪的设置:
<Row>
<Col
xs = { 3 }
xsOffset = { 1 }>
3 offset 1
</Col>
<Col
xs = { 7 }
xsOffset = { 1 }>
7 offset 1
</Col>
</Row>
两个列在这里都开始于一个偏移量。这将在每个列的开始处创建一个空白的列:
<Row>
<Col
xs = { 4 }
xsPush = { 1 }>
4 push 1 (overlaps)
</Col>
<Col
xs={ 7 }
xsOffset = { 1 }>
7 offset 1
</Col>
</Row>
Push将列移动到右边,但不会强制其他列移动,因此它将覆盖下一个列。这意味着第二列的偏移量将被第一列的内容覆盖:
</Grid>
</div>
);
}
});
module.exports = GridExample;
为了查看这个示例,请打开app.jsx并替换内容为以下代码:
'use strict';
import React from 'react';
import ReactDom from 'react-dom';
import GridExample from './examples/grid.jsx';
ReactDom.render ((<div>
<GridExample />
</div>),
document.getElementById('container')
);
小贴士
在本章中,我们将创建许多组件,并且它们都可以通过在代码头部添加一个import语句来添加到app.jsx中。ReactJS 要求你在导入组件时首字母大写。你可以通过在括号中添加名称来在渲染代码中使用你给导入的名称。
在创建网格时,在设置时使其可见非常有好处。你可以将其添加到app.css中,使其在浏览器中显示:
div[class*="col-"] {
border: 1px dotted rgba(60, 60, 60, 0.5);
padding: 10px;
background-color: #eee;
text-align: center;
border-radius: 3px;
min-height: 40px;
line-height: 40px;
}
这种样式将使查看和调试我们添加的列变得容易。
Bootstrap 的网格系统非常灵活,可以很容易地以你想要的方式构建你的页面。在这个示例中创建的网格在所有你抛向它的设备上都是可见的且流动的。
创建一个响应式菜单和导航栏
这在第二章中进行了广泛介绍,创建一个网店,所以我们在这里只设置一个基本的菜单,并参考上一章的细节,了解如何连接到路由器并设置工作链接。
在你的source/examples文件夹中创建一个文件,命名为navbar.jsx,并添加以下代码:
'use strict';
import React from 'react';
import { Nav,
Navbar,
NavBrand,
NavItem,
NavDropdown,
MenuItem
} from 'react-bootstrap';
const Navigation = React.createClass ({
render() {
return (
<Navbar inverse fixedTop>
<Navbar.Header>
<Navbar.Brand>
Responsive Web app
</Navbar.Brand>
<Navbar.Toggle/>
</Navbar.Header>
<Navbar.Collapse>
自动添加Navbar.Collapse将使这个导航栏成为一个移动友好的导航栏,当视口小于 768 像素时,它将用汉堡按钮替换菜单项:
<Nav role="navigation" eventKey={0} pullRight>
<NavItem
eventKey={ 1 }
href = "#">
Link
</NavItem>
<NavItem
eventKey = { 2 }
href = "#">
Link
</NavItem>
<NavDropdown
eventKey = { 3 }
title = "Dropdown"
id = "collapsible-nav-dropdown">
<MenuItem eventKey={ 3.1 }>
Action
</MenuItem>
<MenuItem eventKey={ 3.2 }>
Another action
</MenuItem>
<MenuItem eventKey={ 3.3 }>
Something else here
</MenuItem>
<MenuItem divider />
<MenuItem eventKey={ 3.3 }>
Separated link
</MenuItem>
</NavDropdown>
</Nav>
<Nav pullRight>
<NavItem eventKey = { 1 } href = "#">
Link Right
</NavItem>
<NavItem eventKey = { 2 } href = "#">
Link Right
</NavItem>
</Nav>
</Navbar.Collapse>
</Navbar>
);
}
});
module.exports = Navigation;
你可以在主要的Navbar组件上设置以下属性:
-
defaultExpanded:如果设置为true,这将展开小设备上的Navbar -
expanded:这将在运行时设置Navbar组件展开(需要onToggle) -
fixedBottom:这将固定Navbar组件在视口的底部 -
fixedTop:这将固定Navbar组件在视口的顶部 -
staticTop:这将使Navbar随着页面浮动 -
fluid:这与网格中的fluid设置工作方式相同 -
inverse:这将反转Navbar中的颜色 -
onToggle:这是一个当Navbar被切换时可以运行的函数 -
componentClass:这用于向Navbar添加你自己的类
创建响应式井
井是一个可以用于良好效果的嵌入元素。这是一种简单但有效的方式来强调内容。在 Bootstrap 中设置它也非常简单。
在source/examples中添加一个新文件,命名为wells.jsx,并添加以下代码:
'use strict';
import React from 'react';
import { Well } from 'react-bootstrap';
const Wells = React.createClass ({
render() {
return (
<Well bsSize = "large">
Hi, I'm a large well.
</Well>
);
}
});
module.exports = Wells;
你可以在Wells组件上设置以下属性:
bsSize:井可以是小或大
创建响应式面板
面板就像一口井,但拥有更多的信息和功能。
它可以有一个标题,也可以是可折叠的,因此它是一个很好的信息展示、表单包含等的候选者。
让我们创建一个基本的面板。在source/components中添加一个新文件,命名为panels.jsx,并添加以下代码:
'use strict';
import React from 'react';
import { Panel, Button, PanelGroup, Accordion }
from 'react-bootstrap';
const Panels = React.createClass ({
getInitialState() {
return {
open: false,
activeKey: 1
}
},
render() {
return (
<div>
<h2>Panels</h2>
<div>
<Button
onClick = { ()=> this.setState ({
open: !this.state.open })}>
{ !this.state.open ? "Open" : "Close" }
</Button>
<Panel
collapsible
expanded = { this.state.open }>
This text is hidden until you click the button.
</Panel>
这个面板默认是关闭的,并由组件的状态变量open控制。当你点击按钮时,它执行内部的setState函数。状态只是使用相当聪明的not运算符反转open变量的布尔值。当我们使用它时,我们说我们想要当前值的相反,这可能是true或false:
</div>
</div>
);
}
});
module.exports = Panels;
我们还可以对面板组件做更多的事情,但让我们先简要看看我们可以在Panel上设置哪些其他属性:
-
header (string): 将此添加到Panel初始化器中,并传递一个值以给标题添加一些内容。 -
footer (string): 这与标题相同,但会在底部而不是顶部创建信息块。 -
bsStyle (string): 这通过添加上下文类使内容有意义。你可以选择所有常见的 Bootstrap 上下文名称:primary、success、danger、info、warning以及default。 -
expanded (boolean): 这可以是true或false。这需要与collapsible一起使用。 -
defaultExpanded (boolean): 这也可以是true或false。这不会覆盖expanded函数。
你通常会想要显示多个面板并将它们分组在一起。这可以通过添加一个名为PanelGroup的组件来实现。
PanelGroups是一个包装器,你可以在你想要分组的所有面板周围设置它。如果你想分组两个面板,代码看起来是这样的:
<PanelGroup
activeKey = { this.state.activeKey }
onSelect = { (activeKey)=>
this.setState({ activeKey: activeKey })}
accordion>
<Panel
collapsible
expanded = { this.state.open }
header = "Panel 1 - Controlled PanelGroup"
eventKey = "1"
bsStyle = "info">
Panel 1 content
</Panel>
<Panel
collapsible
expanded = {this.state.open}
header = "Panel 2 - Controlled PanelGroup"
eventKey = "2"
bsStyle = "info">
Panel 2 content
</Panel>
</PanelGroup>
这是一个受控的PanelGroup实例。这意味着在任何时候只有一个面板会打开,这是通过在PanelGroup初始化器中添加activeKey属性来表示的。当你点击组中的面板时,onSelect()方法中的函数会被调用,并更新活动面板状态,然后告诉 ReactJS 打开活动面板并关闭非活动面板。
你也可以通过简单地从PanelGroup初始化器中删除activeKey和onSelect属性,以及从Panel初始化器中删除expanded属性来创建一个无控制的PanelGroup实例:
<PanelGroup accordion>
<Panel
collapsible
header = "Panel 3 - Uncontrolled PanelGroup"
eventKey = "3"
bsStyle = "info">
Panel 3 content
</Panel>
<Panel
collapsible
header = "Panel 4 - Uncontrolled PanelGroup"
eventKey = "4"
bsStyle = "info">
Panel 4 content
</Panel>
</PanelGroup>
它们之间的主要区别在于,在有控制组的情形下,每次只会打开一个面板,但在无控制组的情形下,用户可以关闭所有面板。
最后,如果你只想使用无控制面板组,你可以丢弃PanelGroup组件,转而导入Accordion组件。<Accordion />是<PanelGroup accordion />的别名。它实际上并没有节省多少代码,但可能更容易记住。代码看起来是这样的:
<Accordion>
<Panel
collapsible
header = "Panel 5 - Accordion"
eventKey = "5"
bsStyle = "info">
Panel 5 content
</Panel>
<Panel
collapsible
header = "Panel 6 - Accordion"
eventKey = "6"
bsStyle = "info">
Panel 6 content
</Panel>
</Accordion>
创建响应式警报
与面板类似,警报是填充了少量附加功能的信息块,非常适合向用户展示及时信息。
让我们看看你可以用警报做什么。
创建一个名为examples/alerts.jsx的文件,并添加以下代码:
'use strict';
import React from 'react';
import { Alert, Button } from "react-bootstrap";
const AlertExample = React.createClass ({
getInitialState() {
return {
alertVisible: true
};
},
这是我们的标志,用于保持警报可见。当这个设置为false时,警报会被隐藏:
render(){
if(this.state.alertVisible){
return (<Alert bsStyle="danger" isDismissableonDismiss={()=>{this.setState({alertVisible:false})}}>
在这里,有两个需要注意的属性。第一个是isDismissable,它渲染一个按钮,允许用户取消警报。这个属性是可选的。
第二个是onDismiss,这是一个在用户点击取消按钮时被调用的函数。在这种情况下,alertVisible标志被设置为 0,并且render函数现在返回null而不是Alert组件:
<h4>An error has occurred!</h4>
<p>Try something else and hope for the best.</p>
<p>
<Button bsStyle="danger">Do this</Button>
<span> or </span>
<Button onClick=
{()=>{this.setState({alertVisible:false})}}>
Forget it</Button>
操作按钮尚未设置任何功能,因此点击它目前是徒劳的。隐藏按钮接收一个函数,该函数将alertVisible标志设置为 0 并隐藏Alert框:
</p>
</Alert>)}
else {
return null;
}
}
});
module.exports = Alerts;
响应式嵌入媒体和视频内容
在你的网站上嵌入 YouTube 视频可以是一个值得考虑的添加项,因此让我们创建一个自定义组件来处理这个问题。
对于这个模块,我们还需要另一个依赖项,所以请继续打开终端,导航到根目录,并执行以下install命令:
npm install --save classnames
classnames组件允许你通过简单的true和false比较动态定义要包含的类,它比依赖于字符串连接和if...else语句更容易使用和理解。
创建一个名为components的文件夹,并在该文件夹中创建一个名为media.jsx的文件,然后添加以下代码:
'use strict';
import React from 'react';
import ClassNames from 'classnames';
const Media = React.createClass ({
propTypes: {
wideScreen: React.PropTypes.bool,
type: React.PropTypes.string,
src: React.PropTypes.string.isRequired,
width: React.PropTypes.number,
height: React.PropTypes.number
},
getDefaultProps() {
return {
src: "",
type: "video",
wideScreen: false,
allowFullScreen: false,
width:0,
height:0
}
},
我们需要一个属性:YouTube 源。其他的是可选的。如果没有提供宽屏,组件将以 4:3 的宽高比显示视频:
render() {
let responsiveStyle = ClassNames ({
"embed-responsive": true,
"embed-responsive-16by9": this.props.wideScreen,
"embed-responsive-4by3": !this.props.wideScreen
});
let divStyle, ifStyle;
divStyle = this.props.height ?
{paddingBottom:this.props.height} : null;
ifStyle = this.props.height ?
{height:this.props.height, width:this.props.width} : null;
if(this.props.src) {
if(this.props.type === "video") {
return (<div className={responsiveStyle}
style={divStyle}>
<iframe className="embed-responsive-item"
src={ this.props.src }
style={ifStyle}
allowFullScreen={ this.props.allowFullScreen }>
</iframe>
</div>);
} else {
return (<div className={ responsiveStyle }
style={ divStyle }>
<embed frameBorder='0'
src={ this.props.src }
style={ ifStyle }
allowFullScreen={ this.props.allowFullScreen }/>
</div>)
}
}
else {
return null;
}
}
});
module.exports = Media;
这个片段根据传递的媒体类型返回iframe或embed元素。响应式类基于 Bootstrap 提供的类,并将媒体自动缩放到任何视口。
打开app.jsx并添加以下导入:
import Media from './components/media;
然后,将< Media src="img/x7cQ3mrcKaY"/>添加到render()方法(或你想要显示的任何其他视频)。你也可以添加wideScreen可选属性来以 16 x 9 的尺寸显示视频,以及allowFullScreen,如果你希望允许用户全屏查看视频。你还可以传递height和width参数,以便使其与你的布局保持一致。
当然,这个组件不仅限于视频,任何类型的媒体内容都可以。例如,尝试用以下代码替换app.jsx中的代码:
'use strict';
import React from 'react';
import ReactDom from 'react-dom';
import Media from './components/media.jsx';
import { Grid, Row, Col } from "react-bootstrap";
ReactDom.render((<Grid fluid={true}>
<Row>
<Col xs={12} md={6}>
<Media type="image/svg+xml"
src="img/Black-crowned_Night_Heron.svg" />
</Col>
<Col xs = { 12 } md = { 6 }>
<Media
type = "video"
src = "//www.youtube.com/embed/x7cQ3mrcKaY" />
</Col>
</Row>
</Grid>),
document.getElementById( 'container' )
);
这将显示一个两列的网格,一列是 SVG,另一列是 YouTube 的视频。
创建响应式按钮
按钮在任何 Web 应用中都很常见。它们负责你在应用中进行的许多用户交互,因此了解你可用到的多种按钮类型是很有价值的。
您可以选择的选项包括超小号、小号和大号按钮,全宽按钮,激活和禁用状态,分组,上拉和下拉,以及加载状态。让我们看看代码。
创建一个名为examples/buttons.jsx的文件,并添加以下代码:
'use strict';
import React from 'react';
import { Button, ButtonGroup, ButtonToolbar, DropdownButton,
MenuItem, SplitButton } from 'react-bootstrap';
const Buttons = React.createClass({
getInitialState() {
return {
isLoading: false
}
},
setLoading() {
this.setState({ isLoading: true });
setTimeout(() => {
this.setState({ isLoading: false });
}, 2000);
},
当我们执行setLoading时,我们将isLoading状态设置为true,然后,我们设置一个计时器,在 2 秒后将状态重置为false:
render() {
let isLoading = this.state.isLoading;
return (
<div>
<h2> Buttons </h2>
<h5> Simple buttons </h5>
<ButtonToolbar>
ButtonToolbar和ButtonGroup是您可以用于分组按钮的两个组件。它们之间的主要区别在于ButtonToolbar将保留多个内联按钮或按钮组的间距,而ButtonGroup则不会:
<Button> Default </Button>
<Button bsStyle = "primary"> Primary </Button>
<Button bsStyle = "success"> Success </Button>
<Button bsStyle = "info"> Info </Button>
<Button bsStyle = "warning"> Warning </Button>
<Button bsStyle = "danger"> Danger </Button>
<Button bsStyle = "link"> Link </Button>
样式提供了视觉重量并标识了按钮的主要操作。最后的样式link使按钮看起来像普通链接,但保持了按钮的行为:
</ButtonToolbar>
<h5>Full-width buttons</h5>
<ButtonToolbar>
<Button
bsStyle = "primary"
bsSize = "xsmall"
block>
Extra small block button (full-width)
</Button>
<Button
bsStyle = "info"
bsSize = "small"
block>
Small block button (full-width)
</Button>
<Button
bsStyle = "success"
bsSize = "large"
block>
Large block button (full-width)
</Button>
</ButtonToolbar>
添加block属性将其转换为全宽按钮。bsSize属性适用于所有按钮,可以是xsmall、small或large:
<h5> Active, non-active and disabled buttons </h5>
<ButtonToolbar>
<Button> Default button - Non-active </Button>
<Button active> Default button – Active </Button>
要设置按钮的激活状态,只需添加active属性:
<Button disabled> Default button – Disabled </Button>
</ButtonToolbar>
添加disabled属性会使按钮看起来不可点击,通过将其透明度降低到原始的 50%:
<h5>Loading state</h5>
<Button
bsStyle = "primary"
disabled = { isLoading }
onClick = { !isLoading ? this.setLoading : null }>
{ isLoading ? 'Loading...' : 'Loading state' }
</Button>
此按钮接收一个click动作并将其传递给setLoading函数,如前所述代码所示。只要isLoading状态设置为false,它将有一个disabled属性并显示文本加载中…:
<h5> Groups and Toolbar </h5>
<ButtonToolbar>
<ButtonGroup>
<Button> 1 </Button>
<Button> 2 </Button>
<Button> 3 </Button>
</ButtonGroup>
<ButtonGroup>
<Button> 4 </Button>
<Button> 5 </Button>
</ButtonGroup>
</ButtonToolbar>
此部分展示了您如何结合ButtonToolbar和ButtonGroup来保持两套或多套视觉上分组的按钮。您还可以添加到ButtonGroup的一个引人注目的效果是vertical属性,它将按钮堆叠而不是并排显示:
<h5> Dropdown buttons </h5>
<ButtonToolbar>
<DropdownButton
title = "Dropdown"
id = "bg-nested-dropdown">
<MenuItem
bsStyle = "link"
eventKey = "1">
Dropdown link
</MenuItem>
<MenuItem
bsStyle = "link"
eventKey = "2">
Dropdown link
</MenuItem>
</DropdownButton>
我们最终的按钮集合展示了您可以通过哪些方式添加下拉和分割按钮效果。此前的代码是最简单的下拉按钮集合,您只需将它们包裹在DropdownButton组件内部即可:
<DropdownButton
noCaret
title = "Dropdown noCaret"
id = "bg-nested-dropdown-nocaret">
<MenuItem
bsStyle="link"
eventKey="1">
Dropdown link
</MenuItem>
<MenuItem
bsStyle = "link"
eventKey = "2">
Dropdown link
</MenuItem>
</DropdownButton>
下一个集合添加了noCaret属性,以展示您如何创建一个点击时不会显示任何视觉提示的下拉按钮:
<DropdownButton
dropup
title = "Dropup"
id="bg-nested-dropup">
<MenuItem
bsStyle = "link"
eventKey = "1">
Dropdown link
</MenuItem>
<MenuItem
bsStyle = "link"
eventKey = "2">
Dropdown link
</MenuItem>
</DropdownButton>
您可以通过添加dropup属性将下拉菜单转换为上拉菜单:
<SplitButton
bsStyle = "success"
title="Splitbutton down"
id="successbutton">
<MenuItem eventKey = "1"> Action </MenuItem>
<MenuItem eventKey = "2"> Another action </MenuItem>
</SplitButton>
<SplitButton
dropup
bsStyle = "success"
title = "Splitbutton up"
id = "successbutton">
<MenuItem eventKey = "1"> Action </MenuItem>
<MenuItem eventKey = "2"> Another action </MenuItem>
</SplitButton>
同样,您可以通过将按钮包裹在SplitButton组件内部而不是DropdownButton组件内部来创建分割按钮效果:
</ButtonToolbar>
</div>
);
}
});
module.exports = Buttons;
下面的截图显示了此代码的输出:
创建动态进度条
进度条可以用来显示用户进程的状态以及完成前还有多少工作要做。
创建一个名为examples/progressbars.jsx的文件,并添加此代码:
'use strict';
import React from 'react';
import { ProgressBar } from 'react-bootstrap';
let tickInterval;
在此组件中,我们想要为进度条创建一个间隔。我们创建一个变量来保存间隔,因为我们希望在unmount方法中稍后访问它:
const ProgressBars = React.createClass ({
getInitialState() {
return {
progress: 0
}
},
componentDidMount() {
tickInterval = setInterval(this.tick, 500);
},
componentWillUnmount() {
clearInterval(tickInterval);
},
当我们挂载组件时,我们创建一个间隔,告诉它每 500 毫秒执行一次我们的 tick 方法:
tick() {
this.setState({ progress: this.state.progress < 100 ?
++this.state.progress : 0 })
},
tick() 方法通过向内部 progress 变量添加 1 来更新它,如果它小于 100,或者如果它不是,则重置为 0:
render() {
return (
<div>
<h2> ProgressBars </h2>
<ProgressBar
active
now = { this.state.progress } />
<ProgressBar
striped
bsStyle = "success"
now = { this.state.progress } />
<ProgressBar
now = { this.state.progress }
label = "%(percent)s%" />
所有的进度条现在将更新并显示一个不断增加的进度,直到完全填满,然后重置为 empty。
如果您应用 active 属性,进度条将动画化。您还可以通过添加 striped 属性来提供条纹。
您可以添加自己的自定义标签或使用以下之一来插值当前值:
-
%(percent)s%: 这添加了一个百分比值 -
%(bsStyle)s: 这显示了当前的样式 -
%(now)s: 这显示了当前值 -
%(max)s: 这显示了最大值(通过设置max={x}来配合,其中 x 是任何数字) -
%(min)s: 这表示最小值(通过设置min={x}来配合,其中 x 是任何数字)
让我们看看下面的代码片段:
<ProgressBar>
<ProgressBar
bsStyle = "warning"
now = { 20 }
key = { 1 }
label = "System Files" />
<ProgressBar
bsStyle="danger"
active
striped
now = { 40 }
key = { 3 }
label = "Crunching" />
</ProgressBar>
可以通过将它们包裹在 ProgressBar 中来嵌套多个进度条:
</div>
);
}
});
module.exports = ProgressBarExample;
创建流体轮播图
轮播图是一个用于循环显示元素(如幻灯片)的组件。其功能相当复杂,但可以用很少的代码实现。
让我们看看它。创建一个名为 examples/carousels.jsx 的新文件并添加此代码:
'use strict';
import React from 'react';
import {Carousel,CarouselItem} from 'react-bootstrap';
const Carousels = React.createClass({
getInitialState() {
return {
index: 0,
direction: null
};
},
handleSelect(selectedIndex, selectedDirection) {
this.setState({
index: selectedIndex,
direction: selectedDirection
});
},
方向可以是 prev 或 next:
render() {
return (
<div>
<h2>Uncontrolled Carousel</h2>
<Carousel>
<CarouselItem>
<img
width = "100%"
height = { 150 }
alt = "600x150"
src = "http://placehold.it/600x150"/>
<div className = "carousel-caption">
<h3> Slide label 1 </h3>
<p> Lorem ipsum dolor sit amet </p>
</div>
</CarouselItem>
<CarouselItem>
<img
width = "100%"
height = { 150 }
alt = "600x150"
src = "http://placehold.it/600x150"/>
<div className = "carousel-caption">
<h3> Slide label 2 </h3>
<p> Nulla vitae elit libero, a pharetra augue. </p>
</div>
</CarouselItem>
</Carousel>
我们创建的第一个轮播图是不受控的。也就是说,它自动动画,但可以被用户手动触发:
<h2>Controlled Carousel</h2>
<Carousel activeIndex = {this.state.index}
direction = {this.state.direction}
onSelect = {this.handleSelect}>
第二个轮播图是受控的,并且不会自动动画,直到用户点击左侧或右侧的箭头。当用户点击其中一个箭头时,handleSelect 函数会接收到期望的方向并动画化轮播图。
默认情况下,轮播图使用包含的 Glyphicon 集中的左右箭头图标。您可以使用 nextIcon 和 prevIcon 属性指定自己的箭头:
<CarouselItem>
<img
width = "100%"
height = {150}
alt = "600x150"
src = "http://placehold.it/600x150"/>
<div className = "carousel-caption">
<h3> Slide label 1 </h3>
<p> Lorem ipsum dolor sit amet </p>
</div>
</CarouselItem>
<CarouselItem>
<img
width = "100%"
height = {150}
alt = "600x150"
src = "http://placehold.it/600x150"/>
<div className = "carousel-caption">
<h3> Slide label 2 </h3>
<p> Nulla vitae elit libero, a pharetra augue. </p>
</div>
</CarouselItem>
</Carousel
</div>
);
}
});
module.exports = CarouselExample;
与流体图像和 picture 元素一起工作
响应式图像的主题是一个充满困难的话题。一方面,有简单缩放和以响应方式呈现图像的问题。另一方面,您通常会希望为小型设备下载较小的图像,并为桌面设备提供较大的图像。
让我们先看看如何设置响应式代码。
创建一个名为 examples/images.jsx 的文件,并添加以下代码:
'use strict';
import React from 'react';
import { Image, Thumbnail, Button, Grid, Row, Col }
from 'react-bootstrap';
const Images = React.createClass ({
render() {
return (
<div>
<h2> Images </h2>
<Grid fluid = { true }>
<Row>
<Col xs={ 12 } sm={ 4 }>
<Image src="img/140x180" portrait />
</Col>
<Col xs={ 12 } sm={ 4 }>
<Image src="img/140x180" circle />
</Col>
<Col xs={ 12 } sm={ 4 }>
<Image src="img/140x180" rounded />
</Col>
</Row>
我们将首先定义 Grid,然后创建一组三列(在小型移动设备上为两列)。在列中,我们添加三张图片,有三种可用的属性:portrait、circle 和 rounded。
这将很好地适应任何视口。
接下来,我们创建另一行,这次使用一个名为Thumbnail的组件而不是Image组件。这个组件使我们能够轻松地添加与你的图片一起的任何 HTML 数据,例如标题、描述和操作按钮:
<Row>
<Col xs={ 12 } sm={ 4 }>
<Thumbnail
src = "http://placehold.it/140x180">
<h3> Thumbnail label </h3>
<p> Description </p>
<p>
<Button
bsSize = "large"
bsStyle = "danger">
Button
</Button>
</p>
</Thumbnail>
</Col>
<Col xs={ 12 } sm={ 4 }>
<Thumbnail
src="img/140x180">
<h3> Thumbnail label </h3>
<p> Description </p>
<p>
<Button
bsSize = "large"
bsStyle = "warning">
Button
</Button>
</p>
</Thumbnail>
</Col>
<Col xs={ 12 } sm={ 4 }>
<Thumbnail
src="img/140x180">
<h3> Thumbnail label </h3>
<p> Description </p>
<p>
<Button
bsSize = "large"
bsStyle = "info">
Button
</Button>
</p>
</Thumbnail>
</Col>
</Row>
</Grid>
</div>
);
}
});
module.exports = Images;
要在你的应用中显示此组件,打开app.jsx并添加以下导入:
import Images from './examples/images.jsx';
然后,将<Images />添加到render()方法中。
减少你的足迹
当为小型设备提供服务时,限制它们需要下载以查看应用内容的数据量是一个好主意。毕竟,如果你的目标受众是手机用户,提供可能需要几秒钟才能下载的高分辨率图片可能不是一个好主意。
目前还没有针对这个问题的通用解决方案,但有一些相当不错的处理方法。让我们看看一些你可以用来解决这个问题的方式。
一种选择是查看用户查看你的应用的设备。这被称为嗅探,通常意味着识别用户代理和视口大小等指标,以便为桌面和手机提供不同的图片。这种解决方案的问题在于它并不非常可靠。用户代理可以被伪造,而小的视口大小并不自动意味着用户在小型设备上浏览你的应用。
另一种选择是媒体查询(我们将在稍后更深入地讨论)。这对于静态元素效果很好,例如你可以将其放置在菜单、工具栏和其他固定内容中的图片,但对于动态元素则不然。
最近出现的一个相当不错的解决方案是使用一个名为<picture>的新元素。此元素允许你动态地使用媒体查询的概念,并根据你指定的要求加载不同的图片。
让我们看看这在 HTML 中是如何工作的:
<picture>
<source
media="(min-width: 750px)"
srcSet="http://placehold.it/500x300" />
<source
media="(min-width: 375px)"
srcSet="http://placehold.it/250x150" />
<img
src="img/100x100"
alt="The default image" />
</picture>
如果浏览器视口至少为 750 像素,此块将下载并显示大图片;如果视口至少为 375 像素,则显示中等图片;如果不符合这些条件,则显示小图片。此元素可以优雅地缩放,如果用户使用的浏览器不支持此元素,它将显示<img>元素中命名的图片。
此处的媒体查询相对简单。你可以用你的查询和include属性相当有创意,例如方向和像素比。以下是一个匹配竖直模式下的智能手机的媒体查询:
only screen and (max-device-width: 721px) and (orientation: portrait) and (-webkit-min-device-pixel-ratio: 1.5), only screen and (max-device-width: 721px) and (orientation: portrait) and (min-device-pixel-ratio: 1.5), only screen and (max-width: 359px)
这个匹配在竖直模式下的视网膜显示屏的表格:
only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: portrait) and (-webkit-min-device-pixel-ratio: 2)
创建一个 React 化的图片元素
我们想在 ReactJS 的范围内工作,所以我们不想使用像之前那样的段,我们跳出常规使用纯 HTML 而不是 ReactJS 组件来显示我们的图片。然而,由于它不存在,我们需要创建一个。
对于这个模块,我们需要另一个依赖项,所以请继续在你的终端中执行以下命令(如果你还没有这样做的话):
npm install --save classnames
接下来,在你的components文件夹中创建一个新文件,命名为picture.jsx。让我们从以下代码开始:
'use strict';
import React from 'react';
import ClassNames from 'classnames';
const Picture = React.createClass ({
propTypes: {
imgSet: React.PropTypes.arrayOf(
React.PropTypes.shape({
media: React.PropTypes.string.isRequired,
src: React.PropTypes.string.isRequired
}).isRequired
),
defaultImage: React.PropTypes.shape ({
src: React.PropTypes.string.isRequired,
alt: React.PropTypes.string.isRequired
}).isRequired,
rounded: React.PropTypes.bool,
circle: React.PropTypes.bool,
thumbnail: React.PropTypes.bool,
portrait: React.PropTypes.bool,
width: React.PropTypes.any,
height: React.PropTypes.any
},
getDefaultProps() {
return {
imgSet: [],
defaultImage: {},
rounded: false,
circle: false,
thumbnail: false,
portrait: false,
width: "auto",
height: "auto"
}
},
我们将首先添加一组property类型及其默认值。请注意,其中两个值,imgSet和defaultImage,被定义为形状。这是因为我们想在对象内部定义property类型,并指导 ReactJS 在忘记某些值或传递错误值类型时通知我们。
我们还需要一些特定于 Bootstrap 的值,你可能从之前的Image示例中认出了它们。由于我们正在创建自己的图片组件,我们希望能够添加诸如rounded和portrait之类的属性,这就是我们确保这样做的方式:
render() {
let classes = ClassNames ({
'img-responsive': this.props.responsive,
'img-portrait': this.props.portrait,
'img-rounded': this.props.rounded,
'img-circle': this.props.circle,
'img-thumbnail': this.props.thumbnail
});
在这里,我们使用ClassNames组件添加正确的 Bootstrap 类,如果我们传递之前提到的属性:
return (
<picture>
{ this.props.imgSet.map((img, idx)=> {
return <source key={ idx }
media={ img.media }
srcSet={ img.src } />
}) }
对于imgSet中的每个元素,我们添加一个source项:
{ <img className={ classes }
src={ this.props.defaultImage.src }
width={ this.props.width }
height={ this.props.height }
alt={ this.props.defaultImage.alt }/> }
然后,我们添加默认图片以及width和height属性。如果您没有指定宽度和高度,它将设置为auto。通常设置宽度和高度是一个好主意,因为这使浏览器更容易在最初布局页面,并防止在文档在图片完全下载之前提供时发生跳动:
</picture>
)
}
});
module.exports = Picture;
让我们在examples/images.jsx中使用这个新组件。打开文件并添加此导入:
import Picture from './../components/picture';
在导入行之后立即添加以下变量:
let imgSet = [
{media: "only screen and (min-width: 650px) and (orientation: landscape)", src: "http://placehold.it/500x300"},
{media: "only screen and (min-width: 465px) and (orientation: portrait)", src: "http://placehold.it/200x500"},
{media: "only screen and (min-width: 465px) and (orientation: landscape)", src: "http://placehold.it/250x150"}
];
let defaultImage = {src: "http://placehold.it/100x100",
alt: "The default image"};
最后,在render()方法中</Grid>标签之前添加此代码:
<Row>
<Col xs={12}>
<Picture
imgSet={ imgSet }
defaultImage={ defaultImage }
circle />
</Col>
</Row>
当你在浏览器中重新加载应用程序时,你会在浏览器中看到一个圆形图片,并且根据你的视口大小,你会看到一个尺寸为 500 x 300、200 x 500、250 x 150 或 100 x 100 的图片。调整浏览器大小并尝试不同的设置,以查看它在实际中的工作情况。
创建响应式表单字段
表单很棘手,因为你通常会需要验证输入并在用户做了你没有预料到的事情时提供一些反馈。我们将在这里探讨这两个问题,创建响应式表单并向用户展示反馈。
创建一个新文件,命名为examples/formfields.jsx,并添加此代码:
'use strict';
import React from 'react';
import ClassNames from 'classnames';
import { FormGroup, FormControl, InputGroup, ButtonInput }
from 'react-bootstrap';
const Formfields = React.createClass ({
getInitialState() {
return {
name: '',
email: '',
password: ''
};
},
validateEmail() {
let length = this.state.email.length;
let validEmail = this.state.email
.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
if (validEmail) return 'success';
else if (length > 5) return 'warning';
else if (length > 0) return 'error';
},
当这个函数执行时,它会从状态中获取电子邮件字符串,然后使用一个相当复杂的regex查询来检查电子邮件是否以正确的格式编写。它几乎不是万无一失的,但已经足够好了。如果电子邮件被认为是有效的,函数将返回'success'。如果不是,它将返回'error'或'warning',两者都向用户提供视觉线索,表明电子邮件输入不正确:
validatePassword() {
let pw = this.state.password;
if (pw.length < 5) return null;
let containsNumber = pw.match(/[0-9]/);
let hasCapitalLetter = pw.toLowerCase() !== pw;
return containsNumber && hasCapitalLetter ? 'success' : 'error';
},
这个简单的验证函数检查密码是否包含数字和大小写字母。如果包含并且长度为五个字符或更多,它将返回'success'。如果不包含,它将返回'error':
handlePasswordChange() {
this.setState({password: this.refs.inputPassword.getValue()})
},
handleEmailChange() {
this.setState({email: this.refs.inputEmail.getValue()})
},
这两个函数通过this.refs获取输入值并将它们存储为状态变量。如果你想了解更多关于 refs 的信息,请回到第一章,ReactJS 初探:
validateForm() {
return (this.validateEmail() === this.validatePassword());
},
如果两个验证函数都返回'success'字符串,此函数将返回true:
render() {
return (
<form>
<Input type="text" label="Name"
placeholder="Enter your name"/>
<Input type="email" label="Email Address"
placeholder="Enter your email"
onChange={this.handleEmailChange}
ref="inputEmail"
bsStyle={this.validateEmail()}/>
第二个输入字段有几个有趣的属性。它有一个onChange属性,确保在字段中输入新内容时调用一个函数。它有一个ref属性,这样就可以稍后通过this.refs找到它。最后,它有一个bsStyle属性,可以接收null、'success'、'warning'或'error'。它将在'success'时将边框变为绿色,在'warning'时变为黄色,在'error'时变为红色:
<Input type="password"
label="Password"
onChange={ this.handlePasswordChange }
ref="inputPassword"
bsStyle={ this.validatePassword() }/>
<ButtonInput type="submit"
value="Submit this form"
disabled={ !(this.validateForm()) }
/>
只要验证函数不返回'success',此按钮就会禁用。当它们这样做时,用户被允许继续并按下按钮:
</form>
);
}
});
module.exports = Forms;
要在你的应用中显示此组件,打开app.jsx并添加此导入:
import Formfields from './examples/formfields.jsx';
然后,将<Formfields />添加到render()方法中。
我们在这里创建的Formfields组件可以通过添加更多输入字段和验证器进行扩展。让我们简要地看看你可以使用的不同输入类型:
选择框:
<Input type="select"
label="Select"
placeholder="select"
ref="inputSelect">
<option value="1">First select</option>
<option value="2">Second select</option>
</Input>
<Input type="select"
label="Multiple Select"
multiple
ref="inputMultipleSelect">
<option value="1">First select</option>
<option value="2">Second select</option>
</Input>
这两个选择框允许用户一次选择一个或多个项目,通过添加multiple属性。
文件:
<Input type="file" label="File" help="Instructions"/>
help中的文本将在文件上传框下方显示。你可以添加一个onChange处理程序来立即上传文件。
复选框:
<Input type="checkbox"
label="Checkbox"
checked={ this.state.inputCheckBoxOne }
onChange={ this.handleCheckboxChange }
ref={ CheckBoxOne }
readOnly={ false }
ref="inputCheckboxOne"/>
由于 ReactJS 会逐字渲染一切,你需要明确控制你的复选框的选中状态,或者完全将其省略。在上面的代码片段中,我们通过在handleCheckboxChange中设置CheckBoxOne的状态来控制选中状态。
备注
注意,如果你提供了checked属性,你必须提供一个onChange处理程序;否则,ReactJS 将在你的控制台中抛出一个警告。如果你想向复选框提供一个已选值而不控制它,请使用defaultChecked属性代替。
单选按钮:
<Input type="radio"
label="Radio"
checked={ this.state.checkedRadioButton=="RadioOne" }
onChange={ this.handleRadioChange.bind(null,"RadioOne") }
readOnly={ false }/>
<Input type="radio"
label="Radio"
checked={ this.state.checkedRadioButton=="RadioTwo" }
onChange={ this.handleRadioChange.bind(null,"RadioTwo") }
readOnly={ false } />
在表单中,只能选择一个单选按钮。与复选框一样,你可以通过添加checked属性和onChange处理程序来控制选中状态,或者如果你想预先选中单选按钮,可以使用defaultChecked。
在前面的代码片段中,我们使用了bind而不是refs来将值传递给函数。在 JavaScript 中,bind()产生一个新的函数,其this将设置为传递给bind()的第一个参数。我们对此不感兴趣;然而,那只是合成的鼠标点击事件,因此我们将this设置为null,并使用部分函数应用修复绑定到bind的另一个参数。简单来说,我们将单选按钮名称提供给handleRadioChange。
handleRadioChange()函数看起来像这样:
handleRadioChange(val) {
this.setState({ checkedRadioButton: val });
}
我们之所以这样做,是因为除非为每个单选按钮创建一个唯一的onChange处理程序,否则很难知道你需要哪个单选按钮的引用来获取数据。尽管这种情况并不少见,但两种方式都可行。
文本区域:
<Input type="textarea"
label="Text Area"
placeholder="textarea" />
文本区域是输入字段,你可以在此处输入较长的文本段落。如果你需要在文本输入时应用函数,可以添加一个onChange处理程序。
使用 Glyphicons 和 font-awesome 图标
Glyphicons是 Bootstrap 附带的大约 200 个符号集合。我们在本章开头将它们添加到index.html文件中,当时我们从 CDN 获取 Bootstrap,因此它们已经包含在内,并准备好在你的应用程序中使用。
你可以在任何使用文本字符串的地方使用 Glyphicons,因为它们提供的是字体集而不是图像集。
你可以通过以下代码行将它们添加到你的代码中:
import { Glyphicon } from "react-bootstrap";
你可以通过编写<Glyphicon glyph="cloud"/>来添加云图标,或者通过编写<Glyphicon glyph="envelope"/>来添加信封图标。
你可以使用一组特殊属性轻松地将符号添加到输入元素中:addonBefore、addonAfter、buttonBefore或buttonAfter。
例如,如果你想在输入字段前添加美元或欧元符号,该字段作为输入参数接受货币,可以使用如下代码块:
const euro = <Glyphicon glyph = "euro" />;
const usd = <Glyphicon glyph = "usd" />;
<Input type = "text"
addonBefore={ usd }
addonAfter = ".00" />
<Input type = "text"
addonBefore={ euro }
addonAfter = ".00" />
书中附带的所有符号及其外观的完整集合可在本书提供的代码中找到。它在examples文件夹中,文件名为glyphicons.jsx。如果你导入此文件并将其添加到app.jsx中,整个符号集将在你的浏览器中显示。
Bootstrap 还提供了一套名为font awesome的图标。我们在本章开头除了 Glyphicons 外还包含了此库。在构建你的应用程序之前,决定使用 font-awesome 或 Glyphicons 图标是有用的,这样你的用户就少下载一个库。
Font-awesome 库没有与 Glyphicons 相当的组件,所以让我们创建一个。在你的components文件夹中创建一个名为fontawesome的文件,并添加以下代码:
import React from 'react';
const FontAwesome = React.createClass ({
propTypes: {
icon: React.PropTypes.string
},
getDefaultProps() {
return {
icon: ""
}
},
render() {
if(this.props.icon){
return (<i className={ "fa fa-" + this.props.icon } />);
} else {
return null;
}
}
});
module.exports = FontAwesome;
上述代码应该非常熟悉。它的作用是获取一个名为icon的单个属性,并返回一个 font-awesome 图标元素。它不会验证图标是否存在,因此你需要提前熟悉集合中的 500 多个图标。
要使用此组件,请在app.jsx中添加import FontAwesome from './components/fontawesome.jsx';导入语句,然后在你的渲染代码中添加<FontAwesome icon="facebook"/>以显示 Facebook 图标。你可以像使用 Glyphicon 组件一样使用此组件,包括前面的输入元素示例。
创建响应式着陆页
在开发响应式 Web 应用时,您需要在代码中区分小设备和大型设备。让我们创建一个着陆页,并演示您如何使用代码中的视口大小来展示您的应用内容。
这个应用将完全包含在app.jsx中。删除app.jsx中的现有代码(或者如果您想保留所做的副本,可以将其重命名为example.jsx),并且删除app.css中的所有代码。将以下内容添加到app.jsx中:
'use strict';
import React from 'react';
import ReactDom from 'react-dom';
import { Grid, Row, Col, Button, Carousel, CarouselItem,
FormGroup, FormControl, InputGroup } from "react-bootstrap";
import FontAwesome from './components/fontawesome.jsx';
我们将依赖于我们之前创建的FontAwesome组件:
const App = React.createClass ({
getInitialState() {
return {
vHeight: 320,
vWidth: 480
}
},
我们将视口的高度和宽度存储为状态变量:
componentDidMount() {
window.addEventListener('resize', (e) => {
this.calculateViewport();
}, true);
this.calculateViewport();
},
状态变量最初将被设置为 320 x 480,但一旦应用挂载,我们将计算实际值。首先,我们将添加一个事件监听器,该监听器将在视口发生变化时执行一个函数。其次,我们将运行该函数第一次:
calculateViewport() {
let vHeight = Math.max(document.documentElement.clientHeight,
window.innerHeight || 0);
let vWidth = Math.max(document.documentElement.clientWidth,
window.innerWidth || 0);
this.setState({
vHeight: vHeight,
vWidth: vWidth
})
},
视口计算将使用最合适的值并将其存储为组件的状态:
renderSmallForm() {
return (
<form style={{ paddingTop: 15 }}>
<div
style={{
width: (this.state.vWidth/2),
textAlign:'center',
margin:'0 auto'
}}>
<FormGroup>
<FormControl
type="text"
bsSize="large"
placeholder="Enter your email address" />
<br/>
<Button
bsSize="large"
bsStyle="primary"
onClick={ this.handleClick }>
Sign up
</Button>
</FormGroup>
</div>
</form>);
},
我们将为着陆页上的表单创建两个render函数。请注意,我们将在双大括号内设置所有 CSS,并且宽度将自动设置为视口宽度的一半:
renderLargeForm() {
return (
<form style={{ paddingTop:30 }}>
<div
style = {{ width:(this.state.vWidth/2),
textAlign:'center',
margin:'0 auto' }}>
<FormGroup>
<FormControl
type="text"
bsSize="large"
placeholder = "Enter your email address" /><InputGroup.Button>
<Button
bsSize = "large"
bsStyle = "primary"
onClick = { this.handleClick }>
Sign up
</Button>
</InputGroup.Button>
</FormGroup>
</div>
</form>);
},
小表单和大表单之间的主要区别在于,大表单使用输入组在同一水平线上显示输入字段和提交按钮。小表单将按钮放在输入字段下方。
我们在我们的表单中添加了一个onClick处理程序,所以让我们继续添加这个函数:
handleClick(event){
// process the input any way you like
console.log(event.target.form[0].value);
},
我们实际上不会处理点击事件以外的日志记录值,但这个函数展示了如何根据用户点击提交按钮时发生的事件从表单中获取值。
接下来,我们将编写用于社交图标的功能。
renderSocialIcons() {
return (<Row>
<Col xs={12} style={{fontSize:32,paddingTop:35,position:'fixed',
bottom:10,textAlign:'center'}}>
<a href="#" style={{color:'#eee'}}><FontAwesome icon="google-plus"/></a>
<a href="#" style={{paddingLeft:15,color:'#eee'}}><FontAwesome icon="facebook"/></a>
<a href="#" style={{paddingLeft:15,color:'#eee'}}><FontAwesome icon="twitter"/></a>
<a href="#" style={{paddingLeft:15,color:'#eee'}}><FontAwesome icon="github"/></a>
<a href="#" style={{paddingLeft:15,color:'#eee'}}><FontAwesome icon="pinterest"/></a>
</Col>
</Row>)
},
社交图标使用font-awesome库中的图像。字体大小设置为 32 像素,以便在智能手机上显示大而清晰的按钮,便于用手指点击:
render() {
let vWidth = this.state.vWidth;
let vHeight = this.state.vHeight;
let formCode = vWidth <= 480 ?
this.renderSmallForm() : this.renderLargeForm();
let socialIcons = vHeight >= 320 ?
this.renderSocialIcons() : null;
这个简单的代码片段在视口高度小于 320 像素时切换小表单和大表单的渲染,并隐藏社交图标:
return (<div>
<Grid fluid style = {{
margin: '0 auto',
width: '100%',
minHeight: '100%',
background: '#114',
color: '#eee',
overflow: 'hidden'
}}>
<Row style = {{ height: vHeight }}>
<Col
sm = {12}
style = {{ marginTop: (vHeight/20) }}>
页眉的顶部将设置为等于视口高度的 1/20 的动态像素值:
<h1 style = {{ textAlign: 'center' }}>
Welcome!
</h1>
<div style = {{maxHeight: 250,
maxWidth: 500,
margin: '0 auto' }}>
<Carousel>
<CarouselItem
style = {{ maxHeight: 250,
maxWidth: 500 }}>
<img
width = "100%"
alt = "500x200" src="img/008800?text=It+will+amaze+you"/>
</CarouselItem>
<CarouselItem
style = {{ maxHeight: 250,
maxWidth: 500 }}>
<img
width="100%"
alt="500x200" src="img/f0f0f0?text=It+will+excite+you"/>
</CarouselItem>
<CarouselItem
style = {{ maxHeight: 250,
maxWidth: 500 }}>
<img
width = "100%"
alt = "500x200" src="img/eeeeee?text=Sign+up+now!"/>
</CarouselItem>
</Carousel>
</div>
</Col>
<Col xs = { 12 }>
{ formCode }
</Col>
<Col xs = { 12 } >
<p style = {{ textAlign:'center',
paddingTop: 15 }}>
Your email will not be shared and will only be
used once to notify you when the app
launches.
</p>
</Col>
</Row>
{ socialIcons }
这就是我们添加socialIcons变量的方式。它将是一个 ReactJS 元素或null:
</Grid>
</div>)
}
});
ReactDom.render ((
<App />),
document.getElementById( 'container' )
);
我们在这个简单的应用中重用了本章的一些组件,并添加了一些新技术。您可以使用媒体查询和 CSS 得到相同的结果,但您将需要编写更多的代码,并在 JavaScript 和 CSS 之间分割逻辑。在代码中内联编写样式代码看起来可能有些奇怪,但这种方法的主要好处之一是它允许您使用与您的应用其他部分相同的编程语言编写非常高级的样式规则。
摘要
在本章中,我们讨论了创建一个适用于任何设备的响应式 Web 应用的各个方面。我们探讨了可用于 ReactJS 的一些不同框架,并深入研究了如何使用 react-bootstrap 来满足我们的需求。在大多数情况下,我们可以通过使用 React-Bootstrap 的组件来完成任务,但在某些情况下,例如图片和媒体,我们也创建了自定义组件。
最后,我们将之前创建的一些组件和一些新技术结合起来,例如程序化内联样式和事件监听器来处理视口调整大小,从而制作了一个简单、响应式的着陆页。
在下一章中,我们将着手开发一个实时搜索应用。我们将介绍数据存储和高效查询的概念,并为用户提供流畅、响应式的体验。翻到下一页开始工作。
第四章:构建实时搜索应用程序
搜索是大多数应用程序中的一个重要功能。根据您正在开发的应用程序类型,您可能只需设置一个用于查找简单关键词的字段,或者您可能需要深入研究模糊算法和查找表的世界。在本章中,我们将创建一个实时搜索应用程序,该应用程序模仿了网络搜索引擎。我们将处理您输入时出现的快速搜索,显示搜索结果并提供无限滚动功能。我们还将创建自己的搜索 API 来处理我们的请求。
这些技术的应用仅限于您的想象力。在这方面,让我们开始吧。
这些是我们将在本章中介绍的主要主题:
-
创建您自己的搜索 API
-
将您的 API 连接到 MongoDB
-
设置 API 路由
-
基于正则表达式的搜索
-
保护您的 API
-
创建 ReactJS 搜索应用程序
-
设置 react-router 以处理非哈希路由
-
监听事件处理器
-
创建服务层
-
连接到您的 API
-
分页
-
无限滚动
创建您自己的搜索 API
数据获取是一个充满不确定性的话题,实际上并不存在一种让所有人都觉得合理的推荐方法来处理它。
您可以在以下两种主要策略之间进行搜索:直接查询数据源或查询 API。哪一个更具可扩展性和未来性?让我们从您的搜索控制器角度来探讨这个问题。直接查询数据源意味着在您的应用程序内部设置连接器和相关逻辑。您需要构建一个合适的搜索查询,然后通常需要解析结果。您的数据获取逻辑现在与数据源紧密相连。
查询 API 意味着发送一个搜索查询并检索预格式化的结果。现在,您的应用程序与 API 的联系仅是松散的,更换它通常只是更改 API URL 的问题。
通常,建立松散的联系比建立紧密的联系更可取,因此我们将从创建一个 Node.js API 开始,然后再转向将显示搜索结果给用户的 ReactJS 应用程序。
开始使用您的 API
让我们从创建一个空项目开始。创建一个文件夹来存储您的文件,打开终端,并将目录更改为该文件夹。运行 npm init。安装程序将向您提出许多问题,但默认值都是可以接受的,所以请继续按下 Enter 键,直到命令完成。您将留下一个仅包含 package.json 文件的裸骨项目,npm 将使用它来存储您的依赖配置。接下来,通过执行以下命令安装 express、mongoose、cors、morgan 和 body-parser:
npm install --save express@4.12.3 mongoose@4.0.2 body-parser@1.12.3 cors@2.7.1 morgan@1.7.0
Morgan 是一个为自动记录请求和响应而设计的中间件工具。
Mongoose 是一个连接到 MongoDB 的实用工具,MongoDB 是一个非常简单且流行的面向文档的非关系型数据库。它非常适合我们想要创建的 API 类型,因为它在查询速度上表现出色,并且默认输出 JSON 数据。
在继续之前,请确保您已经在系统上安装了 MongoDB。您可以在终端中输入 mongo 来完成此操作。如果已安装,它将显示类似以下内容:
MongoDB shell version: 3.0.7
connecting to: test
>
如果它显示错误或 命令未找到,您在继续之前需要安装 MongoDB。根据您计算机上安装的操作系统,有不同方法可以完成此操作。如果您使用的是 Mac,可以通过发出 brew install mongodb 命令使用 Homebrew 安装 MongoDB。如果您没有 Homebrew,您可以访问 brew.sh/ 获取有关如何安装它的说明。Windows 用户以及不想使用 Homebrew 的 Mac 用户可以通过从 www.mongodb.org/downloads 下载可执行文件来安装 MongoDB。
创建 API
在 root 文件夹中创建一个名为 server.js 的文件,并添加以下代码:
'use strict';
var express = require('express');
var bodyparser = require('body-parser');
var app = express();
var morgan = require('morgan');
var cors = require('cors');
app.use(cors({credentials: true, origin: true}));
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/websearchapi/sites');
这将设置我们的依赖项并使其准备好使用。我们正在使用 cors 库来打开我们的应用以支持跨源请求。当我们不在与应用程序相同的域名和端口上运行 API 时,这是必要的。
我们将创建一个描述我们将要处理的数据类型的模式。在 Mongoose 中,模式映射到 MongoDB 集合,并定义了该集合中文档的形状。
注意
注意,这是 Mongoose 的习惯用法,因为 MongoDB 默认是无模式的。
将此模式添加到 server.js:
var siteSchema = new mongoose.Schema({
title: String,
link: String,
desc: String
});
如您所见,这是一个非常简单的模式,并且所有属性都共享相同的 SchemaType 对象。允许的类型有 String、Number、Date、Buffer、Boolean、Mixed、ObjectId 和 Array。
要使用我们的模式定义,我们需要将我们的 siteSchema 对象转换为我们可以工作的模型。为此,我们将它传递给 mongoose.model(modelName, schema):
var searchDb = mongoose.model('sites', siteSchema);
接下来,我们需要定义我们的路由。我们将从一个简单的搜索路由开始,该路由接受一个标题作为查询并返回一组匹配的结果:
var routes = function (app) {
app.use(bodyparser.json());
app.get('/search/:title', function (req, res) {
searchDb.find({title: req.params.title}, function (err, data) {
if (err) return res.status(500)
.send({
'msg': 'couldn\'t find anything'
});
res.json(data);
});
});
};
让我们通过启动服务器来完成它:
var router = express.Router();
routes(router);
app.use('/v1', router);
var port = process.env.PORT || 5000;
app.listen(port, function () {
console.log('server listening on port ' + (process.env.PORT || port));
});
在这里,我们告诉 express 使用我们定义的路由,并使用 v1 作为前缀。API 的完整路径将是 http://localhost:5000/v1/search/title。您现在可以通过执行 node server.js 来启动 API。
我们已经将 process.env 添加到一些变量中。这样做是为了在启动应用程序时轻松覆盖这些值。如果我们想以端口 2999 启动应用程序,我们需要使用 PORT=2999 node server.js 来启动应用程序。
导入文档
将文档插入到 MongoDB 集合中并不复杂。您通过终端登录 MongoDB,选择数据库,然后运行 db.collection.insert({})。手动插入文档看起来是这样的:
$ mongo
MongoDB shell version: 3.0.7
connecting to: test
> use websearchapi
switched to db websearchapi
> db.sites.insert({"title": ["Algorithm Design Paradigms"], "link": ["http://www.csc.liv.ac.uk/~ped/teachadmin/algor/algor.html"], "desc": ["A course by Paul Dunne at the University of Liverpool. Slides and notes in HTML and PS.\r"]})
WriteResult({ "nInserted" : 1 })
>
这当然会花费很多时间,而且制作一组标题、链接和描述并不是一项特别有成效的工作。幸运的是,有许多免费和开源的集合可供我们使用。其中一个数据库是dmoz.org,我已经下载了数据库的一个样本选择,并以 JSON 格式在websearchapi.herokuapp.com/v1/sites.json上提供。下载这个集合,并使用mongoimport工具导入,如下所示:
mongoimport --host localhost --db websearchapi --collection sites < sites.json
执行时,它将在你的 API 数据库中放置 598 个文档。
查询 API
一个get查询可以通过你的浏览器执行。只需输入地址和样本 JSON 文件中的一个标题,例如,http://localhost:5000/v1/search/CoreChain。
你也可以使用命令行和像cURL或HTTPie这样的工具。后者旨在使与 Web 服务的命令行交互比 cURL 等人性化,因此绝对值得检查,我们将在本章中使用它来测试我们的 API。
这是先前的查询使用 HTTPie 的输出:
$ http http://localhost:5000/v1/search/CoreChain
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 144
Content-Type: application/json; charset=utf-8
Date: Thu, 05 May 2016 11:09:48 GMT
ETag: W/"90-+q3XcPaDzte23IiyDJxmow"
X-Powered-By: Express
[
{
"_id": "56336529aed5e6116a772bb0",
"desc": "JavaScript library for displaying graphs.\r",
"link": "http://www.corechain.com/",
"title": "CoreChain"
}
]
这很好,但请注意,我们创建的路由要求标题完全匹配。搜索corechain或Corechain将不会返回任何结果。查询Cubism.js将返回一个结果,但*Cubism*将不会返回任何结果。
显然,这不是一个非常友好的查询 API。
创建通配符搜索
引入通配符搜索可以使 API 更易于使用,但你不能使用传统的基于 SQL 的方法,例如LIKE,因为 MongoDB 不支持这些类型的操作。
另一方面,MongoDB 完全支持正则表达式,因此可以构建一个模仿LIKE的查询。
在 MongoDB 中,你可以使用正则表达式对象创建正则表达式:
{ <field>: /pattern/<options> }
你也可以使用以下任何一种语法创建正则表达式:
{ <field>: { $regex: /pattern/, $options: '<options>' } }
{ <field>: { $regex: 'pattern', $options: '<options>' } }
{ <field>: { $regex: /pattern/<options> } }
以下<options>可用于与正则表达式一起使用:
-
i:这是为了对大小写不敏感,以匹配大写和小写字符。 -
m:对于包含锚点(即,^表示开始和$表示结束)的模式,对于多行值的字符串,在每行的开始或结束处匹配锚点。如果没有这个选项,这些锚点将只匹配字符串的开始或结束。 -
x:这是“扩展”功能,可以忽略$regex模式中的所有空白字符,除非它们被转义或包含在character类中。 -
s:这允许点字符(.)匹配所有字符,包括换行符。
使用x和s需要与$options语法一起使用$regex。
现在我们知道了这些,让我们先创建一个通配符查询:
app.get('/search/:title', function (req, res) {
searchDb.find({title:
{ $regex: '^' + req.params.title + '*', $options: 'i' } },
function (err, data) {
res.json(data);
});
});
注意
记住每次更改查询逻辑时都要重新启动您的节点服务器实例。您可以通过使用键盘快捷键(如 CTRL + C(Mac))中断实例,然后再次运行 node server.js 来完成此操作。
此查询返回任何以搜索词开头的标题,并且它将执行不区分大小写的搜索。
如果您移除第一个锚点(^),它将匹配字符串中该词的所有出现:
app.get('/search/:title', function (req, res) {
searchDb.find({title:
{ $regex: req.params.title +'*', $options: 'ix' } },
function (err, data) {
res.json(data);
});
});
这是我们将用于快速搜索的查询。它将返回 立体主义、cubism 和甚至 ubi 的命中:
$ http http://localhost:5000/v1/search/ubi
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Connection: keep-alive
Content-Length: 1235
Content-Type: application/json; charset=utf-8
Date: Thu, 05 May 2016 11:07:00 GMT
ETag: W/"4d3-Pr1JAiSI46vMRz2ogRCF0Q"
Vary: Origin
X-Powered-By: Express
[
{
"_id": "572b29507d406be7852e8279",
"desc": "The component oriented simple scripting language with a robust component composition model.\r",
"link": "http://www.lubankit.org/",
"title": "Luban"
},
{
"_id": "572b29507d406be7852e82a4",
"desc": "A specification of a new 'bubble sort' in three or more dimensions, with illustrative images.\r",
"link": "http://www.tropicalcoder.com/3dBubbleSort.htm",
"title": "Three Dimensional Bubble Sort"
},
{
"_id": "572b29507d406be7852e82ab",
"desc": "Comprehensive list of publications by L. Barthe on modelling from sketching, point based modelling, subdivision surfaces and implicit modelling.\r",
"link": "http://www.irit.fr/~Loic.Barthe/",
"title": "Publications by Loic Barthe"
},
{
"_id": "572b29507d406be7852e8315",
"desc": "D3 plugin for visualizing time series.\r",
"link": "http://square.github.io/cubism/",
"title": "Cubism.js"
},
{
"_id": "572b29507d406be7852e848a",
"desc": "Browserling and Node modules.\r",
"link": "http://substack.net/",
"title": "Substack"
},
{
"_id": "572b29507d406be7852e848d",
"desc": "Google tech talk presented by Ryan Dahl creator of the node.js. Explains its design and how to get started with it.\r",
"link": "https://www.youtube.com/watch?v=F6k8lTrAE2g",
"title": "Youtube : Node.js: JavaScript on the Server"
}
]
这对我们现在正在构建的应用类型来说已经足够了。构建正则表达式有许多方法,您可以根据需要进一步细化它。通过实现 soundex、模糊匹配 或 Levenshtein 距离,可以实现更高级的匹配,尽管 MongoDB 都不支持这些。
Soundex 是一种音位算法,用于通过英语中的发音来索引名称。当您想要进行名称查找并允许用户在拼写略有差异的情况下找到正确结果时,它是非常合适的。
模糊匹配 是一种寻找与字符串近似匹配而不是精确匹配的字符串的技术。匹配的接近程度是通过将字符串转换为精确匹配所需的操作来衡量的。一个众所周知且经常使用的算法是 Levenshtein。这是一个简单的算法,可以提供良好的结果,但它不受 MongoDB 支持。因此,必须通过检索整个结果集然后对搜索查询中的所有字符串应用算法来测量 Levenshtein 距离。操作的速度会随着数据库中文档数量的线性增长而增长,所以除非您有非常小的文档集,否则这很可能不值得做。
如果您想要这些功能,您需要另寻他处。Elasticsearch (www.elastic.co/) 是一个值得考虑的良好替代品。您可以将我们刚刚创建的节点 API 与后端的 Elasticsearch 实例轻松结合,而不是使用 MongoDB,或者两者的组合。
保护您的 API
目前,如果您将其上线,您的 API 对任何人都是可访问的。这不是一个理想的情况,尽管您可以争辩说,由于您只支持 GET 请求,这并不比建立一个简单的网站有太大的不同。
假设您在某个时候添加了 PUT 和 DELETE。您肯定希望保护它,防止任何人完全访问。
让我们看看通过向我们的应用程序添加令牌来简单保护它的方法。我们将使用 Node.js 身份验证模块 Passport 来保护我们的 API。Passport 有超过 300 种不同适用性的策略。我们将选择令牌策略,因此请安装以下两个模块:
npm install --save passport@0.3.0 passport-http-bearer@1.0.1
在 index.js 文件开头添加以下导入语句:
var passport = require('passport');
var Strategy = require('passport-http-bearer').Strategy;
接下来,在 mongoose.connect 行下面添加以下代码:
var appToken = '1234567890';
passport.use(new Strategy(
function (token, cb) {
console.log(token);
if (token === appToken) {
return cb(null, true);
}
return cb(null, false);
})
);
你还需要更改路由,所以将搜索路由替换为以下内容:
app.get('/search/:title',
passport.authenticate('bearer', {session: false}),
function (req, res) {
searchDb.find({title: { $regex: '^' + req.params.title + '*', $options: 'i' } },
function (err, data) {
if(err) return console.log('find error:', err);
if(!data.length)
return res.status(500)
.send({
'msg': 'No results'
})
res.json(data);
});
});
当你重新启动应用时,现在请求将需要用户发送一个包含内容为 1234567890 的 bearer token。如果令牌正确,应用将返回 true 并执行查询;如果不正确,它将返回一个简单的消息说 未授权:
$ http http://localhost:5000/v1/search/react 'Authorization:Bearer 1234567890'
Access-Control-Allow-Credentials: true
Connection: keep-alive
Content-Length: 290
Content-Type: application/json; charset=utf-8
Date: Thu, 05 May 2016 11:15:32 GMT
ETag: W/"122-7QHSA2Gb7qRseLzxE1QBhg"
Vary: Origin
X-Powered-By: Express
[
{
"_id": "572b29507d406be7852e8388",
"desc": "A JavaScript library for building user interfaces.\r",
"link": "http://facebook.github.io/react/",
"title": "React"
},
{
"_id": "572b29507d406be7852e8479",
"desc": "Node.js humour.\r",
"link": "http://nodejsreactions.tumblr.com/",
"title": "Node.js Reactions"
}
]
诚然,bearer tokens 提供了一个非常薄弱的安全层。仍然有可能让潜在的攻击者嗅探你的 API 请求并重用你的令牌,但使令牌短暂有效并时不时地更改它们可以帮助提高安全性。为了使其真正安全,它通常与用户身份验证结合使用。
创建你的 ReactJS 搜索应用
通过复制 第一章,ReactJS 深入浅出,中的脚手架来启动这个项目,你将在 Packt Publishing 网站上找到这个代码文件以及这本书的代码包),然后将 React-Bootstrap 添加到你的项目中。打开终端,转到项目根目录,并运行 npm install 命令来安装 React-Bootstrap:
npm install --save react-bootstrap@0.29.3 classnames@2.2.5 history@2.1.1 react-router@2.4.0 react-router-bootstrap@0.23.0 superagent@1.8.3 reflux@0.4.1
package.json 中的 dependencies 部分现在应该看起来像这样:
"dependencies": {
"babel-preset-es2015": "⁶.6.0",
"babel-preset-react": "⁶.5.0",
"babel-tape-runner": "².0.0",
"babelify": "⁷.3.0",
"browser-sync": "².12.5",
"browserify": "¹³.0.0",
"browserify-middleware": "⁷.0.0",
"classnames": "².2.5",
"easescroll": "0.0.10",
"eslint": "².9.0",
"history": "².1.1",
"lodash": "⁴.11.2",
"react": "¹⁵.0.2",
"react-bootstrap": "⁰.29.3",
"react-dom": "¹⁵.0.2",
"react-router": "².4.0",
"react-router-bootstrap": "⁰.23.0",
"reactify": "¹.1.1",
"reflux": "⁰.4.1",
"serve-favicon": "².3.0",
"superagent": "¹.8.3",
"tape": "⁴.5.1",
"url": "⁰.11.0",
"basic-auth": "¹.0.3"
}
如果 package.json 不是这个样子,请更新它,然后在项目根目录下从终端运行 npm install。你还需要将 Bootstrap CSS 文件添加到你的 index.html 文件的 <head> 部分中:
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
<link rel="stylesheet" type="text/css" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" />
将上述代码放在带有 app.css 的行之上,这样你就可以覆盖 Bootstrap 的样式。
最后,在 source 文件夹内创建一个 components 文件夹,然后将来自 第三章,使用 ReactJS 进行响应式网页开发,的组件 fontawesome.jsx 和 picture.jsx 复制到这个文件夹中。
设置你的应用
让我们从应用程序的根目录开始,source/app.jsx。将内容替换为以下代码:
'use strict';
import React from 'react';
import { Router, Route, DefaultRoute }
from 'react-router';
import { render } from 'react-dom'
import Search from './components/search.jsx';
import Results from './components/results.jsx';
import Layout from './components/layout.jsx';
import SearchActions from './actions/search.js';
为了使应用能够编译,你需要在你 components 文件夹中创建这四个文件。我们很快就会这样做。现在,参考以下内容:
import { browserHistory } from 'react-router'
上述代码使用浏览器历史库设置了一个路由。这个库的主要优点之一是你可以避免在 URL 中使用哈希标签,因此应用可以引用绝对路径,例如 http://localhost:3000/search 和 http://localhost:3000/search/term:
render((
<Router history={ browserHistory }>
<Route component={Layout}>
<Route path="/" component={Search}>
<Route path="search" component={Results}/>
</Route>
</Route>
</Router>
), document.getElementById('container'));
让我们为 SearchActions、Search、Results 和完整的 Layout 文件创建骨架文件。
创建 source/actions/search.js 并添加以下内容:
'use strict';
import Reflux from "reflux";
let actions = {
performSearch: Reflux.createAction("performSearch"),
emitSearchData: Reflux.createAction("emitSearchData")
};
export default actions;
这设置了我们在 search.jsx 中将使用的两个操作。
创建 source/components/search.jsx 并添加以下内容:
'use strict';
import React from 'react';
const Search = React.createClass({
render() {
return <div/>;
}
});
export default Search;
创建 source/components/results.jsx 并添加以下内容:
'use strict';
import React from 'react';
const Results = React.createClass({
render() {
return <div/>;
}
});
export default Results;
创建 source/components/layout.jsx 并添加以下内容:
'use strict';
import React from 'react';
import Reflux from 'reflux';
import {Row} from "react-bootstrap";
import Footer from "./footer.jsx";
const Layout = React.createClass({
render() {
return (<div>
{this.props.children}
此代码传播了我们在 app.jsx 中设置的路由层次结构中的页面:
<Footer />
我们还将为我们的应用创建一个基本的固定页脚,如下所示:
</div>);
}
});
export default Layout;
创建 source/components/footer.jsx 并添加以下内容:
'use strict';
import React from 'react';
const Footer = React.createClass({
render(){
return (<footer className="footer text-center">
<div className="container">
<p className="text-muted">The Web Searcher</p>
</div>
</footer>);
}
});
export default Footer;
应用程序现在应该可以编译,您将看到一个页脚消息。我们需要应用一些样式来将其固定在页面底部。打开public/app.css并替换其内容为以下样式:
html {
position: relative;
min-height: 100%;
}
body {
margin-top: 60px;
margin-bottom: 60px;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px;
background-color: #f5f5f5;
}
将页面设置为 100%最小高度,并将页脚设置为绝对位置在底部,这将确保它保持固定。现在,看看这个:
*:focus {
outline: none
}
上述代码是为了避免在点击焦点部分时出现轮廓边框。接下来,使用以下样式完成public/app.css,以使搜索结果突出显示:
.header {
background-color: transparent;
border-color: transparent;
}
.quicksearch {
padding-left: 0;
margin-bottom: 20px;
width: 95.5%;
background: white;
z-index: 1;
}
.fullsearch .list-group-item{
border:0;
z-index: 0;
}
ul.fullsearch li:hover, ul.quicksearch li:active, ul.quicksearch li:focus {
color: #3c763d;
background-color: #dff0d8;
outline: 0;
border: 0;
}
ul.quicksearch li:hover, ul.quicksearch li:active, ul.quicksearch li:focus {
color: #3c763d;
background-color: #dff0d8;
outline: 0;
border: 0;
}
.container {
width: auto;
max-width: 680px;
padding: 0 15px;
}
.container .text-muted {
margin: 20px 0;
}
创建搜索服务
在您继续为搜索创建视图层之前,您需要一种连接到您的 API 的方法。您可以通过多种方式来完成这项工作,而且这将是您找不到权威答案的情况之一。有些人喜欢将其放在动作层,有些人喜欢放在存储层,有些人可能会非常乐意将其添加到视图层。
我们将借鉴 MVC 架构并创建一个服务层。我们将从您之前创建的action文件中访问服务。我们这样做的原因很简单,因为它将搜索分离成我们代码中的一个较小且易于测试的子部分。为了简化开发和便于测试,您总是希望使您的组件尽可能小。
在您的source文件夹中创建一个名为in service的文件夹,并添加以下三个文件:index.js、request.js和search.js。
让我们从添加request.js的代码开始:
'use strict';
import Agent from 'superagent';
SuperAgent是一个轻量级的客户端 HTTP 请求库,它使得使用 AJAX 比通常要容易得多。它也与node完全兼容,这在执行服务器端渲染时是一个巨大的好处。我们将在第九章创建共享应用程序中深入探讨服务器端渲染。查看以下示例:
class Request {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
get(query, params) {
return this.httpAgent(query, 'get', params, null);
}
post(url, params, data, options) {
return this.httpAgent(url, 'post', params, data)
}
put(url, params, data) {
return this.httpAgent(url, 'put', params, data)
}
我们实际上只会在我们的应用程序中使用get函数。其他方法已添加为示例。您可以在其中添加或删除内容,甚至将它们合并到一个公共函数中(尽管这会增加使用该函数的复杂性)。
所有操作都发送到httpAgent函数:
httpAgent(url, httpMethod, params, data) {
const absoluteUrl = this.baseUrl + url;
let req = AgenthttpMethod
.timeout(5000);
let token = '1234567890';
req.set('Authorization', 'Bearer ' + token);
req.withCredentials();
我们将添加我们在 API 中早期开发的 bearer token 方案。如果您跳过了那部分,您可以删除前面的两行,尽管如果 API 收到 bearer token 但没有处理它的方法,这对 API 来说并不重要。在这种情况下,它将简单地丢弃信息。
值得注意的是,在服务中硬编码 token 非常不安全。为了使其更安全,例如,您可以设置一个方案,在浏览器会话存储中定期创建新的 token,并用查找替换硬编码的变量。让我们看看以下代码片段:
if (data)
req.send(data);
if (params)
req.query(params);
return this.sendAgent(req);
}
在我们添加完参数后,我们需要通过sendAgent函数发送请求。这个函数返回一个我们可以监听的 promise,它最终要么被拒绝要么被解决。promise是一个用于同步的构造。它是对最初未知的结果的代理。当我们在代码中返回一个 promise 时,我们得到一个最终将包含我们想要的数据的对象:
sendAgent(req) {
return new Promise(function (resolve, reject) {
req.end(function (err, res) {
if (err) {
reject(err);
} else if (res.error) {
reject(res.error);
}
else {
resolve(JSON.parse(res.text));
}
});
});
}
}
export default Request;
我们将要编写的下一个文件是search.js:
'use strict';
import Request from './request.js';
}
export default SearchService;
这只是导入并扩展我们在request.js中创建的代码。由于我们不需要扩展或修改任何请求代码,我们将保持原样。
最后一个文件是index.js:
'use strict';
import SearchService from './search.js';
exports.searchService = new SearchService('http://localhost:5000/v1/search/');
这是我们指定连接到我们的 API 的端点的地方。前面的设置指定了运行在 localhost 上的 API。如果您想使用外部服务测试您的代码,可以将此替换为websearchapi.herokuapp.com/v1/search/的示例接口。
通常,将端点和其他配置细节存储在单独的configuration文件中是个好主意。让我们创建一个config.js文件,并将其放置在source文件夹中:
'use strict';
export const Config = {
'urls':{
'search' : 'http://localhost:5000/v1/search/'
}
};
然后,将service/index.js的内容更改为以下内容:
import {Config} from '../config.js';
import SearchService from './search.js';
exports.searchService = new SearchService(Config.urls.search);
注意,我们需要从config.js中取消对配置名称的引用。这是因为我们使用exports而不是module.exports作为命名导出。如果我们首先声明变量并使用module.exports导出它,我们就不需要取消引用。
差别在于exports仅仅是module的一个辅助工具。最终,模块将使用module.exports,并且Config将作为模块的一个命名属性可用。
您也可以使用以下命令导入它:const Config = require('../config.js') 或 import * as Config from '../config.js'。这两种方式都将设置一个Config变量,您可以通过Config.Config来访问它。
测试服务
我们已经创建了服务,但它是否工作呢?让我们来看看。我们将使用一个小巧而高效的测试框架Tape。使用以下命令安装它:
npm install --save babel-tape-runner@2.0.0 tape@4.5.1
我们添加babel-tape-runner,因为我们整个应用程序都在使用 ECMAScript 6,并且我们希望在测试脚本中也使用它。
在项目的根目录中创建test/service文件夹,并添加一个名为search.js的文件,并添加以下代码:
import test from 'tape';
import {searchService} from '../../source/service/index.js';
test('A passing test', (assert) => {
searchService.get('Understanding SoundEx Algorithms')
.then((result)=> {
assert.equals(result[0].title,
"Understanding SoundEx Algorithms","Exact match found for \"Understanding SoundEx Algorithms\"");
assert.end();
});
});
这个测试将导入搜索服务并在数据库中搜索特定的标题。如果找到精确匹配,它将返回pass。您可以通过在终端中进入根文件夹并执行./.bin/babel-tape-runner test/service/search.js来运行它。
注意
注意,在您开始测试之前,API 服务器必须处于运行状态。
结果应该看起来像这样:
$ ./.bin/babel-tape-runner test/service/search.js
TAP version 13
# A passing test
ok 1 Exact match found for "Understanding SoundEx Algorithms"
1..1
# tests 1
# pass 1
# ok
注意
注意,如果您使用-g标志全局安装tape和babel-tape-runner,那么您不需要从node_modules指定二进制版本,只需使用babel-tape-runner test/service/search.js运行测试即可。为了使运行测试更加容易,您可以在package.json文件的scripts部分添加一个脚本。如果您将测试命令添加到tests脚本中,只需执行npm test即可执行测试。
设置存储
存储将非常简单。我们将在动作中执行服务调用,所以存储将简单地持有服务调用的结果并将它们传递给组件。
在source文件夹中,创建一个新的文件夹并命名为store。然后创建一个新的文件,命名为search.js并添加以下代码:
"use strict";
import Reflux from "reflux";
import SearchActions from "../actions/search";
import {searchService} from "../service/index.js";
let _history = {};
这是存储状态。在存储定义之外设置变量会自动使其成为一个私有变量,只能由存储本身访问,而不能由存储的实例访问。请参考以下代码:
const SearchStore = Reflux.createStore ({
init() {
this.listenTo(SearchActions.emitSearchData, this.emitSearchResults)
},
init()中的行设置了一个监听器,用于监听emitSearchData动作。每当这个动作被调用时,emitSearchResults函数就会被执行:
emitSearchResults(results) {
if (!_history[JSON.stringify(results.query)])
_history[JSON.stringify(results.query)] = results.response;
this.trigger(_history[JSON.stringify(results.query)]);
}
这些行看起来有点复杂,所以让我们从最后一行开始检查逻辑。触发动作在results.query键下发出_history变量的结果,这是正在使用的搜索词。搜索词被JSON.stringify包裹,这是一个将 JSON 数据转换为字符串的方法。这允许我们保留带有空格的查询并将其用作_history变量的对象键。
在触发检查之前的两行代码检查搜索词是否已存储在_history中,如果没有则添加它。我们目前没有处理历史记录的方法,但可以设想将来可能通过扩展存储添加这样的功能:
});
export default SearchStore;
创建搜索视图
我们终于准备好开始处理视图组件了。让我们打开search.jsx并添加一些内容。我们会添加很多代码,所以我们将一步一步来。
首先,将内容替换为以下代码:
import React, { Component, PropTypes } from 'react';
import {Grid,Col,Row,Button,Input,Panel,ListGroup,ListGroupItem} from 'react-bootstrap';
import FontAwesome from '../components/fontawesome.jsx';
import Picture from '../components/picture.jsx';
记得将FontAwesome和Picture组件从第三章,使用 ReactJS 进行响应式 Web 开发,复制到source/components文件夹中,让我们看看以下代码片段:
import SearchActions from '../actions/search.js';
import Reflux from 'reflux';
import { findDOMNode } from 'react-dom';
import { Router, Link } from 'react-router'
import Footer from "./footer.jsx";
import SearchStore from "../store/search.js";
const Search = React.createClass ({
contextTypes: {
router: React.PropTypes.object.isRequired
},
getInitialState() {
return {
showQuickSearch: false
}
},
QuickSearch会在你输入时弹出搜索结果集。我们希望最初将其隐藏,让我们看看以下代码:
renderQuickSearch() {
},
快速搜索目前没有任何作用,让我们看看以下代码片段:
renderImages() {
const searchIcon = <FontAwesome style={{fontSize:20}} icon="search"/>;
const imgSet = [
{
media: "only screen and (min-width: 601px)",
src: " http://websearchapp.herokuapp.com/large.png"
},
{
media: "only screen and (max-width: 600px)",
src: "http://websearchapp.herokuapp.com/small.png"
}
];
const defaultImage = {
src: "http://websearchapp.herokuapp.com/default.png",
alt: "SearchTheWeb logo"
};
return {
searchIcon: searchIcon,
logoSet: imgSet,
defaultImage: defaultImage
}
},
使用Picture组件意味着我们可以为桌面和平板用户提供一个高分辨率版本,为移动用户提供一个较小版本。该组件的完整描述可以在第三章,使用 ReactJS 进行响应式 Web 开发中找到。现在请参考以下代码:
render() {
return (<Grid>
<Row>
<Col xs={ 12 } style={{ textAlign:"center" }}>
<Picture
imgSet={ this.renderImages().logoSet }
defaultImage={ this.renderImages().defaultImage }/>
</Col>
</Row>
<Row>
<Col xs={12}>
<form>
<FormGroup>
<InputGroup>
<InputGroup.Addon>
{ this.renderImages().searchIcon }
</InputGroup.Addon>
<FormControl
ref="searchInput"
type="text" />
<InputGroup.Button>
<Button onClick={ this.handleSearchButton }>
Search
</Button>
</InputGroup.Button>
</InputGroup>
</FormGroup>
</form>
<ListGroup style={{display:this.state.showQuickSearch ?
'block':'none'}}
className="quicksearch">
{this.renderQuickSearch()}
</ListGroup>
</Col>
</Row>
<Row>
<Col xs={12}>
{this.props.children}
这将从一个名为 app.jsx 的路由设置中传播一个子页面:
</Col>
</Row>
</Grid>);
}
});
export default Search;
屏幕上的事情终于开始了。如果你现在打开你的网络浏览器,你会在屏幕上看到一个标志;在其下方,你会找到一个带有左侧放大镜和右侧搜索按钮的搜索字段。
然而,当你开始输入时,没有任何反应,点击搜索按钮时也没有出现结果。显然,还有更多工作要做。
让我们具体实现 QuickSearch 方法。用以下代码替换空块:
renderQuickSearch(){
return this.state.results.map((result, idx)=> {
if (idx < 5) {
return (<ListGroupItem key={"f"+idx}
onClick={this.handleClick.bind(null,idx)}
header={result.title}>{result.desc}
<br/>
<a bsStyle="link" style={{padding:0}}
href={result.link} target="_blank">{result.link}
</a>
</ListGroupItem>)
}
})
},
此外,用以下代码替换初始状态块:
getInitialState(){
return {
showQuickSearch: false,
results: [],
numResults: 0
}
},
QuickSearch 方法现在遍历状态中的结果,并添加一个带有 onClick 处理器、标题、描述和链接的 ListGroupItem 项目。我们将 results 变量添加到初始状态中,以避免应用程序因为未定义的 state 变量而停止。
接下来,我们需要在代码中添加 onClick 处理器。为此,添加以下代码:
handleClick(targetIndex) {
if (this.state.numResults >= targetIndex) {
window.open(this.state.results[targetIndex].link, "_blank");
}
},
这段代码将强制浏览器加载目标索引中包含的 URL,这对应于 targetIndex。
然而,在输入字段中输入任何内容仍然没有任何反应。让我们来解决这个问题。
进行搜索
现在的想法是在用户在搜索输入中输入时展示实时搜索。我们已经为这种情况创建了设置;我们只需要将输入动作与行动连接起来。
第一个想法是在输入字段本身添加一个 onChange 处理器。这是实现第一个里程碑、展示搜索的最简单方法。它看起来像这样:
<form>
<FormGroup>
<InputGroup>
<InputGroup.Addon>
{ this.renderImages().searchIcon }
</InputGroup.Addon>
<FormControl
ref="searchInput"
type="text" />
<InputGroup.Button>
<Button onClick={ this.handleSearchButton }>
Search
</Button>
</InputGroup.Button>
</InputGroup>
</FormGroup>
</form>
接下来,你需要在代码中添加一个 performSearch 方法,如下所示:
performSearch() {
console.log(findDOMNode(this.refs.searchInput).value);
},
当你开始输入时,控制台日志将立即开始填充值:
这相当不错,但对于一个只包含单个输入字段而没有其他内容的搜索页面,最好不需要手动将焦点放在搜索字段上以便输入值。
让我们删除 onChange 处理器,并在用户输入数据时立即开始搜索过程。
将以下两个方法添加到 search.jsx 中:
componentDidMount() {
document.getElementById("container")
.addEventListener('keypress', this. handleKeypress);
document.getElementById("container")
.addEventListener('keydown', this.handleKeypress);
},
componentWillUnmount() {
document.getElementById("container")
.removeEventListener('keypress', this.handleKeypress);
document.getElementById("container").removeEventListener('keydown', this.handleKeypress);
},
这将在组件挂载时设置两个事件监听器。keypress 事件监听器负责处理普通按键事件,而 keydown 事件监听器确保我们可以捕获箭头键输入。
handleKeypress 方法相当复杂,所以让我们添加代码并逐步检查它。
当你注册了这些事件监听器后,你将能够捕获用户的每一个按键事件。如果用户按下键 A,一个包含大量关于事件信息的对象将被发送到 handleKeypress 函数。以下是事件对象中对我们特别感兴趣的属性的一部分:
altKey: false
charCode: 97
ctrlKey: false
keyCode: 97
shiftKey: false
metaKey: false
type: "keypress"
它告诉我们这是一个 keypress 事件(箭头键将注册为 keydown 事件)。charCode 参数是 97,并且没有使用 Alt 键、Meta 键、Ctrl 键或 Shift 键与事件一起使用。
我们可以使用原生的 JavaScript 函数解码 charCode。如果您执行 String.fromCharCode(97),您将得到一个包含小写字母 a 的字符串。
基于数字处理按键事件是可行的,但将数字映射到更友好的字符串会更好,因此我们将添加一个对象来保存我们的 charCode 参数。将此添加到文件顶部,位于导入语句下方但 createClass 定义之上:
const keys = {
"BACKSPACE": 8,
"ESCAPE": 27,
"UP": 38,
"LEFT": 37,
"RIGHT": 39,
"DOWN": 40,
"ENTER": 13
};
现在,我们可以输入 keys.BACKSPACE 并发送数字 8,依此类推。
让我们添加 handleKeypress 函数:
handleKeypress (e) {
if (e.ctrlKey || e.metaKey) {
return;
}
如果我们检测到用户正在使用 Ctrl 或 Meta 键(在 Mac 上为 CMD),我们将终止函数。这允许用户使用常规的操作系统方法,例如复制/粘贴或 Ctrl + A 选择所有文本,让我们看一下以下代码片段:
const inputField = findDOMNode(this.refs.searchInput);
const charCode = (typeof e.which == "number") ?
e.which : e.keyCode
我们定义一个变量来保存输入字段,这样我们就不必多次查找它。出于兼容性原因,我们还确保通过检查传递给我们的字符类型是否为数字来获取有效的字符代码。请参阅以下内容:
switch (charCode) {
case keys.BACKSPACE:
inputField.value.length <= 0 ?
this.closeSearchField(e) : null;
break;
我们添加一个 closeSearchField 函数,以便即使在搜索结果已填充的情况下也能隐藏搜索结果。我们这样做是因为我们不希望当用户清除所有文本并准备开始新的搜索时,它仍然保持打开状态,让我们看一下以下代码片段:
case keys.ESCAPE:
this.closeSearchField(e);
break;
如果用户按下 Esc 键,我们还将隐藏搜索结果,让我们看一下以下代码片段:
case keys.LEFT:
case keys.RIGHT:
// allow left and right but don't perform search
break;
这些检查没有任何作用,但它们将防止开关触达 default 并因此触发搜索,让我们看一下以下代码片段:
case keys.UP:
if (this.state.activeIndex > -1) {
this.setState(
{activeIndex: this.state.activeIndex - 1}
);
}
if (this.state.activeIndex < 0) {
inputField.focus();
e.preventDefault();
}
break;
我们为箭头键添加了特殊处理。当用户按下上箭头键时,只要 activeIndex 为零或更高,它就会递减。这将确保我们永远不会处理无效的 activeIndex 参数(小于 -1):
case keys.DOWN:
if (this.state.activeIndex < 5
&& this.state.numResults > (1 + this.state.activeIndex)) {
this.setState({activeIndex: this.state.activeIndex + 1});
}
e.preventDefault();
break;
我们已定义快速搜索的最大结果数为 5。此代码片段将确保 activeIndex 永远不会超过 5:
case keys.ENTER:
e.preventDefault();
if (this.state.activeIndex === -1 ||
inputField === document.activeElement) {
if (inputField.value.length > 1) {
this.context.router.push(null,
`/search?q=${inputField.value}`, null);
this.closeSearchField(e);
SearchActions.showResults();
}
}
else {
if (this.state.numResults >= this.state.activeIndex) {
window.open( this.state.results[this.state.activeIndex].link, '_blank');
}
}
break;
此开关执行两种操作之一。首先,如果 activeIndex 为 -1,则表示用户尚未导航到任何快速搜索结果,我们将直接跳转到所有匹配项的结果页面。如果 activeIndex 不是 -1 但 inputfield 仍然具有焦点(inputField === document.activeElement),也会发生相同的情况。
其次,如果 activeIndex 不是 -1,则表示用户已导航到输入字段下方并做出了选择。在这种情况下,我们将用户发送到所需的 URL:
default:
inputField.focus();
this.performSearch();
if (!this.state.showQuickSearch) {
this.setState({showQuickSearch: true});
}
SearchActions.hideResults();
break;
}
},
最后,如果没有一个开关有效,例如,按下了常规键,那么我们将执行搜索。我们还将使用 SearchActions.hideResults() 动作隐藏任何潜在的不完整结果。
此代码在添加hideResults到我们的操作之前无法编译,所以打开actions/search.js并在操作对象中添加这些行:
hideResults: Reflux.createAction("hideResults"),showResults: Reflux.createAction("showResults"),
代码将编译,并且当您在浏览器中开始输入时,输入字段将获得焦点并接收输入。现在是时候将我们的搜索服务连接起来,我们将在您刚刚编辑的actions文件中这样做。在文件顶部,在第一个导入下面添加这两行:
import {searchService} from "../service/index.js";
let _history = {};
我们将创建一个私有的_history变量来保存我们的搜索历史。这并不是严格必要的,但我们将使用它来减少我们将要进行的 API 调用次数。
接下来,添加此片段:
actions.performSearch.listen( (query) => {
if(_history[JSON.stringify(query)]){
actions.emitSearchData({query:query,response:
_history[JSON.stringify(query)]});
}
else {
searchService.get(query)
.then( (response) => {
_history[JSON.stringify(query)]=response;
actions.emitSearchData({query:query,response:response});
}).catch( (err) => {
// do some error handling
})
}
});
此代码将确保每当触发performSearch时,我们都会调用我们的 API。每当搜索服务返回结果时,我们将它存储在我们的_history对象中,并在我们向搜索服务发送新查询之前,确保有结果准备好。这将节省我们一次 API 调用,并且用户将获得更快的响应。
接下来,添加当我们在文本框中输入或点击按钮时实际执行搜索的代码。用以下代码替换performSearch()内部的代码:
performSearch(){
const val = findDOMNode(this.refs.searchInput).value;
val.length > 1 ?
SearchActions.performSearch(val) :
this.setState({results: []});
},
在我们能够在浏览器中看到结果之前,我们还需要做一件事,但您可以通过输入搜索查询并在开发者工具中检查网络流量来验证它是否工作:
要在浏览器中显示我们的结果,我们需要添加一个监听器,它可以对存储中的变化做出反应。
打开components/search.jsx并在getInitialState之前添加此代码:
mixins: [
Reflux.listenTo(SearchStore, "getSearchResults")
],
getSearchResults(res) {
this.setState({results: res, numResults:
res.length < 5 ? res.length : 5});
},
此代码的作用是告诉 React 在SearchStore发出新数据时调用getSearchResults。它调用的函数将最多存储五个结果在组件状态中。现在,当您输入某些内容时,一个列表组将出现在搜索字段下方,显示结果。
您可以使用鼠标悬停在任何结果上,然后点击它以访问它所引用的链接。
使用箭头键导航搜索结果
由于我们已经对键盘事件做了很多工作,不进一步利用它将是一件遗憾的事情。您在搜索时已经在使用键盘,所以能够使用箭头键导航搜索结果似乎很自然,然后按Enter键访问您选择的那一页。
打开search.jsx。在getInitialState中添加此键:
activeIndex: -1,
然后,在renderQuickSearch函数中,添加带有className的高亮行:
renderQuickSearch() {
return this.state.results.map((result, idx)=> {
if (idx < 5) {
return (<ListGroupItem key={ "f" + idx }
className={ this.state.activeIndex === idx ?
"list-group-item-success":""}
onClick={this.handleClick.bind(null,idx)}
header={result.title}>{ result.desc }
<br/>
<a bsStyle="link" style={{padding:0}}
href={ result.link } target="_blank">
{ result.link }
</a>
</ListGroupItem>)
}
})
},
现在,你将能够使用箭头键上下移动,并按Enter键访问活动链接。然而,这个解决方案有几个小问题让人感到有些烦恼。首先,当你上下导航时,输入字段保持聚焦。如果你输入其他内容,你会得到一组新的搜索结果,但活动索引将保持不变,如果新结果返回的结果少于上一个结果,可能会超出范围。其次,上下动作将光标移动到输入字段中,这会让人感到不安。
第一个问题很容易解决;只需将activeIndex:-1添加到getSearchResults函数中即可,但第二个问题需要我们求助于一个老牌的网页开发者技巧。简单地来说,没有方法可以“取消聚焦”输入字段,因此,我们将创建一个隐藏且不可见的输入字段,并将焦点发送到该字段。
在render方法中,将此代码添加到输入字段上方:
<input type="text" ref="hiddeninput"
style={{left:-100000,top:-100000,position: 'absolute',
display:'block',height:0,width:0,zIndex:0,
padding:0,margin:0}}/>
然后转到switch方法,并将高亮行添加到向下箭头动作中:
case keys.DOWN:
if (this.state.activeIndex < 5
&& this.state.numResults > (1 + this.state.activeIndex)) {
this.setState({activeIndex: this.state.activeIndex + 1});
}
findDOMNode(this.refs.hiddeninput).focus();
e.preventDefault();
break;
当应用重新编译时,你将能够使用箭头键上下导航,并且只有当你导航到顶部时,正确的输入字段才会激活。其余时间,隐藏的输入字段将拥有焦点,但由于它放置在视口之外,没有人会看到它或能够使用它。让我们看看下面的截图:
搜索防抖
每个keypress类都会向 API 提交一个新的搜索。即使我们有实现的历史变量系统,这也给我们的 API 带来了很大的压力。这对用户来说也不是最好的选择,因为它可能会引发一系列不相关的搜索结果。想象一下,你想要搜索 JavaScript。你可能对 j、ja、jav、Java、javas、javasc、javascri、javascri 和 JavaScript 的结果不感兴趣,但当前的情况就是这样。
幸运的是,通过简单地延迟搜索,我们可以很容易地提高用户体验。转到switch语句,并用以下内容替换内容:
default:
inputField.focus();
delay(() => {
if (inputField.value.length >= 2) {
this.performSearch();
}
}, 400);
if (!this.state.showQuickSearch) {
this.setState({showQuickSearch: true});
}
SearchActions.hideResults();
break;
你还需要delay函数,所以将其添加到文件顶部,紧接在导入之后:
let delay = (() => {
var timer = 0;
return function (callback, ms) {
clearTimeout(timer);
timer = setTimeout(callback, ms);
};
})();
这段代码将确保结果延迟足够长,以便用户在退出前可以输入查询,但不会感觉迟缓。你应该根据需要调整毫秒设置。
从快速搜索到结果页面的过渡
现在我们几乎完成了组件的开发。接下来,我们将向search.jsx添加最后一段代码,用于处理搜索按钮和准备进入下一页。为此,请添加以下代码:
handleSearchButton(e) {
const val = findDOMNode(this.refs.searchInput).value;
if (val.length > 1) {
this.context.router.push(`/search?q=${val}`);
this.closeSearchField(e);
SearchActions.showResults();
}
},
closeSearchField(e) {
e.preventDefault();
this.setState({showQuickSearch: false});
},
这段代码将关闭搜索字段,并使用push从 react-router 导航到新的路由。
push参数由 react-router 的 2.0 分支支持,所以我们只需要在我们的组件中添加一个上下文类型。我们可以通过在组件顶部添加以下行(在React.createClass行下方)来完成此操作:
contextTypes: {
router: React.PropTypes.object.isRequired
},
childContextTypes: {
location: React.PropTypes.object
},
getChildContext() {
return { location: this.props.location }
},
设置结果页面
结果页面的目的是显示所有搜索结果。传统上,你显示 10-20 个结果和分页功能,这允许你显示更多结果,直到达到末尾。
让我们设置结果页面,并从传统的分页器开始。
打开components/results.jsx,并用以下内容替换其内容:
import React, { Component, PropTypes } from 'react';
import Reflux from 'reflux';
import {Router, Link, Lifecycle } from 'react-router'
import SearchActions from '../actions/search.js';
import SearchStore from "../store/search.js";
import {Button,ListGroup,ListGroupItem} from 'react-bootstrap';
import {findDOMNode} from 'react-dom';
const Results = React.createClass ({
contextTypes: {
location: React.PropTypes.object
},
设置contextType对象是为了从 URL 中检索query参数。现在看看以下内容:
getInitialState() {
return {
results: [],
resultsToShow: 10,
numResults: 0,
showResults: true
}
},
在这里我们定义结果默认是可见的。这对于直接访问搜索页面的用户是必要的。我们还定义了每页显示 10 个结果,让我们看看以下代码:
componentWillMount() {
SearchActions.performSearch(this.context.location.query.q);
},
我们希望尽可能快地启动搜索,以便向用户显示一些内容。如果我们是从首页继续,结果已经在_history变量中准备好了,并且将在组件挂载之前可用。参看以下代码:
mixins: [
Reflux.listenTo(SearchStore, "getSearchResults"),
Reflux.listenTo(SearchActions.hideResults, "hideResults"),
Reflux.listenTo(SearchActions.showResults, "showResults")
],
hideResults和showResults方法是在用户开始新的查询时使用的操作。我们不是将结果向下推送或在上面的结果上方显示快速搜索,而是简单地隐藏现有的结果:
hideResults() {
this.setState({showResults: false});
},
showResults() {
this.setState({showResults: true});
},
这些setState函数响应前面的操作,如下所示:
getSearchResults(res) {
let resultsToShow = this.state.resultsToShow;
if (res.length < resultsToShow) {
resultsToShow = res.length;
}
this.setState({results: res, numResults: res.length,
resultsToShow: resultsToShow});
},
当我们检索的结果少于this.state.resultsToShow时,我们将状态变量调整为集合中的结果数量,让我们看看以下代码片段:
renderSearch(){
return this.state.results.map((result, idx)=> {
if (idx < this.state.resultsToShow) {
return <ListGroupItem key={"f"+idx}
header={result.title}>{result.desc}<br/>
<Button bsStyle="link" style={{padding:0}}>
<a href={result.link}
target="_blank">{result.link}</a>
</Button>
</ListGroupItem>
}
})
},
这个渲染器几乎与search.jsx中的那个相同。主要区别在于我们返回一个具有link样式的按钮,并且我们没有检查activeIndex属性,让我们看看剩余的代码:
render() {
return (this.state.showResults) ? (
<div>
<div style={{textAlign:"center"}}>
Showing {this.state.resultsToShow} out of {this.state.numResults} hits
</div>
<ListGroup className="fullsearch">
{this.renderSearch()}
</ListGroup>
</div>
): null;
}
});
export default Results;
设置分页
让我们先给getInitialState添加一个属性和一个resetState函数:
getInitialState() {
return {
results: [],
resultsToShow: 10,
numResults: 0,
showResults: true,
activePage: 1
}
},
resetState() {
this.setState({
resultsToShow: 10,
showResults: true,
activePage: 1
})
},
需要在getSearchResults中添加resetState函数:
getSearchResults(res) {
this.resetState();
let resultsToShow = this.state.resultsToShow;
if (res.length < resultsToShow) {
resultsToShow = res.length;
}
this.setState({results: res, numResults: res.length,
resultsToShow: resultsToShow});
},
依次运行两个setStates对象完全没有问题。它们将简单地按先来先服务的顺序排队。
接下来,添加一个分页器:
renderPager() {
return (<Pagination
prev
next
items={Math.ceil(this.state.results.length/this.state.resultsToShow)}
maxButtons={10}
activePage={this.state.activePage}
onSelect={this.handleSelect}/>)
},
这个分页器将自动在页面上填充一定数量的按钮,在这个例子中是 10 个。项目的数量由结果数量除以每页显示的项目数量确定。Math.ceil向上取整到最接近的整数,所以如果你得到 54 个结果,页数将从 5.4 向上取整到 6。前五页将显示十个结果,最后一页将显示剩余的四个结果。
为了使用分页组件,我们需要将其添加到导入部分,所以用以下内容替换react-bootstrap导入:
import {Button,ListGroup,ListGroupItem,Pagination} from 'react-bootstrap';
要显示分页器,用以下内容替换render:
render() {
let start = -this.state.resultsToShow +
(this.state.activePage*this.state.resultsToShow);
let end=this.state.activePage*this.state.resultsToShow;
return (this.state.showResults) ? (
<div>
<div style={{textAlign:"center"}}>
Showing {start}-{end} out of {this.state.numResults} hits
</div>
<ListGroup className="fullsearch">
{this.renderSearch()}
</ListGroup>
<div style={{textAlign:"center"}}>
{this.renderPager()}
</div>
</div>
) : null;
}
然后,添加handleSelect函数:
handleSelect(eventKey) {
this.setState ({
activePage: eventKey
});
},
这就是你需要设置分页器的所有内容。只有一个问题。当你点击下一步时,你会被留在底部位置,作为一个用户,这感觉并不对。让我们用这个依赖项添加一个漂亮的滚动效果:
npm install --save easescroll@0.0.10
我们将它添加到导入部分:
import Scroller from 'easescroll';
将以下内容添加到handleSelect函数中:
handleSelect(event, selectedEvent) {
this.setState({
activePage: selectedEvent.eventKey
});
Scroller(220, 50, 'easeOutSine');
},
有很多滚动变体可供选择。以下是一些你可以尝试的其他设置:
Scroller(220, 500, 'elastic');
Scroller(220, 500, easeInOutQuint);
Scroller(220, 50, 'bouncePast');
让我们看看下面的截图:
设置无限滚动
无限滚动是一个非常受欢迎的功能,而且它很容易在 ReactJS 中实现。让我们回到添加分页器之前代码的状态,并实现无限滚动。
无限滚动通过简单地在你到达页面底部时加载更多项目来工作。没有分页器参与。你只需滚动,然后继续滚动。
让我们看看如何将这个功能添加到我们的代码中。
首先,我们需要向getInitialState添加几个属性:
getInitialState() {
return {
results: [],
resultsToShow: 10,
numResults: 0,
threshold: -60,
increase: 3,
showResults: true
}
},
threshold变量以像素为单位给出,当达到底部 60 像素时激活。increase变量是我们一次将加载多少个项目的数量。它通常与resultToShow变量相同,但在这个例子中,三个看起来非常直观。
我们将添加一个事件监听器来挂载(并在我们完成时移除它):
componentDidMount: function () {
this.attachScrollListener();
},
componentWillUnmount: function () {
this.detachScrollListener();
},
attachScrollListener: function () {
window.addEventListener('scroll', this.scrollListener);
this.scrollListener();
},
detachScrollListener: function () {
window.removeEventListener('scroll', this.scrollListener);
},
这些事件监听器将监听滚动事件。它也会在组件挂载后立即启动scrollListener。
接下来,我们将添加实际的功能:
scrollListener: function () {
const component = findDOMNode(this);
if(!component) return;
let scrollTop;
if((window.pageYOffset != 'undefined')) {
scrollTop = window.pageYOffset;
} else {
scrollTop = (document.documentElement ||
document.body.parentNode || document.body).scrollTop;
}
const reachedTreshold = (this.topPosition(self) +
self.offsetHeight - scrollTop -
window.innerHeight < Number(this.state.threshold));
const hasMore = (this.state.resultsToShow +
this.state.increase < this.state.numResults);
if(reachedTreshold && hasMore) {
当我们还有更多结果时,通过this.state.increase中的数字增加要显示的结果数量,让我们看看以下代码:
this.setState ({
resultsToShow: (this.state.increase +
this.state.resultsToShow <= this.state.numResults) ?
this.state.increase + this.state.resultsToShow :
this.state.numResults
});
} else {
this.setState({resultsToShow: this.state.numResults});
当我们不能再增加时,我们将resultsToShow设置为与接收到的结果数量相同,让我们看看以下代码片段:
}
},
topPosition: function (el) {
if (!el) {
return 0;
}
return el.offsetTop + this.topPosition(el.offsetParent);
},
这个函数只是找到组件在视口中的顶部位置。
现在当你向下滚动时,页面将加载新的片段,直到没有更多结果。这绝对可以被认为是一个简单的无限滚动,它既不是无限的,实际上也没有加载更多内容。
然而,很容易修改它,使其不是立即设置新状态,而是发送一个触发服务调用以加载更多数据的动作调用。在这种情况下,监听器需要在新的数据集到达之前断开连接,然后重新连接监听器并设置新的状态,就像我们之前做的那样。如果你确实有无限多的数据要获取,这种方法不会让你失望。
我们接近完成。只剩下一件事要添加。当你直接访问结果页面时,输入字段不会被填充。这不是至关重要,但这是一个很好的功能,所以让我们添加它。
在results.jsx中的componentWillMount函数中添加以下行:
SearchActions.setInputText(this.context.location.query.q);
然后,再次打开search.jsx并添加以下行到 mixins 中:
Reflux.listenTo(SearchActions.setInputText, "setInputText")
在同一文件中,添加设置输入文本的函数:
setInputText(val) {
findDOMNode(this.refs.searchInput).value = val;
},
最后,在actions/search.js中,将以下内容添加到actions对象中:
setInputText: Reflux.createAction("setInputText")
如果你现在直接导航到结果页面,例如,通过本地访问你的测试站点http://localhost:3001/search?q=javascript或远程访问示例应用程序websearchapp.herokuapp.com/search?q=javascript,你会发现输入字段被设置为你要添加到q变量中的任何内容。
摘要
在本章中,我们创建了一个可工作的 API,并将其连接到 MongoDB 实例,然后着手制作一个快速搜索应用程序,该应用程序可以实时显示搜索结果。此外,我们还研究了键盘动作和滚动动作的事件监听器,并将它们投入使用。
恭喜!这是一项艰巨的工作。
注意
完成的项目可以在网上查看,地址为reactjsblueprints-chapter4.herokuapp.com。
你可以通过许多方式改进项目。例如,搜索组件相当长,难以维护。将其拆分为多个较小的组件是个好主意。
你还可以实现一个update方法,以便将每个搜索结果的点击都存储在你的 MongoDB 实例中。这使得你能够在你用户中查看热门点击。
在下一章中,我们将走出室内,探讨如何制作基于地图的应用程序,并使用 HTML5 地理位置 API。