是时候完成我们应用的原型了,天哪,我们有没有工作要做呢。
框架已经就位,所有路线都已设置,登录屏幕已完全完成。然而,我们的聊天和用户视图到目前为止还是空白的,这就是 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}
我们的消息现在按如下方式分组:
![](img/00049.jpeg)
# 向下滚动
试着把你的浏览器缩小一些,这样你的信息列表就几乎被切断了;然后,提交另一条消息。请注意,如果它超过了消息容器的截止日期,您必须向下滚动才能看到它。这是糟糕的 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 技术之一——服务工作器。