DevGang
Авторизоваться

Создайте приложение для голосования в реальном времени с помощью WebSockets, React и TypeScript 

WebSockets позволяет вашему приложению иметь функции «в реальном времени», когда обновления происходят мгновенно, поскольку они передаются по открытому двустороннему каналу.

Это отличается от приложений CRUD, которые обычно используют HTTP-запросы, которые должны установить соединение, отправить запрос, получить ответ, а затем закрыть соединение.

Чтобы использовать WebSockets в вашем приложении React, вам понадобится выделенный сервер, например, приложение ExpressJS с NodeJS, чтобы поддерживать постоянное соединение.

К сожалению, бессерверные решения (например, NextJS, AWS lambda) изначально не поддерживают WebSockets.

Почему нет? Что ж, бессерверные службы включаются и выключаются в зависимости от того, поступает ли запрос. С WebSockets нам нужно «всегда активное» соединение, которое может обеспечить только выделенный сервер (хотя вы можете платить за сторонние услуги в качестве обходного пути).

К счастью, мы поговорим о двух отличных способах реализации WebSockets:

  1. Самостоятельная реализация и настройка с помощью React, NodeJS и Socket.IO
  2. Используя Wasp, полнофункциональную платформу React-NodeJS, для настройки и интеграции Socket.IO в ваше приложение.

Эти методы позволяют вам создавать забавные вещи, такие как это мгновенно обновляемое приложение «голосование с друзьями», которое мы создали здесь (ознакомьтесь с репозиторием GitHub):

Прежде чем мы начнем

К вашему сведению, Wasp — это единственная полностью серверная полностековая среда React/Node с открытым исходным кодом со встроенным компилятором и функциями с поддержкой искусственного интеллекта, которая позволяет очень быстро создавать приложения.

Почему веб-сокеты?

Итак, представьте, что вы на вечеринке отправляете текстовые сообщения другу, чтобы сказать ему, какую еду принести.

Теперь, не было бы проще, если бы вы позвонили своему другу по телефону, чтобы вы могли говорить постоянно, вместо того, чтобы отправлять спорадические сообщения? Это в значительной степени то, что WebSockets в мире веб-приложений.

Например, традиционные HTTP-запросы (например, CRUD/RESTful) подобны этим текстовым сообщениям — ваше приложение должно спрашивать сервер каждый раз, когда ему нужна новая информация, точно так же, как вам приходилось отправлять текстовое сообщение своему другу каждый раз, когда вы думали о еде. для вашей вечеринки.

Но с WebSockets, как только соединение установлено, оно остается открытым для постоянной двусторонней связи, поэтому сервер может отправлять новую информацию в ваше приложение в тот момент, когда она становится доступной, даже если клиент не запрашивал ее.

Это идеально подходит для приложений реального времени, таких как приложения для чата, игровые серверы или когда вы отслеживаете цены на акции. Например, такие приложения, как Google Docs, Slack, WhatsApp, Uber, Zoom и Robinhood, используют WebSockets для обеспечения своих функций связи в реальном времени.

Так что помните, когда вашему приложению и серверу есть о чем поговорить, используйте WebSockets и дайте беседе течь свободно!

Как работают веб-сокеты

Если вам нужны возможности реального времени в вашем приложении, вам не всегда нужны WebSockets. Вы можете реализовать аналогичную функциональность, используя ресурсоемкие процессы, такие как:

  1. Долгий опрос, т.е. запуск setInterval для периодического обращения к серверу и проверки обновлений.
  2. Односторонние «события, отправленные сервером», например. поддержание однонаправленного соединения сервер-клиент открытым для получения новых обновлений только с сервера.

Веб-сокеты, с другой стороны, обеспечивают двусторонний (также известный как «полнодуплексный») канал связи между клиентом и сервером.

Как показано на изображении выше, как только соединение установлено через HTTP-«рукопожатие», сервер и клиент могут свободно обмениваться информацией мгновенно, прежде чем соединение будет окончательно закрыто любой из сторон.

Хотя внедрение WebSockets действительно усложняет работу из-за асинхронных и управляемых событиями компонентов, выбор правильных библиотек и фреймворков может упростить задачу.

В следующих разделах мы покажем вам два способа реализации WebSockets в приложении React-NodeJS:

  1. Самостоятельная настройка вместе с собственным автономным сервером Node/ExpressJS
  2. Позвольте Wasp, полнофункциональному фреймворку с суперспособностями, легко настроить его для вас

