使用 WebSockets、React 和 TypeScript 构建实时投票应用程序🔌⚡️

2025-06-07

使用 WebSockets、React 和 TypeScript 构建实时投票应用程序🔌⚡️

TL;DR

WebSockets 允许您的应用具有“实时”功能,其中更新是即时的,因为它们是在开放的双向通道上传递的。

这与 CRUD 应用程序不同,后者通常使用 HTTP 请求,必须建立连接、发送请求、接收响应,然后关闭连接。

即时的

要在 React 应用中使用 WebSockets,您需要一个专用服务器(例如带有 NodeJS 的 ExpressJS 应用),以便维持持久连接。

可惜的是,无服务器解决方案(例如 NextJS、AWS lambda)本身并不支持 WebSocket。真倒霉。😞

为什么不呢?因为无服务器服务会根据请求的到来而开启或关闭。而使用 WebSocket,我们需要这种只有专用服务器才能提供的“始终在线”连接(尽管你可以付费使用第三方服务作为解决方法)。

幸运的是,我们将讨论实现 WebSocket 的两种好方法:

  1. 使用 React、NodeJS 和 Socket.IO 自行实现和配置
  2. 通过使用Wasp(一个全栈 React-NodeJS 框架)为您配置并将 Socket.IO 集成到您的应用程序中。

这些方法允许您构建有趣的东西,例如我们在此处构建的即时更新“与朋友一起投票”应用程序(查看GitHub repo 了解详情):

开始之前

我们正在努力帮助您尽可能轻松地构建高性能 Web 应用程序 - 包括创建每周发布一次的此类内容!

如果您能通过在 GitHub 上关注我们的 repo 来支持我们,我们将非常感激:https://www.github.com/wasp-lang/wasp 🙏

仅供参考,Wasp = }是唯一开源、完全服务器化的全栈 React/Node 框架,具有内置编译器和 AI 辅助功能,可让您超快速地构建应用程序。

图片描述

就连 Ron 也会在 GitHub 上为 Wasp 加星标 🤩

为什么选择 WebSocket?

想象一下,你在一个聚会上向朋友发短信告诉他们带什么食物。

现在,如果你给朋友打电话,你们可以随时通话,而不是断断续续地发信息,那不是更方便吗?这几乎就是 WebSocket 在 Web 应用世界中的作用。

例如,传统的 HTTP 请求(例如 CRUD/RESTful)就像那些短信一样——您的应用程序必须在每次需要新信息时询问服务器,就像您每次想到聚会上的食物时都必须向您的朋友发送短信一样。

但是使用 WebSockets,一旦建立连接,它就会保持开放状态以进行持续的双向通信,因此服务器可以在新信息可用时立即向您的应用程序发送新信息,即使客户端没有要求。

这对于聊天应用、游戏服务器等实时应用或追踪股票价格的应用来说非常理想。例如,Google Docs、Slack、WhatsApp、Uber、Zoom 和 Robinhood 等应用都使用 WebSocket 来实现实时通信功能。

https://media3.giphy.com/media/26u4hHj87jMePiO3u/giphy.gif?cid=7941fdc6hxgjnub1rcs80udcj652956fwmm4qhxsmk6ldxg7&ep=v1_gifs_search&rid=giphy.gif&ct=g

所以请记住,当您的应用程序和服务器有很多事情要讨论时,请使用 WebSockets 并让对话自由进行!

WebSocket 的工作原理

如果您希望应用具备实时功能,则并非一定需要 WebSocket。您可以使用资源密集型进程来实现类似的功能,例如:

  1. 长轮询,例如setInterval定期运行以访问服务器并检查更新。
  2. 单向“服务器发送事件”,例如保持单向服务器到客户端连接开放以仅从服务器接收新更新。

另一方面,WebSockets 在客户端和服务器之间提供了双向(又称“全双工”)通信通道。

图片描述

如上图所示,一旦通过 HTTP“握手”建立连接,服务器和客户端就可以在任何一方最终关闭连接之前立即自由交换信息。

