【问题标题】:Using WebSockets with Next.js将 WebSocket 与 Next.js 一起使用
【发布时间】:2021-09-16 15:24:51
【问题描述】:

我想知道使用 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。同样令人不安的是我无法关闭连接。 Context 根本不知道页面何时关闭,而且库也不提供关闭连接的钩子。

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

【问题讨论】:

  • 不太明白这个问题。页面关闭时连接会自动关闭。为什么new WebSocket 不起作用?你有什么错误吗?
  • @Danila Next.js 进行服务器端渲染和静态站点生成。最初呈现页面时,WebSocket API 不可用。他们必须以 useEffect() 钩子之类的方式调用 new Websocket() 以在客户端实例化它或使用其他方式来推迟 WebSocket 的实例化。
  • @Calvin 对,但是如果我使用一个可以创建新 WebSocket 连接的钩子,我会在每次需要使用 WebSocket 时创建一个新连接,不是吗?这就是我要避免的。

标签: reactjs websocket next.js


【解决方案1】:

为了让 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 上下文来保存实例,完全可以将其保持在全局范围并导入实例,除非您希望延迟打开连接。

如果您仍然决定使用反应上下文(或在反应树中的任何位置初始化 ws 客户端),请务必memoize实例,这样它就不会在每次更新时创建你的反应节点。

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(...)
 }
}, [])

【讨论】:

  • 多么棒的答案!
【解决方案2】:

我这样配置上下文:

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

【讨论】:

    猜你喜欢
    • 2018-10-04
    • 1970-01-01
    • 2019-02-05
    • 1970-01-01
    • 2016-06-09
    • 1970-01-01
    • 2020-09-18
    • 1970-01-01
    相关资源
    最近更新 更多