是时候完成我们应用的原型了,天哪,我们有没有工作要做呢。
框架已经就位,所有路线都已设置,登录屏幕已完全完成。然而,我们的聊天和用户视图到目前为止还是空白的,这就是 Chatastrophe 的核心功能所在。所以,在我们向董事会展示我们的原型之前,让我们让它实际工作。
本章内容如下:
- 加载和显示聊天信息
- 发送和接收新消息
- 在用户配置文件页面上仅显示某些聊天信息
- 反应状态管理
让我们简要回顾一下我们在第 1 章创建我们的应用结构中定义的用户故事,看看我们已经完成了哪些。
我们已完成以下工作:
用户应该能够登录和退出应用。
以下内容尚未完成,但属于我们稍后构建的 PWA 功能的一部分:
- 即使在脱机状态下,用户也应该能够查看他们的邮件
- 当另一个用户发送消息时,用户应收到推送通知
- 用户应该能够将应用安装到他们的移动设备上
- 即使在不稳定的网络条件下,用户也应该能够在 5 秒内加载应用
这给我们留下了一个在原型完成之前需要完成的故事列表:
- 用户应该能够实时发送和接收消息
- 用户应该能够查看给定作者的所有消息
这些故事中的每一个都符合特定的视图(聊天视图和用户视图)。让我们从ChatContainer开始,开始构建我们的聊天室。
我们的聊天视图将有两个主要部分:
- 列出所有聊天记录的消息显示
- 用于用户键入新消息的聊天框
我们可以从添加适当的div标记开始:
render() {
return (
<div id="ChatContainer">
<Header>
<button className="red" onClick={this.handleLogout}>
Logout
</button>
</Header>
<div id="message-container">
</div>
<div id="chat-input">
</div>
</div>
);
}Reminder to ensure that your IDs and classNames are the same as mine, lest your CSS be different (or even worse).
我们先填写输入框。在div#chat-input内放置一个textarea,占位符为"Add your message…”:
<textarea placeholder="Add your message..." />我们会将其配置为允许用户按回车发送消息,但最好还有一个发送按钮。在textarea下方添加button,在其内部添加SVG图标:
<div id="chat-input">
<textarea placeholder="Add your message..." />
<button>
<svg viewBox="0 0 24 24">
<path fill="#424242" d="M2,21L23,12L2,3V10L17,12L2,14V21Z" />
</svg>
</button>
</div>确保您的path fill和svg viewBox属性与上述相同。
SVGs are a type of image that can be scaled (made larger) without any loss of quality. In this case, we're essentially creating a box (the svg tag) and then drawing a line within the path tag. The browser does the actual drawing, so there's never any pixelation.
为了 CSS 的目的,我们也给我们的div#ChatContainer一个inner-container类:
<div id="ChatContainer" className="inner-container">如果一切顺利,您的应用现在应该如下所示:
这就是聊天视图的基本结构。现在,我们可以开始讨论如何管理数据——来自 Firebase 的消息列表。
React 的一个重要原则是称为单向数据流。
在原型 React app 中,数据以最高级别组件的状态存储,并通过props将向下传递给较低级别组件。当用户与应用交互时,交互事件通过 props 通过组件树向上传递,直到到达最高级别的组件,然后组件根据动作修改状态。
然后,应用形成一个大循环——数据下降,事件上升,新数据下降。你也可以把它想象成一部电梯,从充满数据的顶层出发,然后返回充满事件的顶层。
这种方法的优点是很容易跟踪数据流。您可以看到它要去哪里(到哪个子组件),以及它为什么要改变(对哪些事件作出反应)。
现在,这个模型在一个包含数百个组件的复杂应用中遇到了问题。将您的所有状态存储在顶级组件中,并通过道具传递所有数据和事件,这将变得非常困难。
想想你的顶层组件(App.js)和底层组件(比如button)之间的一条大链。如果有几十个nested组件,button需要一个从App状态派生的道具,则必须将该道具向下传递到链中的每个组件。不用了,谢谢。
对于这个状态管理问题有很多解决方案,但大多数都致力于在组件树中创建容器组件;这些组件具有状态,并将其传递给数量有限的子组件。现在我们有多部电梯,有的在一楼到三楼,有的在五楼到十二楼,依此类推。
我们不会在应用中处理任何状态管理,因为我们只有四个组件,但最好在 React 应用扩展时记住这一点。
The top two React state management libraries are Redux (https://github.com/reactjs/redux) and MobX (https://github.com/mobxjs/mobx). I've worked extensively with both, and both have their advantages and tradeoffs. In short, MobX is better for developer productivity, while Redux is better for keeping large applications organized.
出于我们的目的,我们可以将所有状态存储在App组件中,并将其传递给子组件。我们不是将信息存储在ChatContainer中,而是将其存储在App中,并将其传递给ChatContainer。这立即为我们提供了将其传递给UserContainer的优势。
也就是说,我们的消息处于App状态,通过props与UserContainer和ChatContainer共享。
状态是应用中唯一的真实来源,不应重复。存储两个消息数组没有意义:一个在ChatContainer中,另一个在UserContainer中。相反,根据需要将状态存储到尽可能高的位置,并向下传递。
长话短说,我们需要在App中加载我们的消息,然后将它们传递给ChatContainer。让App负责发送消息也很有意义,这样我们所有的消息功能都在一个地方。
让我们从发送第一条信息开始!
正如在我们的LoginContainer中一样,我们需要在textarea的状态发生变化时存储其值。
我们使用LoginContainer的状态来存储该值。让我们对ChatContainer也这样做。
You may be wondering, after the preceding discussion: why don't we just keep all our state in App? Some will argue for that approach, to keep everything in one place; however, this will bloat our App component and require us to pass multiple props between components. It's better to keep state as high as necessary, and no higher; the new message in the chat input will only be relevant to App when it's done and submitted, not before that.
让我们把它设置好。
将其添加到ChatContainer.js中:
state = { newMessage: '' };另外,添加一个方法来处理它:
handleInputChange = e => {
this.setState({ newMessage: e.target.value });
};现在,修改我们的textarea:
<textarea
placeholder="Add your message..."
onChange={this.handleInputChange}
value={this.state.newMessage}
/>Best practices say that you should always make your JSX element multiline when it has more than two props (or the props are particularly long).
当我们的用户单击 Send 时,我们希望将消息发送到App,然后由App将其发送到 Firebase。之后,我们重置字段:
handleSubmit = () => {
this.props.onSubmit(this.state.newMessage);
this.setState({ newMessage: ‘’ });
};我们尚未在App中添加此onSubmit道具功能,但我们可以很快完成:
<button onClick={this.handleSubmit}>
<svg viewBox="0 0 24 24">
<path fill="#424242" d="M2,21L23,12L2,3V10L17,12L2,14V21Z" />
</svg>
</button>但是,我们也希望让用户通过按进入进行提交。我们怎么能这样做呢?
此时,我们在textarea上侦听更改事件,然后调用handleInputChange方法。textarea上监听其值变化的道具是onChange,但还有另一个事件,即按键向下,每当用户按键时都会发生。
我们可以观察那个事件,然后检查按下了什么键;如果是进入,我们发送消息!
让我们看看它的实际行动:
<textarea
placeholder="Add your message..."
onChange={this.handleInputChange}
onKeyDown={this.handleKeyDown}
value={this.state.newMessage} />以下是此事件的处理程序:
handleKeyDown = e => {
if (e.key === 'Enter') {
e.preventDefault();
this.handleSubmit();
}
}调用事件处理程序(handleKeyDown),并自动将事件作为第一个参数传入。此事件有一个名为key的属性,该属性是一个指示键值的字符串。在提交消息之前,我们还需要防止默认行为(在textarea中创建换行符)。
您可以将这种事件监听器用于所有类型的用户输入,从将鼠标悬停在元素上到按住 shift 键并单击某些内容。
在我们转到App.js之前,这里是ChatContainer的当前状态:
import React, { Component } from 'react';
import Header from './Header';
export default class ChatContainer extends Component {
state = { newMessage: '' };
handleLogout = () => {
firebase.auth().signOut();
};
handleInputChange = e => {
this.setState({ newMessage: e.target.value });
};
handleSubmit = () => {
this.props.onSubmit(this.state.newMessage);
this.setState({ newMessage: '' });
};
handleKeyDown = e => {
if (e.key === 'Enter') {
e.preventDefault();
this.handleSubmit();
}
};
render() {
return (
<div id="ChatContainer" className="inner-container">
<Header>
<button className="red" onClick={this.handleLogout}>
Logout
</button>
</Header>
<div id="message-container" />
<div id="chat-input">
<textarea
placeholder="Add your message..."
onChange={this.handleInputChange}
onKeyDown={this.handleKeyDown}
value={this.state.newMessage}
/>
<button onClick={this.handleSubmit}>
<svg viewBox="0 0 24 24">
<path fill="#424242" d="M2,21L23,12L2,3V10L17,12L2,14V21Z" />
</svg>
</button>
</div>
</div>
);
}
}好的,让我们添加链中的最后一个链接来创建消息。在App.js中,我们需要为onSubmit事件添加一个处理程序,我们将其作为道具传递给ChatContainer:
// in App.js
handleSubmitMessage = msg => {
// Send to database
console.log(msg);
};我们想将一个onSubmit道具传递给ChatContainer,该道具等于此方法,但请稍候,我们的ChatContainer当前呈现如下:
<Route exact path="/" component={ChatContainer} />ChatContainer本身就是我们Route的支柱。我们怎么能给ChatContainer任何props呢?
事实证明,React 路由提供了三种不同的方法来呈现Route内部的组件。最简单的方法是我们之前选择的路线(哈哈),将其作为一个名为component的道具传递进来。
对于我们的目的,还有另一个更好的方法——一个名为render的道具,我们将返回组件的函数传递到该道具中。
在Route中呈现组件的第三种方式是通过名为children的道具,它接受一个带有match参数的函数,该参数是已定义的还是空的,这取决于path道具是否与浏览器的 URL 匹配。函数返回的 JSX 总是呈现的,但是您可以根据match参数对其进行修改。
让我们把Route切换到这个方法:
<Route
exact
path="/"
render={() => <ChatContainer onSubmit={this.handleSubmitMessage} />}
/>The preceding example uses an ES6 arrow function with implicit return. This is the same as writing () => { return <ChatContainer onSubmit={this.handleSubmitMessage} /> } or, in ES5, function() { return <ChatContainer onSubmit={this.handleSubmitMessage} /> }.
现在,我们可以传递所有我们喜欢的道具ChatContainer。
让我们确保它有效。尝试发送消息,并确保您看到我们在App.js中的handleSubmit中添加的console.log。
如果是这样,那太好了!是时候继续讲好的部分了——实际发送信息。
要写入 Firebase 数据库,首先我们获取它的一个实例,带有firebase.database()。与firebase.auth()类似,这个实例附带了一些我们可以使用的内置方法。
我们将在本书中讨论的是firebase.database().ref(refName)。Ref代表引用,但最好将其视为我们数据的一个类别(在 SQL 数据库中,可能构成表的内容)。
如果我们想获取对用户的引用,我们使用firebase.database().ref(‘/users’)。对于消息,它是firebase.database().ref(‘/messages’)。。。等等现在,我们可以通过多种方式对该引用进行操作,例如听取更改(本章后面将介绍),或者将新数据推送到其中(我们将立即处理)。
要向引用添加新数据,请使用firebase.database().ref(‘/messages’).push(data)。在这种情况下,将ref看作一个简单的 JavaScript 数组是很有用的,我们正在将新数据推送到该数组中。
Firebase 将从那里获取数据,将数据保存到 NoSQL 数据库,并向应用的所有实例推出一个“值”事件,稍后我们将对此进行深入研究。
当然,我们希望将消息文本保存到数据库中,但我们也希望保存更多的信息。
我们的用户需要能够看到谁发送了消息(最好是电子邮件地址),并能够导航到他们的users/:id页面。因此,我们需要将作者的电子邮件地址与消息以及唯一的用户 ID 一起保存。为了更好地衡量,让我们加入一个timestamp:
// App.js
handleSubmitMessage = msg => {
const data = {
msg,
author: this.state.user.email,
user_id: this.state.user.uid,
timestamp: Date.now()
};
// Send to database
}The preceding example uses ES6’s property shorthand for the message field. Instead of writing { msg: msg }, we can simply write { msg }.
在这里,我们利用了将当前用户保存到App组件状态的事实,并从中获取电子邮件和 uid(唯一 ID)。然后,我们用Date.now()创建一个timestamp。
好的,让我们把它送走吧
handleSubmitMessage = (msg) => {
const data = {
msg,
author: this.state.user.email,
user_id: this.state.user.uid,
timestamp: Date.now()
};
firebase
.database()
.ref('messages/')
.push(data);
}在测试之前,让我们在console.Firebase.google.com打开 Firebase 控制台,然后转到数据库选项卡。在这里,我们可以看到数据库数据的实时表示,因此我们可以检查以确保正确创建消息。
到目前为止,应该是这样的:
让我们在聊天输入中键入一条消息,然后按回车。
您应该会立即在 Firebase 控制台上看到以下内容:
伟大的我们发送了第一条聊天信息,但应用中没有显示任何内容。让我们来解决这个问题。
如前所述,我们可以监听对数据库中特定引用的更改。换句话说,我们可以定义一个函数,每当firebase.database().ref(‘/messages’)发生变化时,当一条新消息出现时,该函数就会运行。
在我们继续之前,我鼓励你们考虑两件事:我们应该在哪里定义这个侦听器,以及函数应该做什么。
看看你是否能想出一个可行的实施方案!在你头脑风暴出一个想法之后,让我们来构建它。
事情是这样的:我们的应用中已经有一个非常类似的案例。我们App#componentDidMount中的firebase.auth().onAuthStateChanged监听我们当前用户的变化,并更新我们App的state.user。
我们将对 messages 引用执行完全相同的操作,尽管语法有点不同:
class App extends Component {
state = { user: null, messages: [] }
componentDidMount() {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
this.setState({ user });
} else {
this.props.history.push('/login')
}
});
firebase
.database()
.ref('/messages')
.on('value', snapshot => {
console.log(snapshot);
});
}我们使用.on函数监听数据库中的'value'事件。我们的回调随后被一个名为snapshot的参数调用。让我们插入此插件并发送另一条消息,看看快照的外观:
啊,它对开发人员不是很友好。
快照是该对象中某个位置的数据库结构的图像/messages。我们可以通过调用val()来访问更可读的表单:
firebase.database().ref('/messages').on('value', snapshot => {
console.log(snapshot.val());
});现在,我们可以得到一个包含每个消息的对象,消息 ID 作为键。
在这里,我们需要做一些诡计。我们想用消息数组更新我们的state.messages,但我们想将消息 ID 添加到消息对象中(因为消息 ID 当前是snapshot.val()中的键)。
如果这听起来让人困惑,希望我们在实际行动中看到它会更清晰。我们将创建一个名为messages的新数组,并在对象上迭代(使用名为Object.keys的方法),然后将消息(带有 ID)推送到新数组中。
让我们将其提取到一个新函数:
class App extends Component {
state = { user: null, messages: [] }
componentDidMount() {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
this.setState({ user });
} else {
this.props.history.push('/login')
}
});
firebase
.database()
.ref('/messages')
.on('value', snapshot => {
this.onMessage(snapshot);
});
}此外,新方法:
onMessage = snapshot => {
const messages = Object.keys(snapshot.val()).map(key => {
const msg = snapshot.val()[key];
msg.id = key;
return msg;
});
console.log(messages);
};在我们的console.log中,我们最终得到的是一系列带有 ID 的消息:
最后一步是将其保存到以下状态:
onMessage = (snapshot) => {
const messages = Object.keys(snapshot.val()).map(key => {
const msg = snapshot.val()[key]
msg.id = key
return msg
});
this.setState({ messages });
}现在,我们可以将我们的消息传递到ChatContainer,并开始显示它们:
<Route
exact
path="/"
render={() => (
<ChatContainer
onSubmit={this.handleSubmitMessage}
messages={this.state.messages}
/>
)}
/>我们对App.js做了很多修改。以下是当前代码:
import React, { Component } from 'react';
import { Route, withRouter } from 'react-router-dom';
import LoginContainer from './LoginContainer';
import ChatContainer from './ChatContainer';
import UserContainer from './UserContainer';
import './app.css';
class App extends Component {
state = { user: null, messages: [] };
componentDidMount() {
firebase.auth().onAuthStateChanged(user => {
if (user) {
this.setState({ user });
} else {
this.props.history.push('/login');
}
});
firebase
.database()
.ref('/messages')
.on('value', snapshot => {
this.onMessage(snapshot);
});
}
onMessage = snapshot => {
const messages = Object.keys(snapshot.val()).map(key => {
const msg = snapshot.val()[key];
msg.id = key;
return msg;
});
this.setState({ messages });
};
handleSubmitMessage = msg => {
const data = {
msg,
author: this.state.user.email,
user_id: this.state.user.uid,
timestamp: Date.now()
};
firebase
.database()
.ref('messages/')
.push(data);
};
render() {
return (
<div id="container">
<Route path="/login" component={LoginContainer} />
<Route
exact
path="/"
render={() => (
<ChatContainer
onSubmit={this.handleSubmitMessage}
messages={this.state.messages}
/>
)}
/>
<Route path="/users/:id" component={UserContainer} />
</div>
);
}
}
export default withRouter(App);我们将使用Array.map()函数迭代消息数组,并创建一个 div 数组来显示数据。
Array.map()自动返回一个数组,这意味着我们可以将该功能嵌入到 JSX 中。这是 React 中的常见模式(通常用于显示这样的数据集合),因此值得密切关注。
在我们的message-container中,我们创建了打开和关闭的波形括号:
<div id="message-container">
{
}
</div>然后,我们在消息数组上调用map,并传入一个函数来创建新消息div:
<div id="message-container">
{this.props.messages.map(msg => (
<div key={msg.id} className="message">
<p>{msg.msg}</p>
</div>
))}
</div>如果一切顺利,您应该看到以下内容,以及您发送的所有消息:
您甚至可以尝试编写一条新消息,并观看它立即出现在消息容器中。魔术
关于上述代码的一些注释:
map函数遍历 messages 数组中的每个元素,并根据其数据创建div。迭代完成后,它返回 div 数组,然后将其显示为 JSX 的一部分。- React 的一个怪癖是屏幕上的每个元素都需要一个唯一的标识符,以便 React 可以正确地更新它。当我们在这里创建相同元素的集合时,React 很难做到这一点。因此,我们必须为每个消息 div 提供一个保证唯一的关键道具。
For more on lists and keys, visit https://facebook.github.io/react/docs/lists-and-keys.html.
让我们添加更多功能,并在消息下方显示作者姓名,以及指向其用户页面的链接。我们可以使用 React 路由Link组件来实现这一点;它类似于锚定标签(<a>,但针对 React 路由进行了优化:
import { Link } from 'react-router-dom';然后,将其添加到以下内容中:
<div id="message-container">
{this.props.messages.map(msg => (
<div key={msg.id} className="message">
<p>{msg.msg}</p>
<p className="author">
<Link to={`/users/${msg.user_id}`}>{msg.author}</Link>
</p>
</div>
))}
</div>The to prop on the Link uses ES6 string interpolation. If you wrap your string in backticks (```jsx) instead of quotation marks, you can use ${VARIABLE} to embed variables right into it.
现在,我们将使我们的信息看起来更好!
在转到用户配置文件页面之前,让我们花一些时间对消息显示进行一些快速的 UI 改进。
如果尝试注销并使用新用户登录,则会显示来自所有用户的所有消息,如图所示:
我的消息和其他用户的消息没有区别。经典的聊天应用模式是将一个用户的消息放在一边,另一个放在另一边。我们的 CSS 已经准备好处理这个问题,我们只需要将类“mine”分配给与当前用户匹配的消息。
由于我们可以访问msg.author中消息作者的电子邮件,我们可以将其与我们在App状态下存储的用户进行比较。让我们把它作为道具传给ChatContainer:
<Route
exact
path="/"
render={() => (
<ChatContainer
onSubmit={this.handleSubmitMessage}
user={this.state.user}
messages={this.state.messages}
/>
)}
/>
```jsx
然后,我们可以在`className`属性中添加一个条件:
这使用 ES6 字符串插值和短路评估来创建我们想要的效果。这些都是精妙的术语,可以归结为:如果消息作者在state中匹配用户电子邮件,则将className设置为message mine;否则,将其设置为message。
结果应该是这样的:
在前面的屏幕截图中,您会注意到我们在每条消息下显示作者电子邮件,即使行中有两条消息的作者相同。让我们变得棘手,并使之成为我们将来自同一作者的消息分组在一起。
换句话说,我们只希望在下一条消息不是由同一作者发送时显示作者电子邮件:
<div id="message-container">
{this.props.messages.map(msg => (
<div
key={msg.id}
className={`message ${this.props.user.email === msg.author &&
'mine'}`}>
<p>{msg.msg}</p>
// Only if the next message's author is NOT the same as this message's author, return the following: <p className="author">
<Link to={`/users/${msg.user_id}`}>{msg.author}</Link>
</p>
</div>
))}
</div>
```jsx
我们怎样才能做到这一点?我们需要一种方法从当前消息检查数组中的下一条消息。
幸运的是,`Array.map()`函数将索引作为第二个元素传递给回调函数。我们可以使用它,如图所示:
现在,我们说:“如果有下一条消息,并且下一条消息的作者与当前消息的作者不同,请显示此消息的作者。”
然而,在我们的render方法中,这是许多复杂的逻辑。让我们将其提取到一个方法:
<div id="message-container">
{this.props.messages.map((msg, i) => (
<div
key={msg.id}
className={`message ${this.props.user.email === msg.author &&
'mine'}`}>
<p>{msg.msg}</p>
{this.getAuthor(msg, this.props.messages[i + 1])}
</div>
))}
</div>
```jsx
此外,该方法本身:
getAuthor = (msg, nextMsg) => { if (!nextMsg || nextMsg.author !== msg.author) { return (
<Link to={/users/${msg.user_id}}>{msg.author}
我们的消息现在按如下方式分组:

# 向下滚动
试着把你的浏览器缩小一些,这样你的信息列表就几乎被切断了;然后,提交另一条消息。请注意,如果它超过了消息容器的截止日期,您必须向下滚动才能看到它。这是糟糕的 UX。让我们将其设置为当新消息到达时自动向下滚动。
在本节中,我们将深入探讨两个强大的 React 概念:`componentDidUpdate`方法和 REF。
让我们从讨论我们想要实现的目标开始。我们希望我们的消息容器向下滚动到底部,以便始终可以看到最新的消息(除非用户决定向上滚动以查看较旧的消息)。这意味着我们需要在两种情况下向下滚动消息容器:
* 渲染第一个组件时
* 当新消息到达时
让我们从第一个用例开始。我们需要一个 React 生命周期方法——一个我们已经使用过的方法。我们将在`ChatContainer`中添加`componentDidMount`方法,就像我们在`App`中所做的一样。
我们来定义它,还有一个`scrollToBottom`方法:export default class ChatContainer extends Component { state = { newMessage: '' };
componentDidMount() { this.scrollToBottom(); }
scrollToBottom = () => {
};
我们还希望在屏幕上出现新消息时触发`scrollToBottom`方法。React 为我们提供了另一种处理这种情况的方法--`componentDidUpdate`。只要您的 React 组件由于新的`props`或状态而更新,就会调用此方法。最好的一点是,该方法将前面的`props`作为第一个参数传递,因此我们可以比较它们并找出差异,如下所示:componentDidUpdate(previousProps) { if (previousProps.messages.length !== this.props.messages.length) { this.scrollToBottom(); } }
我们查看前面`props`中消息数组的长度,并将其与当前`props`中消息数组的长度进行比较。如果它已更改,我们将滚动到底部。
好的,看起来都不错。让我们继续,让我们的`scrollToBottom`方法发挥作用。
# 反应参考
React 中的 REF 是获取特定 DOM 元素的一种方法。对于那些熟悉 jQuery 的人来说,REF 弥补了使用道具创建元素的 React 方法和从 DOM 抓取并操作元素的 jQuery 方法之间的差距。
我们可以在以后要使用的任何 JSX 元素中添加一个`ref`(我们将在以后参考)。让我们向消息容器中添加一个。`ref`prop 始终是一个函数,它与相关元素一起调用,然后用于将该元素分配给组件的属性,如图所示:我们的应用即将完成。最后一步是用户配置文件页面。
# 个人资料页面
`UserContainer`的代码将与`ChatContainer`相同,但有两个主要区别:
* 我们只想显示消息数组中与 URL 参数 ID 匹配的消息
* 我们希望在页面顶部显示作者的电子邮件,然后再显示任何其他消息
首先,在`App.js`中,将`UserContainer`路线转换为使用`render`道具,与`ChatContainer`相同,并通过以下道具:<Route path="/users/:id" render={({ history, match }) => ( )} />
请注意,React Router 会在我们的`render`方法中自动为我们提供历史记录和匹配`props`,我们在这里使用该方法从 URL 参数中获取用户 ID。
然后,在`UserContainer`中,让我们设置装载指示器。此外,确保您出于 CSS 目的给出了`UserContainer`和`inner-container`的`className`:为了显示我们的消息,我们只想显示其中msg.user_id等于props.userID的消息。我们可以添加一个if语句,而不是回调Array.map():
{this.props.messagesLoaded ? (
<div id="message-container">
{this.props.messages.map(msg => {
if (msg.user_id === this.props.userID) {
return (
<div key={msg.id} className="message">
<p>{msg.msg}</p>
</div>
);
}
})}
</div>
) : (
<div id="loading-container">
<img src="/img/icon.png" alt="logo" id="loader" />
</div>
)}
```jsx
这应该只显示我们在其个人资料上的作者的消息。但是,我们现在需要在顶部显示作者电子邮件。
挑战在于,我们在加载邮件之前不会知道用户的电子邮件,并且会迭代与 ID 匹配的第一条邮件,因此我们不能像以前那样使用`map()`索引,也不能使用道具。
相反,我们将添加一个`class`属性来跟踪是否已经显示了用户电子邮件。
在`UserContainer`顶部声明:
export default class UserContainer extends Component { renderedUserEmail = false;
render() { return (
然后在代码中调用一个`getAuthor`方法:这将检查是否已呈现作者,如果未呈现,则返回:
getAuthor = author => {
if (!this.renderedUserEmail) {
this.renderedUserEmail = true;
return <p className="author">{author}</p>;
}
};
```jsx
有点迂回——对于我们的生产应用,我们可能希望添加更复杂的逻辑,只加载来自作者的消息。然而,这对我们的原型来说很好。
以下是`UserContainer`的完整代码:
import React, { Component } from 'react'; import { Link } from 'react-router-dom'; import Header from './Header';
export default class UserContainer extends Component { renderedUserEmail = false;
getAuthor = author => { if (!this.renderedUserEmail) { this.renderedUserEmail = true; return
{author}
; } };render() { return (
); } }
# 总结
就这样!我们已经构建了完整的 React 应用。你的朋友对最终产品很兴奋,但我们还远远没有完成。
我们已经构建了一个 web 应用。它看起来不错,但还不是一个进步的网络应用。还有很多工作要做,但这就是乐趣的开始。
我们的下一步是开始将此应用转换为 PWA。我们将从研究如何使我们的 web 应用更像本地应用开始,并深入研究近年来最令人兴奋的 web 技术之一——服务工作器。