尽管引入 WebSockets 确实会由于异步和事件驱动组件而增加复杂性,但选择正确的库和框架可以使其变得简单。

在下面的部分中,我们将向您展示在 React-NodeJS 应用程序中实现 WebSockets 的两种方法:

  1. 与您自己的独立 Node/ExpressJS 服务器一起自行配置
  2. 让 Wasp 这个拥有超能力的全栈框架轻松帮你配置

在 React-NodeJS 应用中添加 WebSockets 支持

您不应该使用的内容:无服务器架构

但首先,需要提醒您的是:尽管无服务器解决方案对于某些用例来说是一个很好的解决方案,但它并不是完成这项工作的合适工具。

这意味着,流行的框架和基础设施(如 NextJS 和 AWS Lambda)不支持开箱即用的 WebSockets 集成。

此类解决方案并非在专用的传统服务器上运行,而是利用无服务器函数(也称为 lambda 函数),旨在在收到请求后立即执行并完成任务。就好像它们在请求进入时“打开”,然后在请求完成后“关闭”。

这种无服务器架构并不适合保持 WebSocket 连接处于活动状态,因为我们想要持久的“始终在线”连接。

这就是为什么如果你想构建实时应用,就需要“服务器化”架构。虽然有一些方法可以在无服务器架构上实现 WebSocket,例如使用第三方服务,但这也存在一些缺点:

  • 成本:这些服务以订阅形式存在,随着应用程序的扩展,成本可能会变得昂贵
  • 有限的定制:您使用的是预先构建的解决方案,因此您的控制力较弱
  • 调试:由于您的应用程序不在本地运行,修复错误变得更加困难

图片描述
💪

使用 ExpressJS 和 Socket.IO — 复杂/可定制的方法

好的,让我们从第一个更传统的方法开始:为您的客户端创建一个专用服务器以建立双向通信渠道。

👨‍💻提示:如果您想参与编程,可以按照以下说明操作。或者,如果您只想查看完成的 React-NodeJS 全栈应用,请查看此处的 GitHub 仓库。

在这个例子中,我们将使用ExpressJSSocket.IO库。虽然市面上还有其他库,但 Socket.IO 是一个非常棒的库,它能让你更轻松地在 NodeJS 中使用 WebSocket 。

如果您想要一起编写代码,请首先克隆start分支:

git clone --branch start https://github.com/vincanger/websockets-react.git
Enter fullscreen mode Exit fullscreen mode

您会注意到里面有两个文件夹:

  • 📁 ws-client用于我们的 React 应用
  • 📁 ws-server用于我们的 ExpressJS/NodeJS 服务器

让我们cd进入服务器文件夹并安装依赖项:

cd ws-server && npm install
Enter fullscreen mode Exit fullscreen mode

我们还需要安装用于使用 TypeScript 的类型:

npm i --save-dev @types/cors
Enter fullscreen mode Exit fullscreen mode

npm start现在使用终端中的命令运行服务器。

您应该看到listening on *:8000打印到控制台的内容!

目前,我们的index.ts文件如下所示:

import cors from 'cors';
import express from 'express';

const app = express();
app.use(cors({ origin: '*' }));
const server = require('http').createServer(app);

app.get('/', (req, res) => {
  res.send(`<h1>Hello World</h1>`);
});

server.listen(8000, () => {
  console.log('listening on *:8000');
});
Enter fullscreen mode Exit fullscreen mode

这里没有太多内容,所以让我们安装Socket.IO包并开始将 WebSockets 添加到我们的服务器!

首先,让我们终止服务器ctrl + c,然后运行:

npm install socket.io
Enter fullscreen mode Exit fullscreen mode

让我们继续index.ts用下面的代码替换文件。我知道代码量很大,所以我留下了一些注释来解释发生了什么 ;):

import cors from 'cors';
import express from 'express';
import { Server, Socket } from 'socket.io';

