如何在 Next.js 中快速集成 @microsoft/signalr
如何在 Next.js 中快速集成 @microsoft/signalr
虽然只有少数项目需要集成 WebSockets 来在界面发生变化时实时响应而不重新获取数据,但这些项目仍然数量庞大。
这是一项必不可少的工作,我们不会讨论它们或者比较提供更好开发体验的第三方库。
我的目标是展示如何快速集成 @microsoft/signalr
到 Next.js 中,以及在开发过程中遇到的问题如何解决。
首先,我希望每个人都已经在本地安装和部署了 Next.js 项目。在我的案例中,版本是 13.2.4
。让我们添加一些重要的库:swr
(版本 2.1.5
)用于数据获取以及与本地缓存进一步工作,以及 @microsoft/signalr
(版本 7.0.5
) - 用于 WebSockets 的 API。
npm install --save @microsoft/signalr swr
让我们从创建一个简单的 fetcher
函数和一个名为 useChatData
的新 hook 开始,以从我们的 REST API 获取初始数据。它返回了聊天消息列表、检测错误和加载状态的字段,以及允许更改缓存数据的 mutate
方法。
// hooks/useChatData.ts
import useSWR from 'swr';
type Message = {
content: string;
createdAt: Date;
id: string;
};
async function fetcher<TResponse>(url: string, config: RequestInit): Promise<TResponse> {
const response = await fetch(url, config);
if (!response.ok) {
throw response;
}
return await response.json();
}
export const useChatData = () => {
const { data, error, isLoading, mutate } = useSWR<Message[]>('OUR_API_URL', fetcher);
return {
data: data || [],
isLoading,
isError: error,
mutate,
};
};
为了测试它是否按预期工作,让我们更新我们的页面组件。在顶部导入我们的 hook,并像下面的代码片段中那样从中提取数据。如果它工作正常,你将看到渲染的数据。如你所见,这很简单。
// pages/chat.ts
import { useChatData } from 'hooks/useChatData';
const Chat: NextPage = () => {
const { data } = useChatData();
return (
<div>
{data.map(item => (
<div key={item.id}>{item.content}</div>
))}
</div>
);
};
下一步需要连接我们未来的页面到 WebSockets,捕获 NewMessage
事件,并使用新消息更新缓存。我建议从一个单独的文件中构建 socket 服务。
根据 SignalR 文档中的示例,我们需要创建一个连接实例以后续监听事件。我还添加了一个 connections 对象,以防止重复连接,以及两个用于启动/停止连接的帮助函数。
// api/socket.ts
import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr';
let connections = {} as { [key: string]: { type: string; connection: HubConnection; started: boolean } };
function createConnection(messageType: string) {
const connectionObj = connections[messageType];
if (!connectionObj) {
console.log('SOCKET: Registering on server events ', messageType);
const connection = new HubConnectionBuilder()
.withUrl('API_URL', {
logger: LogLevel.Information,
withCredentials: false,
})
.withAutomaticReconnect()
.build();
connections[messageType] = {
type: messageType,
connection: connection,
started: false,
};
return connection;
} else {
return connections[messageType].connection;
}
}
function startConnection(messageType: string) {
const connectionObj = connections[messageType];
if (!connectionObj.started) {
connectionObj.connection.start().catch(err => console.error('SOCKET: ', err.toString()));
connectionObj.started = true;
}
}
function stopConnection(messageType: string) {
const connectionObj = connections[messageType];
if (connectionObj) {
console.log('SOCKET: Stoping connection ', messageType);
connectionObj.connection.stop();
connectionObj.started = false;
}
}
function registerOnServerEvents(
messageType: string,
callback: (payload: Message) => void,
) {
try {
const connection = createConnection(messageType);
connection.on('NewIncomingMessage', (payload: Message) => {
callback(payload);
});
connection.onclose(() => stopConnection(messageType));
startConnection(messageType);
} catch (error) {
console.error('SOCKET: ', error);
}
}
export const socketService = {
registerOnServerEvents,
stopConnection,
};
因此,现在我们的页面可能如下所示。我们获取并提取了包含消息列表的 data
,并进行了渲染。此外,上面的 useEffect
注册了 NewMessage
事件,创建了连接,并监听后端。
当事件触发时,hook 中的 mutate
方法会使用新对象更新现有列表。
// pages/chat.ts
import { useChatData } from 'hooks/useChatData';
import { socketService } from 'api/socket';
const Chat: NextPage = () => {
const { data } = useChatData();
useEffect(() => {
socketService.registerOnServerEvents(
'NewMessage',
(payload: Message) => {
mutate(() => [...data, payload], { revalidate: false });
}
);
}, [data]);
useEffect(() => {
return () => {
socketService.stopConnection('NewMessage');
};
}, []);
return (
<div>
{data.map(item => (
<div key={item.id}>{item.content}</div>
))}
</div>
);
};
看起来不错,它可以工作,我们可以看到新消息如何出现在消息列表中。我选择了一个聊天的基本示例,因为它非常清晰且易于理解
。当然,你可以根据自己的逻辑应用它。
小额奖励
使用其中一个版本的 @microsoft/signalr
,我们遇到了重复的问题。这与 useEffect
中的依赖数组有关。每次依赖项更改时,connection.on(event, callback);
都会缓存回调并一次又一次地触发它。
useEffect(() => {
// data 默认为空数组(registerOnServerEvents 第1次运行),
// 但在初始数据获取后它会变化(registerOnServerEvents 第2次运行)
// 每个事件都会更改数据并触发 registerOnServerEvents 的运行
socketService.registerOnServerEvents(
'NewMessage',
// 回调函数被缓存
(payload: Message) => {
// 每次数据更改都会多次调用 mutate
mutate(() => [...data, payload], { revalidate: false });
}
);
}, [data]);
// 在收到3条消息事件后,我们竟然渲染了4条消息,哈哈
我们找到的最快最可靠的解决方案是在 React ref
中保留数据的副本,并在 useEffect
中使用它进行未来的更新。
// pages/chat.ts
import { useChatData } from 'hooks/useChatData';
import { socketService } from 'api/socket';
const Chat: NextPage = () => {
const { data } = useChatData();
const messagesRef = useRef<Message[]>([]);
useEffect(() => {
messagesRef.current = chatData;
}, [chatData]);
useEffect(() => {
socketService.registerOnServerEvents(
'NewMessage',
(payload: Message) => {
const messagesCopy = messagesRef.current.slice();
mutate(() => [...messagesCopy, payload], { revalidate: false });
}
);
}, [data]);
useEffect(() => {
return () => {
socketService.stopConnection('NewMessage');
};
}, []);
return (
<div>
{data.map(item => (
<div key={item.id}>{item.content}</div>
))}
</div>
);
};
目前,我们使用了 @microsoft/signalr
的新版本,它似乎已经修复了必要的问题。但无论如何,如果有人发现这个解决方案有用,并使用了这个解决办法,我会很高兴。总之,我想说,我对 SignalR 的经验非常积极,安装不需要任何特定的依赖项或设置,它运行良好且满足我们的需求。
请注意,在文档中有一些 `OUR_API_URL`、`API_URL` 等需要替换为实际的 API 地址的占位符。此外,如果需要更多的翻译或有任何其他问题,请随时提出。