使用 React Hooks 在 100 行代码内构建聊天应用

Avatar of Akash Joshi
Akash Joshi

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

我们之前在 CSS-Tricks 中介绍了 React Hooks。我还有一篇 介绍 React Hooks 的文章,文章说明了如何使用它们通过函数创建组件。这两篇文章都很好地概述了它们的运作方式,但它们也打开了无限的可能性。

所以,这就是我们将在本文中要做的事情。我们将通过构建一个聊天应用程序来体验 Hooks 如何使我们的开发过程更轻松、更快。

具体来说,我们将使用 Create React App 构建一个聊天应用程序。在构建过程中,我们将使用一些 React Hooks 来简化开发过程,并删除许多对工作而言不必要的样板代码。

有许多可用的开源 React Hooks,我们也将使用它们。这些 Hooks 可以直接使用来构建功能,否则构建这些功能需要更多代码。它们通常也遵循任何功能的公认标准。实际上,这提高了编写代码的效率并提供了安全的功能。

让我们看看需求

我们将构建的聊天应用程序将具有以下功能

  • 获取服务器发送的过去消息列表
  • 连接到房间进行群聊
  • 当人员从房间断开连接或连接到房间时获取更新
  • 发送和接收消息

在深入研究时,我们假设了一些事项

  • 我们将 要使用的服务器 视为黑盒。不用担心它是否完美运行,因为我们将使用简单的套接字与它进行通信。
  • 所有样式都包含在一个 CSS 文件中,可以复制到 src 目录中。应用程序中使用的所有样式都 链接到存储库中

准备工作

好的,我们现在需要准备开发环境,以便开始编写代码。首先,React 需要 Node 和 npm。您可以 在这里设置它们

让我们从终端启动一个新项目

npx create-react-app socket-client
cd socket-client
npm start

现在,我们应该能够在浏览器中导航到 http://localhost:3000 并获得项目的默认欢迎页面。

从这里开始,我们将根据使用的 Hooks 将工作分解。这将有助于我们在将它们用于实际应用时理解这些 Hooks。

使用 useState Hook

我们将要使用的第一个 Hook 是 useState。它允许我们在组件中维护状态,而不是,例如,使用 this.state 编写和初始化类。例如,用户名等不变数据存储在 useState 变量中。这确保了数据可以轻松获取,同时需要编写的代码更少。

useState 的主要优势在于,每当我们更新应用程序的状态时,它都会自动反映在渲染的组件中。如果我们使用普通变量,它们将不被视为组件的状态,并且必须作为 props 传递以重新渲染组件。因此,我们再次节省了很多工作,并简化了流程。

该 Hook 内置于 React 中,因此我们可以使用一行代码导入它

import React, { useState } from 'react';

我们将创建一个简单的组件,如果用户已登录,则返回“Hello”,如果用户已注销,则返回登录表单。我们检查 id 变量来判断。

我们的表单提交将由我们创建的函数 handleSubmit 处理。它将检查“Name”表单字段是否已填写。如果已填写,我们将设置该用户的 idroom 值。否则,我们将显示一条消息,提醒用户需要填写“Name”字段才能继续。

// App.js

import React, { useState } from 'react';
import './index.css';