type PollState = {
  question: string;
  options: {
    id: number;
    text: string;
    description: string;
    votes: string[];
  }[];
};
interface ClientToServerEvents {
  vote: (optionId: number) => void;
  askForStateUpdate: () => void;
}
interface ServerToClientEvents {
  updateState: (state: PollState) => void;
}
interface InterServerEvents { }
interface SocketData {
  user: string;
}

const app = express();
app.use(cors({ origin: 'http://localhost:5173' })); // this is the default port that Vite runs your React app on
const server = require('http').createServer(app);
// passing these generic type parameters to the `Server` class
// ensures data flowing through the server are correctly typed.
const io = new Server<
  ClientToServerEvents,
  ServerToClientEvents,
  InterServerEvents,
  SocketData
>(server, {
  cors: {
    origin: 'http://localhost:5173',
    methods: ['GET', 'POST'],
  },
});

// this is middleware that Socket.IO uses on initiliazation to add
// the authenticated user to the socket instance. Note: we are not
// actually adding real auth as this is beyond the scope of the tutorial
io.use(addUserToSocketDataIfAuthenticated);

// the client will pass an auth "token" (in this simple case, just the username)
// to the server on initialize of the Socket.IO client in our React App
async function addUserToSocketDataIfAuthenticated(socket: Socket, next: (err?: Error) => void) {
  const user = socket.handshake.auth.token;
  if (user) {
    try {
      socket.data = { ...socket.data, user: user };
    } catch (err) {}
  }
  next();
}

// the server determines the PollState object, i.e. what users will vote on
// this will be sent to the client and displayed on the front-end
const poll: PollState = {
  question: "What are eating for lunch ✨ Let's order",
  options: [
    {
      id: 1,
      text: 'Party Pizza Place',
      description: 'Best pizza in town',
      votes: [],
    },
    {
      id: 2,
      text: 'Best Burger Joint',
      description: 'Best burger in town',
      votes: [],
    },
    {
      id: 3,
      text: 'Sus Sushi Place',
      description: 'Best sushi in town',
      votes: [],
    },
  ],
};

io.on('connection', (socket) => {
  console.log('a user connected', socket.data.user);

    // the client will send an 'askForStateUpdate' request on mount
    // to get the initial state of the poll
  socket.on('askForStateUpdate', () => {
    console.log('client asked For State Update');
    socket.emit('updateState', poll);
  });

  socket.on('vote', (optionId: number) => {
    // If user has already voted, remove their vote.
    poll.options.forEach((option) => {
      option.votes = option.votes.filter((user) => user !== socket.data.user);
    });
    // And then add their vote to the new option.
    const option = poll.options.find((o) => o.id === optionId);
    if (!option) {
      return;
    }
    option.votes.push(socket.data.user);
        // Send the updated PollState back to all clients
    io.emit('updateState', poll);
  });

  socket.on('disconnect', () => {
    console.log('user disconnected');
  });
});

server.listen(8000, () => {
  console.log('listening on *:8000');
});
Enter fullscreen mode Exit fullscreen mode

太好了,重新启动服务器npm start,让我们将Socket.IO客户端添加到前端。

cd进入ws-client目录并运行

cd ../ws-client && npm install
Enter fullscreen mode Exit fullscreen mode

接下来,启动开发服务器npm run dev,您应该在浏览器中看到硬编码的启动应用程序:

图片描述

你可能已经注意到,轮询与服务器端的轮询不匹配PollState。我们需要安装Socket.IO客户端并进行所有设置,才能启动实时通信并从服务器获取正确的轮询。

继续并终止开发服务器并ctrl + c运行:

npm install socket.io-client
Enter fullscreen mode Exit fullscreen mode

现在让我们创建一个钩子,用于在建立连接后初始化并返回我们的 WebSocket 客户端。为此,请在 中创建一个./ws-client/src名为 的新文件useSocket.ts

import { useState, useEffect } from 'react';
import socketIOClient, { Socket } from 'socket.io-client';

