React 示例(三)
原文:
zh.annas-archive.org/md5/b5a25aa2a02a14cf60f32ae6c400d782译者:飞龙
第九章:React Router 和数据模型
在上一章中,我们探讨了可以提高 React 应用性能的 React 性能工具。我们探讨了使用 PERF 插件、PureRenderMixin 等,并查看了一些与 React 提供的性能工具相关的问题。
在本章中,我们将更深入地了解 react-router,并在不同级别执行路由。我们将探讨嵌套路由和传递参数,以及 react-router 在执行路由任务时如何维护历史记录。我们还将探讨传递和使用上下文来渲染 React 组件。最后,我们将探索数据模型,并将它们与其他框架混合匹配,用作 React 中的数据模型,例如 Backbone。
在本章中,我们将涵盖以下主题:
-
在你的应用中使用 React
-
使用 react-router 进行路由
-
不同的路由机制
-
设置路由和传递路由上下文
-
React 和数据存储/模型
-
使用 Backbone 模型/集合作为数据存储
在本章结束时,我们将能够开始使用 react-router 和不同的路由模式,并在路由中传递上下文数据。我们还将能够用 Backbone.js 等类似的东西替换纯数据模型的部分。
新的冒险
"嗨,Shawn 和 Mike!"Carla 大声说道。
Shawn 和 Mike 吃了一惊。他们刚刚到达,正准备开始新的一天。过去几天他们一直在探索 React。
"我有一些好消息要告诉你们。我们得到了一个新的项目,我们需要构建一个基于猫的兴趣网站。就像说——Pinterest?用户可以喜欢猫的图片和资料。然后他们可以看到并喜欢相关的销售文章,”Carla 继续说。
"哦,不错,”Shawn 回应道。
Shawn 和 Mike 重新集结,开始讨论他们刚刚从 Carla 那里听说的新项目。
"这很好。所以,我想,我们想在面板形状中显示一个小型的 Pinterest 风格的图片画廊吗?"Shawn 询问道。
"正确,”Mike 接着说,“我们还想以大尺寸显示图片,也许在用户点击图片后显示模态窗口。Carla 说她想在页脚中展示随机的猫,这将带我们到一个完整的猫展示页面。”
"你知道什么,我知道我们将要使用的完美东西。让我们今天看看 react-router!我也知道一个完美的示例开始。我们将查看 react-router 中的 Pinterest 示例,github.com/rackt/react-router/tree/master/examples/pinterest。然后我们将在其基础上构建我们的应用。”
"不错,”Shawn 说,“我可以看到现有的示例中包含了一些我们讨论过的事情,比如模态显示。让我看看这个示例的样子。”
Shawn 看了以下示例:
import React from 'react'
import { render } from 'react-dom'
import { browserHistory, Router, Route, IndexRoute, Link } from 'react-router'
…
const PICTURES = [
{ id: 0, src: 'http://placekitten.com/601/601' },
{ id: 1, src: 'http://placekitten.com/610/610' },
{ id: 2, src: 'http://placekitten.com/620/620' }
]
const Modal = React.createClass({
… // Modal Class implementation
})
const App = React.createClass({
componentWillReceiveProps(nextProps) {
// takes care of context in case of Modals
},
render() {
// Main render for Modal or Cat Pages
}
})
const Index = React.createClass({
render() {
// Index page render
..
<div>
{PICTURES.map(picture => (
<Link key={picture.id}
to={{
pathname: `/pictures/${picture.id}`,
state: { modal: true, returnTo: this.props.location.pathname }
}}
>
<img style={{ margin: 10 }} src={picture.src} height="100" />
</Link>
))}
</div>
.. // Usage of React Router Links
<p><Link to="/some/123/deep/456/route">Go to some deep route</Link></p>
</div>
)
}
})
const Deep = React.createClass({
render() {
// Render handler for some deep link
)
}
})
const Picture = React.createClass({
render() {
return (
<div>
// Pictures display
<img src={PICTURES[this.props.params.id].src} style={{ height: '80%' }} />
</div>
)
}
})
// The actual Routing logic using Router Library.
render((
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute component={Index}/>
<Route path="/pictures/:id" component={Picture}/>
<Route path="/some/:one/deep/:two/route" component={Deep}/>
</Route>
</Router>
), document.getElementById('example'))
"看起来很有趣,”Shawn 说。
"是的,让我们逐一查看我们需要创建的组件。首先,让我们看看我们将如何存储我们的数据并在整个系统中显示猫的数据。目前,图片存储在 PICTURES 常量中。我们希望存储更多内容。"
创建 Backbone 模型
"所以,肖恩,让我们继续构建我们想要展示的猫的收藏。为了开发目的,我们将使用 lorempixel 服务提供的猫图片,例如,lorempixel.com/600/600/cats/。这将给我们一个 600 x 600 像素的随机猫图片。"
"接下来,我们将创建一个使用不同于常规对象的数据存储。我们想探索如何将不同的模型流程嵌入到我们的 React 应用中。在我们的例子中,让我们使用 Backbone 模型,而不是 PICTURES 常量。我知道你已经使用过 Backbone。"
"是的,我在我的前一个项目中使用过它。"
"那么,让我们定义我们的 Cat 模型。"
const PictureModel = Backbone.Model.extend({
defaults: {
src: 'http://lorempixel.com/601/600/cats/',
name: 'Pusheen',
details: 'Pusheen is a Cat'
}
});
"在这里,我们存储猫图片的 src、它的名字以及一些关于它的细节。正如你所见,我们为这些属性提供了一些默认值。"
"接下来,让我们定义我们的 Cats 集合,包含所有的 Cat 记录。"
const Cats = new Backbone.Collection;
Cats.add(new PictureModel({src: "http://lorempixel.com/601/600/cats/",
name: Faker.Name.findName(),
details: Faker.Lorem.paragraph()}));
Cats.add(new PictureModel({src: "http://lorempixel.com/602/600/cats/",
name: Faker.Name.findName(),
details: Faker.Lorem.paragraph()}));
…
"在这里,我们使用 Faker 模块通过 Faker.Name.findName() 创建猫的随机名字,使用 Faker.Lorem.paragraph() 添加随机描述,并按需传递源信息。"
"酷,肖恩说。让我看看现在看起来怎么样。"
//models.js
import Backbone from 'backbone';
import Faker from 'faker';
const PictureModel = Backbone.Model.extend({
defaults: {
src: 'http://lorempixel.com/601/600/cats/',
name: 'Pusheen',
details: 'Pusheen is a Cat'
}
});
const Cats = new Backbone.Collection;
Cats.add(new PictureModel({src: "http://lorempixel.com/601/600/cats/", name: Faker.Name.findName(), details: Faker.Lorem.paragraph()}));
…
Cats.add(new PictureModel({src: "http://lorempixel.com/606/600/cats/", name: Faker.Name.findName(), details: Faker.Lorem.paragraph()}));
module.exports = {Cats, PictureModel};
集成定义的 Backbone 模型
"接下来,让我们定义我们的索引,以及我们需要路由如何工作以及路由应该响应哪些路径。从那里,我们将继续构建我们的组件。"
"明白了。"
import React from 'react'
import { render } from 'react-dom'
import { createHistory, useBasename } from 'history'
import { Router, Route, IndexRoute, Link } from 'react-router'
import Backbone from 'backbone';
import Modal from './Modal'
import App from './App'
import { Cats, PictureModel } from './models';
import Picture from './Picture'
import Sample from './Sample'
import Home from './Home'
const history = useBasename(createHistory)({
basename: '/pinterest'
});
render((
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="/pictures/:id" component={Picture}/>
<Route path="/this/:cid/is/:randomId/sampleroute" component={Sample}/>
</Route>
</Router>
), document.getElementById('rootElement'));
"所以,我看到的第一件事是我们正在创建一个会话历史记录?"
"正确,我们在这里创建了一个会话历史记录。我们将使用它作为我们的路由器。"
"在这里,我们使用历史模块的 useBasename 方法,它提供了在 base URL 下运行应用的支撑,在我们的例子中是 /pinterest。"
"明白了。"
"接下来,我们将展示我们实际上希望路由如何工作。我们将我们的路由器包装进 <Router/> 组件中,并指定不同的 <Route/> 作为路径。"
"这被称为路由配置,它基本上是一组规则或指令,用于将 URL 匹配到某些 React 组件以便显示。"
"哦,我们可以讨论一下这个配置吗?它看起来很有趣。"
"确实如此。首先,让我们看看 <IndexRoute component={Home}/> 做了什么。当我们到达应用的 / 页面时,在我们的例子中将是 /pinterest,由 IndexRoute 定义的组件将被渲染。正如你可能猜到的,要渲染的组件是通过路由的组件参数传递的。请注意,这是在 App 组件中显示的,它是所有基础组件。"
与 IndexRoute 类似,我们有不同的 <Route/> 定义。在我们的示例中,如果你看到 <Route path="/pictures/:id" component={Picture}/>,它显示了路由是如何被使用的,以及我们是如何传递相同属性的。在这里,路径属性是一个匹配表达式,组件属性指定了在匹配路由后要显示的组件。
"注意这里的路径是如何定义的,它被指定为一个表达式。"
基于 URL 对路由进行匹配是基于三个组件:
-
路由嵌套
-
路径属性
-
路由优先级
肖恩开始说,“我明白了嵌套的部分。我看到我们已经以嵌套的方式安排了我们的路由,就像一棵树。路由匹配和构建是基于这个树状匹配结构的。"
"对。其次,我们有路径属性。我们可以看到这些示例:"
<Route path="/pictures/:id" component={Picture}/>
<Route path="/this/:cid/is/:randomId/sampleroute" component={Sample}/>
"路径值是一个字符串,它作为一个正则表达式,可以由以下部分组成:"
-
:paramName: 例如,ID,这是在 URL 中传递的参数,如/pictures/12。12被解析为param id。 -
(): 这可以用来指定一个可选的路径,例如/pictures(/:id),这将匹配/pictures以及/pictures/12。 -
*: 就像正则表达式的情况一样,*可以用来匹配表达式的任何部分,直到下一个/、?或#出现。例如,为了匹配所有 JPEG 图像,我们可以使用/pictures/*.jpg。 -
**: 贪婪匹配,类似于*,但它贪婪地匹配。例如,/**/*.jpg将匹配/pictures/8.jpg以及/photos/10.jpg。
"明白了。最后,什么是优先级?最可能的是,它应该使用文件中定义的第一个路由,并满足用于匹配路径的条件?"
"没错," 迈克大声说道。
"哦,在我忘记之前,我们还有一个 <Redirect> 路由。这可以用来将一些路由匹配到其他路由操作。例如,我们希望 /photos/12 匹配 /pictures/12 而不是,我们可以将其定义为代码。"
<Redirect from="/photos/:id" to="/pictures/:id" />
"太棒了。"
"接下来,让我们看看我们正在导入和使用的一切,我们将它们定义为组件。"
import React from 'react'
…
import Modal from './Modal'
import App from './App'
import { Cats, PictureModel } from './models';
import Picture from './Picture'
import Sample from './Sample'
import Home from './Home'
"让我们首先定义我们的 App 组件,它将作为容器:"
..
import { Router, Route, IndexRoute, Link } from 'react-router'
import Modal from './Modal'
const App = React.createClass({
componentWillReceiveProps(nextProps) {
if ((
nextProps.location.key !== this.props.location.key &&
nextProps.location.state &&
nextProps.location.state.modal
)) {
this.previousChildren = this.props.children
}
},
render() {
let { location } = this.props;
let isModal = ( location.state && location.state.modal && this.previousChildren );
return (
<div>
<h1>Cats Pinterest</h1>
<div>
{isModal ?
this.previousChildren :
this.props.children
}
{isModal && (
<Modal isOpen={true} returnTo={location.state.returnTo}>
{this.props.children}
</Modal>
)}
</div>
</div>
)
}
});
export {App as default}
"我们这里不会改变太多,这是从我们已经看到的示例中来的。"
"我看到了这里的位置使用。这是来自 react-router 吗?"
"正如我们所看到的,我们的 App 被包裹在路由器中。路由器通过 props 传递位置对象。位置对象实际上类似于 window.location,这是我们使用的 history 模块定义的。Location 对象在其上定义了各种特殊属性,我们将利用这些属性,如下所示:"
-
pathname: URL 的实际路径名 -
search: 查询字符串 -
state: 从 react-router 传递并绑定到位置的对象 -
action:PUSH、REPLACE或POP操作之一 -
key: 位置的唯一标识符
"明白了。我看到我们正在使用之前看到的props.children。"
componentWillReceiveProps(nextProps) {
if ((
nextProps.location.key !== this.props.location.key &&
nextProps.location.state &&
nextProps.location.state.modal
)) {
this.previousChildren = this.props.children
}
}
"我想,当 Modal 显示时,我们将子元素和上一个屏幕存储到App对象上。"
"是的。我们首先检查是否显示了一个不同的组件,通过匹配 location 的 key 属性。然后我们检查是否在 location 上传递了状态属性,以及状态中的 modal 是否设置为 true。我们将在 Modal 显示的情况下做这件事。这是我们将状态传递给链接的方式:"
<Link … state={{ modal: true .. }}.. />
"当我们使用它来显示图片时,我们将查看Link对象。"
"明白了,肖恩说。"
"然后我看到我们正在传递子 props 或渲染上一个布局,然后,如果点击 modal,就在其上方显示Modal:"
{isModal ?
this.previousChildren :
this.props.children
}
{isModal && (
<Modal isOpen={true} returnTo={location.state.returnTo}>
{this.props.children}
</Modal>
)}
"没错!你在这一方面做得越来越好了,迈克兴奋地说。"
"现在,让我们看看我们的主要索引页面组件,好吗?"
// home.js
import React from 'react'
import { Cats, PictureModel } from './models';
import { createHistory, useBasename } from 'history'
import { Router, Route, IndexRoute, Link } from 'react-router'
const Home = React.createClass({
render() {
let sampleCat = Cats.sample();
return (
<div>
<div>
{Cats.map(cat => (
<Link key={cat.cid} to={`/pictures/${cat.cid}`} state={{ modal: true, returnTo: this.props.location.pathname }}>
<img style={{ margin: 10 }} src={cat.get('src')} height="100" />
</Link>
))}
</div>
<p><Link to={`/this/${sampleCat.cid}/is/456/sampleroute`}>{`Interesting Details about ${sampleCat.get('name')}`}</Link></p>
</div>
)
}
});
export {Home as default}
"所以肖恩,我们首先导入在Cats集合中生成的所有数据。我们将遍历它们并显示带有链接到 Modals 的图片。你可以在这里看到这个过程:"
{Cats.map(cat => (
<Link key={cat.cid} to={`/pictures/${cat.cid}`} state={{ modal: true, returnTo: this.props.location.pathname }}>
<img style={{ margin: 10 }} src={cat.get('src')} height="100" />
</Link>
))}
"是的,我看到我们正在使用cat对象的cid从 backbone 对象设置键。我们必须为链接指定路径,即它应该链接到的位置,我想?"
"没错。对于每只显示的猫,我们都生成一个唯一的动态路由,例如/pictures/121等等。现在,当我们点击它以显示放大后的猫时,我们正在将modal: true传递到<Link/>的状态中。"
"我们还传递了一个returnTo属性,它与从当前location.pathname获取的当前路径相关。我们将使用这个returnTo属性从状态中设置组件上的回链。我们将在 Modal 上显示一个,这样当点击时我们可以回到主页,并且 Modal 将被关闭。"
"明白了。我看到我们还在这里定义了一个用于样本猫展示页面的链接:"
let sampleCat = Cats.sample();
…
render(
…
<p><Link to={`/this/${sampleCat.cid}/is/456/sampleroute`}>{`Interesting Details about ${sampleCat.get('name')}`}</Link></p>
…
);
"是的,我们打算在这里随机展示一只猫。我们将在样本页面上显示关于猫的详细信息。现在,我想向你展示我们是如何在这里创建链接的:"
`/this/${sampleCat.cid}/is/456/sampleroute`
"在这里,我们正在创建一个嵌套的随机路由,例如,这可以匹配以下 URL:"
/this/123/is/456/sampleroute
"123和456作为位置的参数。"
"很好,肖恩接着说。让我定义 Modal?让我重用示例中的那个。"
import React from 'react'
import { Router, Route, IndexRoute, Link } from 'react-router'
const Modal = React.createClass({
styles: {
position: 'fixed',
top: '20%',
right: '20%',
bottom: '20%',
left: '20%',
padding: 20,
boxShadow: '0px 0px 150px 130px rgba(0, 0, 0, 0.5)',
overflow: 'auto',
background: '#fff'
},
render() {
return (
<div style={this.styles}>
<p><Link to={this.props.returnTo}>Back</Link></p>
{this.props.children}
</div>
)
}
})
export {Modal as default}
"很简单,肖恩。我们还需要定义如何显示图片。让我们定义一下。"
import React from 'react'
import { Cats, PictureModel } from './models';
const Picture = React.createClass({
render() {
return (
<div>
<img src={Cats.get(this.props.params.id).get('src')} style={{ height: '80%' }} />
</div>
)
}
});
export {Picture as default}
"为了显示猫并获取它的详细信息,我们使用从 params 接收到的 ID。这些是通过params属性发送给我们的。然后我们从Cats集合中获取 ID。"
Cats.get(this.props.params.id)
"使用id属性,回忆一下我们是如何在定义如下链接时发送 ID 的:"
<Route path="/pictures/:id" component={Picture}/>
"最后,让我们看看如何使用示例组件来显示猫的信息:"
import React from 'react'
import { Cats, PictureModel } from './models';
import { createHistory, useBasename } from 'history'
import { Router, Route, IndexRoute, Link } from 'react-router'
const Sample = React.createClass({
render() {
let cat = Cats.get(this.props.params.cid);
return (
<div>
<p>CID for the Cat: {this.props.params.cid}, and Random ID: {this.props.params.randomId}</p>
<p>Name of this Cat is: {cat.get('name')}</p>
<p>Some interesting details about this Cat:</p>
<p> {cat.get('details')} </p>
</p>
</div>
)
}
});
export {Sample as default};
"有了这个,看起来我们已经完成了!让我们看看它看起来怎么样,好吗?"
"首页看起来很整洁。"
"接下来,让我们看看 Modal 和链接与 URL 的样子。"
"这只猫看起来真不错。" 肖恩笑着说。
"哈哈,是的。"
注意
注意 URL。点击时,模态链接变成了锚标签上的链接。我们处于同一页面,并且模态被显示。
"最后,我们有样本页面,在这里我们显示猫的详细信息。让我们看看它的样子:"
"太棒了!"
数据模型和 Backbone
"肖恩,我想讨论一下我们在这里如何使用 Backbone 模型,或者我们如何存储数据。我们从以下代码迁移到使用 Backbone 集合。这帮助我们更好地定义我们的数据:"
PICTURES =[{array of objects}]
"然而,如果你注意到,我们最终定义了一个静态的对象集合。此外,这个集合是全局的,需要传递给其他部分。"
"这是真的。我也注意到我们为数据在全局范围内有一个固定的 state。我相信,我们在这里可能没有做什么。如果我们更新了,Views 仍然会保持不变吗?"
"没错!在我们这个案例中,我们以固定的方式发送、使用/修改数据,全局范围内。对这部分应用中的数据进行的任何更新都不会影响我们视图的显示方式,甚至不同组件中已经访问的数据也不会改变。例如,考虑一下 Home 组件改变了 Cats 常量。首先,它不会与 Sample、Modal 或其他组件同步更改。"
"其次,Home 组件对 Cats 集合的更改甚至不会改变 Home 组件的显示!"
"啊,这相当棘手。我想,我们最终会将所有这些集合状态存储在一个全局组件状态中,比如 App 组件,它只渲染一次。" 肖恩接着说。
"是的,我们可以这样做。但问题在于,在这种情况下,我们需要手动维护状态,并将子组件的状态更新到 App 组件,等等。想象一下,比如有人点击了一个猫的图片,需要改变猫的状态。事件会在 Picture 组件上发生,我们需要手动将事件传播到 Home 或 Modal 组件,然后再传播到 App 组件,以便真正更新全局集合。"
"这不会很好。我相信这将很难跟踪和调试。"
"没错。在我们接下来的重构中,我们将尝试改变这种做法,将其限制在 App 中。从长远来看,我们将尝试使用 Flux。"
"哦,对了,我听说过它。它是用于传递或访问数据,以及通过事件或其他方式管理数据变化的吗?"
“嗯,不是完全如此,它帮助我们简化了单向数据流中的数据流。维护的状态会传播到组件中,并按需更新。例如,拥有一只猫的事件可能会改变数据存储,进而改变组件。”
“无论如何,我只是想给你一个关于这个的想法,以及为什么我们稍后会探索 Flux。现在,我们的解决方案按预期工作。”
天色渐晚。在 Adequate LLC 公司又度过了一个有趣的一天。肖恩和迈克合作,使用 react-router 并与之混合 Backbone 模型构建了一个简单的应用程序。
摘要
在本章中,我们构建了一个简单的类似 Pinterest 的应用程序,利用 react-router 并对其在不同级别的路由执行时进行了更深入的研究。我们还探讨了嵌套路由、传递参数、react-router 如何维护历史记录等问题,在执行路由任务时。我们还研究了如何传递和使用上下文来渲染 React 组件,以及如何将 Backbone 模型与它混合以维护 Cats 显示数据。
在下一章中,我们将探讨在现有应用程序的基础上添加动画和一些其他显示功能。
第十章。动画
在上一章中,我们了解了 react-router 并在不同级别执行路由。我们还探讨了嵌套路由、传递参数以及 react-router 在执行路由任务时如何维护历史记录。我们学习了传递上下文和使用上下文来渲染 React 组件。我们探讨了数据模型,并将它们与其他框架混合匹配,用作 React-like Backbone 中的数据模型,并介绍了 Flux。
在本章中,我们将探索一个有趣的 React 插件,动画。我们将从继续我们的猫 Pinterest 应用开始,并增强它以支持星标和共享数据来更新视图。然后我们将探索添加动画处理器。我们将看到组件是如何被包装进行动画的,以及 React 是如何为不同事件添加处理器的。我们还将探索不同的事件,以及我们如何轻松增强我们的应用程序以创建惊人的效果。
在本章中,我们将涵盖以下主题:
-
修改数据流并从 react-router 链接传递数据
-
React 中的动画
-
CSS 过渡
-
转换组
-
转换处理器
-
动画我们的动态组件
在本章末尾,我们将能够开始为不同的动作如添加新内容、更改数据和位置等对 React 组件进行动画处理。我们还将能够添加不同类型事件的处理器,并探索除了核心动画插件之外的不同动画选项。
在 Adequate LLC 有很多有趣的事情!
"嗨,肖恩和迈克!" 卡拉加入了迈克和肖恩的对话。
在前一天,卡拉要求他们为他们的一个客户构建一个 Pinterest 风格的猫应用。
"今天怎么样?"她询问道。
"一切顺利,卡拉。肖恩,你想向卡拉展示我们昨天建造的东西吗?"
"当然。"
"看起来不错!我们接下来要添加点赞/星标猫的按钮吗?"
"是的,我们正准备做这件事。"
"酷。昨天客户打电话来了。他们除了显示猫之外还想显示屏幕上猫的更新流。这将在有人点赞猫时发生,这样我们就可以向其他用户展示它。"
"明白了。我们将开始工作,并模拟添加猫到屏幕上以开始。"
"太棒了,我就让你们俩处理吧。"
模型更新
"所以肖恩,我们不如将 Backbone 集合移动到一个类中,以独立的方式使用它,让它随机添加新的猫并提供一些其他工具,如下所示:"
const PictureModel = Backbone.Model.extend({
defaults: {
src: 'http://lorempixel.com/601/600/cats/',
name: 'Pusheen',
details: 'Pusheen is a Cat',
faved: false
}
});
"我们的PictureModel保持不变。我们在这里添加一个新的faved属性来维护用户是否喜欢这只猫的状态。
"我们将把这个新类命名为CatGenerator,它将提供我们用来显示猫的组件,以及显示、获取和添加新猫的数据。"
"明白了。需要我试一试吗?"
"当然。"
import Backbone from 'backbone';
import Faker from 'faker';
import _ from 'underscore';
…
class CatGenerator {
constructor() {
this.Cats = new Backbone.Collection;
[600, 601, 602, 603, 604, 605].map( (height)=>{
this.createCat(height, 600);
})
}
createCat(height = _.random(600, 650), width = 600) {
console.log('Adding new cat');
this.Cats.add(new PictureModel({
src: `http://lorempixel.com/${height}/${width}/cats/`,
name: Faker.Name.findName(),
details: Faker.Lorem.paragraph()
}));
}
}
"做得好,肖恩。"
"谢谢。我已经将createCat作为一个独立的方法移动,这样我们就可以在运行时向集合中添加猫。我现在正在添加一个随机的一个,随机高度为 600-650 和随机宽度来创建一个新的PictureModel实例。"
"此外,首先,我在类属性上创建了一个cats集合。接下来,我一开始就添加了六只猫。"
"酷。我们现在将开始更改它在我们的组件中的使用。"
提示
"记住,当新数据到来时,我们将更新组件。这样做的一个简单方法是开始在Home组件上存储CatGenerator作为状态对象。"
"让我们开始定义和更改我们的Home组件,如下所示:"
class Home extends React.Component {
constructor() {
super();
this.timer = null;
this.state = {catGenerator: new CatGenerator()};
}
componentDidMount() {
this.timer = setInterval(::this.generateCats, 1000);
}
generateCats() {
let catGenerator = this.state.catGenerator;
catGenerator.createCat();
clearInterval(this.timer);
this.timer = setInterval(::this.generateCats, catGenerator.randRange());
this.setState({catGenerator: catGenerator});
}
…
"所以,我们在这里做的是创建一个计时器来跟踪时间间隔。我们将使用一个随机的时间间隔来模拟在这里添加新的猫流。"
"明白了," 肖恩接着说。
"为此,我添加了generateCats()方法。在我们的componentDidMount中,我们在第一次创建后添加并设置计时器来调用此方法。"
"在方法本身中,我添加了清除旧间隔,并且我们调用catGenerator.createCat()方法来实际上从我们的CatGenerator类创建猫。"
"然后我们重置计时器并设置一个新的,基于随机的时间间隔。我在CatGenerator类中添加了catGenerator.randRange()方法来生成随机的时间间隔。这就是它在CatGenerator类中的样子:"
randRange() {
return _.random(5000, 10000);
}
"明白了。这应该会在 5-10 秒的范围内创建一个新的猫流。"
"接下来,让我们看看我们的渲染方法看起来怎么样。我打算在猫旁边添加一个星号。"
render() {
let Cats = this.state.catGenerator.Cats;
return (
<div>
<div>
{Cats.map(cat => (
<div key={cat.cid} style={{float: 'left'}}>
<Link to={`/pictures/${cat.cid}`}
state={{ modal: true, returnTo: this.props.location.pathname, cat: cat }}>
<img style={{ margin: 10 }} src={cat.get('src')} height="100"/>
</Link>
<span key={`${cat.cid}`} className="fa fa-star"></span>
</div>
))}
</div>
</div>
)
}
"我在这里做了两个更改。首先,我添加了一个默认未收藏的星号。"
<span key={`${cat.cid}`} className="fa fa-star"></span>
"其次,我开始在模态链接的状态中传递猫对象。"
<Link to={`/pictures/${cat.cid}`}
state={{ modal: true,
returnTo: this.props.location.pathname,
cat: cat }}>
"在我们的PictureModel框中,我们之前可以访问全局猫集合。从现在起,情况将不再是这样,我们需要将猫对象传递给Picture组件。"
"这很棒,我们能够将对象传递给从路由<Link/>对象来的组件。"
"是的,让我们继续更改图片组件,以便它能够正确地处理这个新的数据传递变化。我们的Modal保持不变:"
const Modal = React.createClass({
styles: {
…
},
render() {
return (
<div style={this.styles}>
<p><Link to={this.props.returnTo}>Back</Link></p>
{this.props.children}
</div>
)
}
})
…
export {Modal as default}
"现在Picture组件开始使用猫对象。"
import React from 'react'
import { PictureModel } from './models';
const Picture = React.createClass({
render() {
let { location } = this.props;
let cat = location.state.cat;
console.log(this.props);
return (
<div>
<div style={{ float: 'left', width: '40%' }}>
<img src={cat.get('src')} style={{ height: '80%' }}/>
</div>
<div style={{ float: 'left', width: '60%' }}>
<h3>Name: {cat.get('name')}.</h3>
<p>Details: {cat.get('details')} </p>
</div>
</div>
)
}
});
export {Picture as default}
"正如你所看到的,猫对象是通过location.state对象从 props 接收的。"
"我已经扩展了图片以显示有关猫的更多详细信息,例如名称等,而不是在单独的页面上显示。之前它看起来相当空白。"
"酷,让我们看看它看起来怎么样,好吗?"
"很好,星星看起来不错。我们很快需要检查我添加的样式。"
"模态看起来也不错,看看所有这些作为流生成的猫!"
"太棒了!" 迈克和肖恩欢呼。
动画
"React 允许我们通过其 react-addons-css-transition-group 扩展插件轻松地动画化对象。"
"这为我们提供了对 ReactCSSTransitionGroup 对象的引用,这是我们用来动画化数据变化(如添加猫、点赞/取消点赞等)的。"
"让我们先从动画化新猫添加到流中开始,怎么样?"
render() {
let Cats = this.state.catGenerator.Cats;
return (
<div>
<div>
<ReactCSSTransitionGroup transitionName="cats"
transitionEnterTimeout={500}
transitionLeaveTimeout={300}
transitionAppear={true}
transitionAppearTimeout={500}>
{Cats.map(cat => (
<div key={cat.cid} style={{float: 'left'}}>
<Link to={`/pictures/${cat.cid}`}
state={{ modal: true, returnTo: this.props.location.pathname, cat: cat }}>
<img style={{ margin: 10 }} src={cat.get('src')} height="100"/>
</Link>
<span key={`${cat.cid}`} className="fa fa-star"></span>
</div>
))}
</ReactCSSTransitionGroup>
</div>
</div>
)
}
"在这里,我更改了我们的渲染方法,并简单地用 ReactCSSTransitionGroup 元素包裹了猫集合的显示,如下所示。"
<ReactCSSTransitionGroup transitionName="cats"
transitionEnterTimeout={500}
transitionLeaveTimeout={300}
transitionAppear={true}
transitionAppearTimeout={500}>
"让我们逐一在以下内容中查看它们:"
-
transitionName:此属性用于定义应用于不同事件(如元素进入、离开等)的 CSS 类的前缀。 -
transitionEnterTimeout:这是元素在渲染后新鲜显示的超时时间。 -
transitionLeaveTimeout:这与transitionEnterTimeout类似,但用于元素从页面移除时。 -
transitionAppear:有时,我们想在元素首次渲染时动画化元素集合的添加,在我们的例子中是猫。我们可以通过将此属性设置为true来实现这一点。注意
注意,在第一个元素显示之后添加的元素将应用
transitionEnter属性。 -
transitionAppearTimeout:这与其他超时值类似,但用于transitionAppear。 -
transitionEnter:默认情况下,此属性设置为true。如果我们不想动画化元素进入过渡,则可以将其设置为false。 -
transitionLeave:默认情况下,此属性设置为true。如果我们不想动画化元素离开过渡动画,则可以将其设置为false。
"现在,基于过渡和过渡名称,类被应用到 <ReactCSSTransitionGroup/> 组件内的元素上。例如,对于进入过渡,以及我们的 cats 前缀,cats-enter 将被应用到元素上。"
"在下一个周期中,cats-enter-active 将应用到元素应该处于的最终类。"
"明白了。"
"让我们检查一下我们可以根据这个定义的所有不同过渡。"
.cats-enter {
opacity: 0.01;
}
.cats-enter.cats-enter-active {
opacity: 1;
transition: opacity 1500ms ease-in;
}
.cats-leave {
opacity: 1;
}
.cats-leave.cats-leave-active {
opacity: 0.01;
transition: opacity 300ms ease-in;
}
.cats-appear {
opacity: 0.01;
}
.cats-appear.cats-appear-active {
opacity: 1;
transition: opacity 1.5s ease-in;
}
"这里的动画过渡相当简单。当在开始时添加新元素,从我们初始化的六只猫开始,将应用 .cats-appear 类。在下一次计时器滴答声后,将添加 .cats-appear-active 类到元素上。"
"接下来,在过渡成功后,类将被移除,如下面的截图所示:"
"肖恩,如果你能看到,你会注意到猫是如何淡入然后以全不透明度显示其最终状态的。"
"不错。看起来很棒。当新元素被添加时,这是一个很好的效果。"
"确实。你想尝试动画化星星吗?"
"当然!"
"让我首先检查一下我们为星星设置的类。我看到你已经使用了 font-beautiful 星星并为他们添加了样式。"
.fa {
transition: all .1s ease-in-out;
color: #666;
}
.star{
display: inline-block;
width: 20px;
position: relative;
}
.star span{
position: absolute;
left: 0;
top: 0;
}
.fa-star{
color: #fa0017;
}
.fa-star-o{
color: #fa0017;
}
.fa-star-o:active:before {
content: "\f005"!important;
}
"是的,就在那里。"
"首先,让我处理星星的点赞和取消点赞功能。"
faveUnfave(event){
let catCid = event.target.dataset;
let catGenerator = this.state.catGenerator;
let Cats = catGenerator.Cats;
let cat = Cats.get(catCid);
cat.set('faved', !cat.get('faved'));
catGenerator.Cats = Cats;
this.setState({catGenerator: catGenerator});
}
"将元素改为添加 data-cid 和 handler,如下所示:"
<span key={`${cat.cid}`} className="fa fa-star" onClick={::this.faveUnfave} data-cid={cat.cid}></span>
"首先,我将 faveUnfave 作为 onClick 事件传递,这个事件在这里绑定到了类上下文中。接下来,我为 data-cid 传递了 cat.cid 的值。"
"在 faveUnfave 方法中,我将拉取点赞元素的猫 ID。基于此,我将从猫生成器的猫集合中拉取猫对象。稍后,我将切换当前点赞值的状态并重置集合的状态。"
"看起来不错。"
"接下来,我将根据当前的点赞状态显示点赞或取消点赞的星星,并将其包装为 CSS 过渡,这样我们就可以开始显示显示和隐藏星星、更改颜色等动画。"
<ReactCSSTransitionGroup transitionName="faved"
transitionEnterTimeout={500}
transitionLeaveTimeout={300}
transitionAppear={true}
transitionAppearTimeout={500}
className="star">
{()=>{
if(cat.get('faved') === true){
return <span key={`${cat.cid}`} className="fa fa-star" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
} else {
return <span key={`${cat.cid}`} className="fa fa-star-o" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
}
}()}
</ReactCSSTransitionGroup>
"太完美了,”迈克接着说。
"现在让我们为这个点赞添加样式。"
.faved-enter {
transform: scale(1.5);
}
.faved-enter.faved-enter-active {
transform: scale(3);
transition: all .5s ease-in-out;
}
.faved-leave {
transform: translateX(-100%);
transform: scale(0);
}
.faved-leave.faved-leave-active {
transform: scale(0);
transition: all .1s ease-in-out;
}
"在这里,我添加了动画,当点击星星时,它会放大,类似于 Twitter 的点赞功能。然后,它会恢复缩放并保持在点赞状态。"
"同样,在取消点赞时,它将放大并恢复到原始大小。"
"看起来不错,让我们检查一下,”迈克接着说。
"嗯,我觉得所有元素都在这里,但它似乎不起作用,迈克?"
"让我看看。啊,所以罪魁祸首是这个:"
{()=>{
if(cat.get('faved') === true){
return <span key={`${cat.cid}`} className="fa fa-star" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
} else {
return <span key={`${cat.cid}`} className="fa fa-star-o" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
}
}()}
注意
注意我们在这里使用的关键值?它是相同的。TransitionGroup 会跟踪元素的变化,并根据键值执行动画任务。TransitionGroup 需要知道元素中发生了什么变化,以便执行动画任务,它还需要键来识别元素。
"在这种情况下,点赞或取消点赞时,键将保持为 cat.cid,因此元素保持不变。"
"让我们给键添加一个后缀或前缀,以及点赞状态。"
{()=>{
if(cat.get('faved') === true){
return <span key={`${cat.cid}_${cat.get('faved')}`} className="fa fa-star" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
} else {
return <span key={`${cat.cid}_${cat.get('faved')}`} className="fa fa-star-o" onClick={::this.faveUnfave} data-cid={cat.cid}></span>;
}
}()}
"太完美了。现在它工作了,迈克。"
"是的。肖恩,你在 CSS 动画方面做得很好。星星看起来不错。让我们看看现在是什么样子。"
"这就是我们给猫点赞时的样子:"
"这个是在点赞过渡完成后。"
"最后,当我们尝试取消点赞猫时,也会发生相同的动画。"
"太完美了,卡拉会喜欢的!"
在 Adequate LLC 度过了一个愉快的一天。肖恩和迈克致力于重构他们的应用程序,以便数据更改能够反映视图更改,并对添加和删除的猫进行动画处理。他们还研究了如何点赞/取消点赞星星。
摘要
在本章中,我们围绕改变数据流和直接从 react-router 链接传递数据进行了操作。我们查看了对添加/删除或出现的对象集合进行动画处理。我们看到了 ReactCSSTransitionGroup 支持的不同过渡事件以及如何使用相关类来动画化我们的对象。
在下一章中,我们将学习如何使用 Jest 和 React TestUtils 测试我们的应用程序。
第十一章。React 工具
在上一章中,我们学习了如何使用动画插件和 CSS 过渡。我们还探索了不同的事件,并研究了如何通过动画轻松增强我们的应用程序,以创建令人惊叹的效果。
在本章中,我们将探讨 React 生态系统中的各种工具,这些工具在整个应用程序的生命周期中都有用——开发、调试和构建工具。我们将看到这些工具如何使开发 React 应用程序成为一种美好的体验。
在本章中,我们将研究以下工具:
-
Babel
-
ESLint
-
React 开发者工具
-
Webpack
-
使用 Webpack 进行热重载
迈克和肖恩在开始他们的下一个项目之前有一些空闲时间。他们决定利用这段时间学习更多关于他们在 React 项目中迄今为止使用的各种工具,这些工具用于开发、测试和打包应用程序。
开发工具
"肖恩,今天我想讨论一下我们在今天构建 React 应用程序过程中所使用的工具。React 是一个非常小的库,它做了一件事情做得很好——渲染 UI。然而,在我们迄今为止的旅程中,我们不得不使用很多其他工具与 React 一起使用。今天是我们讨论所有这些工具的一天。" 迈克说。
"太棒了,迈克!我总是准备好了。让我们开始吧。" 肖恩兴奋地说。
使用 Babel 进行 ES6 和 JSX
"肖恩,我们从一开始就使用了 ES6 或 ES2015 代码。我们也非常看好使用 JSX。有时,我们也使用了 ES7 代码,比如我们最新的 Cats Pinterest 项目中函数bind操作符。"
// src/Home.js
class Home extends React.Component {
componentDidMount() {
this.timer = setInterval(::this.generateCats, 1000);
}
}
"是的,迈克。我喜欢这些新功能的简洁性。" 肖恩说。
"然而,当前的浏览器仍然不理解我们编写的 ES6 或 ES7 代码。我们使用 Babel 将这些代码转换为 ES5 JavaScript,这样当前的浏览器就可以运行。它允许我们今天使用未来的 JavaScript 语法。Babel 还支持 JSX,因此它与 React 一起使用非常方便。" 迈克解释说。
"Babel 非常模块化,并带有插件架构。它为不同的 ES6/ES7 语法提供了插件。通常,我们希望使用特定于 React 和特定于 ES6 的插件。Babel 将这些常见插件组合成称为预设的东西。Babel 为 ES6、React 以及未来的语言提案的不同阶段提供了各种插件。"
"我们主要对使用 ES2015 和 React 预设感兴趣,这些预设包含所有与 ES6 和 React 相关的插件。偶尔,我们确实需要一些高级功能,例如 ES7 函数绑定语法,因此我们需要单独配置它。在这种情况下,我们直接使用单个插件,就像我们使用transform-function-bind来处理bind函数语法一样。" 迈克解释说。
注意
所有这些预设和插件都有自己的 npm 包。Babel 就是这样构建的——一个小型核心和庞大的插件架构,周围有许多配置选项。
"因此,我们将不得不分别安装所有这些包。"
npm install babel-core --save
npm install babel-loader --save
npm install babel-preset-react --save
npm install babel-preset-es2015 --save
npm install babel-plugin-transform-function-bind –save
"明白了。我也看到了我们 Webpack 配置中的一些 Babel 相关配置。" 肖恩说。
"是的。虽然 Babel 允许我们从命令行转换文件,但我们不想手动转换每个文件。因此,我们已经以这种方式配置了 Webpack,它将在启动应用程序之前使用 Babel 转换我们的 ES6/ES7 代码。它使用 babel-loader 包。等我们今天晚些时候讨论 Webpack 时再详细讨论这个问题吧。" 迈克说。
注意
我们在这本书中一直使用 Babel 版本 6。查看有关 Babel 的更多详细信息,请访问babeljs.io/。
ESLint
"肖恩,你看到我们项目中关于 linting 的提交了吗?"
"是的。最初我对这些小小的变化感到很烦恼,但后来我习惯了。"
"Linting非常重要,尤其是如果我们想在不同的项目中保持代码质量。幸运的是,使用 ESLint 来 lint React 项目非常容易。它还支持 ES6 语法和 JSX,这样我们也可以 lint 我们的下一代代码。" 迈克说道。
"我们使用 eslint-plugin-react 和 babel-eslint npm 包来 lint ES6 和 React 代码。我们还全局安装了 ESLint npm 包。"
注意
查看有关如何开始使用 ESLint 的详细信息,请访问eslint.org/docs/user-guide/getting-started。
"迈克,我也看到你在package.json的scripts下添加了 lint 命令。" 肖恩补充道。
// package.json
"scripts": {
"lint": "eslint src"
}
"是的,肖恩。在大项目中,这里那里可能会遗漏一些事情是很常见的。有一个命令来 lint 项目有助于我们找到这些事情。我们需要 eslint、eslint-babel 和 eslint-plugin-react 包来在我们的代码中使用 ESLint。因此,在尝试运行此命令之前,我们需要安装它。"
npm install eslint --save
npm install babel-eslint --save
npm install eslint-plugin-react –save
"我们也在使用一些标准的 ESLint 配置选项。这些选项存在于我们项目的.eslintrc文件中。我们在这个文件中定义了 ESLint 要检查的规则。我们还启用了 ES6 功能,让 ESLint 将其列入白名单。否则,它会对原生只支持 ES5 的此类代码引发 linting 错误。我们还指定 ESLint 应使用 babel-eslint 作为解析器,以便 ES6 代码能够被 ESLint 正确解析。"
// .eslintrc
{
"parser": "babel-eslint",
"env": {
"browser": true,
"es6": true,
"node": true,
"jquery": true
},
"plugins": [
"react"
],
"ecmaFeatures": {
"arrowFunctions": true,
"blockBindings": true,
"classes": true,
"defaultParams": true,
"destructuring": true,
"forOf": true,
"generators": true,
"modules": true,
"spread": true,
"templateStrings": true,
"jsx": true
},
"rules": {
"consistent-return": [0],
"key-spacing": [0],
"quotes": [0],
"new-cap": [0],
"no-multi-spaces": [0],
"no-shadow": [0],
"no-alert": [0],
"no-unused-vars": [0],
"no-underscore-dangle": [0],
"no-use-before-define": [0, "nofunc"],
"comma-dangle": [0],
"space-after-keywords": [2],
"space-before-blocks": [2],
"camelcase": [0],
"eqeqeq": [2]
}
}
"我们现在已经准备好了。去运行我们的 Pinterest 项目,并修复剩余的 linting 问题。" 迈克说道。
$ npm run lint
> react-router-flux@0.0.1 lint /Users/prathamesh/Projects/sources/reactjs-by-example/chapter11
> eslint src
/reactjs-by-example/chapter11/src/Home.js
29:20 error Missing space before opening brace space-before-blocks
x 1 problem (1 error, 0 warnings)
"啊,它抱怨缺少了一个空格。让我快速修复一下。"
// Before
faveUnfave(event){
…
}
// After
faveUnfave(event) {
…
}
"完美,肖恩!"
注意
ESLint 也可以与您的文本编辑器集成。有关更多详细信息,请查看eslint.org/docs/user-guide/integrations.html。
React Dev Tools
"肖恩,React 在提升开发者体验方面非常出色。他们发布了 react-dev-tools 来帮助我们调试我们的应用程序。React 开发者工具是 Chrome 和 Firefox 的插件,这使得调试 React 应用程序变得有趣。"
“一旦安装了插件,你将在运行 React 应用的浏览器控制台中看到一个 React 选项卡。有趣的是,这个选项卡也会显示在生产中使用 React 的网站上,例如,Facebook。”
“一旦我们点击 React 选项卡,它就会显示我们应用中的所有组件。”
“肖恩,正如你可能注意到的,我们可以在左侧面板中看到所有的组件。在右侧,我们看到左侧面板中选择的组件的属性和状态。因此,我们可以在任何时间点检查 UI 状态。我们不需要添加 console.log 语句来查看我们的组件发生了什么。”
“不仅如此,它还为我们提供了一个临时变量——r。”
“让我们看看控制台中的 $r 能给我们带来什么,这样我们就可以直接在控制台中调试所选组件。”
“它还允许我们滚动到 UI 中的所选组件,以查看组件的实际源代码。它还可以显示特定类型的所有组件。”
“肖恩,你对这些开发工具有什么看法?”迈克问道。
“我非常印象深刻。这真的很棒!从现在开始,我将在每一个 React 项目中使用它们。”肖恩对看到 React 开发工具的力量感到非常兴奋。
注意
查看更多关于 React 开发工具的详细信息:github.com/facebook/react-devtools
构建工具
“肖恩,当创建新的 Web 应用程序时,构建系统可能是我们首先应该关心的事情。它不仅是一个运行脚本的工具,在 JavaScript 世界中,它通常塑造我们应用程序的基本结构。”
以下责任应由构建系统执行:
-
外部依赖以及内部依赖都应该被管理
-
它应该运行编译器/预处理器
-
它应该优化生产环境中的资源
-
开发服务器、浏览器重新加载器和文件监视器应该由它运行
“有很多不同的工具,如 Grunt、Gulp 和 Browserify,可以用作我们构建系统的一部分。每个工具都有其自身的优缺点。然而,我们已经决定在我们的项目中使用 Webpack。”迈克说道。
什么是 Webpack?
“Webpack 是一个模块打包器。它将我们的 JavaScript 及其依赖项打包成一个单独的包。”
"与 Browserify 和其他工具不同,Webpack 还可以捆绑其他资源,如 CSS、字体和图像。它支持 CommonJS 模块语法,这在 node.js 和 npm 包中非常常见。因此,它使事情变得更容易,因为我们不需要使用另一个包管理器来处理前端资源。我们可以只使用 npm,并在服务器端代码和前端代码之间共享依赖项。它还足够智能,能够按正确顺序加载依赖项,这样我们就不需要担心显式和隐式依赖项的顺序。"(内联代码不需要翻译)
"因此,Webpack 本身就可以执行 Browserify 以及其他构建工具如 Grunt 和 Gulp 的任务。"(内联代码不需要翻译)
Note(内联代码不需要翻译)
"本节不会涵盖 Webpack 的所有方面。然而,我们将讨论如何有效地使用 Webpack 与 React 结合。"(内联代码不需要翻译)
Webpack configuration(内联代码不需要翻译)
"肖恩,在一个典型的 React 应用中,我们在组件中使用 ES6 代码和 JSX。我们还在同一个组件中使用前端资源,使其更易于携带。因此,我们的 Webpack 配置必须正确处理所有这些方面。" 迈克解释道。
"让我们以我们的 Pinterest 应用为例,看看 Webpack 是如何配置来运行它的。"(内联代码不需要翻译)
"首先,我们需要通知 Webpack 关于我们应用的入口点。在我们的例子中,它是index.js文件,它将App组件挂载到 DOM 中。"(内联代码不需要翻译)
// src/index.js
render((
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="/pictures/:id" component={Picture}/>
</Route>
</Router>
), document.getElementById('rootElement'));
"因此,我们在webpack.config.js文件中提到入口点为src/index.js。"(内联代码不需要翻译)
// webpack.config.js
path = require('path');
var webpack = require('webpack');
module.exports = {
// starting point of the application
entry: [ './src/index']
};
"其次,我们需要通知 Webpack 将生成的捆绑代码放在哪里。这是通过添加一个输出配置来完成的。"(内联代码不需要翻译)
// webpack.config.js
var path = require('path');
var webpack = require('webpack');
module.exports = {
entry: ['./src/index'],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/static/'
}
}
"输出选项告诉 Webpack 将编译后的文件写入当前目录的 dist 文件夹。文件名将是bundle.js。我们可以通过运行webpack命令来查看bundle.js的输出。"(内联代码不需要翻译)
$ webpack
Hash: f8496f13702a67943730
Version: webpack 1.12.11
Time: 2690ms
Asset Size Chunks Chunk Names
bundle.js 1.81 MB 0 [emitted] main
[0] multi main 52 bytes {0} [built]
+ 330 hidden modules
"这将创建一个包含所有编译代码的dist/bundle.js文件。"(内联代码不需要翻译)
"The publicPath specifies the public URL address of the output files, when referenced in a browser. This is the path that we use in our index.html file, which will be served by the web server to the users."(内联代码不需要翻译)
// index.html
<html>
<head>
<title>React Router/ Data Models</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" type="text/css" />
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css" rel="stylesheet">
</head>
<body>
<div id='rootElement' class="container"></div>
</body>
<script src="img/jquery-2.1.4.min.js"></script>
<script src="img/bootstrap.min.js"></script>
<script src="img/bundle.js"></script>
</html>
Loaders(内联代码不需要翻译)
"之后,我们必须指定不同的加载器来正确转换我们的 JSX、ES6 代码和其他资源。加载器是对你的应用资源文件应用的一种转换。它们是运行在 node.js 中的函数,它们将资源文件的源作为参数,并返回新的源。我们使用babel-loader来处理我们的 ES6 和 JSX 代码。"(内联代码不需要翻译)
// webpack.config.js
module.exports = {
module: {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react'],
plugins: ['transform-function-bind']
},
include: path.join(__dirname, 'src')
}]
}
};
"我们通过 npm 安装了babel-loader包,并将其包含在package.json中。之后,我们在 Webpack 配置中指定了它。测试选项匹配给定的正则表达式。给定的加载器解析这些文件。因此,babel-loader将编译src目录中由include选项指定的源文件中的.jsx和.js文件。我们还指定babel-loader应使用 es2015 和 react 预设以及 function-bind 转换器插件,这样 Babel 就能正确解析我们所有的代码。"(内联代码不需要翻译)
"对于其他类型的资产,如 CSS、字体和图像,我们使用它们自己的加载器。"
// webpack.config.js
module.exports = {
module: {
loaders: [
{
test: /\.jsx?$/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react'],
plugins: ['transform-function-bind']
},
include: path.join(__dirname, 'src')
},
{ test: /\.css$/, loader: "style-loader!css-loader" },
{ test: /\.woff(\d+)?$/, loader: 'url?prefix=font/&limit=5000&mimetype=application/font-woff' },
{ test: /\.ttf$/, loader: 'file?prefix=font/' },
{ test: /\.eot$/, loader: 'file?prefix=font/' },
{ test: /\.svg$/, loader: 'file?prefix=font/' },
{ test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&minetype=application/font-woff"},
{ test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" }
]
}
};
"所有这些加载器都包含在其自己的 npm 包中。我们必须为style-loader、css-loader、url-loader和file-loader安装 npm 包,并更新package.json。"
注意
查阅webpack.github.io/docs/using-loaders.html以获取有关使用和配置加载器的更多详细信息。
热模块替换
"肖恩,Webpack 最酷的特性之一是热模块替换(HMR)。这意味着每次我们修改一个组件并保存文件时,Webpack 都会在不重新加载浏览器和丢失组件状态的情况下替换页面上的模块。"迈克告知。
"哇!这听起来非常令人印象深刻。"肖恩惊呼。
"为了使热重载工作,我们必须使用出色的 react-hot-loader 包和 webpack-dev-server。webpack-dev-server 包在启动服务器之前,每次文件更改之前都无需重复运行 Webpack,它会为我们使用webpack.config.js中提供的config选项运行应用。设置 webpack-dev-server 的关键点是配置它以启用热重载。这可以通过添加hot: true配置选项来完成。"
// server.js
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.config');
new WebpackDevServer(webpack(config), {
publicPath: config.output.publicPath,
hot: true,
historyApiFallback: true
}).listen(9000, 'localhost', function (err, result) {
if (err) {
console.log(err);
}
console.log('Listening at localhost:9000');
});
"这将确保 webpack-dev-server 将在 localhost 端口9000上启动,并启用热重载。它还将使用我们在webpack.config.js中定义的所有配置。"迈克说。
"我们将必须修改我们的package.json以运行server.js脚本。"
// package.json
"scripts": {
"start": "node server.js",
}
"这将确保npm start命令将运行webpack-dev-server。"
"我们还需要在我们的 Webpack 配置中进行一些更改,以便使热重载工作。我们必须配置入口选项以包含开发服务器和热重载服务器。"
entry: [
'webpack-dev-server/client?http://localhost:9000',
'webpack/hot/only-dev-server',
'./src/index'
]
"接下来,我们需要通知 Webpack 使用 hot-loader 与我们已经添加的其他加载器。"
module: {
loaders: [
{ test: /\.jsx?$/,
loader: 'react-hot',
include: path.join(__dirname, 'src')
}
.. .. ..
]
}
"最后,Webpack 的热模块替换插件必须包含在配置的插件部分。"
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
]
"最终的 Webpack 配置看起来像这样"
// webpack.config.js
var path = require('path');
var webpack = require('webpack');
module.exports = {
devtool: 'eval',
entry: [
'webpack-dev-server/client?http://localhost:9000',
'webpack/hot/only-dev-server',
'./src/index'
],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/static/'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
],
resolve: {
extensions: ['', '.js', '.jsx']
},
module: {
loaders: [
{ test: /\.jsx?$/,
loader: 'react-hot',
include: path.join(__dirname, 'src')
},
{
test: /\.jsx?$/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react'],
plugins: ['transform-function-bind']
},
include: path.join(__dirname, 'src')
},
{ test: /\.css$/, loader: "style-loader!css-loader" },
{ test: /\.woff(\d+)?$/, loader: 'url?prefix=font/&limit=5000&mimetype=application/font-woff' },
{ test: /\.ttf$/, loader: 'file?prefix=font/' },
{ test: /\.eot$/, loader: 'file?prefix=font/' },
{ test: /\.svg$/, loader: 'file?prefix=font/' },
{ test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&minetype=application/font-woff"},
{ test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" }
]
}
};
"现在如果我们使用npm start启动应用,那么它将使用 webpack-dev-server 的热重载器。肖恩,尝试更改一些代码并检查代码是否在浏览器中更新而无需刷新页面。魔法!!"迈克解释说。
"太好了,迈克。是的,它确实有效。万岁 Webpack 和热重载!"
摘要
在本章中,你学习了 React 生态系统中的各种工具——开发、测试和生产工具,我们在应用开发的各个阶段都使用了这些工具。我们讨论了 Babel,JavaScript 转译器,将我们的下一代 JavaScript 代码转换为 ES5。我们还看到了如何使用 ESLint 和 React 开发者工具使 React 开发变得容易。最后,我们看到了如何使用 Webpack 及其强大的加载器和配置选项与 React 一起使用。我们看到了这些工具如何使开发 React 应用成为一种美好的体验。"
在下一章中,我们将深入探讨 Flux 作为架构的应用。我们已了解到在组件间共享数据时会出现问题。我们将学习如何使用 Flux 来克服这些问题。
第十二章。Flux
在上一章中,我们查看了一系列在应用程序整个生命周期中都有用的 React 生态系统中的工具——开发、测试和生产。我们还看到了 React 如何使用开发者工具来提高开发者体验。我们了解到了可以与 React 一起使用的各种测试工具。为了总结,我们看到了如何使用构建工具,如 Webpack 和 Browserify,以及它们如何与 React 一起使用。
在本章中,我们将深入探讨 Flux 作为一种架构。我们已经看到了在组件间数据共享过程中出现的问题。我们将看到如何通过拥有一个单一的数据存储点来克服这些问题。接下来,我们将检查如何使用 React 来克服这个问题。
分发器充当一个中央枢纽来管理这种数据流和通信以及动作如何调用它们。最后,我们将查看在构建我们的社交媒体追踪器应用程序时发生的完整数据流。
在本章中,我们将涵盖以下主题:
-
Flux 架构
-
存储库
-
动作
-
分发器
-
Flux 实现
在本章结束时,我们将能够开始用 Flux 替换应用程序中具有紧密数据耦合的部分。我们将能够为 Flux 搭建必要的基础,并轻松地在我们的 React 视图中开始使用它。
Flux 架构和单向流
“嘿,迈克和肖恩!”卡拉在一个晴朗的早晨说道。
“嗨,卡拉,你今天怎么样?”
“太棒了。你之前构建的应用程序很好,客户喜欢它。他们很快会为它添加更多功能。同时,我们还有一个小型应用程序要构建。”
“哦,不错。我们打算构建什么?”迈克问道。
“我们需要构建一种社交追踪器。首先,我们展示用户的 reddits、tweets 等内容。我们稍后会扩展它以显示其他信息。”
“明白了,”肖恩重复道。
“祝你有美好的一天;我将把它留给你。”
“肖恩,你对这个新项目有什么看法?”
“这应该会很有趣。嗯……我们能否探索 Flux 并在应用程序中使用它?我们在构建上一个应用程序时讨论过它。”
“是的,我们可以。这将是一个了解 Flux 如何工作的完美机会。在我们开始使用它之前,让我们先了解一下 Flux 实际上是什么。”
“Flux 是 React 使用单向流的一个简单架构。我们之前讨论过单向流如何适合 React。当数据有任何更改时,React 遵循始终渲染的模型。数据不会像双向绑定的情况那样有其他方向。”
“这并不完全符合模型-视图-控制器(MVC)的工作方式。它由模型(存储库)、动作和分发器组成,最后是视图(React 视图)。”
“目前还没有完整的 Flux 作为框架的模块,因为它不是为此而设计的。Facebook 提供了 Flux 模块,它由分发器组成。其他部分,如视图和存储库,可以在没有太多支持的情况下完成。让我们一一过一遍,好吗?”
"当然。我相信我们可以讨论它们是如何相互关联的,以及为什么当应用开始增长时它们特别有用。"
"是的。"
"正如您在下面的图像中可以看到的,各种组件相互连接并独立工作。数据在一个循环中以单一流向流动。"
"正如我之前提到的,分发器充当中央枢纽。每当视图发生事件时,例如用户点击按钮或 Ajax 调用完成,就会调用动作。动作也可能由分发器调用。"
"动作是简单的结构,将有效载荷传递给分发器,分发器识别动作以及从动作和数据中获取的更新当前状态所需的其他细节。"
"然后分发器将其传播到存储。分发器就像一个回调注册表,所有存储都注册自己。每当发生某些动作时,分发器都会通知并回调存储。无论动作是什么,它都会被发送到所有存储。"
"分发器不做任何复杂的活动,它只是将有效载荷转发给已注册的存储,并且不处理任何数据。"
"执行逻辑和复杂决策以及数据更改的责任委托给了存储。这有助于将数据更改点集中在单一位置,并避免在应用程序周围进行更改,这些更改更难追踪。"
"在接收到分发器的回调后,存储根据动作类型决定是否需要执行任何操作。基于回调,它可以更新当前存储。它也可以等待其他存储更新。在完成更改后,它继续通知视图。在我们的简单 Flux 版本中,可以通过使用从 events 模块可用的EventEmitter模块来实现这一点。"
"与动作类似,视图会注册自己以监听存储中的变化。在某些变化发生时,EventEmitter会发出一个事件。根据事件类型,它将调用一个已注册监听该事件的View方法。"
"接收事件的视图可以根据它可用的任何存储的当前状态更新其自身状态。状态更新然后触发视图更新。"
"这个过程通过视图事件继续,导致对动作和分发器等的调用。"
"希望,现在这有点清晰了?" 迈克问道。
"嗯...是的,让我理清思路。我们有动作来执行动作,基于一个事件。然后它通知分发器,然后通知任何已注册监听更改的存储。存储根据动作类型更新自己,并通知 React 视图更新自己。"
"正确!让我们立即深入到应用中。我们将基于官方 Flux 示例构建我们的应用。它将这样结构化。"
js/
├── actions
│ └── SocialActions.js
├── app.js
├── components
│ └── SocialTracker.react.js
├── constants
│ └── SocialConstants.js
├── dispatcher
│ └── AppDispatcher.js
├── stores
│ └── SocialStore.js
└── utils
└── someutil.js
"现在,正如卡拉提到的,我们需要显示来自 Twitter 和 Reddit 的用户数据。对于 Reddit,它可以通过 API 调用公开获取,正如我们很快将看到的。"
"对于 Twitter,我们需要做一些基础设置并创建一个 Twitter 应用。我们可以在apps.twitter.com/上创建一个新的。我已经为我们的应用创建了一个。"
"然后我们将使用twitter模块来访问 Twitter 并从用户那里获取 tweets。让我们设置一个config.js文件来存储我们之前创建的访问令牌,如下所示:"
module.exports ={
twitter_consumer_key: 'xxxx',
twitter_consumer_secret: 'xxxx',
twitter_access_token_key: 'xxxx',
twitter_access_token_secret: 'xxxx'
}
"这些对应于我们在我们的应用中创建的相对键和秘密。接下来,我们将创建一个客户端,使用前面的凭证访问数据。"
var Twitter = require('twitter');
var config = require('./config');
var client = new Twitter({
consumer_key: config.twitter_consumer_key,
consumer_secret: config.twitter_consumer_secret,
access_token_key: config.twitter_access_token_key,
access_token_secret: config.twitter_access_token_secret
});
"我们将在我们的 express 服务器应用程序中使用这个客户端。正如我说的,对于 Reddit,我们可以直接调用 Reddit API 来访问 reddits。对于 Twitter,它将首先击中我们的 node App并返回 tweets 到我们的 React 组件。"
"你想定义这个吗,肖恩?"
"当然。"
var express= require('express');
var app = new (require('express'))();
var port = 3000
app.get('/tweets.json', function (req, res) {
var params = {screen_name: req.query.username};
client.get('statuses/user_timeline', params, function (error, tweets, response) {
if (!error) {
res.json(tweets);
} else {
res.json({error: error});
}
});
});
"我在这里定义了一个名为tweets.json的 JSON 端点。它将调用client.get()方法,这是一个 REST API 包装器,用于调用 Twitter API。我们调用statuses/user_timeline API 来获取用户的用户时间线,这是从请求中传递给我们的。"
在收到响应后,它将把这个信息发送回调用它的 React 组件。"
"看起来不错。现在,让我们从 App 开始。我们将首先定义 Dispatcher。"
// AppDispatcher.js
var Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();
"我们通过从flux.Dispatcher中引入它来定义我们的 dispatcher。然后我们将在各个地方使用它。"
Flux actions
"现在我们需要定义我们将要作为常量在各种地方引用的动作类型,例如从 Actions 发送类型到 store,并在我们的 store 中,决定传递给 store 的动作类型以采取适当的行动。
//SocialConstants.js
var keyMirror = require('keymirror');
module.exports = keyMirror({
FILTER_BY_TWEETS: null,
FILTER_BY_REDDITS: null,
SYNC_TWEETS: null,
SYNC_REDDITS: null
});
"在这里,我们使用github.com/STRML/keyMirror包根据键创建对象的关键和值。这将转换为类似于以下的对象。"
{
FILTER_BY_TWEETS: 'FILTER_BY_TWEETS',
…
}
"当添加新键时,这很方便,可以避免再次重复相同的内容。"
"我们现在可以开始使用动作常量了。它们代表我们将要执行的四种动作,如下所示:"
-
SYNC_TWEETS: 这将获取给定用户的 tweets -
SYNC_REDDITS: 这将获取给定主题的 reddits -
FILTER_BY_TWEETS: 这仅显示 tweets,而不是 tweets 和 reddits -
FILTER_BY_REDDITS: 这仅显示 reddits,而不是 tweets 和 reddits
"接下来,让我们定义将在我们的视图的不同地方调用的动作。"
// file: SocialActions.js
var AppDispatcher = require('../dispatcher/AppDispatcher');
var SocialConstants = require('../constants/SocialConstants');
var assign = require('object-assign');
var JSONUtil = require('../utils/jsonutil');
var SocialActions = {
filterTweets: function (event) {
AppDispatcher.dispatch({
type: SocialConstants.FILTER_BY_TWEETS,
showTweets: event.target.checked
});
},
filterReddits: function (event) {
AppDispatcher.dispatch({
type: SocialConstants.FILTER_BY_REDDITS,
showReddits: event.target.checked
});
},
syncTweets: function (json) {
AppDispatcher.dispatch({
type: SocialConstants.SYNC_TWEETS,
tweets: json.map((tweet) => {
return assign(tweet, {type: 'tweet'})
}),
receivedAt: Date.now()
});
},
syncReddits: function (json) {
AppDispatcher.dispatch({
type: SocialConstants.SYNC_REDDITS,
reddits: json.data.children.map((child) => {
return assign(child.data, {type: 'reddit'})
}),
receivedAt: Date.now()
});
},
fetchTweets: function (username) {
fetch(`/tweets.json?username=${username}`)
.then(JSONUtil.parseJSON)
.then(json => SocialActions.syncTweets(json)).catch(JSONUtil.handleParseException)
},
fetchReddits: function (topic) {
fetch(`https://www.reddit.com/r/${topic}.json`)
.then(JSONUtil.parseJSON)
.then(json => SocialActions.syncReddits(json)).catch(JSONUtil.handleParseException)
}
};
module.exports = SocialActions;
"让我们逐个分析这些动作:"
fetchTweets: function (username) {
fetch(`/tweets.json?username=${username}`)
.then(JSONUtil.parseJSON)
.then(json => SocialActions.syncTweets(json)).catch(JSONUtil.handleParseException)
}
"在这里,我们使用 fetch,这与我们之前使用的 Ajax 类似,用于从我们的tweets.json API 获取推文,其中我们传递了需要获取推文的用户名。我们在这里使用我们定义的 JSON 实用方法。"
var JSONUtil = (function () {
function parseJSON(response){
return response.json()
}
function handleParseException(ex) {
console.log('parsing failed', ex)
}
return {'parseJSON': parseJSON, 'handleParseException': handleParseException}
}());
module.exports = JSONUtil;
"它们帮助我们将响应转换为 JSON,或者在失败的情况下记录它们:"
"在从 API 接收到成功响应后,我们从同一模块调用SocialActions.syncTweets(json)方法。"
syncTweets: function (json) {
AppDispatcher.dispatch({
type: SocialConstants.SYNC_TWEETS,
tweets: json.map((tweet) => {
return assign(tweet, {type: 'tweet'})
}),
receivedAt: Date.now()
});
}
"接下来,syncTweets接受 JSON。然后,它将 JSON 包装成一个对象有效载荷,发送给分发器。在这个对象中,我们创建了一个推文数组,从有效载荷中。我们还为每个对象标记了类型,以表示它是推文,这样我们就可以在同一个数组中混合和匹配推文和 Reddit,并识别它代表的是推文还是 Reddit。"
assign(tweet, {type: 'tweet'})
"我们使用Object.assign,它将两个对象合并在一起。我们在这里使用object-assign包。"
"现在,我们通知分发器关于最终要传递给存储器的有效载荷,如下所示:"
AppDispatcher.dispatch({ payload…});
"同样,我们还有一个syncReddits方法,如下所示:"
fetchReddits: function (topic) {
fetch(`https://www.reddit.com/r/${topic}.json`)
.then(JSONUtil.parseJSON)
.then(json => SocialActions.syncReddits(json)).catch(JSONUtil.handleParseException)
}
"这从https://www.reddit.com/r/${topic}.json获取 Reddit,例如www.reddit.com/r/twitter.json。"
"在获取数据后,它将数据传递给SocialActions.syncReddits(json)),从而为分发器创建有效载荷,如下所示:"
syncReddits: function (json) {
AppDispatcher.dispatch({
type: SocialConstants.SYNC_REDDITS,
reddits: json.data.children.map((child) => {
return assign(child.data, {type: 'reddit'})
}),
receivedAt: Date.now()
});
}
"请注意,我们在这里传递了类型属性给动作。这是为了通知存储器在接收到有效载荷时采取什么动作。"
"明白了。看到我们如何基于这个对象进行操作会很有趣。"
"是的。接下来,我们有两个简单的方法,将事件传递到存储器中,如下所示:"
filterTweets: function (event) {
AppDispatcher.dispatch({
type: SocialConstants.FILTER_BY_TWEETS,
showTweets: event.target.checked
});
},
filterReddits: function (event) {
AppDispatcher.dispatch({
type: SocialConstants.FILTER_BY_REDDITS,
showReddits: event.target.checked
});
},
"我们将使用这些方法作为onClick方法。点击复选框时,复选框的值——无论是 Reddit 还是 Twitter——将在event.target.checked中可用。"
"我们将这些包裹在一个简单的对象中,用动作调用的类型标记它们,并将相同的对象发送给分发器。这样,我们将知道我们将要显示推文、Reddit 还是什么都没有。"
Flux 存储
"很好,看起来我们现在都准备好创建我们的存储器了。"
"是的,肖恩。我们将从定义我们将不断更新并用作存储器的状态对象开始。"
var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var SocialConstants = require('../constants/SocialConstants');
var assign = require('object-assign');
var _ = require('underscore');
var CHANGE_EVENT = 'change';
var _state = {
tweets: [],
reddits: [],
feed: [],
showTweets: true,
showReddits: true
};
"我们还定义了一个CHANGE_EVENT常量,我们将其用作标识符来监听来自存储器事件发射器的更改类型的事件。"
"然后我们定义一个方法来更新状态,创建一个新的状态。"
function updateState(state) {
_state = assign({}, _state, state);
}
"这合并了需要更新和合并到现有状态中的新属性,并更新了当前状态。"
"很好,这看起来与 React 的setState方法有些相似,”肖恩说。"
"是的。现在我们将定义我们的存储器,它将更新当前状态。"
var SocialStore = assign({}, EventEmitter.prototype, {
getState: function () {
return _state;
},
emitChange: function () {
this.emit(CHANGE_EVENT);
},
addChangeListener: function (callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function (callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});
"在这里,我们通过继承EventEmitter来定义我们的SocialStore。这使它能够被组件用来注册监听事件,在我们的例子中是CHANGE_EVENT。addChangeListener和removeChangeListener方法接受应该在事件上调用并移除监听器的方法,如下:this.on(CHANGE_EVENT, callback);和this.removeListener(CHANGE_EVENT, callback);
"每当我们要通知监听器时,我们调用。"
this.emit(CHANGE_EVENT);
"最后,我们的视图可以使用以下函数从存储中获取当前状态:"
getState: function () {
return _state;
}
"最后,Shawn,让我们用我们的单个分发器将这些全部结合起来,如下:"
AppDispatcher.register(function (action) {
switch (action.type) {
case SocialConstants.FILTER_BY_TWEETS:
updateState({
showTweets: action.showTweets,
feed: mergeFeed(_state.tweets, _state.reddits, action.showTweets, _state.showReddits)
});
SocialStore.emitChange();
break;
case SocialConstants.FILTER_BY_REDDITS:
updateState({
showReddits: action.showReddits,
feed: mergeFeed(_state.tweets, _state.reddits, _state.showTweets, action.showReddits)
});
SocialStore.emitChange();
break;
case SocialConstants.SYNC_TWEETS:
updateState({
tweets: action.tweets,
feed: mergeFeed(action.tweets, _state.reddits, _state.showTweets, _state.showReddits)
});
SocialStore.emitChange();
break;
case SocialConstants.SYNC_REDDITS:
updateState({
reddits: action.reddits,
feed: mergeFeed(_state.tweets, action.reddits, _state.showTweets, _state.showReddits)
});
SocialStore.emitChange();
break;
default:
// no op
}
});
"每当AppDispatcher.dispatch被有效载荷调用时,前面的方法就会被调用。"
"让我们看看这些操作中的一个。"
case SocialConstants.SYNC_TWEETS:
updateState({
tweets: action.tweets,
feed: mergeFeed(action.tweets, _state.reddits, _state.showTweets, _state.showReddits)
});
SocialStore.emitChange();
break;
"我们在这里所做的就是调用updateState来通过提供更新的推文和基于mergeFeed方法的更新来更新当前状态。"
"让我们看看它。"
function mergeFeed(tweets, reddits, showTweets, showReddits) {
let mergedFeed = [];
mergedFeed = showTweets ? mergedFeed.concat(tweets) : mergedFeed;
mergedFeed = showReddits ? mergedFeed.concat(reddits) : mergedFeed;
mergedFeed = _.sortBy(mergedFeed, (feedItem) => {
if (feedItem.type == 'tweet') {
let date = new Date(feedItem.created_at);
return date.getTime();
} else if ((feedItem.type == 'reddit')) {
return feedItem.created_utc * 1000;
}
})
return mergedFeed;
};
"我根据是否选择了showTweets和showReddits来组合了各种要处理的操作。"
"所以,这个方法所做的就是接受推文和 reddit 数组数据,以及检查是否选中了显示 reddits 或显示推文。我们根据这些选中的/未选中的字段将这些字段构建到mergedFeed数组中。"
"然后,我们使用underscorejs的sortBy方法对这个混合的推文和 reddits 数据数组mergedFeed进行排序,基于两种类型对象的time字段。对于推文,这个字段是created_at字段,而对于 reddit,它是created_utc字段。我们使用 UTC 时间戳来规范化时间以进行比较。"
"回到同步推文操作,在更新状态后,我们在存储上调用发射器方法:"
SocialStore.emitChange();
"这会从存储中调用我们的发射器,最终将更新传递给组件。"
"明白了。我认为下一步是创建我们的视图。"
"没错。我们将视图拆分为三个组件——Header、MainSection和SocialTracker容器组件。"
"我们首先从Header开始,如下:"
var React = require('react');
var ReactBootstrap = require('react-bootstrap');
var Row = ReactBootstrap.Row, Jumbotron = ReactBootstrap.Jumbotron;
var Header = React.createClass({
render: function () {
return (
<Row>
<Jumbotron className="center-text">
<h1>Social Media Tracker</h1>
</Jumbotron>
</Row>
);
}
});
module.exports = Header;
"这是一个简单的显示组件,包含标题。"
"啊,Mike。我注意到你正在使用 react-bootstrap 模块。这看起来很整洁。它帮助我们用属性将它们包裹在 React 组件中,而不是在普通元素和 bootstrap 属性中定义。"
"是的。我们在这里使用Jumbotron和Row。这个Row将被包裹在一个 bootstrap 网格组件中。"
"接下来,我们将设置我们的MainSection组件,这将显示获取 Twitter 和 Reddit 主题用户名的输入,以及检查它们:"
var React = require('react');
…
var SocialActions = require('../actions/SocialActions');
var SocialStore = require('../stores/SocialStore');
var MainSection = React.createClass({
getInitialState: function () {
return assign({twitter: 'twitter', reddit: 'twitter'}, SocialStore.getState());
},
componentDidMount: function () {
SocialStore.addChangeListener(this._onChange);
this.syncFeed();
},
componentWillUnmount: function () {
SocialStore.removeChangeListener(this._onChange);
},
render: function () {
return (
<Row>
<Col xs={8} md={8} mdOffset={2}>
<Table striped hover>
<thead>
<tr>
<th width='200'>Feed Type</th>
<th>Feed Source</th>
</tr>
</thead>
<tbody>
<tr>
<td><Input id='test' type="checkbox" label="Twitter" onChange={SocialActions.filterTweets}
checked={this.state.showTweets}/></td>
<td><Input onChange={this.changeTwitterSource} type="text" addonBefore="@" value={this.state.twitter}/>
</td>
</tr>
<tr>
<th><Input type="checkbox" label="Reddit" onChange={SocialActions.filterReddits}
checked={this.state.showReddits}/></th>
<td><Input onChange={this.changeRedditSource} type="text" addonBefore="@"
value={this.state.reddit}/></td>
</tr>
<tr>
<th></th>
<td><Button bsStyle="primary" bsSize="large" onClick={this.syncFeed}>Sync Feed</Button>
</td>
</tr>
</tbody>
</Table>
</Col>
</Row>
);
},
changeTwitterSource: function (event) {
this.setState({twitter: event.target.value});
},
changeRedditSource: function (event) {
this.setState({reddit: event.target.value});
},
syncFeed: function () {
SocialActions.fetchReddits(this.state.reddit);
SocialActions.fetchTweets(this.state.twitter);
},
_onChange: function () {
this.setState(SocialStore.getState());
}
});
module.exports = MainSection;
"现在组件在这里做了一些事情。首先,它根据存储设置状态。"
getInitialState: function () {
return assign({twitter: 'twitter', reddit: 'twitter'}, SocialStore.getState());
},
"它还在跟踪两个不同的字段——Twitter 和 Reddit——用户名信息。我们根据之前看到的字段输入绑定这些值:"
changeTwitterSource: function (event) {
this.setState({twitter: event.target.value});
},
changeRedditSource: function (event) {
this.setState({reddit: event.target.value});
},
"然后在输入字段上使用这个更改处理程序,就像这样。"
<Input onChange={this.changeTwitterSource} type="text" addonBefore="@" value={this.state.twitter}/>
"接下来,我们有 componentDidMount 和 componentWillUnmount 函数来注册和注销,以便监听来自 SocialStore 的事件:"
componentDidMount: function () {
SocialStore.addChangeListener(this._onChange);
this.syncFeed();
},
componentWillUnmount: function () {
SocialStore.removeChangeListener(this._onChange);
},
"在这里,我们将 _onChange 方法注册为每当 SocialStore 发生变化时被调用。_onChange 方法反过来根据存储的状态更新组件的当前状态,如下所示:"
this.setState(SocialStore.getState());
"接下来,我们指定要为检查/取消检查推特/Reddit 显示和同步推文和 Reddit 调用等事件调用的 SocialAction 方法。在调用同步数据时,syncFeed 被调用,它从 SocialActions 调用相关的同步方法,传入当前的推特名称和 Reddit 主题。"
syncFeed: function () {
SocialActions.fetchReddits(this.state.reddit);
SocialActions.fetchTweets(this.state.twitter);
},
"最后,我们将使用 SocialTracker 组件来封装一切,如下所示:"
var ArrayUtil = require('../utils/array');
var assign = require('object-assign');
var Header = require('./Header.react');
var MainSection = require('./MainSection.react');
var React = require('react');
var SocialStore = require('../stores/SocialStore');
var SocialActions = require('../actions/SocialActions');
var ReactBootstrap = require('react-bootstrap');
var Col = ReactBootstrap.Col, Grid = ReactBootstrap.Grid, Row = ReactBootstrap.Row;
var SocialTracker = React.createClass({
getInitialState: function() {
return assign({}, SocialStore.getState());
},
componentDidMount: function() {
SocialStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
SocialStore.removeChangeListener(this._onChange);
},
render: function() {
return (
<Grid className="grid">
<Header/>
<MainSection/>
{this.renderFeed()}
</Grid>
)
},
renderFeed: function() {
var feed = this.state.feed;
var feedCollection = ArrayUtil.in_groups_of(feed, 3);
if (feed.length > 0) {
return feedCollection.map((feedGroup, index) => {
console.log(feedGroup);
return <Row key={`${feedGroup[0].id}${index}`}>
{feedGroup.map((feed) => {
if (feed.type == 'tweet') {
return <Col md={4} key={feed.id}><div className="well twitter"><p>{feed.text}</p></div></Col>;
} else {
var display = feed.selftext == "" ? `${feed.title}: ${feed.url}` : feed.selftext;
return <Col md={4} key={feed.id}><div className="well reddit"><p>{display}</p></div></Col>;
}
})}
</Row>
});
} else {
return <div></div>
}
},
_onChange: function() {
this.setState(SocialStore.getState());
}
});
module.exports = SocialTracker;
"我们使用了之前用来监听存储更新并更新组件当前状态的相同设置。"
"很好,我看到,剩下的只是遍历信息流并将它们显示出来,”肖恩继续说。
"我看到我们正在以每行三组的格式显示信息流,并根据是否是推文等应用单独的样式。为了分组,我们似乎使用了 ArrayUtil。"
var ArrayUtil = (function () {
function in_groups_of(arr, n) {
var ret = [];
var group = [];
var len = arr.length;
for (var i = 0; i < len; ++i) {
group.push(arr[i]);
if ((i + 1) % n == 0) {
ret.push(group);
group = [];
}
}
if (group.length) ret.push(group);
return ret;
};
return {'in_groups_of': in_groups_of}
}());
module.exports = ArrayUtil;
"没错。有了这个,看起来我们一切都准备好了。我们最终将按常规显示组件。"
var React = require('react');
var ReactDOM = require('react-dom');
var SocialTracker = require('./components/SocialTracker.react');
ReactDOM.render(
<SocialTracker />,
document.getElementById('container')
);
"让我们看看它看起来怎么样,好吗?"
"这是它的样子,没有推文:"
"当更改推特用户时,它看起来是这样的:"
"这看起来很棒,迈克!"
摘要
"我们深入研究了 Flux 作为架构。我们看到 Dispatcher 充当中央枢纽来传输我们的数据和动作,以及处理它们的动作。我们看到主要责任是操纵状态和更新状态被委托给了存储本身。最后,我们看到它们是如何结合在一起,并使其在视图中使用和跨组件共享存储变得容易。"