export default () => {
  const [id, setId] = useState("");
  const [nameInput, setNameInput] = useState("");
  const [room, setRoom] = useState("");

  const handleSubmit = e => {
    e.preventDefault();
    if (!nameInput) {
      return alert("Name can't be empty");
    }
    setId(name);
    socket.emit("join", name, room);
  };

  return id !== '' ? (
    <div>Hello</div>
  ) : (
    <div style={{ textAlign: "center", margin: "30vh auto", width: "70%" }}>
      <form onSubmit={event => handleSubmit(event)}>
        <input
          id="name"
          onChange={e => setNameInput(e.target.value.trim())}
          required
          placeholder="What is your name .."
        />
        <br />
        <input
          id="room"
          onChange={e => setRoom(e.target.value.trim())}
          placeholder="What is your room .."
        />
        <br />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

这就是我们在聊天应用程序中使用 useState Hook 的方式。再次,我们从 React 中导入该 Hook,构造用户的 ID 和聊天室位置的值,如果用户状态为已登录,则设置这些值,如果用户已注销,则返回登录表单。

使用 useSocket Hook

我们将使用一个名为 useSocket 的开源 Hook 来维护与服务器的连接。与 useState 不同,此 Hook 不是内置于 React 中的,因此我们需要在将其导入应用程序之前将其添加到项目中。

npm add use-socket.io-client

服务器连接是通过使用 socket.io 库 的 React Hooks 版本来维护的,这是一种更轻松地维护与服务器的 WebSocket 连接的方式。我们使用它来发送和接收实时消息,以及维护事件,例如连接到房间。

默认的 socket.io 客户端库具有全局声明,即我们定义的套接字变量可以被任何组件使用。但是,我们的数据可以从任何地方进行操作,我们不知道这些更改在哪里发生。套接字 Hooks 通过在组件级别约束 Hook 定义来解决此问题,这意味着每个组件都负责自己的数据传输。

useSocket 的基本用法如下所示

const [socket] = useSocket('socket-url')

在我们继续之前,我们将使用一些套接字 API。为了参考,所有这些 API 都在 socket.io 文档 中概述。但现在,让我们导入该 Hook,因为我们已经安装了它。

import useSocket from 'use-socket.io-client';

接下来,我们需要通过连接到服务器来初始化该 Hook。然后,我们将套接字记录到控制台中,以检查它是否已正确连接。

const [id, setId] = useState('');
const [socket] = useSocket('<https://open-chat-naostsaecf.now.sh>');

socket.connect();
console.log(socket);

打开浏览器控制台,该代码段中的 URL 应该被记录。

使用 useImmer Hook

我们的聊天应用程序将使用 useImmer Hook 来管理数组和对象的 state,而不会改变原始 state。它将 useStateImmer 相结合,以实现不可变状态管理。这将有助于管理在线人员列表和需要显示的消息。

在 useState 中使用 Immer 允许我们通过从当前 state 创建一个新的 state 来更改数组或对象,同时防止直接对当前 state 进行更改。这为我们提供了更多安全性,因为我们可以保持当前 state 不变,同时能够根据不同的条件操作 state。

再次,我们正在使用一个没有内置于 React 中的 Hook,因此让我们将其导入项目

npm add use-immer

基本用法非常简单。构造函数中的第一个值是当前 state,第二个值是更新该 state 的函数。然后,useImmer Hook 获取当前 state 的初始值。

const [data, setData] = useImmer(default_value)

使用 setData

注意上一个示例中的 setData 函数?我们使用它来创建当前数据的草稿副本,我们可以使用它来安全地操作数据,并在更改变为不可变时将其用作下一个 state。因此,我们的原始数据会保留,直到我们完成运行函数,并且我们可以绝对清晰地更新当前数据。

setData(draftState => { 
  draftState.operation(); 
});

// ...or

setData(draft => newState);

// Here, draftState is a copy of the current data

使用 useEffect Hook

好的,我们回到了一个直接内置在 React 中的钩子。我们将使用 useEffect 钩子,仅在应用程序加载时运行一段代码。这样可以确保我们的代码只运行一次,而不是每次组件使用新数据重新渲染时都运行,这对性能有好处。

要开始使用这个钩子,我们只需要导入它 - 不需要安装!

import React, { useState, useEffect } from 'react';

我们需要一个组件,根据数组中是否存在 sende ID 来渲染 消息更新。由于我们富有创意,让我们将该组件命名为 Messages

const Messages = props => props.data.map(m => m[0] !== '' ? 
(<li key={m[0]}><strong>{m[0]}</strong> : <div className="innermsg">{m[1]}</div></li>) 
: (<li key={m[1]} className="update">{m[1]}</li>) );

让我们将套接字逻辑放在 useEffect 内,这样当组件重新渲染时,我们不会重复地重复同一组消息。我们将在组件中定义消息钩子,连接到套接字,然后在 useEffect 钩子本身中设置新的消息和更新的监听器。我们还将在监听器内设置更新函数。

const [socket] = useSocket('<https://open-chat-naostsaecf.now.sh>');      
socket.connect();

const [messages, setMessages] = useImmer([]);
useEffect(()=>{
  socket.on('update', message => setMessages(draft => {
    draft.push(['', message]);
  }));

  socket.on('message que',(nick, message) => {
    setMessages(draft => {
      draft.push([nick, message])
    })
  });
},0);

为了确保万无一失,我们还会添加一个“加入”消息,如果用户名和房间名正确。这会触发其他事件监听器,我们可以接收在该房间中发送的过去消息以及所需的任何更新。

// ...
  socket.emit('join', name, room);
};

return id ? (
  <section style={{ display: "flex", flexDirection: "row" }}>
      <ul id="messages">
        <Messages data={messages} />
      </ul>
      <ul id="online">
        {" "}
        &#x1f310; : <Online data={online} />{" "}
      </ul>
      <div id="sendform">
        <form onSubmit={e => handleSend(e)} style={{ display: "flex" }}>
          <input id="m" onChange={e => setInput(e.target.value.trim())} />
          <button style={{ width: "75px" }} type="submit">
            Send
          </button>
        </form>
      </div>
    </section>
) : (
// ...

收尾工作

我们只需要进行一些调整就可以完成我们的聊天应用程序。具体来说,我们还需要

  • 一个显示在线人员的组件
  • 一个带有套接字监听器的 useImmer 钩子
  • 一个带有相应套接字的消息提交处理程序

所有这些都建立在我们已经涵盖的内容的基础上。我将插入 App.js 文件的完整代码,以展示所有内容如何组合在一起。

// App.js

import React, { useState, useEffect } from 'react';
import useSocket from 'use-socket.io-client';
import { useImmer } from 'use-immer';

import './index.css';

const Messages = props => props.data.map(m => m[0] !== '' ? (<li><strong>{m[0]}</strong> : <div className="innermsg">{m[1]}</div></li>) : (<li className="update">{m[1]}</li>) );

const Online = props => props.data.map(m => <li id={m[0]}>{m[1]}</li>);

export default () => {
  const [id, setId] = useState('');
  const [nameInput, setNameInput] = useState('');
  const [room, setRoom] = useState('');
  const [input, setInput] = useState('');

  const [socket] = useSocket('https://open-chat-naostsaecf.now.sh');
  socket.connect();

  const [messages, setMessages] = useImmer([]);
  const [online, setOnline] = useImmer([]);

  useEffect(()=>{
    socket.on('message que',(nick,message) => {
      setMessages(draft => {
        draft.push([nick,message])
      })
    });

    socket.on('update',message => setMessages(draft => {
      draft.push(['',message]);
    }));

    socket.on('people-list',people => {
      let newState = [];
      for(let person in people){
        newState.push([people[person].id,people[person].nick]);
      }
      setOnline(draft=>{draft.push(...newState)});
      console.log(online)
    });

    socket.on('add-person',(nick,id)=>{
      setOnline(draft => {
        draft.push([id,nick])
      })
    });

    socket.on('remove-person',id=>{
      setOnline(draft => draft.filter(m => m[0] !== id))
    });

    socket.on('chat message',(nick,message)=>{
      setMessages(draft => {draft.push([nick,message])})
    });
  },0);

  const handleSubmit = e => {
    e.preventDefault();
    if (!nameInput) {
      return alert("Name can't be empty");
    }
    setId(name);
    socket.emit("join", name,room);
  };

  const handleSend = e => {
    e.preventDefault();
    if(input !== ''){
      socket.emit('chat message',input,room);
      setInput('');
    }
  };

  return id ? (
    <section style={{display:'flex',flexDirection:'row'}} >
      <ul id="messages"><Messages data={messages} /></ul>
      <ul id="online"> &#x1f310; : <Online data={online} /> </ul>
      <div id="sendform">
        <form onSubmit={e => handleSend(e)} style={{display: 'flex'}}>
            <input id="m" onChange={e=>setInput(e.target.value.trim())} /><button style={{width:'75px'}} type="submit">Send</button>
        </form>
      </div>
    </section>
  ) : (
    <div style={{ textAlign: 'center', margin: '30vh auto', width: '70%' }}>
      <form onSubmit={event => handleSubmit(event)}>
        <input id="name" onChange={e => setNameInput(e.target.value.trim())} required placeholder="What is your name .." /><br />
        <input id="room" onChange={e => setRoom(e.target.value.trim())} placeholder="What is your room .." /><br />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

总结

就是这样!我们一起构建了一个功能齐全的群聊应用程序!这太酷了!该项目的完整代码可以在 GitHub 上 找到这里

我们在本文中介绍的只是 React Hooks 如何提高您的生产力并帮助您使用强大的前端工具构建强大应用程序的一个缩影。我在 这个综合教程中构建了一个更强大的聊天应用程序。如果您想进一步提升 React Hooks 的水平,请关注它。

现在您已经对 React Hooks 有了实践经验,请使用您新获得的知识进行更多练习!以下是一些您可以从这里构建的想法

  • 一个博客平台
  • 您自己的 Instagram 版本
  • Reddit 的克隆

沿途有问题?请留言,让我们一起创造很棒的东西。