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

举报
千锋教育 发表于 2023/08/10 13:43:54 2023/08/10
【摘要】 长话短说WebSocket 允许您的应用程序具有“实时”功能,其中更新是即时的,因为它们是在开放的双向通道上传递的。这与 CRUD 应用程序不同,CRUD 应用程序通常使用 HTTP 请求,必须建立连接、发送请求、接收响应,然后关闭连接。要在 React 应用程序中使用 WebSockets,您需要一个专用服务器,例如带有 NodeJS 的 ExpressJS 应用程序,以维持持久连接。不幸...

长话短说

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

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

即时的

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

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

为什么不?嗯,无服务器服务的开启和关闭取决于请求是否传入。使用 WebSockets,我们需要这种只有专用服务器才能提供的“始终开启”连接(尽管您可以支付第三方服务作为解决方法)。

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

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

这些方法允许您构建有趣的东西,例如立即更新我们在这里构建的“与朋友投票”应用程序(查看GitHub 存储库):


在我们开始之前

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

如果您能在 GitHub 上为我们的存储库加注星标以支持我们,我们将不胜感激:https://www.github.com/wasp-lang/wasp 🙏

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

甚至 Ron 也会在 GitHub 上为 Wasp 加注星标 🤩

为什么选择 WebSocket?

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

现在,如果您打电话给您的朋友,这样你们就可以不断地交谈,而不是偶尔发送消息,不是更容易吗?这几乎就是 Web 应用程序世界中的 WebSocket。

例如,传统的 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“握手”建立连接,服务器和客户端就可以在连接最终被任何一方关闭之前立即自由地交换信息。

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

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

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

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

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

但首先,请注意:尽管无服务器解决方案对于某些用例来说是一个很好的解决方案,但它并不是完成这项工作的正确工具。

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

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

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

这就是为什么如果您想构建实时应用程序,您需要一个“服务器化”架构。尽管有一种解决方法可以在无服务器架构上获取 WebSocket,例如使用第三方服务,但这有许多缺点:

  • 成本:这些服务以订阅形式存在,并且随着应用程序的扩展而变得昂贵
  • 有限的定制:您使用的是预构建的解决方案,因此您的控制权较少
  • 调试:修复错误变得更加困难,因为您的应用程序没有在本地运行

图片描述
💪

将 ExpressJS 与 Socket.IO 结合使用 — 复杂/可定制的方法

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

👨‍💻提示:如果您想一起编码,可以按照以下说明进行操作。或者,如果您只想查看完成的 React-NodeJS 全栈应用程序,请查看此处的 github 存储库

在此示例中,我们将使用ExpressJSSocket.IO库。尽管还有其他库,Socket.IO 是一个很棒的库,它使得在 NodeJS 中使用 WebSockets 变得更加容易

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

git clone --branch start https://github.com/vincanger/websockets-react.git

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

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

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

cd ws-server && npm install

我们还需要安装使用打字稿的类型:

npm i --save-dev @types/cors

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

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

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

npm install socket.io

让我们继续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');
});

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

cd进入ws-client目录并运行

cd ../ws-client && npm install

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

图片描述

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

继续并ctrl + c运行以下命令来终止开发服务器:

npm install socket.io-client

现在让我们创建一个钩子,在建立连接后初始化并返回 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,
  };
}

现在让我们回到主页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;

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

如果我们做得正确,我们应该会看到我们完成的、工作的、实时的应用程序!🙂

如果您在两个或三个浏览器选项卡中打开它,它看起来和工作起来都很棒。一探究竟:


动图图片描述


好的!

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

主要是,每次安装应用程序时,我们都会创建一个随机的假用户。您可以通过刷新页面并再次投票来检查这一点。您会看到投票不断增加,因为我们每次都会创建一个新的随机用户。我们不想要那样!

我们应该为在我们的数据库中注册的用户验证并保留会话。但另一个问题:我们在这个应用程序中根本没有数据库!

您可以开始看到即使只是一个简单的投票功能,复杂性也是如何增加的

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

那么让我们继续尝试吧!

使用 Wasp 实现 WebSocket - 快速/零配置方法

由于 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 

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

git clone --branch start https://github.com/vincanger/websockets-wasp.git

您会注意到 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=}

// ...

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

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

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

现在我们必须定义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);
    });
  });
};

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

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

一切都由 Wasp 为您处理。通知!

图片描述

现在让我们继续运行应用程序来看看我们现在有什么。

首先,我们需要初始化数据库,以便我们的身份验证正常工作。由于复杂性很高,我们在前面的示例中没有这样做,但使用 Wasp 很容易做到:

wasp db migrate-dev

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

wasp start

这次您应该会看到登录屏幕。首先注册一个用户,然后登录:

图片描述

登录后,您将看到与上一个示例相同的硬编码轮询数据,因为我们还没有在前端设置 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;

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

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

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

图片描述

现在,如果您打开一个新的私人/隐身选项卡,注册一个新用户并登录,您将看到一个完全运行的实时投票应用程序。最好的部分是,与以前的方法相比,我们可以注销并重新登录,并且我们的投票数据仍然存在,这正是我们对生产级应用程序的期望。🎩


图片描述


太棒了…😏

比较两种方法

现在,仅仅因为一种方法看起来更容易,并不总是意味着它总是更好。让我们快速总结一下上述两种实现的优点和缺点。

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

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

优点:

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

缺点:

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

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

优点:

  1. 完全集成* /更少的代码*:Wasp 提供了有用的抽象,例如useSocket用于useSocketListenerReact 组件的钩子(除了其他功能,如身份验证、异步作业、电子邮件发送、数据库管理和部署),简化了客户端代码,并允许以更少的配置进行完全集成。
  2. 类型安全:Wasp 促进 WebSocket 事件和有效负载的全栈类型安全。这降低了由于数据类型不匹配而导致运行时错误的可能性,并且使您无需编写更多样板文件。

缺点:

  1. 学习曲线:不熟悉 Wasp 的开发人员需要学习该框架才能有效地使用它。
  2. 控制较少:虽然 Wasp 提供了很多便利,但它抽象了一些细节,使开发人员对套接字管理的某些方面的控制稍少。
    帮助我帮助你🌟 如果您还没有,请在 GitHub 上给我们加注星标,特别是如果您发现这很有用的话!如果您这样做,它将有助于支持我们创建更多此类内容。如果你不……好吧,我想我们会处理它。


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


⭐️感谢您的支持🙏


结论

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

不要忘记,如果您想查看我们的“午餐投票”示例全栈应用程序的完整完成代码,如果您知道在应用程序中实现 WebSocket 的更好、更酷、更时尚的方法,请在下面的评论中告诉我们

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。