如何在 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 地址的占位符。此外,如果需要更多的翻译或有任何其他问题,请随时提出。
信令服务器代码如下: using Microsoft.AspNetCore.SignalR; using Newtonsoft.Json; using System.Collections.Concurrent; using webrtc_net_api.Models; namespace webrtc_net_api.Service { public class WebRtcHub : Hub { private static readonly ConcurrentDictionary<string, string> _peerConnections = new(); public override async Task OnConnectedAsync() { Console.WriteLine($"新客户端连接: {Context.ConnectionId}"); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception exception) { Console.WriteLine($"客户端断开: {Context.ConnectionId}"); _peerConnections.TryRemove(Context.ConnectionId, out _); await base.OnDisconnectedAsync(exception); } public async Task JoinRoom(string peerId) { Console.WriteLine($"加入房间: {peerId}"); _peerConnections.TryAdd(Context.ConnectionId, peerId); await Groups.AddToGroupAsync(Context.ConnectionId, peerId); } public async Task SendSignal(SignalingMessage message) { Console.WriteLine($"收到信令: {JsonConvert.SerializeObject(message)}"); // 需引用Newtonsoft.Json Console.WriteLine($"目标PeerId: {message.PeerId}, 当前连接数: {_peerConnections.Count}"); // 查找目标连接(根据目标peerId) var targetConnection = _peerConnections .FirstOrDefault(p => p.Value == message.PeerId).Key; if (!string.IsNullOrEmpty(targetConnection)) { Console.WriteLine($"返回数据:{message.Type}"); await Clients.Client(targetConnection).SendAsync("receive_signal", message); } } } } namespace webrtc_net_api.Models { public class SignalingMessage { public string PeerId { get; set; } public string Type { get; set; } public string Sdp { get; set; } public RTCIceCandidate Candidate { get; set; } } public class RTCIceCandidate { public string Candidate { get; set; } public string SdpMid { get; set; } public int SdpMLineIndex { get; set; } } } WebRtcClient.vue代码如下: <template> <div> <video ref="localVideo" autoplay muted></video> <video ref="remoteVideo" autoplay></video> </div> </template> <script> import SimplePeer from 'simple-peer'; import SignalRService from '@/socket'; export default { data() { return { peer: null, signalRService: null, isInitiator: false }; }, async mounted() { this.signalRService = new SignalRService(); await this.signalRService.startConnection('target_peer_id'); // 获取媒体设备 const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); this.$refs.localVideo.srcObject = stream; // 创建 Peer 连接 this.peer = new SimplePeer({ initiator: this.isInitiator, trickle: true, stream: stream, config: { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] } }); // 处理信令 this.peer.on('signal', (data) => { this.signalRService.sendSignal({ type: data.type, sdp: data.sdp, peerId: 'target_peer_id' }); }); // 处理远程流 this.peer.on('stream', (remoteStream) => { this.$refs.remoteVideo.srcObject = remoteStream; }); // 处理 ICE 候选 this.peer.on('iceCandidate', (candidate) => { if (candidate) { this.signalRService.sendSignal({ type: 'ice-candidate', candidate: { candidate: candidate.candidate, sdpMid: candidate.sdpMid, sdpMLineIndex: candidate.sdpMLineIndex }, peerId: 'target_peer_id' }); } }); } }; </script> socket.js代码如下: import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; export default class SignalRService { constructor() { this.connection = null; } async startConnection(peerId) { this.connection = new HubConnectionBuilder() .withUrl('http://192.168.10.114:5012/signalr/webrtc', { accessTokenFactory: () => localStorage.getItem('jwt_token') // 可选认证 }) .configureLogging(LogLevel.Information) .withAutomaticReconnect() .build(); this.connection.on('receive_signal', (message) => { this.handleSignal(message); }); try { await this.connection.start(); await this.connection.invoke('JoinRoom', peerId); console.log('SignalR 连接已建立'); } catch (err) { console.error('SignalR 连接失败:', err); setTimeout(() => this.startConnection(peerId), 5000); } } handleSignal(message) { this.peer.signal(message); } sendSignal(message) { this.connection.invoke('SendSignal', message); } } 请提供一个完整的,基于ROS2的WEBRTC视频流推送的功能,代码需完整,并且都在一个文件内,推送地址为:http://192.168.10.114:5012/signalr/webrtc
06-28
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值