发布于 2026-01-05 0 阅读
0

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

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

太长不看

WebSocket 允许你的应用程序具有“实时”功能,更新是即时的,因为它们是通过开放的双向通道传递的。

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

即时的

要在你的 React 应用中使用 WebSocket,你需要一个专用服务器,例如使用 NodeJS 的 ExpressJS 应用,以便保持持久连接。

可惜的是,无服务器解决方案(例如 NextJS、AWS Lambda)本身并不原生支持 WebSocket。真遗憾。😞

为什么不行呢?因为无服务器服务会根据是否有请求传入而开启或关闭。而使用 WebSocket,我们需要这种“始终在线”的连接,这只有专用服务器才能提供。

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

  1. 进阶:使用 React、NodeJS 和 Socket.IO 自行实现和配置
  2. 简单:使用Wasp(一个全栈 React-NodeJS 框架)来配置 Socket.IO 并将其集成到您的应用程序中。

这些方法可以让你构建一些有趣的东西,比如我们在这里构建的这款可以即时更新的“与朋友一起投票”应用程序:

您可以在这里试用在线演示应用。
如果您只想获取应用代码,可以在 GitHub 上找到。

开始之前

我们正在努力帮助您尽可能轻松地构建高性能的 Web 应用程序——包括创建像这样的内容,我们每周都会发布!

如果您能为我们的 GitHub 代码库点个星标,我们将不胜感激:https://www.github.com/wasp-lang/wasp 🙏

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

图片描述

连 Ron 都会在 GitHub 上给 Wasp 点赞🤩

为什么选择 WebSocket?

想象一下,你正在参加一个聚会,给朋友发短信告诉他们该带什么食物。

现在,如果能打电话给朋友,持续通话,而不是断断续续地发信息,岂不是更方便?WebSocket 在 Web 应用领域的作用就大致如此。

例如,传统的 HTTP 请求(例如 CRUD/RESTful)就像短信一样——你的应用程序每次想要获取新信息时都必须向服务器发出请求,就像你每次想到聚会要吃什么时都必须给朋友发短信一样。

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

这非常适合实时应用,例如聊天应用、游戏服务器,或者用于跟踪股票价格。例如,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

所以记住,当你的应用和服务器有很多东西要交流时,请使用 WebSocket,让对话自由进行!

WebSocket 的工作原理

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

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

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

图片描述

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

虽然引入 WebSocket 会因为异步和事件驱动组件而增加复杂性,但选择合适的库和框架可以使其变得容易。

以下各节将向您展示在 React-NodeJS 应用中实现 WebSocket 的两种方法:

  1. 您可以自行配置它,并将其与您自己的独立 Node/ExpressJS 服务器一起使用。
  2. Wasp 是一款功能强大的全栈框架,它可以轻松地为您进行配置。

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

你不应该使用:无服务器架构

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

这意味着,像 NextJS 和 AWS Lambda 这样的流行框架和基础设施,并不提供开箱即用的 WebSocket 集成支持。

这类解决方案并非运行在传统的专用服务器上,而是利用无服务器函数(也称为 Lambda 函数),这些函数旨在收到请求后立即执行并完成任务。它们就像在收到请求时“启动”,并在任务完成后“关闭”一样。

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

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

  • 成本:这些服务以订阅形式提供,随着应用规模的扩大,成本可能会增加。
  • 自定义程度有限:您使用的是预构建的解决方案,因此控制权较少。
  • 调试:由于应用程序未在本地运行,修复错误变得更加困难。

图片描述
💪

使用 ExpressJS 和 Socket.IO — 复杂/可自定义方法

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

这种方法更高级,也更复杂一些,但允许更精细的自定义。如果您正在寻找一种更直接、更简单的方法将 WebSocket 集成到您的 React/NodeJS 应用中,我们将在下一节中介绍。

👨‍💻提示:如果您想跟着代码一起编写,可以参考下面的说明。或者,如果您只想查看这个完整的 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包,然后开始向我们的服务器添加 WebSocket 功能吧!

首先,我们用以下命令终止服务器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名为 ` webSocket.js` 的新文件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 集成了身份验证和数据库管理功能。更重要的是,它还帮我们处理了很多 WebSocket 配置工作。

那我们就来试试吧!

