Skip to content

Latest commit

 

History

History
1227 lines (945 loc) · 41.1 KB

File metadata and controls

1227 lines (945 loc) · 41.1 KB

六、完成我们的应用

是时候完成我们应用的原型了,天哪,我们有没有工作要做呢。

框架已经就位,所有路线都已设置,登录屏幕已完全完成。然而,我们的聊天和用户视图到目前为止还是空白的,这就是 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 fillsvg 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状态,通过propsUserContainerChatContainer共享。

状态是应用中唯一的真实来源,不应重复。存储两个消息数组没有意义:一个在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 数据库,首先我们获取它的一个实例,带有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 加载数据

如前所述,我们可以监听对数据库中特定引用的更改。换句话说,我们可以定义一个函数,每当firebase.database().ref(‘/messages’)发生变化时,当一条新消息出现时,该函数就会运行。

在我们继续之前,我鼓励你们考虑两件事:我们应该在哪里定义这个侦听器,以及函数应该做什么。

看看你是否能想出一个可行的实施方案!在你头脑风暴出一个想法之后,让我们来构建它。

事情是这样的:我们的应用中已经有一个非常类似的案例。我们App#componentDidMount中的firebase.auth().onAuthStateChanged监听我们当前用户的变化,并更新我们Appstate.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`属性中添加一个条件:

{this.props.messages.map(msg => (

{msg.msg}

{msg.author}

))}
```jsx

这使用 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()`函数将索引作为第二个元素传递给回调函数。我们可以使用它,如图所示:

{this.props.messages.map((msg, i) => (

{msg.msg}

{(!this.props.messages[i + 1] || this.props.messages[i + 1].author !== msg.author) && (

{msg.author}

)}
))}
```jsx

现在,我们说:“如果有下一条消息,并且下一条消息的作者与当前消息的作者不同,请显示此消息的作者。”

然而,在我们的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 始终是一个函数,它与相关元素一起调用,然后用于将该元素分配给组件的属性,如图所示:
{ this.messageContainer = element; }}> ```jsx

在我们的scrollToBottom方法中,我们使用ReactDOM.findDOMNode获取所讨论的元素(别忘了导入 react dom!):

import ReactDOM from 'react-dom';
```jsx

scrollToBottom = () => { const messageContainer = ReactDOM.findDOMNode(this.messageContainer); }

在下一节中,我们将使其仅在加载消息时显示消息容器。因此,我们需要一个`if`语句来检查我们的`messageContainer`DOM 节点当前是否存在。完成后,我们可以将`messageContainer.scrollTop`(当前向下滚动的距离)设置为其高度,使其位于底部:

scrollToBottom = () => { const messageContainer = ReactDOM.findDOMNode(this.messageContainer); if (messageContainer) { messageContainer.scrollTop = messageContainer.scrollHeight; } }

现在,如果您尝试缩小浏览器的大小并发送消息,则应始终将您带到消息容器的底部,以便它自动显示在视图中。美好的

# 装载指示器

Firebase 的加载速度非常快,但是如果我们的用户连接速度慢,他们会看到一个空白屏幕,直到他们的消息加载为止,并且会想,“我的精彩聊天在哪里?”让我们给他们一个加载指示器。

在我们的`ChatContainer`中,我们只想在名为`messagesLoaded`的道具为真时显示消息(稍后我们将对其进行定义)。我们将使消息容器的呈现以该属性为条件。我们可以使用**ternerary**语句来实现这一点。

Ternerary statements in JavaScript are a short way of doing if else. Instead of if (true) `{ // this code }`, else `{ // that code }`, we can write `true ? // this code : // that code`, which is short and sweet.

代码如下所示:

// Beginning of ChatContainer

Logout {this.props.messagesLoaded ? (
{ this.messageContainer = element; }}> {this.props.messages.map((msg, i) => (

{msg.msg}

{this.getAuthor(msg, this.props.messages[i + 1])}
))}
) : (
logo
)}
// Rest of ChatContainer ```jsx

花点时间仔细阅读这篇文章,并确保你准确地理解了正在发生的事情。Ternerary 语句在 React 中很常见,只是因为它们使有条件地呈现 JSX 变得容易。如果一切正常,您应该看到以下内容,并在徽标上显示一个脉冲动画:

下一步是在消息加载时更新messagesLoaded属性。让我们跳到App.js

这里的逻辑很简单——当我们从 Firebase 数据库接收到消息值时,如果我们以前没有收到过值(换句话说,这是我们收到的第一条消息),我们知道我们的消息是第一次加载的:

class App extends Component {
  state = { user: null, messages: [], messagesLoaded: false };
```jsx

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); if (!this.state.messagesLoaded) { this.setState({ messagesLoaded: true }); } }); }

<Route exact path="/" render={() => ( )} />

现在,如果重新加载应用页面,您应该会短暂地看到加载指示器(取决于您的 internet 连接),然后看到显示的消息。

以下是迄今为止`ChatContainer`的代码:

import React, { Component } from 'react'; import { Link } from 'react-router-dom'; import ReactDOM from 'react-dom'; import Header from './Header';

export default class ChatContainer extends Component { state = { newMessage: '' };

componentDidMount() { this.scrollToBottom(); }

componentDidUpdate(previousProps) { if (previousProps.messages.length !== this.props.messages.length) { this.scrollToBottom(); } }

scrollToBottom = () => { const messageContainer = ReactDOM.findDOMNode(this.messageContainer); if (messageContainer) { messageContainer.scrollTop = messageContainer.scrollHeight; } };

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(); } };

getAuthor = (msg, nextMsg) => { if (!nextMsg || nextMsg.author !== msg.author) { return (

<Link to={/users/${msg.user_id}}>{msg.author}

); } };

render() { return (

Logout {this.props.messagesLoaded ? ( <div id="message-container" ref={element => { this.messageContainer = element; }}> {this.props.messages.map((msg, i) => ( <div key={msg.id} className={message ${this.props.user.email === msg.author && 'mine'}}>

{msg.msg}

{this.getAuthor(msg, this.props.messages[i + 1])}
))}
) : (
logo
)}
<textarea placeholder="Add your message..." onChange={this.handleInputChange} onKeyDown={this.handleKeyDown} value={this.state.newMessage} />
); } }

我们的应用即将完成。最后一步是用户配置文件页面。

# 个人资料页面

`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`
Back To Chat {this.props.messagesLoaded ? (

Messages go here

) : (
logo </div> )}
```jsx

为了显示我们的消息,我们只想显示其中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`方法:
{this.props.messages.map(msg => { if (msg.user_id === this.props.userID) { return (
{this.getAuthor(msg.author)}

{msg.msg}

); } })}
```jsx

这将检查是否已呈现作者,如果未呈现,则返回:

  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 (

Back To Chat {this.props.messagesLoaded ? (
{this.props.messages.map(msg => { if (msg.user_id === this.props.userID) { return (
{this.getAuthor(msg.author)}

{msg.msg}

); } })}
) : (
logo
)}
); } }


# 总结

就这样!我们已经构建了完整的 React 应用。你的朋友对最终产品很兴奋,但我们还远远没有完成。

我们已经构建了一个 web 应用。它看起来不错,但还不是一个进步的网络应用。还有很多工作要做,但这就是乐趣的开始。

我们的下一步是开始将此应用转换为 PWA。我们将从研究如何使我们的 web 应用更像本地应用开始,并深入研究近年来最令人兴奋的 web 技术之一——服务工作器。