export type PollState = {
  question: string;
  options: {
    id: number;
    text: string;
    description: string;
    votes: string[];
  }[];
};
interface ServerToClientEvents {
  updateState: (state: PollState) => void;
}
interface ClientToServerEvents {
  vote: (optionId: number) => void;
  askForStateUpdate: () => void;
}

export function useSocket({endpoint, token } : { endpoint: string, token: string }) {
  // initialize the client using the server endpoint, e.g. localhost:8000
    // and set the auth "token" (in our case we're simply passing the username
    // for simplicity -- you would not do this in production!)
    // also make sure to use the Socket generic types in the reverse order of the server!
    const socket: Socket<ServerToClientEvents, ClientToServerEvents>  = socketIOClient(endpoint,  {
    auth: {
      token: token
    }
  }) 
  const [isConnected, setIsConnected] = useState(false);

  useEffect(() => {
    console.log('useSocket useEffect', endpoint, socket)

    function onConnect() {
      setIsConnected(true)
    }

    function onDisconnect() {
      setIsConnected(false)
    }

    socket.on('connect', onConnect)
    socket.on('disconnect', onDisconnect)

    return () => {
      socket.off('connect', onConnect)
      socket.off('disconnect', onDisconnect)
    }
  }, [token]);

    // we return the socket client instance and the connection state
  return {
    isConnected,
    socket,
  };
}
Enter fullscreen mode Exit fullscreen mode

现在让我们回到主页App.tsx并将其替换为以下代码(我再次留下了注释来解释):

import { useState, useMemo, useEffect } from 'react';
import { Layout } from './Layout';
import { Button, Card } from 'flowbite-react';
import { useSocket } from './useSocket';
import type { PollState } from './useSocket';

const App = () => {
    // set the PollState after receiving it from the server
  const [poll, setPoll] = useState<PollState | null>(null);

    // since we're not implementing Auth, let's fake it by
    // creating some random user names when the App mounts
  const randomUser = useMemo(() => {
    const randomName = Math.random().toString(36).substring(7);
    return `User-${randomName}`;
  }, []);

    // 🔌⚡️ get the connected socket client from our useSocket hook! 
  const { socket, isConnected } = useSocket({ endpoint: `http://localhost:8000`, token: randomUser });

  const totalVotes = useMemo(() => {
    return poll?.options.reduce((acc, option) => acc + option.votes.length, 0) ?? 0;
  }, [poll]);

    // every time we receive an 'updateState' event from the server
    // e.g. when a user makes a new vote, we set the React's state
    // with the results of the new PollState 
  socket.on('updateState', (newState: PollState) => {
    setPoll(newState);
  });

  useEffect(() => {
    socket.emit('askForStateUpdate');
  }, []);

  function handleVote(optionId: number) {
    socket.emit('vote', optionId);
  }

  return (
    <Layout user={randomUser}>
      <div className='w-full max-w-2xl mx-auto p-8'>
        <h1 className='text-2xl font-bold'>{poll?.question ?? 'Loading...'}</h1>
        <h2 className='text-lg italic'>{isConnected ? 'Connected ✅' : 'Disconnected 🛑'}</h2>
        {poll && <p className='leading-relaxed text-gray-500'>Cast your vote for one of the options.</p>}
        {poll && (
          <div className='mt-4 flex flex-col gap-4'>
            {poll.options.map((option) => (
              <Card key={option.id} className='relative transition-all duration-300 min-h-[130px]'>
                <div className='z-10'>
                  <div className='mb-2'>
                    <h2 className='text-xl font-semibold'>{option.text}</h2>
                    <p className='text-gray-700'>{option.description}</p>
                  </div>
                  <div className='absolute bottom-5 right-5'>
                    {randomUser && !option.votes.includes(randomUser) ? (
                      <Button onClick={() => handleVote(option.id)}>Vote</Button>
                    ) : (
                      <Button disabled>Voted</Button>
                    )}
                  </div>
                  {option.votes.length > 0 && (
                    <div className='mt-2 flex gap-2 flex-wrap max-w-[75%]'>
                      {option.votes.map((vote) => (
                        <div
                          key={vote}
                          className='py-1 px-3 bg-gray-100 rounded-lg flex items-center justify-center shadow text-sm'
                        >
                          <div className='w-2 h-2 bg-green-500 rounded-full mr-2'></div>
                          <div className='text-gray-700'>{vote}</div>
                        </div>
                      ))}
                    </div>
                  )}
                </div>
                <div className='absolute top-5 right-5 p-2 text-sm font-semibold bg-gray-100 rounded-lg z-10'>
                  {option.votes.length} / {totalVotes}
                </div>
                <div
                  className='absolute inset-0 bg-gradient-to-r from-yellow-400 to-orange-500 opacity-75 rounded-lg transition-all duration-300'
                  style={{
                    width: `${totalVotes > 0 ? (option.votes.length / totalVotes) * 100 : 0}%`,
                  }}
                ></div>
              </Card>
            ))}
          </div>
        )}
      </div>
    </Layout>
  );
};
export default App;
Enter fullscreen mode Exit fullscreen mode

