長話短說

WebSocket 允許您的應用程式具有「即時」功能,其中更新是即時的,因為它們是在開放的雙向通道上傳遞的。

這與 CRUD 應用程式不同,CRUD 應用程式通常使用 HTTP 請求,必須建立連線、傳送請求、接收回應,然後關閉連線。

即時的

要在 React 應用程式中使用 WebSockets,您需要一個專用伺服器,例如帶有 NodeJS 的 ExpressJS 應用程式,以維持持久連接。

不幸的是,無伺服器解決方案(例如 NextJS、AWS lambda)本身並不支援 WebSocket。真糟糕。 😞

為什麼不?嗯,無伺服器服務的開啟和關閉取決於請求是否傳入。

幸運的是,我們將討論兩種實作 WebSocket 的好方法:

  1. 進階:使用 React、NodeJS 和 Socket.IO 自行實作和配置

  2. 簡單:透過使用Wasp這個全端 React-NodeJS 框架,為您配置 Socket.IO 並將其整合到您的應用程式中。

這些方法允許您建立有趣的東西,例如我們在這裡建立的立即更新的「與朋友投票」應用程式:

{% 嵌入 https://www.youtube.com/watch?v=Twy-2P0Co6M %}

您可以在此處嘗試即時演示應用程式

如果您只想要應用程式程式碼,可以在 GitHub 上找到

在我們開始之前

我們正在努力幫助您盡可能輕鬆地建立高效能的網路應用程式 - 包括建立這樣的內容,每週發布一次!

如果您能在 GitHub 上為我們的儲存庫加註星標以支持我們,我們將不勝感激:https://www.github.com/wasp-lang/wasp 🙏

僅供參考, Wasp = }是唯一一個開源、完全伺服器化的全端 React/Node 框架,具有內建編譯器和 AI 輔助功能,可讓您超快速地建立應用程式。

圖片描述

{% cta https://www.github.com/wasp-lang/wasp %} 連 Ron 也會在 GitHub 上為 Wasp 加註星標 🤩 {% endcta %}

為什麼選擇 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_

因此請記住,當您的應用程式和伺服器有很多主題要討論時,請使用 WebSockets,讓對話自由進行!

WebSocket 的工作原理

如果您希望應用程式具有即時功能,則並非總是需要 WebSocket。您可以透過使用資源密集型進程來實現類似的功能,例如:

  1. 長輪詢,例如執行setInterval定期存取伺服器並檢查更新。

  2. 單向“伺服器發送事件”,例如保持單向伺服器到客戶端連接開啟以僅接收來自伺服器的新更新。

另一方面,WebSockets 在用戶端和伺服器之間提供雙向(也稱為「全雙工」)通訊通道。

圖片描述

如上圖所示,一旦透過 HTTP「握手」建立連接,伺服器和客戶端就可以在連接最終被任何一方關閉之前立即自由地交換資訊。

儘管引入 WebSocket 確實會因為非同步和事件驅動的元件而增加複雜性,但選擇正確的程式庫和框架可以使事情變得簡單。

在下面的部分中,我們將向您展示在 React-NodeJS 應用程式中實作 WebSocket 的兩種方法:

  1. 與您自己的獨立 Node/ExpressJS 伺服器一起自行配置

  2. 讓Wasp這個擁有超強能力的全端框架為您輕鬆配置

在 React-NodeJS 應用程式中新增 WebSockets 支持

你不應該使用什麼:無伺服器架構

但首先,請注意:儘管無伺服器解決方案對於某些用例來說是一個很好的解決方案,但它並不是完成這項工作的正確工具。

這意味著,流行的框架和基礎設施(例如 NextJS 和 AWS Lambda)不支援開箱即用的 WebSocket 整合。

{% 嵌入 https://www.youtube.com/watch?v=e5Cye4pIFeA %}

此類解決方案不是在專用的傳統伺服器上執行,而是利用無伺服器函數(也稱為 lambda 函數),這些函數旨在在收到請求時立即執行並完成任務。關閉」。

這種無伺服器架構對於保持 WebSocket 連線處於活動狀態並不理想,因為我們需要持久的、「始終在線」的連線。

這就是為什麼如果您想建立即時應用程式,您需要一個「伺服器化」架構。儘管有一種解決方法可以在無伺服器架構上取得 WebSocket,例如使用第三方服務,但這有許多缺點:

  • 成本:這些服務以訂閱形式存在,並且隨著應用程式的擴展而變得昂貴

  • 有限的客製化:您使用的是預先建置的解決方案,因此您的控制權較少

  • 除錯:修復錯誤變得更加困難,因為您的應用程式沒有在本地執行

圖片描述

💪

將 ExpressJS 與 Socket.IO 結合使用 — 複雜/可自訂的方法

好吧,讓我們從第一個更傳統的方法開始:為您的客戶端建立一個專用伺服器,以與之建立雙向通訊通道。

這種方法更先進,複雜一些,但允許更精細的客製化。如果您正在尋找一種簡單、更簡單的方法將 WebSockets 引入您的 React/NodeJS 應用程式,我們將在下面的部分中介紹該方法

>

👨‍💻提示:如果您想一起編碼,可以按照以下說明進行操作。或者,如果您只想查看這個特定的已完成的 React-NodeJS 全端應用程式,請查看此處的 github 存儲庫

>

在此範例中,我們將使用ExpressJSSocket.IO庫。儘管還有其他函式庫,Socket.IO 是一個很棒的函式庫,它使得在 NodeJS 中使用 WebSockets 變得更加容易

如果您想一起編碼,請先克隆start分支:

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

您會注意到裡面有兩個資料夾:

  • 📁 我們的 React 應用程式的ws-client

  • 📁 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啟動開發伺服器,您應該在瀏覽器中看到硬編碼的啟動應用程式:

圖片描述

您可能已經注意到 poll 與我們伺服器的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 支援、身份驗證、資料庫管理和開箱即用的全端類型安全性。

{% 嵌入 https://twitter.com/WaspLang/status/1673742264873500673?s=20 %}

Wasp 可以為您處理所有這些繁重的工作,因為它使用配置文件,您可以將其視為 Wasp 編譯器用來幫助將您的應用程式粘合在一起的一組指令。最後,Wasp 會為您處理一堆樣板程式碼,為您節省大量時間和精力。

要查看它的實際效果,讓我們按照以下步驟使用 Wasp 實作 WebSocket 通訊:

>

😎提示如果您想查看完成的應用程式程式碼,您可以在此處查看 GitHub 儲存庫

>

  1. 透過在終端機中執行以下命令來全域安裝 Wasp:
curl -sSL 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.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=}

// ...

這樣,Wasp 編譯器就會知道要做什麼並為我們配置這些功能。

讓我們告訴它我們也需要 WebSockets。將webSocket定義加入到main.wasp檔案中,位於authdependencies之間:

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

現在我們必須定義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);
    });
  });
};