Добавление поддержки WebSockets в приложение React-NodeJS

Что вам не следует использовать: бессерверная архитектура

Но сначала, вот вам предупреждение: несмотря на то, что бессерверные решения являются отличным решением для определенных случаев использования, они не являются подходящим инструментом для этой работы.

Это означает, что популярные фреймворки и инфраструктура, такие как NextJS и AWS Lambda, не поддерживают интеграцию WebSockets «из коробки».

Вместо того, чтобы работать на выделенном традиционном сервере, такие решения используют бессерверные функции (также известные как лямбда-функции), которые предназначены для выполнения и завершения задачи, как только поступает запрос. Они как бы «включаются», когда приходит запрос, а затем «выключается» после его завершения.

Эта бессерверная архитектура не идеальна для поддержания соединения WebSocket, потому что нам нужно постоянное, «всегда активное» соединение.

Вот почему вам нужна «серверная» архитектура, если вы хотите создавать приложения реального времени. И хотя есть обходной путь для получения WebSockets в бессерверной архитектуре, например, использование сторонних сервисов, у него есть ряд недостатков:

  • Стоимость: эти услуги предоставляются по подписке и могут стать дорогостоящими по мере масштабирования вашего приложения
  • Ограниченная настройка: вы используете готовое решение, поэтому у вас меньше контроля
  • Отладка: исправить ошибки становится сложнее, так как ваше приложение не работает локально

Использование ExpressJS с Socket.IO — сложный/настраиваемый метод

Хорошо, давайте начнем с первого, более традиционного подхода: создание выделенного сервера для вашего клиента, чтобы установить с ним двусторонний канал связи.

В этом примере мы будем использовать ExpressJS с библиотекой Socket.IO. Хотя есть и другие, Socket.IO — отличная библиотека, которая упрощает работу с WebSockets в NodeJS.

Если вы хотите кодировать, сначала клонируйте start ветку:

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

Вы заметите, что внутри у нас есть две папки:

  1. 📁 ws-client для нашего приложения React
  2. 📁 ws-server для нашего сервера ExpressJS/NodeJS

Переходим (cd) в папку сервера и устанавливаем зависимости:

cd ws-server && npm install

Также нам необходимо установить типы для работы с typescript:

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 и начнем добавлять WebSockets на наш сервер!

Сначала убьем сервер 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 для нас.

Итак, давайте продолжим и попробуем!

Реализация WebSockets с помощью Wasp — метод Fast/Zero Config

Поскольку Wasp — это инновационный полнофункциональный фреймворк, он делает создание приложений React-NodeJS быстрым и удобным для разработчиков.

Wasp имеет множество функций, позволяющих сэкономить время, включая поддержку WebSocket через Socket.IO, аутентификацию, управление базами данных и стандартную безопасность типов с полным стеком.

Wasp может взять на себя всю эту тяжелую работу за вас благодаря использованию конфигурационного файла, который вы можете представить себе как набор инструкций, которые компилятор Wasp использует, чтобы склеить ваше приложение воедино.

Чтобы увидеть это в действии, давайте реализуем связь WebSocket с помощью Wasp, выполнив следующие действия.

  • Установите 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 разделена:

  1. 🐝 файл конфигурации main.wasp существует в корне
  2. 📁 src/client — это наш каталог для наших файлов React
  3. 📁 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. Добавьте определение webSocket в файл main.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.

Давайте продолжим и запустим приложение, чтобы посмотреть, что у нас есть на данный момент.

Во-первых, нам нужно инициализировать базу данных, чтобы наша Auth работала правильно. Это то, чего мы не делали в предыдущем примере из-за высокой сложности, но это легко сделать с помощью 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, а также создавать собственные хуки.

Кроме того, наведите указатель мыши на переменные в клиентском коде, и вы увидите, что типы автоматически выводятся за вас!

Вот только один пример, но он должен работать для всех:

Теперь, если вы откроете новую вкладку приват/инкогнито, зарегистрируете нового пользователя и войдете в систему, вы увидите полностью работающее приложение для голосования в реальном времени. Самое приятное то, что по сравнению с предыдущим подходом мы можем выйти из системы и снова войти в нее, и наши данные о голосовании сохраняются, чего мы и ожидаем от приложения производственного уровня.