使用 Wasp 实现 WebSocket——更简单/配置更少的方法

因为 Wasp 是一个创新的全栈框架,所以它能够快速构建 React-NodeJS 应用,并且对开发者非常友好。

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

Wasp之所以能帮你处理这些繁重的工作,是因为它使用配置文件。你可以把配置文件理解为一组指令,Wasp编译器用它们来帮助你把应用程序粘合在一起。最终,Wasp会帮你编写大量的样板代码,从而节省你大量的时间和精力。

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

😎小贴士:如果您想查看完整的应用程序代码,可以点击此处访问 GitHub 代码库。

  1. 在终端中运行以下命令,即可全局安装 Wasp:
curl -sSL 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.13.2"
  },
  title: "where-do-we-eat",
  client: {
    rootComponent: import { Layout } from "@src/client/Layout",
  },
  // 🔐 This is how we get Auth in our app. Easy!
  auth: {
    userEntity: User,
    onAuthFailedRedirectTo: "/login",
    methods: {
      usernameAndPassword: {}
    }
  },
}

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

// ...
Enter fullscreen mode Exit fullscreen mode

这样,Wasp 编译器就知道该怎么做,并会为我们配置这些功能。

我们还要告诉它我们需要 WebSocket。将webSocket定义添加到main.wasp文件中,就在 `<script>`auth和 `</script> ` 之间dependencies

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

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

import { getUsername } from 'wasp/auth';
import { type WebSocketDefinition } from 'wasp/server/webSocket';

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 {}

export const webSocketFn: WebSocketDefinition<ClientToServerEvents, ServerToClientEvents, InterServerEvents> = (
  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;
    }

    const connectionUsername = getUsername(socket.data.user);

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

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

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

您可能已经注意到,与传统的 React/NodeJS 方法相比,Wasp 实现所需的配置和样板代码要少得多。这是因为:

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

这一切都由 Wasp 为您处理。太棒了!

图片描述

现在我们运行一下应用程序,看看目前的情况如何。

首先,我们需要初始化数据库,以便身份验证功能正常工作。由于之前的示例中过于复杂,我们没有这样做,但在 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,将内容替换为以下代码:

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

const MainPage = () => {
  // Wasp provides a bunch of pre-built hooks 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]);

  const { socket } = useSocket();

  const username = user ? getUsername(user) : null;

  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'>
                  {username && !option.votes.includes(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((username, idx) => {
                      return (
                        <div
                          key={username}
                          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'>{username}</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 开发团队 全栈开发人员、“独立黑客”、初级开发人员
📈 代码复杂度 中高 低的
🚤 速度 更慢,更有条理 速度更快,集成度更高
🧑‍💻 图书馆 任何 Socket.IO
⛑ 型式安全 在服务器端和客户端都实现 服务器端只需实现一次,客户端的 Wasp 即可推断。
🎮 控制程度 高,因为你决定了实施方案 Wasp 决定了基本实现方式,因此它很有主见。
🐛 学习曲线 精通:全面掌握前端和后端技术,包括 WebSocket。 中级:需要掌握全栈开发基础知识。

使用 React 和 Express.js 实现 WebSocket(无需 Wasp)

优势:

  1. 控制与灵活性:您可以根据项目需求选择最适合的方式来实现 WebSocket,并且可以从多个不同的 WebSocket 库中进行选择,而不仅仅是 Socket.IO。

缺点:

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

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

优势:

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

缺点:

  1. 学习曲线:不熟悉 Wasp 的开发人员需要学习该框架才能有效地使用它。
  2. 控制较少:虽然 Wasp 提供了许多便利,但它抽象化了一些细节,使得开发人员对套接字管理的某些方面的控制略有减少。
    帮帮我,帮帮你🌟 如果你还没在 GitHub 上给我们点赞,请点个星,特别是如果你觉得这篇文章有用的话!你的点赞能帮助我们创作更多类似的内容。如果你没点赞……嗯,我们也会想办法的。

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

⭐️感谢您的支持🙏


结论

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

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

如果你用 WebSocket 开发出了什么很棒的东西,欢迎在下方评论区与我们分享。

图片描述

文章来源:https://dev.to/wasp/build-a-real-time-voting-app-with-websockets-react-typescript-3oof