在本章中,将向您介绍react-bootstrap
包。此工具通过利用引导 CSS 框架提供移动 first React 组件。这当然不是移动优先 React 的唯一选择,但它是一个很好的选择,它融合了网络上两种最流行的技术。
我们将从采用移动优先设计策略的动机开始。然后,我们将在本章余下的时间里实现几个react-bootstrap
组件。
Mobile first design 是一种将移动设备作为用户界面主要目标的策略。较大的屏幕,如笔记本电脑或大显示器,是次要目标。这并不一定意味着大多数用户都在他们的手机上访问你的应用。这仅仅意味着移动设备是从几何角度扩展用户界面的起点。
例如,当移动浏览器首次出现时,通常为普通桌面屏幕设计 UI,然后在必要时缩小到较小的屏幕。该方法如下所示:
这里的想法是,您在设计 UI 时考虑到了更大的屏幕,这样您就可以一次在屏幕上安装尽可能多的功能。当使用较小的设备时,您的代码必须使用不同的布局或动态使用不同的组件。
这是非常有限的,原因有很多。首先,很难维护对不同屏幕分辨率有很多特殊情况处理的代码。其次,反对这种方法的更具说服力的论点是,几乎不可能跨设备提供类似的用户体验。如果大屏幕上同时显示大量功能,您就无法在小屏幕上复制这些功能。不仅房地产较少,而且较小设备的处理能力和网络带宽也是限制因素。
mobile first UI 设计方法通过向上扩展 UI 而不是尝试向下扩展来解决这些问题,如下所示:
这种方法从来都没有意义,因为这样会限制应用的功能;周围没有太多的平板电脑或手机。今天的情况并非如此,人们期望用户可以在其移动设备上与应用进行交互而不会出现问题。现在有很多这样的浏览器,移动浏览器现在已经非常强大了。
一旦在移动环境中实现了应用功能,将其放大到更大的屏幕大小是一个相对容易解决的问题。现在让我们来看看如何在 React 应用中实现移动优先。
虽然可以通过滚动您自己的 CSS 来实现 mobile first React 用户界面,但我建议不要这样做。有许多 CSS 库为我们处理看似无穷无尽的边缘情况。在本节中,我们将介绍用于引导的react-bootstrap
包 React 组件。
Bootstrap 是最流行的移动优先库。但是,直接使用它意味着手动将正确的 CSS 类添加到正确的组件中。react-bootstrap
包公开了许多组件,它们充当应用和引导 HTML/CSS 之间的一个精简抽象层。
现在让我们实现一些示例。我向您展示如何使用react-bootstrap
组件的另一个原因是,它们与react-native
组件类似,您将在下一章开始学习。
下面的例子并不是对react-bootstrap
的深入报道,也不是引导本身。相反,这个想法是让你感觉到在 React 中使用 mobilefirst 组件的感觉,通过从容器中传递它们的状态等等。现在,看一下 react 引导文档(http://react-bootstrap.github.io/ 了解详情。
也许移动优先设计最重要的方面是导航。在移动设备上实现这一点尤其困难,因为功能内容几乎没有足够的空间,更不用说工具从一个功能移动到另一个功能了。谢天谢地,Bootstrap 为我们解决了很多困难。
在本节中,您将学习如何实现两种类型的导航。您将从工具栏导航开始,然后构建一个侧栏导航部分。这构成了您将开始使用的 UI 框架的一部分。我发现这种方法真正有用的是,一旦导航机制到位,就可以很容易地添加新页面,并在构建应用时在应用中移动。
让我们从Navbar.
开始,这是大多数应用中的一个组件,静态地位于屏幕顶部。在此栏中,我们将添加一些导航链接。以下是用于此的 JSX 的外观:
{ /* The "NavBar" is statically-placed across the
top of every page. It contains things like the
title of the application, and menu items. */ }
<Navbar className="navbar-top" fluid>
<Navbar.Header>
<Navbar.Brand>
<Link to="/">Mobile-First React</Link>
</Navbar.Brand>
{ /* The "<Navbar.Taggle>" component is used to
replace any navigation links with a drop-down
menu for smaller screens. */ }
<Navbar.Toggle />
</Navbar.Header>
{ /* The actual menu with links to make. It's wrapped
in the "<Navbar.Collapse>" component so that it
works properly when the links have been
collapsed. */ }
<Navbar.Collapse>
<Nav pullRight>
<IndexLinkContainer to="/">
<MenuItem>Home</MenuItem>
</IndexLinkContainer>
<LinkContainer to="forms">
<MenuItem>Forms</MenuItem>
</LinkContainer>
<LinkContainer to="lists">
<MenuItem>Lists</MenuItem>
</LinkContainer>
</Nav>
</Navbar.Collapse>
</Navbar>
以下是导航栏的外观:
<Navbar.Header>
组件定义应用的标题,并放置在导航栏的左侧。链接本身放置在<Nav>
元素中,pullRight
属性将它们与导航栏的右侧对齐。您可以看到,我们使用的不是react-router
包中的<Link>
,而是<LinkContainer>
和<IndexLinkContainer>
。这些组件来自react-router-bootstrap
包。它们是使引导链接与路由一起工作所必需的。
另外值得注意的是,<Nav>
元素被包装在一个<Navbar.Collapse>
元素中,头部包含一个<Navbar.Toggle>
按钮。这些组件是将链接折叠成较小屏幕的下拉菜单所必需的。由于它基于浏览器宽度,因此您只需调整浏览器窗口的大小即可看到它的运行:
显示的链接现在折叠为标准菜单按钮。如您所见,单击此按钮时,相同的链接将以垂直方式显示。这在较小的设备上工作得更好。但对于更大的屏幕,将所有导航显示在顶部导航栏中可能并不理想。标准方法是实现左侧边栏,导航链接垂直堆叠。现在让我们来实现这一点:
{ /* This navigation menu has the same links
as the top navbar. The difference is that
this navigation is a sidebar. It's completely
hidden on smaller screens. */}
<Col sm={3} md={2} className="sidebar">
<Nav stacked>
<IndexLinkContainer to="/">
<NavItem>Home</NavItem>
</IndexLinkContainer>
<LinkContainer to="forms">
<NavItem>Forms</NavItem>
</LinkContainer>
<LinkContainer to="lists">
<NavItem>Lists</NavItem>
</LinkContainer>
</Nav>
</Col>
暂时忽略<Col>
元素。我把它包括在这里的唯一原因是它是<Nav>
的容器,我们在其中添加了自己的类名。你马上就会明白为什么了。在<Nav>
元素中,内容看起来与导航工具栏中的内容完全相同,带有链接容器和菜单项。下面是侧边栏的外观:
现在,我们需要将该自定义sidebar
类名添加到包含元素中的原因是为了能够在较小的设备上完全隐藏它。让我们来看一下简单的 CSS:
.sidebar {
display: none;
}
@media (min-width: 768px) {
.sidebar {
display: block;
position: fixed;
top: 60px;
}
}
这个 CSS 以及这个示例的整体结构都是根据这个引导示例改编的:http://getbootstrap.com/examples/dashboard/ 。这个媒体查询背后的想法很简单。如果最小浏览器宽度为768px
,则在固定位置显示边栏。否则,完全隐藏它,因为我们在一个更小的屏幕上。
有点酷,不是吗?在我们这方面没有太多的努力,我们有两个导航组件相互协作,根据屏幕分辨率改变它们的显示方式。
在移动和桌面环境中,一个常见的 UI 元素是呈现项目列表。如果没有 CSS 库的支持,这很容易做到,但是库有助于保持外观和感觉的一致性。让我们实现一个由一组过滤器控制的列表。首先,我们有呈现react-bootstrap
组件的组件:
import React, { PropTypes } from 'react';
import { Map as ImmutableMap } from 'immutable';
import {
Button,
ButtonGroup,
ListGroupItem,
ListGroup,
Glyphicon,
} from 'react-bootstrap';
import './FilteredList.css';
// Utility function to get the bootstrap style
// for an item, based on the "done" value.
const itemStyle = done =>
ImmutableMap()
.set(true, { bsStyle: 'success' })
.set(false, {})
.get(done);
// Utility component for rendering a bootstrap
// icon based on the value of "done".
const ItemIcon = ({ done }) =>
ImmutableMap()
.set(true, (
<Glyphicon
glyph="ok"
className="item-done"
/>
))
.set(false, null)
.get(done);
// Renders a list of items, and a set of filter
// controls to change what's displayed in the
// list.
const FilteredList = props => (
<section>
{ /* Three buttons that control what's displayed
in the list below. Clicking one of these
buttons will toggle the state of the others. */ }
<ButtonGroup className="filters">
<Button
active={props.todoFilter}
onClick={props.todoClick}
>
Todo
</Button>
<Button
active={props.doneFilter}
onClick={props.doneClick}
>
Done
</Button>
<Button
active={props.allFilter}
onClick={props.allClick}
>
All
</Button>
</ButtonGroup>
{ /* Renders the list of items. It passes the
"props.filter()" function to "items.filter()".
When the buttons above are clicked, the "filter"
function is changed. */ }
<ListGroup>
{props.items.filter(props.filter).map(i => (
<ListGroupItem
key={i.name}
onClick={props.itemClick(i)}
href="#"
{...itemStyle(i.done)}
>
{i.name}
<ItemIcon done={i.done} />
</ListGroupItem>
))}
</ListGroup>
</section>
);
FilteredList.propTypes = {
todoFilter: PropTypes.bool.isRequired,
doneFilter: PropTypes.bool.isRequired,
allFilter: PropTypes.bool.isRequired,
todoClick: PropTypes.func.isRequired,
doneClick: PropTypes.func.isRequired,
allClick: PropTypes.func.isRequired,
itemClick: PropTypes.func.isRequired,
filter: PropTypes.func.isRequired,
items: PropTypes.array.isRequired,
};
export default FilteredList;
首先,我们有<ButtonGroup>
和<Button>
元素。这些是用户可以应用于列表的过滤器。默认情况下,仅显示 todo 项目。但是,他们可以选择按已完成项筛选,或显示所有项。
列表本身是一个<ListGroup>
元素,其中<ListGroupItem>
元素为子元素。根据项目的done
状态,项目呈现方式不同。最终结果如下所示:
只需单击列表项,即可切换列表项的完成状态。该组件工作方式的优点在于,如果您正在查看 todo 项并将其中一项标记为已完成,则它将从列表中删除,因为它不再满足当前的筛选条件。将重新计算过滤器,因为组件将重新渲染。以下是标记为完成的项目的外观:
现在让我们来看一看容器组件,它处理过滤器按钮和项目列表的状态:
import React, { Component } from 'react';
import { fromJS } from 'immutable';
import FilteredList from './FilteredList';
class FilteredListContainer extends Component {
// Controls the state of the the filter buttons
// as well as the state of the function that
// filters the item list.
state = {
data: fromJS({
// The items...
items: [
{ name: 'First item', done: false },
{ name: 'Second item', done: false },
{ name: 'Third item', done: false },
],
// The filter button states...
todoFilter: true,
doneFilter: false,
allFilter: false,
// The default filter...
filter: i => !i.done,
// The "todo" filter button was clicked.
todoClick: () => {
this.data = this.data.merge({
todoFilter: true,
doneFilter: false,
allFilter: false,
filter: i => !i.done,
});
},
// The "done" filter button was clicked.
doneClick: () => {
this.data = this.data.merge({
todoFilter: false,
doneFilter: true,
allFilter: false,
filter: i => i.done,
});
},
// The "all" filter button was clicked.
allClick: () => {
this.data = this.data.merge({
todoFilter: false,
doneFilter: false,
allFilter: true,
filter: () => true,
});
},
// When the item is clicked, toggle it's
// "done" state.
itemClick: item => (e) => {
e.preventDefault();
this.data = this.data.update(
'items',
items => items.update(
items.findIndex(i =>
i.get('name') === item.name),
i => i.update('done', done => !done)
)
);
},
}),
};
// Getter for "Immutable.js" state data...
get data() {
return this.state.data;
}
// Setter for "Immutable.js" state data...
set data(data) {
this.setState({ data });
}
render() {
return (
<FilteredList {...this.state.data.toJS()} />
);
}
}
export default FilteredListContainer;
这段代码看起来比实际情况更复杂。它只是四个状态和四个事件处理函数。三个状态只跟踪选择了哪个过滤器按钮。filter
状态是<FilteredList>
用来过滤项目的回调函数。这里使用的策略是根据过滤器选择将不同的过滤器函数传递给子视图。
在本章的最后一节中,我们将实现react-bootstrap
中的几个表单组件。与上一节中创建的过滤器按钮一样,表单组件具有需要从容器组件传递的状态。
但是,即使是简单的表单控件也有许多移动部件。我们将从查看文本输入开始。有输入本身,也有标签、占位符、错误文本、验证函数等等。为了帮助将所有这些部分粘合在一起,让我们创建一个封装所有引导部分的通用组件:
import React, { PropTypes } from 'react';
import {
FormGroup,
FormControl,
ControlLabel,
HelpBlock,
} from 'react-bootstrap';
// A generic input element that encapsulates several
// of the react-bootstrap components that are necessary
// for event simple scenarios.
const Input = ({
type,
label,
value,
placeholder,
onChange,
validationState,
validationText,
}) => (
<FormGroup validationState={validationState}>
<ControlLabel>{label}</ControlLabel>
<FormControl
type={type}
value={value}
placeholder={placeholder}
onChange={onChange}
/>
<FormControl.Feedback />
<HelpBlock>{validationText}</HelpBlock>
</FormGroup>
);
Input.propTypes = {
type: PropTypes.string.isRequired,
label: PropTypes.string,
value: PropTypes.any,
placeholder: PropTypes.string,
onChange: PropTypes.func,
validationState: PropTypes.oneOf([
undefined,
'success',
'warning',
'error',
]),
validationText: PropTypes.string,
};
export default Input;
这种方法有两个关键优势。一个是,我们不需要使用<FormGroup>
、<FormControl>
、<HelpBlock>
等等,我们只需要我们的<Input>
元素。另一个优点是只需要type
属性,这意味着<Input>
可以用于简单和复杂的控件。
现在让我们看看这个组件的运行情况:
import React, { PropTypes } from 'react';
import { Panel } from 'react-bootstrap';
import Input from './Input';
const InputsForm = (props) => (
<Panel header={(<h3>Inputs</h3>)}>
<form>
{ /* Uses the <Input> element to render
a simple name field. There's a lot of
properties passed here, many of them
come from the container component. */ }
<Input
type="text"
label="Name"
placeholder="First and last..."
value={props.nameValue}
onChange={props.nameChange}
validationState={props.nameValidationState}
validationText={props.nameValidationText}
/>
{ /* Uses the "<Input>" element to render a
password input. */ }
<Input
type="password"
label="Password"
value={props.passwordValue}
onChange={props.passwordChange}
/>
</form>
</Panel>
);
InputsForm.propTypes = {
nameValue: PropTypes.any,
nameChange: PropTypes.func,
nameValidationState: PropTypes.oneOf([
undefined,
'success',
'warning',
'error',
]),
nameValidationText: PropTypes.string,
passwordValue: PropTypes.any,
passwordChange: PropTypes.func,
};
export default InputsForm;
正如您所看到的,下面只有一个组件用于创建所有必要的引导程序。一切都是通过一个属性传入的。以下是此表单的外观:
现在让我们看看控制这些输入状态的容器组件:
import React, { Component } from 'react';
import { fromJS } from 'immutable';
import InputsForm from './InputsForm';
// Validates the given "name". It should have a space,
// and it should have more than 3 characters. There are
// many scenarios not accounted for here, but are easy
// to add.
function validateName(name) {
if (name.search(/ /) === -1) {
return 'First and last name, separated with a space';
} else if (name.length < 4) {
return 'Less than 4 characters? Srsly?';
}
return null;
}
class InputsFormContainer extends Component {
state = {
data: fromJS({
// "Name" value and change handler.
nameValue: '',
// When the name changes, we use "validateName()"
// to set "nameValidationState" and
// "nameValidationText".
nameChange: (e) => {
this.data = this.data.merge({
nameValue: e.target.value,
nameValidationState:
validateName(e.target.value) === null ?
'success' : 'error',
nameValidationText: validateName(e.target.value),
});
},
// "Password" value and change handler.
passwordValue: '',
passwordChange: (e) => {
this.data = this.data.set(
'passwordValue', e.target.value
);
},
}),
}
// Getter for "Immutable.js" state data...
get data() {
return this.state.data;
}
// Setter for "Immutable.js" state data...
set data(data) {
this.setState({ data });
}
render() {
return (
<InputsForm {...this.data.toJS()} />
);
}
}
export default InputsFormContainer;
输入的事件处理程序是状态的一部分,并作为属性传递给InputsForm
。现在让我们看看一些复选框和单选按钮。我们将使用<Radio>
和<Checkbox>
react 引导组件:
import React, { PropTypes } from 'react';
import {
Panel,
Radio,
Checkbox,
FormGroup,
} from 'react-bootstrap';
const RadioForm = (props) => (
<Panel header={(<h3>Radios & Checkboxes</h3>)}>
{ /* Renders a group of related radio buttons. Note
that each radio needs to have the same "name"
property, otherwise, the user will be able to
select multiple radios in the same group. The
"checked", "disabled", and "onChange" properties
all come from the container component. */}
<FormGroup>
<Radio
name="radio"
onChange={props.checkboxEnabledChange}
checked={props.checkboxEnabled}
disabled={!props.radiosEnabled}
>
Checkbox enabled
</Radio>
<Radio
name="radio"
onChange={props.checkboxDisabledChange}
checked={!props.checkboxEnabled}
disabled={!props.radiosEnabled}
>
Checkbox disabled
</Radio>
</FormGroup>
{ /* Renders a checkbox and uses the same approach
as the radios above: setting it's properties from
state that's passed in from the container. */}
<FormGroup>
<Checkbox
onChange={props.checkboxChange}
checked={props.radiosEnabled}
disabled={!props.checkboxEnabled}
>
Radios enabled
</Checkbox>
</FormGroup>
</Panel>
);
RadioForm.propTypes = {
checkboxEnabled: PropTypes.bool.isRequired,
radiosEnabled: PropTypes.bool.isRequired,
checkboxEnabledChange: PropTypes.func.isRequired,
checkboxDisabledChange: PropTypes.func.isRequired,
checkboxChange: PropTypes.func.isRequired,
};
export default RadioForm;
这个想法是单选按钮切换复选框的enabled
状态,复选框切换收音机的enabled
状态。请注意,尽管两个<Radio>
元素在相同的<FormGroup>
中,但它们需要具有相同的name
属性值。否则,您将能够同时选择两个收音机。以下是此表单的外观:
最后,让我们看看处理收音机状态的容器组件和复选框:
import React, { Component } from 'react';
import { fromJS } from 'immutable';
import RadioForm from './RadioForm';
class RadioFormContainer extends Component {
// Controls the enabled state of a group of
// radio buttons and a checkbox. The radios
// toggle the state of the checkbox while the
// checkbox toggles the state of the radios.
state = {
data: fromJS({
checkboxEnabled: false,
radiosEnabled: true,
checkboxEnabledChange: () => {
this.data = this.data.set(
'checkboxEnabled', true
);
},
checkboxDisabledChange: () => {
this.data = this.data.set(
'checkboxEnabled', false
);
},
checkboxChange: () => {
this.data = this.data.update(
'radiosEnabled',
enabled => !enabled
);
},
}),
}
// Getter for "Immutable.js" state data...
get data() {
return this.state.data;
}
// Setter for "Immutable.js" state data...
set data(data) {
this.setState({ data });
}
render() {
return (
<RadioForm {...this.data.toJS()} />
);
}
}
export default RadioFormContainer;
本章向您介绍了 mobile first 设计的概念。我们简要概述了为什么要使用移动优先战略。归根结底,将移动设计扩展到更大的设备要比反向扩展容易得多。
接下来,我们讨论了在 React 应用上下文中这意味着什么。特别是,我们希望使用一个框架,比如 Bootstrap,为我们处理缩放细节。然后,我们使用react-bootstrap
包中的组件实现了几个示例。
本书的第一部分到此结束。现在,您已经准备好处理 web 上的 React 项目,包括移动浏览器!移动浏览器正在变得更好,但它们无法与移动平台的本地功能相匹敌。本书的第二部分教你如何使用 React Native。