使用 React 和 Firebase 构建实时聊天应用

Avatar of Deven Rathore
Deven Rathore

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费额度!

在本文中,我们将介绍在实时聊天应用程序中使用 Firebase 对用户进行身份验证的关键概念。 我们将集成第三方身份验证提供商(例如 Google、Twitter 和 GitHub),并且在用户登录后,我们将学习如何将用户聊天数据存储在 Firebase 实时数据库 中,我们可以在其中使用 NoSQL 云数据库同步数据。

客户端应用程序将使用 React 构建,因为它是目前最流行的 JavaScript 框架之一,但这些概念也可以应用于其他框架。

但是首先,什么是 Firebase?

Firebase 是 Google 的移动平台,用于快速开发应用。 Firebase 提供了一套工具,用于对应用程序进行身份验证、构建响应式客户端应用程序、报告分析,以及其他许多有助于管理应用程序的资源。 它还为 Web、iOS、Android 和 Unity(一个 3D 开发平台)提供后端管理。

Firebase 开箱即用,包含有助于像我们这样的开发人员专注于构建应用程序的功能,同时它处理所有服务器端逻辑。 例如:

  • 身份验证:包括对电子邮件和密码身份验证以及单点登录功能(通过 Facebook、Twitter 和 Google)的支持。
  • 实时数据库:这是一个“NoSQL”数据库,可以实时更新。
  • 云函数:这些运行额外的服务器端逻辑。
  • 静态托管:这是一种在运行时之前提供预构建资产的方法。
  • 云存储:这为我们提供了一个存储媒体资产的地方。

Firebase 提供了一个慷慨的免费层,其中包括身份验证和对其实时数据库的访问。 我们将介绍的身份验证提供商(电子邮件和密码、Google 和 GitHub)在这一方面也是免费的。 实时数据库每月允许最多 100 个同时连接和 1 GB 存储空间。 可以在 Firebase 网站 上找到完整的定价表。

我们正在构建什么

我们将构建一个名为 Chatty 的应用程序。 它将只允许经过身份验证的用户发送和读取消息,并且用户可以通过提供其电子邮件和创建密码或通过 Google 或 GitHub 帐户进行身份验证来注册。 如果您想参考它或在我们开始时查看一下,请查看 源代码

最终我们将得到如下所示的内容

设置

您需要一个 Google 帐户才能使用 Firebase,因此如果您还没有,请获取一个。 然后,我们就可以正式开始测试这个东西了。

首先,转到 Firebase 控制台 并点击“添加项目”选项。

接下来,让我们为项目输入一个名称。 我将使用 Chatty。

您可以选择将分析添加到您的项目中,但这不是必需的。 无论哪种方式,请点击继续以继续,Firebase 将花费几秒钟来为该项目分配资源。

完成后,我们将转到 Firebase 仪表板。 但是,在我们开始在 Web 应用中使用 Firebase 之前,我们必须为我们的项目获取配置详细信息。 因此,点击仪表板中的 Web 图标。

然后,为应用输入一个名称,然后点击注册应用

接下来,我们将复制并在下一个屏幕上的安全位置存储配置详细信息。 这将在下一步中派上用场。

同样,我们将通过电子邮件和密码对用户进行身份验证,并提供使用 Google 或 GitHub 帐户进行单点登录的其他选项。 我们需要在仪表板的身份验证选项卡中启用这些选项,但我们将逐一介绍它们。

电子邮件和密码身份验证

在 Firebase 仪表板中有一个登录方法选项卡。 点击电子邮件/密码选项并启用它。

现在我们可以在我们的应用中使用它了!

设置 Web 应用

对于我们的 Web 应用,我们将使用 React,但大多数概念都可以应用于任何其他框架。 我们需要 Node.js 来设置 React,因此如果您还没有下载并安装它。

我们将使用 create-react-app 来引导一个新的 React 项目。 这将下载并安装 React 应用程序所需的必要软件包。 在终端中,cd 到您希望 Chatty 项目所在的位置,并运行以下命令来初始化它

npx create-react-app chatty

此命令为我们的 React 应用执行初始设置并在 package.json 中安装依赖项。 我们还将安装一些额外的软件包。 因此,让我们 cd 到项目本身并添加 React Router 和 Firebase 的软件包。

cd chatty
yarn add react-router-dom firebase

我们已经知道为什么我们需要 Firebase,但为什么需要 React Router? 我们的聊天应用将有一些我们可以使用 React Router 处理页面之间导航的视图。

完成后,我们就可以正式启动应用了

yarn start

这将启动一个开发服务器并在您的默认浏览器中打开一个 URL。 如果所有内容都正确安装,您应该会看到如下所示的屏幕