现在继续使用 启动客户端npm run dev。打开另一个终端窗口/选项卡,cd进入ws-server目录并运行npm start

如果我们做得正确,我们应该就能看到我们完成的、可以运行的、实时的应用程序了!🙂

如果你在两三个浏览器标签页中打开它,它看起来和运行起来都很棒。来看看吧:

图片描述

好的!

所以我们在这里获得了核心功能,但由于这只是一个演示,因此缺少一些非常重要的部分,导致该应用程序无法在生产中使用。

主要是,每次应用启动时,我们都会创建一个随机的虚假用户。您可以刷新页面并再次投票来检查这一点。您会看到投票数自动累加,因为我们每次都会创建一个新的随机用户。我们可不想看到这种情况!

我们应该对数据库中注册的用户进行身份验证并持久化会话。但另一个问题是:这个应用根本没有数据库!

你可以开始看到,即使只是一个简单的投票功能,其复杂性也会不断增加

幸运的是,我们的下一个解决方案 Wasp 集成了身份验证和数据库管理。更不用说,它还帮我们处理了很多 WebSockets 的配置。

那么让我们继续尝试吧!

使用 Wasp 实现 WebSockets — 快速/零配置方法

由于 Wasp 是一个创新的全栈框架,它使得构建 React-NodeJS 应用程序变得快速且对开发人员友好。

Wasp 具有许多节省时间的功能,包括通过Socket.IO支持的 WebSocket 、身份验证、数据库管理和开箱即用的全栈类型安全。

Wasp 可以使用配置文件来帮您处理所有这些繁重的工作,您可以将其视为 Wasp 编译器用来帮助您将应用程序粘合在一起的一组指令。

为了实际操作,让我们按照以下步骤使用 Wasp 实现 WebSocket 通信

😎提示:如果您只想查看完成的应用程序代码,您可以在此处查看 GitHub 仓库

  1. 通过在终端中运行以下命令来全局安装 Wasp:
