使用WebSockets与Next.js

24

我在想,使用Next.js的页面连接到我的WebSockets服务器的最佳方法是什么?我希望用户可以通过一个连接浏览页面,并在关闭页面时也关闭WebSockets连接。我尝试使用React的Context API:

const WSContext = createContext(null);

const Wrapper = ({ children }) => {
  const instance = WebSocket("ws://localhost:3000/ws");

  return <WSContext.Provider value={instance}>{children}</WSContext.Provider>;
};

export const useWS = () => useContext(WSContext);

export default Wrapper;

它的功能很好,但在创建连接时无法正常工作。基本的new WebSocket语法不起作用,所以我必须使用第三方库,比如react-use-websocket,我不喜欢这种方式。另一个令人困扰的问题是,我无法关闭连接。上下文简单地不知道页面何时关闭,而且该库也没有提供关闭连接的钩子。

我想知道在处理Next.js中的WebSockets连接时最佳方法是什么。


不太理解这个问题。当页面关闭时,连接将自动关闭。为什么 new WebSocket 不起作用?你有错误吗? - Danila
@Danila Next.js 可以进行服务器端渲染和静态网站生成。当页面最初呈现时,WebSocket API 不可用。他们需要在类似于 useEffect() 钩子中调用 new Websocket() 来在客户端实例化它,或者使用其他方式来延迟 WebSocket 的实例化。 - Calvin
@Calvin 对的,但如果我使用一个钩子来创建新的WebSocket连接,那么每次需要使用WebSocket时我都会创建一个新的连接,不是吗?这就是我想要避免的。 - Cholewka
2个回答

65

为了使ws在Next.js上工作,需要完成多项任务。

首先,重要的是要意识到我想让我的ws代码在哪里运行。在Next.js上运行的React代码有两个环境:服务器端(构建页面或使用ssr时)和客户端。

在页面构建期间建立ws连接没有太大的用处,因此我将仅介绍客户端的ws。

全局Websocket类是一个仅适用于浏览器的功能,在服务器上不存在。这就是为什么我们需要防止任何实例化,直到代码在浏览器中运行。一种简单的方法是:

export const isBrowser = typeof window !== "undefined";

export const wsInstance = isBrowser ? new WebSocket(...) : null;

此外,您不必使用React上下文来保存实例,完全可以保持全局范围并导入实例,除非您希望懒惰地打开连接。

如果您仍然决定使用React上下文(或在React树的任何位置初始化ws客户端),则重要的是对实例进行记忆化,以便它不会在React节点的每次更新时创建。

const wsInstance = useMemo(() => isBrowser ? new WebSocket(...) : null, []);

或者

const [wsInstance] = useState(() => isBrowser ? new WebSocket(...) : null);

在React中创建的任何事件处理程序注册都应该包装在一个useEffect中,并带有一个返回函数,该函数会删除事件侦听器,以防止内存泄漏,同时应指定依赖项数组。如果您的组件已卸载,则useEffect钩子也将删除事件侦听器。

如果您希望重新初始化ws并且释放当前连接,则可以执行类似以下操作:

const [wsInstance, setWsInstance] = useState(null);

// Call when updating the ws connection
const updateWs = useCallback((url) => {
   if(!browser) return setWsInstance(null);
   
   // Close the old connection
   if(wsInstance?.readyState !== 3)
     wsInstance.close(...);

   // Create a new connection
   const newWs = new WebSocket(url);
   setWsInstance(newWs);
}, [wsInstance])


// (Optional) Open a connection on mount
useEffect(() => {
 if(isBrowser) { 
   const ws = new WebSocket(...);
   setWsInstance(ws);
 }

 return () => {
  // Cleanup on unmount if ws wasn't closed already
  if(ws?.readyState !== 3) 
   ws.close(...)
 }
}, [])


1
new WebSocket()可以在客户端上运行而无需额外配置吗?我遇到了这个错误:invalid 'instanceof' operand URL,因为if(address instance of URL)失败了。URL未定义。我已经将WebSocket导入为const WebSocket = require("ws");并尝试使用import {WebSocket} from "ws";导入。 - CrippledTable
2
@CrippledTable 中的 Websocket 指的是 web API,可全球使用,无需导入。 - Filip Kaštovský
2
谢谢你。我有“隧道视觉”(指只关注某个具体问题而忽略其他事情)。最近我试图在客户端使用ws npm包,但经过一番“精神之旅”,我意识到这样做是多么天真,并且现在我明白了它的工作原理。 - CrippledTable
1
谢谢@FilipKaštovský,我花了很多时间才找到问题所在,最后终于找到了! - apinanyogaratnam
@CrippledTable 这是一个离题的问题,但你是否知道是否可以在AWS Amplify上托管Next.Js的websocket应用程序?我知道Vercel不支持原生的websockets,想知道Amplify是否支持。 - undefined
@BudLinville 这就是我使用的确切的托管基础设施和技术堆栈。我记不清是否有什么要注意的地方让它正常运行,但这是可能的。 - undefined

3

我像这样配置了上下文:

import { createContext, useContext, useMemo, useEffect, ReactNode } from 'react';

type WSProviderProps = { children: ReactNode; url: string };

const WSStateContext = createContext<WebSocket | null>(null);

function WSProvider({ children, url }: WSProviderProps): JSX.Element {
  const wsInstance = useMemo(
    () => (typeof window != 'undefined' ? new WebSocket(`ws://127.0.0.1:8001/ws${url}`) : null),
    []
  );

  useEffect(() => {
    return () => {
      wsInstance?.close();
    };
  }, []);

  return <WSStateContext.Provider value={wsInstance}>{children}</WSStateContext.Provider>;
}

function useWS(): WebSocket {
  const context = useContext(WSStateContext);

  if (context == undefined) {
    throw new Error('useWS must be used within a WSProvider');
  }

  return context;
}

export { WSProvider, useWS };

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接