查看文件夹结构,您会看到类似于以下内容

对于我们的聊天应用,这是我们将使用的文件夹结构

  • /components:包含在不同页面中使用的可重用部件
  • /helpers:一组可重用函数
  • /pages:应用视图
  • /services:我们正在使用的第三方服务(例如 Firebase)
  • App.js:根组件

文件夹中的任何其他内容对于此项目都是不必要的,可以安全地删除。 从这里开始,让我们向 src/services/firebase.js 添加一些代码,以便应用可以与 Firebase 通信。

import firebase from 'firebase';

让我们将 Firebase 集成到应用中

我们将使用之前在 Firebase 仪表板中注册应用时复制的配置详细信息导入并初始化 Firebase。 然后,我们将导出身份验证和数据库模块。

const config = {
  apiKey: "ADD-YOUR-DETAILS-HERE",
  authDomain: "ADD-YOUR-DETAILS-HERE",
  databaseURL: "ADD-YOUR-DETAILS-HERE"
};
firebase.initializeApp(config);
export const auth = firebase.auth;
export const db = firebase.database();

让我们在 src/App.js 中导入我们的依赖项

import React, { Component } from 'react';
import {
  Route,
  BrowserRouter as Router,
  Switch,
  Redirect,
} from "react-router-dom";
import Home from './pages/Home';
import Chat from './pages/Chat';
import Signup from './pages/Signup';
import Login from './pages/Login';
import { auth } from './services/firebase';

这些是 ES6 导入。 特别地,我们正在导入 React 和构建应用所需的的其他软件包。 我们还导入了应用的所有页面,我们稍后将这些页面配置到我们的路由器中。

接下来是路由

我们的应用具有公共路由(无需身份验证即可访问)和私有路由(仅在经过身份验证后才能访问)。 因为 React 没有提供检查身份验证状态的方法,所以我们将为这两种类型的路由创建 高阶组件 (HOC)。

我们的 HOC 将

  • 包装一个 <Route>
  • 将道具从路由器传递到 <Route>
  • 根据身份验证状态渲染组件,以及
  • 如果条件不满足,则将用户重定向到指定的路由

让我们编写 <PrivateRoute> HOC 的代码。

function PrivateRoute({ component: Component, authenticated, ...rest }) {
  return (
    <Route
      {...rest}
      render={(props) => authenticated === true
        ? <Component {...props} />
        : <Redirect to={{ pathname: '/login', state: { from: props.location } }} />}
    />
  )
}

它接收三个道具:如果条件为真则要渲染的组件、authenticated 状态以及 ES6 展开运算符以获取从路由器传递的其余参数。 它检查 authenticated 是否为真并渲染传递的组件,否则它将重定向到/login。

function PublicRoute({ component: Component, authenticated, ...rest }) {
  return (
    <Route
      {...rest}
      render={(props) => authenticated === false
        ? <Component {...props} />
        : <Redirect to='/chat' />}
    />
  )
}

<PublicRoute> 几乎相同。 它渲染我们的公共路由,如果身份验证状态变为真,则重定向到 /chat 路径。 我们可以将 HOC 用于渲染方法中

render() {
  return this.state.loading === true ? <h2>Loading...</h2> : (
    <Router>
      <Switch>
        <Route exact path="/" component={Home}></Route>
        <PrivateRoute path="/chat" authenticated={this.state.authenticated} component={Chat}></PrivateRoute>
        <PublicRoute path="/signup" authenticated={this.state.authenticated} component={Signup}></PublicRoute>
        <PublicRoute path="/login" authenticated={this.state.authenticated} component={Login}></PublicRoute>
      </Switch>
    </Router>
  );
}

检查身份验证

在验证用户是否已通过身份验证期间显示加载指示器会很好。检查完成后,我们将呈现与 URL 匹配的相应路由。我们有三个公共路由——<Home><Login><Signup>——以及一个名为 <Chat> 的私有路由。

让我们编写逻辑来检查用户是否确实已通过身份验证。

class App extends Component {
  constructor() {
    super();
    this.state = {
      authenticated: false,
      loading: true,
    };
  }
}

export default App;

这里我们正在设置应用程序的初始状态。然后,我们使用 componentDidMount 生命周期钩子 来检查用户是否已通过身份验证。因此,让我们在构造函数之后添加它

componentDidMount() {
  auth().onAuthStateChanged((user) => {
    if (user) {
      this.setState({
        authenticated: true,
        loading: false,
      });
    } else {
      this.setState({
        authenticated: false,
        loading: false,
      });
    }
  })
}