Сравнение двух подходов

Теперь, только потому, что один подход кажется проще, не всегда означает, что он всегда лучше. Давайте кратко рассмотрим преимущества и недостатки обеих реализаций выше.

Без Wasp С Wasp
😎 Предполагаемый пользователь Старшие разработчики, команды веб-разработчиков Разработчики полного стека, индихакеры, младшие разработчики
📈 Сложность кода От среднего до высокого Низкий
🚤 Скорость Медленнее, методичнее Быстрее, более интегрировано
🧑‍💻 Библиотеки Любые Socket.IO&nbsp;&nbsp;
⛑ Безопасность типов Реализовать как на сервере, так и на клиенте Реализовать один раз на сервере, предполагаемый Wasp на клиенте
🎮 Объем контроля Высокий, как вы определяете реализацию Опционально, поскольку Wasp решает базовую реализацию
🐛Кривая обучения Сложная: полное знание фронтенд и бэкэнд технологий, включая WebSockets Средний уровень: необходимо знание основ работы с полным стеком.<br><br>

Реализация WebSockets с помощью React, Express.js (без Wasp)

Преимущества:

  1. Контроль и гибкость: вы можете подойти к реализации WebSockets таким образом, который лучше всего соответствует потребностям вашего проекта, а также выбирать между несколькими различными библиотеками WebSocket, а не только Socket.IO.

Недостатки:

  1. Больше кода и сложности: без абстракций, предоставляемых такими фреймворками, как Wasp, вам может потребоваться написать больше кода и создать собственные абстракции для решения общих задач. Не говоря уже о правильной настройке сервера NodeJS/ExpressJS (приведенный в примере очень простой)
  2. Ручная безопасность типов: если вы работаете с TypeScript, вам нужно быть более осторожным при вводе обработчиков событий и типов полезной нагрузки, поступающих и исходящих с сервера, или самостоятельно реализовать более безопасный подход к типам.

Реализация WebSockets с помощью Wasp (под капотом используются React, ExpressJS и Socket.IO)

Преимущества:

  1. Полностью интегрированный/Меньше кода: Wasp предоставляет полезные абстракции, такие как обработчики useSocket и useSocketListener, для использования в компонентах React (помимо других функций, таких как аутентификация, асинхронные задания, отправка электронной почты, управление БД и развертывание), упрощая клиент. стороннему коду и обеспечивает полную интеграцию с меньшей настройкой.
  2. Безопасность типов: Wasp обеспечивает полную безопасность типов для событий и полезной нагрузки WebSocket. Это снижает вероятность ошибок во время выполнения из-за несоответствия типов данных и избавляет вас от написания еще большего количества шаблонов.

Недостатки:

  1. Кривая обучения: разработчикам, незнакомым с Wasp, необходимо изучить фреймворк, чтобы эффективно его использовать.
  2. Меньше контроля: несмотря на то, что Wasp предоставляет множество удобств, он абстрагирует некоторые детали, предоставляя разработчикам чуть меньше контроля над определенными аспектами управления сокетами.

Заключение

В общем, то, как вы добавляете WebSockets в свое приложение React, зависит от специфики вашего проекта, вашего уровня комфорта с доступными инструментами и компромиссов, на которые вы готовы пойти между простотой использования, контролем и сложностью.

Не забудьте, если вы хотите проверить полный готовый код из нашего примера полнофункционального приложения «Lunch Voting», перейдите сюда: https://github.com/vincanger/websockets-wasp

И если вы знаете лучший, более крутой и изящный способ внедрения WebSockets в свои приложения, сообщите нам об этом в комментариях ниже.

Источник:

#React #TypeScript
Комментарии 1
Евгений Смирнов 12.08.2023 в 12:51

А теперь давайте предположим, что давно есть готовое решение вашей проблемы, которое куда удобнее, быстрее, оптимизированнее и используется крупными компаниями. Такая технология называется graphql. Делаем сервер на express также, подключаем MariaDB какую-нибудь и с помощью аполло на фронте получаем данные в реальном времени... Быстро? Да, меньше затрат, всё чётко и круто. Потому что мы не стали придумывать велосипед, а использовали стандарт. graphQl Subscription

Чтобы оставить комментарий, необходимо авторизоваться