curl -sSL [https://get.wasp-lang.dev/installer.sh](https://get.wasp-lang.dev/installer.sh) | sh 
Enter fullscreen mode Exit fullscreen mode

如果您想一起编写代码,请首先克隆start示例应用程序的分支:

git clone --branch start https://github.com/vincanger/websockets-wasp.git
Enter fullscreen mode Exit fullscreen mode

您会注意到 Wasp 应用程序的结构是分裂的:

  • 🐝main.wasp根目录下有一个配置文件
  • 📁 src/client是我们的 React 文件的目录
  • 📁 src/server是我们的 ExpressJS/NodeJS 函数目录

让我们首先快速浏览一下我们的main.wasp文件。

app whereDoWeEat {
  wasp: {
    version: "^0.11.0"
  },
  title: "where-do-we-eat",
  client: {
    rootComponent: import { Layout } from "@client/Layout.jsx",
  },
    // 🔐 this is how we get auth in our app.
  auth: {
    userEntity: User,
    onAuthFailedRedirectTo: "/login",
    methods: {
      usernameAndPassword: {}
    }
  },
  dependencies: [
    ("flowbite", "1.6.6"),
    ("flowbite-react", "0.4.9")
  ]
}

// 👱 this is the data model for our registered users in our database
entity User {=psl
  id       Int     @id @default(autoincrement())
  username String  @unique
  password String
psl=}

// ...
Enter fullscreen mode Exit fullscreen mode

有了这个,Wasp 编译器就会知道该做什么,并会为我们配置这些功能。

让我们告诉它我们也需要 WebSocket。将webSocket定义添加到main.wasp文件中,就在auth和之间dependencies

app whereDoWeEat {
    // ... 
  webSocket: {
    fn: import { webSocketFn } from "@server/ws-server.js",
  },
    // ...
}
Enter fullscreen mode Exit fullscreen mode

现在我们必须定义webSocketFn。在./src/server目录中创建一个新文件,ws-server.ts并复制以下代码:

import { WebSocketDefinition } from '@wasp/webSocket';
import { User } from '@wasp/entities';

// define the types. this time we will get the entire User object
// in SocketData from the Auth that Wasp automatically sets up for us 🎉
type PollState = {
  question: string;
  options: {
    id: number;
    text: string;
    description: string;
    votes: string[];
  }[];
};
interface ServerToClientEvents {
  updateState: (state: PollState) => void;
}
interface ClientToServerEvents {
  vote: (optionId: number) => void;
  askForStateUpdate: () => void;
}
interface InterServerEvents {}
interface SocketData {
  user: User; 
}

// pass the generic types to the websocketDefinition just like 
// in the previous example
export const webSocketFn: WebSocketDefinition<
  ClientToServerEvents,
  ServerToClientEvents,
  InterServerEvents,
  SocketData
> = (io, _context) => {
  const poll: PollState = {
    question: "What are eating for lunch ✨ Let's order",
    options: [
      {
        id: 1,
        text: 'Party Pizza Place',
        description: 'Best pizza in town',
        votes: [],
      },
      {
        id: 2,
        text: 'Best Burger Joint',
        description: 'Best burger in town',
        votes: [],
      },
      {
        id: 3,
        text: 'Sus Sushi Place',
        description: 'Best sushi in town',
        votes: [],
      },
    ],
  };
  io.on('connection', (socket) => {
    if (!socket.data.user) {
      console.log('Socket connected without user');
      return;
    }

    console.log('Socket connected: ', socket.data.user?.username);
    socket.on('askForStateUpdate', () => {
      socket.emit('updateState', poll);
    });

    socket.on('vote', (optionId) => {
      // If user has already voted, remove their vote.
      poll.options.forEach((option) => {
        option.votes = option.votes.filter((username) => username !== socket.data.user.username);
      });
      // And then add their vote to the new option.
      const option = poll.options.find((o) => o.id === optionId);
      if (!option) {
        return;
      }
      option.votes.push(socket.data.user.username);
      io.emit('updateState', poll);
    });

    socket.on('disconnect', () => {
      console.log('Socket disconnected: ', socket.data.user?.username);
    });
  });
};
Enter fullscreen mode Exit fullscreen mode

您可能已经注意到,Wasp 实现中所需的配置和样板代码要少得多。这是因为:

  • 端点,
  • 验证,
  • 以及 Express 和Socket.IO中间件

都由 Wasp 为您处理。Noice!

图片描述

现在让我们继续运行该应用程序,看看我们现在得到了什么。

首先,我们需要初始化数据库,以确保 Auth 正常工作。由于复杂性较高,我们在上一个示例中没有这样做,但使用 Wasp 很容易做到:

wasp db migrate-dev
Enter fullscreen mode Exit fullscreen mode

完成后,运行应用程序(第一次运行时可能需要一段时间来安装所有依赖项):

wasp start
Enter fullscreen mode Exit fullscreen mode

这次你应该会看到一个登录界面。首先注册一个用户,然后登录:

图片描述

登录后,您将看到与上一个示例相同的硬编码轮询数据,因为我们还没有在前端设置Socket.IO客户端。但这次应该会容易得多。

为什么?嗯,除了配置更少之外,使用TypeScript 和 Wasp的另一个好处是,你只需要在服务器上定义具有匹配事件名称的有效负载类型,这些类型就会自动在客户端上显示!

现在让我们看看它是如何工作的。

在 中.src/client/MainPage.tsx,将内容替换为以下代码:

import { useState, useMemo, useEffect } from "react";
import { Button, Card } from "flowbite-react";
// Wasp provides us with pre-configured hooks and types based on
// our server code. No need to set it up ourselves!
import {
  useSocketListener,
  useSocket,
  ServerToClientPayload,
} from "@wasp/webSocket";
import useAuth from "@wasp/auth/useAuth";

const MainPage = () => {
    // we can easily access the logged in user with this hook
    // that wasp provides for us
  const { data: user } = useAuth();
  const [poll, setPoll] = useState<ServerToClientPayload<"updateState"> | null>(
    null
  );
  const totalVotes = useMemo(() => {
    return (
      poll?.options.reduce((acc, option) => acc + option.votes.length, 0) ?? 0
    );
  }, [poll]);

    // pre-built hooks, configured for us by Wasp
  const { socket } = useSocket(); 
  useSocketListener("updateState", (newState) => {
    setPoll(newState);
  });

  useEffect(() => {
    socket.emit("askForStateUpdate");
  }, []);

  function handleVote(optionId: number) {
    socket.emit("vote", optionId);
  }

  return (
    <div className="w-full max-w-2xl mx-auto p-8">
      <h1 className="text-2xl font-bold">{poll?.question ?? "Loading..."}</h1>
      {poll && (
        <p className="leading-relaxed text-gray-500">
          Cast your vote for one of the options.
        </p>
      )}
      {poll && (
        <div className="mt-4 flex flex-col gap-4">
          {poll.options.map((option) => (
            <Card key={option.id} className="relative transition-all duration-300 min-h-[130px]">
              <div className="z-10">
                <div className="mb-2">
                  <h2 className="text-xl font-semibold">{option.text}</h2>
                  <p className="text-gray-700">{option.description}</p>
                </div>
                <div className="absolute bottom-5 right-5">
                  {user && !option.votes.includes(user.username) ? (
                    <Button onClick={() => handleVote(option.id)}>Vote</Button>
                  ) : (
                    <Button disabled>Voted</Button>
                  )}
                  {!user}
                </div>
                {option.votes.length > 0 && (
                  <div className="mt-2 flex gap-2 flex-wrap max-w-[75%]">
                    {option.votes.map((vote) => (
                      <div
                        key={vote}
                        className="py-1 px-3 bg-gray-100 rounded-lg flex items-center justify-center shadow text-sm"
                      >
                        <div className="w-2 h-2 bg-green-500 rounded-full mr-2"></div>
                        <div className="text-gray-700">{vote}</div>
                      </div>
                    ))}
                  </div>
                )}
              </div>
              <div className="absolute top-5 right-5 p-2 text-sm font-semibold bg-gray-100 rounded-lg z-10">
                {option.votes.length} / {totalVotes}
              </div>
              <div
                className="absolute inset-0 bg-gradient-to-r from-yellow-400 to-orange-500 opacity-75 rounded-lg transition-all duration-300"
                style={{
                  width: `${
                    totalVotes > 0
                      ? (option.votes.length / totalVotes) * 100
                      : 0
                  }%`,
                }}
              ></div>
            </Card>
          ))}
        </div>
      )}
    </div>
  );
};
export default MainPage;
Enter fullscreen mode Exit fullscreen mode

与以前的实现相比,Wasp 使我们无需配置Socket.IO客户端,也无需构建我们自己的钩子。

另外,将鼠标悬停在客户端代码中的变量上,您会看到类型正在自动为您推断!

这只是一个例子,但它适用于所有的例子:

图片描述

现在,如果您打开一个新的私人/隐身标签页,注册一个新用户并登录,您将看到一个功能齐全的实时投票应用。最棒的是,与之前的方法相比,我们可以注销并重新登录,并且投票数据会保留,这正是我们对生产级应用的期望。🎩

图片描述

太棒了…😏

比较两种方法

当然,一种方法看起来更容易,并不意味着它总是更好。让我们快速回顾一下上述两种实现方式的优缺点。

没有黄蜂 与黄蜂
😎 目标用户 高级开发人员、Web 开发团队 全栈开发人员、“独立黑客”、初级开发人员
📈 代码复杂度 中到高 低的
🚤 速度 更慢,更有条理 更快、更集成
🧑‍💻 图书馆 任何 套接字输入输出
⛑ 类型安全 在服务器和客户端上实现 在服务器上实现一次,由客户端上的 Wasp 推断
🎮 控制量 高,由您决定实施 固执己见,因为 Wasp 决定了基本实现
🐛 学习曲线 复杂:全面了解前端和后端技术,包括 WebSockets 中级:需要了解全栈基础知识。

使用 React、Express.js(不使用 Wasp)实现 WebSockets

优点:

  1. 控制和灵活性:您可以按照最适合您项目需求的方式来实现 WebSockets,并且可以在多个不同的 WebSocket 库(而不仅仅是 Socket.IO)之间进行选择。

缺点:

  1. 更多代码和复杂性:如果没有 Wasp 等框架提供的抽象,您可能需要编写更多代码并创建自己的抽象来处理常见任务。更不用说 NodeJS/ExpressJS 服务器的正确配置(示例中提供的服务器非常基础)
  2. 手动类型安全:如果您使用 TypeScript,则必须更加小心地输入进出服务器的事件处理程序和有效负载类型,或者自己实现更类型安全的方法。

使用 Wasp 实现 WebSockets(底层使用 React、ExpressJS 和Socket.IO )

优点:

  1. 完全集成* /更少的代码*:Wasp 提供了有用的抽象,例如用于 React 组件的useSocket钩子useSocketListener(基于其他功能,如 Auth、异步作业、电子邮件发送、数据库管理和部署),简化了客户端代码,并允许以更少的配置实现完全集成。
  2. 类型安全:Wasp 为 WebSocket 事件和负载提供全栈类型安全保障。这降低了由于数据类型不匹配导致运行时错误的可能性,并让您免于编写更多样板代码。

缺点:

  1. 学习曲线:不熟悉 Wasp 的开发人员需要学习该框架才能有效地使用它。
  2. 控制较少:虽然 Wasp 提供了很多便利,但它抽象了一些细节,使得开发人员对套接字管理的某些方面的控制力稍差。
    助我助你🌟 如果您还没有在 GitHub 上为我们点赞,尤其是在您觉得这篇文章有用的情况下!如果您点赞了,将有助于我们创作更多类似的内容。如果您没有点赞……我想,我们会处理。

https://media.giphy.com/media/3oEjHEmvj6yScz914s/giphy.gif

⭐️感谢您的支持🙏


结论

一般来说,如何将 WebSockets 添加到 React 应用程序取决于项目的具体情况、您对可用工具的熟悉程度以及您愿意在易用性、控制和复杂性之间做出的权衡。

别忘了,如果您想查看我们的“午餐投票”示例全栈应用程序的完整完成代码,请访问:https://github.com/vincanger/websockets-wasp

如果你知道在你的应用中实现 WebSockets 的更好、更酷、更时尚的方法,请在下面的评论中告诉我们

图片描述

文章来源:https://dev.to/wasp/build-a-real-time-voting-app-with-websockets-react-typescript-1bm9
PREV
构建您自己的 AI Meme 生成器并学习如何使用 OpenAI 的函数调用☎️ TL;DR 简介第 1 部分待续……
NEXT
Web 应用程序中的身份验证和访问控制指南🔐