Firebase 提供了一种名为 onAuthStateChanged 的直观方法,该方法在身份验证状态更改时触发。我们用它来更新我们的初始状态。如果用户未通过身份验证,则 user 为 null。如果 user 为真,我们将 authenticated 更新为 true;否则,我们将其设置为 false。无论哪种方式,我们也都会将 loading 设置为 false

使用电子邮件和密码注册用户

用户将能够通过电子邮件和密码注册 Chatty。helpers 文件夹包含一组我们将用于处理某些身份验证逻辑的方法。在此文件夹内,让我们创建一个名为 auth.js 的新文件并添加以下内容

import { auth } from "../services/firebase";

我们从之前创建的服务中导入 auth 模块。

export function signup(email, password) {
  return auth().createUserWithEmailAndPassword(email, password);
}


export function signin(email, password) {
  return auth().signInWithEmailAndPassword(email, password);
}

这里我们有两个方法:signupsignin

  • signup 将使用用户的电子邮件和密码创建一个新用户。
  • signin 将登录使用电子邮件和密码创建的现有用户。

让我们通过在 pages 文件夹中创建一个名为 Signup.js 的新文件来创建我们的 <Signup> 页面。这是 UI 的标记

import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { signup } from '../helpers/auth';


export default class SignUp extends Component {


  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <h1>
            Sign Up to
          <Link to="/">Chatty</Link>
          </h1>
          <p>Fill in the form below to create an account.</p>
          <div>
            <input placeholder="Email" name="email" type="email" onChange={this.handleChange} value={this.state.email}></input>
          </div>
          <div>
            <input placeholder="Password" name="password" onChange={this.handleChange} value={this.state.password} type="password"></input>
          </div>
          <div>
            {this.state.error ? <p>{this.state.error}</p> : null}
            <button type="submit">Sign up</button>
          </div>
          <hr></hr>
          <p>Already have an account? <Link to="/login">Login</Link></p>
        </form>
      </div>
    )
  }
}
电子邮件?检查。密码?检查。提交按钮?检查。我们的表单看起来不错。

表单和输入字段绑定到我们尚未创建的方法,因此让我们解决这个问题。在 render() 方法之前,我们将添加以下内容

constructor(props) {
  super(props);
  this.state = {
    error: null,
    email: '',
    password: '',
  };
  this.handleChange = this.handleChange.bind(this);
  this.handleSubmit = this.handleSubmit.bind(this);
}

我们正在设置页面的初始状态。我们还将 handleChangehandleSubmit 方法绑定到组件的 this 作用域。

handleChange(event) {
  this.setState({
    [event.target.name]: event.target.value
  });
}

接下来,我们将添加我们的输入字段绑定的 handleChange 方法。该方法使用 计算属性 来动态确定键并设置相应的 state 变量。

async handleSubmit(event) {
  event.preventDefault();
  this.setState({ error: '' });
  try {
    await signup(this.state.email, this.state.password);
  } catch (error) {
    this.setState({ error: error.message });
  }
}

对于 handleSubmit,我们阻止了表单提交的默认行为(除了其他操作外,它只是重新加载浏览器)。我们还清除错误状态变量,然后使用从 helpers/auth 导入的 signup() 方法传递用户输入的电子邮件和密码。

如果注册成功,用户将被重定向到 /Chats 路由。这可以通过我们之前创建的 onAuthStateChanged 和 HOC 的组合来实现。如果注册失败,我们将设置错误变量,该变量会向用户显示一条消息。

使用电子邮件和密码验证用户

登录页面与注册页面相同。唯一的区别是我们将使用之前创建的 helpers 中的 signin 方法。也就是说,让我们在 pages 目录中再创建一个新文件,这次名为 Login.js,其中包含此代码

import React, { Component } from "react";
import { Link } from "react-router-dom";
import { signin, signInWithGoogle, signInWithGitHub } from "../helpers/auth";


export default class Login extends Component {
  constructor(props) {
    super(props);
    this.state = {
      error: null,
      email: "",
      password: ""
    };
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }


  handleChange(event) {
    this.setState({
      [event.target.name]: event.target.value
    });
  }


  async handleSubmit(event) {
    event.preventDefault();
    this.setState({ error: "" });
    try {
      await signin(this.state.email, this.state.password);
    } catch (error) {
      this.setState({ error: error.message });
    }
  }