您可能已經注意到,與傳統的 React/NodeJS 方法相比,Wasp 實作中所需的配置和樣板要少得多。那是因為:

  • 端點,

  • 驗證,

  • 以及 Express 和Socket.IO中間件

一切都由 Wasp 為您處理。通知!

圖片描述

現在讓我們繼續執行該應用程式來看看我們現在有什麼。

首先,我們需要初始化資料庫,以便我們的身份驗證正常運作。由於複雜性很高,我們在前面的範例中沒有這樣做,但使用 Wasp 很容易做到:

wasp db migrate-dev

完成後,執行應用程式(第一次執行需要一段時間才能安裝所有依賴項):

wasp start

這次您應該會看到登入畫面。先註冊一個用戶,然後登入:

圖片描述

登入後,您將看到與上一個範例相同的硬編碼輪詢資料,因為我們還沒有在前端設定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;

與先前的實作相比,Wasp 使我們不必配置Socket.IO客戶端以及建置我們自己的鉤子。

另外,將滑鼠懸停在客戶端程式碼中的變數上,您將看到系統會自動為您推斷類型!

這只是一個例子,但它應該適用於所有人:

圖片描述

現在,如果您打開一個新的私人/隱身選項卡,註冊一個新用戶並登錄,您將看到一個完全執行的即時投票應用程式。最好的部分是,與以前的方法相比,我們可以註銷並重新登錄,並且我們的投票資料仍然存在,這正是我們對生產級應用程式的期望。 🎩

圖片描述

太棒了…😏

比較兩種方法

現在,僅僅因為一種方法看起來更容易,並不總是意味著它總是更好。讓我們快速總結一下上述兩種實現的優點和缺點。

| |沒有黃蜂|與黃蜂|

| --- | --- | --- |

| 😎 目標用戶 |資深開發人員,Web 開發團隊 |全端開發人員、「Indiehackers」、初級開發人員 |

| 📈 程式碼的複雜性 |中到高 |低|

| 🚤 速度 |更慢、更有條理 |更快、更整合 |

| 🧑‍💻 圖書館 |任何| Socket.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 提供了有用的抽象,例如用於 React 元件的useSocketuseSocketListener掛鉤(除了其他功能,例如身份驗證、非同步作業、電子郵件發送、資料庫管理和部署),簡化了客戶端程式碼,並允許以更少的配置進行完全整合。

  2. 類型安全:Wasp 促進 WebSocket 事件和有效負載的全端類型安全。這降低了由於資料類型不匹配而導致執行時錯誤的可能性,並且使您無需編寫更多樣板檔案。

缺點:

  1. 學習曲線:不熟悉 Wasp 的開發人員需要學習該框架才能有效地使用它。

  2. 控制較少:雖然 Wasp 提供了很多便利,但它抽象化了一些細節,使開發人員對套接字管理的某些方面的控制稍微減少。


幫我幫你🌟

如果您還沒有,請在 GitHub 上為我們加註星標,特別是如果您發現這很有用的話!如果您這樣做,它將有助於支持我們建立更多此類內容。如果你不……好吧,我想我們會處理它。

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

{% cta https://www.github.com/wasp-lang/wasp %} ⭐️ 感謝您的支持🙏 {% endcta %}


結論

一般來說,如何將 WebSocket 加入到 React 應用程式取決於專案的具體情況、您對可用工具的熟悉程度以及您願意在易用性、控制和複雜性之間進行權衡。

不要忘記,如果您想查看我們的“午餐投票”示例全棧應用程式的完整完成程式碼,請轉到此處: https://github.com/vincanger/websockets-wasp

如果您使用 WebSockets 建立了一些很酷的東西,請在下面的評論中與我們分享

圖片描述


原文出處:https://dev.to/wasp/build-a-real-time-voting-app-with-websockets-react-typescript-3oof


共有 0 則留言