  render() {
    return (
      <div>
        <form
          autoComplete="off"
          onSubmit={this.handleSubmit}
        >
          <h1>
            Login to
            <Link to="/">
              Chatty
            </Link>
          </h1>
          <p>
            Fill in the form below to login to your account.
          </p>
          <div>
            <input
              placeholder="Email"
              name="email"
              type="email"
              onChange={this.handleChange}
              value={this.state.email}
            />
          </div>
          <div>
            <input
              placeholder="Password"
              name="password"
              onChange={this.handleChange}
              value={this.state.password}
              type="password"
            />
          </div>
          <div>
            {this.state.error ? (
              <p>{this.state.error}</p>
            ) : null}
            <button type="submit">Login</button>
          </div>
          <hr />
          <p>
            Don't have an account? <Link to="/signup">Sign up</Link>
          </p>
        </form>
      </div>
    );
  }
}

再次,与之前非常相似。当用户成功登录后,他们会被重定向到 /chat

使用 Google 帐户进行身份验证

Firebase 允许我们使用有效的 Google 帐户对用户进行身份验证。我们必须在 Firebase 仪表板中启用它,就像我们对电子邮件和密码所做的那样。

选择 Google 选项并在设置中启用它。

在同一页面上,我们还需要向下滚动以将域添加到授权访问功能的域列表中。这样,我们就可以避免来自任何未列入白名单的域的垃圾邮件。出于开发目的,我们的域是 localhost,因此我们现在将使用它。

现在我们可以切换回我们的编辑器了。我们将向 helpers/auth.js 添加一个新方法来处理 Google 身份验证。

export function signInWithGoogle() {
  const provider = new auth.GoogleAuthProvider();
  return auth().signInWithPopup(provider);
}

在这里,我们正在创建 GoogleAuthProvider 的实例。然后,我们使用提供程序作为参数调用 signInWithPopup。调用此方法时,将出现一个弹出窗口,并引导用户完成 Google 登录流程,然后将其重定向回应用程序。您可能在某个时候自己体验过它。

让我们通过导入方法在我们的注册页面中使用它

import { signin, signInWithGoogle } from "../helpers/auth";

然后,让我们添加一个按钮来触发该方法,就在“注册”按钮下方

<p>Or</p>
<button onClick={this.googleSignIn} type="button">
  Sign up with Google
</button>

接下来,我们将添加 onClick 处理程序

async googleSignIn() {
  try {
    await signInWithGoogle();
  } catch (error) {
    this.setState({ error: error.message });
  }
}

哦,我们应该记住将处理程序绑定到组件

constructor() {
  // ...
  this.githubSignIn = this.githubSignIn.bind(this);
}

这就是我们需要做的全部!单击该按钮后,它将引导用户完成 Google 登录流程,如果成功,应用程序会将用户重定向到聊天路由。

使用 GitHub 帐户进行身份验证

我们将对 GitHub 执行相同的操作。不妨给人们提供多种帐户选择。

让我们逐步完成。首先,我们将像对电子邮件和 Google 所做的那样,在 Firebase 仪表板中启用 GitHub 登录。

您会注意到客户端 ID 和客户端密钥字段为空,但我们在底部确实有我们的授权回调 URL。复制它,因为我们将在执行下一步操作时使用它,即在 GitHub 上 注册应用程序

完成后,我们将获得一个客户端 ID 和密钥,我们现在可以将其添加到 Firebase 控制台中。

让我们切换回编辑器并将一个新方法添加到 helpers/auth.js

export function signInWithGitHub() {
  const provider = new auth.GithubAuthProvider();
  return auth().signInWithPopup(provider);
}

它类似于 Google 登录界面,但这次我们创建了一个 GithubAuthProvider。然后,我们将使用提供程序调用 signInWithPopup

pages/Signup.js 中,我们更新导入以包含 signInWithGitHub 方法

import { signup, signInWithGoogle, signInWithGitHub } from "../helpers/auth";

我们添加一个用于 GitHub 注册的按钮

<button type="button" onClick={this.githubSignIn}>
  Sign up with GitHub
</button>

然后我们添加按钮的点击处理程序,该处理程序触发 GitHub 注册流程

async githubSignIn() {
  try {
    await signInWithGitHub();
  } catch (error) {
    this.setState({ error: error.message });
  }
}

让我们再次记住将处理程序绑定到组件

constructor() {
  // ...
  this.githubSignIn = this.githubSignIn.bind(this);
}

现在,我们将获得与 Google 相同的登录和身份验证流程,但使用的是 GitHub。

从 Firebase 读取数据

Firebase 有两种类型的数据库:一种称为实时数据库的产品,另一种称为 Cloud Firestore。这两个数据库都是类似 NoSQL 的数据库,这意味着数据库的结构为键值对。在本教程中,我们将使用实时数据库。

这是我们将用于应用程序的结构。我们有一个根节点 chats 和子节点。每个子节点都有内容、时间戳和用户 ID。您会注意到其中一个选项卡是“规则”,它是我们如何设置数据库内容的权限。

Firebase 数据库规则也定义为键值对。在这里,我们将规则设置为仅允许经过身份验证的用户读取和写入聊天节点。还有很多 firebase 规则 值得一试。

让我们编写代码从数据库读取数据。首先,在 pages 文件夹中创建一个名为 Chat.js 的新文件,并将此代码添加到其中以导入 React、Firebase 身份验证和实时数据库

import React, { Component } from "react";
import { auth } from "../services/firebase";
import { db } from "../services/firebase"

接下来,让我们定义应用程序的初始状态

export default class Chat extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: auth().currentUser,
      chats: [],
      content: '',
      readError: null,
      writeError: null
    };
  }
  async componentDidMount() {
    this.setState({ readError: null });
    try {
      db.ref("chats").on("value", snapshot => {
        let chats = [];
        snapshot.forEach((snap) => {
          chats.push(snap.val());
        });
        this.setState({ chats });
      });
    } catch (error) {
      this.setState({ readError: error.message });
    }
  }
}

真正的主要逻辑发生在 componentDidMount 中。db.ref("chats") 是数据库中 chats 路径的引用。我们侦听 value 事件,该事件在每次向 chats 节点添加新值时触发。从数据库返回的是一个类似数组的对象,我们遍历它并将每个对象推送到数组中。然后,我们将 chats 状态变量设置为我们得到的结果数组。如果发生错误,我们将 readError 状态变量设置为错误消息。

这里需要注意的一点是,因为我们使用了 .on() 方法,所以在客户端和我们的 Firebase 数据库之间创建了一个连接。这意味着每当向数据库添加新值时,客户端应用程序都会实时更新,这意味着用户无需刷新页面即可查看新的聊天内容。不错!

componentDidMount 之后,我们可以这样渲染我们的聊天内容

render() {
  return (
    <div>
      <div className="chats">
        {this.state.chats.map(chat => {
          return <p key={chat.timestamp}>{chat.content}</p>
        })}
      </div>
      <div>
        Login in as: <strong>{this.state.user.email}</strong>
      </div>
    </div>
  );
}

这将渲染聊天数组。我们渲染当前登录用户的电子邮件。

将数据写入 Firebase

目前,用户只能从数据库读取数据,但无法发送消息。我们需要一个带有输入字段的表单,该字段接受消息,并带有一个按钮将消息发送到聊天。

因此,让我们像这样修改标记

return (
    <div>
      <div className="chats">
        {this.state.chats.map(chat => {
          return <p key={chat.timestamp}>{chat.content}</p>
        })}
      </div>
      {# message form #}
      <form onSubmit={this.handleSubmit}>
        <input onChange={this.handleChange} value={this.state.content}></input>
        {this.state.error ? <p>{this.state.writeError}</p> : null}
        <button type="submit">Send</button>
      </form>
      <div>
        Login in as: <strong>{this.state.user.email}</strong>
      </div>
    </div>
  );
}

我们添加了一个带有输入字段和按钮的表单。输入字段的值绑定到我们的状态变量content,当它的值发生变化时,我们调用handleChange

handleChange(event) {
  this.setState({
    content: event.target.value
  });
}

handleChange从输入字段获取值并设置到我们的状态变量上。要提交表单,我们调用handleSubmit

async handleSubmit(event) {
  event.preventDefault();
  this.setState({ writeError: null });
  try {
    await db.ref("chats").push({
      content: this.state.content,
      timestamp: Date.now(),
      uid: this.state.user.uid
    });
    this.setState({ content: '' });
  } catch (error) {
    this.setState({ writeError: error.message });
  }
}

我们将任何先前的错误设置为null。我们在数据库中创建对chats节点的引用,并使用push()创建唯一的键并将对象推送到其中。

与往常一样,我们必须将我们的方法绑定到组件

constructor(props) {
  // ...
  this.handleChange = this.handleChange.bind(this);
  this.handleSubmit = this.handleSubmit.bind(this);
}

现在用户可以向聊天添加新消息并实时查看它们!这有多酷?

演示时间!

享受你的新聊天应用!

恭喜!您刚刚构建了一个使用电子邮件和密码对用户进行身份验证的聊天工具,以及通过 Google 或 GitHub 帐户进行身份验证的选项。

我希望这能让你了解 Firebase 在应用程序上启动身份验证方面有多么方便。我们开发了一个聊天应用程序,但真正的亮点是我们创建的注册和登录方法。这对许多应用程序都有用。

问题?想法?反馈?请在评论中告诉我!