Azınlık ama yine de çok sayıda proje, verileri yeniden getirmeden bir arayüzün değişikliklere anında tepki vermesini sağlamak için web soketleri entegrasyonu gerektirir.
Bu çok önemli bir şey ve biz onlar hakkında konuşmayacağız veya daha iyi bir geliştirici deneyimi için API sağlayan 3. taraf kütüphaneler arasında bir karşılaştırma yapmayacağız.
Amacım @microsoft/signalr
NextJs ile hızlı bir şekilde nasıl entegre edileceğini göstermek. Ve geliştirme sırasında karşılaştığımız sorunları nasıl çözeceğimizi.
Umarım herkes NextJS projesini yerel olarak kurmuş ve konuşlandırmıştır. Benim durumumda sürüm 13.2.4
. Biraz daha önemli kütüphaneler ekleyelim: veri almak ve yerel önbellekle daha fazla çalışmak için swr
(sürüm 2.1.5
) ve web yuvaları için @microsoft/signalr
(sürüm 7.0.5
) - API.
npm install --save @microsoft/signalr swr
REST API'mizden ilk verileri almak için basit bir fetcher
işlevi ve useChatData
adında yeni bir kanca oluşturmaya başlayalım. Sohbete ilişkin mesajların bir listesini, hataları algılayan alanları ve yükleme durumunu ve önbelleğe alınan verileri değiştirmeye izin veren mutate
yöntemini döndürür.
// 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, }; };
Beklendiği gibi çalıştığını test etmek için sayfa bileşenimizi güncelleyelim. En üstteki kancamızı içe aktarın ve aşağıdaki kod parçasındaki gibi verileri çıkarın. Çalışırsa, işlenmiş verileri göreceksiniz. Gördüğünüz gibi oldukça basit.
// 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> ); };
Bir sonraki adım, gelecekteki sayfamızı web soketlerine bağlamayı, NewMessage
olaylarını yakalamayı ve önbelleği yeni bir mesajla güncellemeyi gerektirir. Soket hizmetini ayrı bir dosyada oluşturmaya başlamayı öneriyorum.
SignalR belgelerindeki örneklere göre, olayları daha fazla dinlemek için bir bağlantı örneği oluşturmamız gerekiyor. Ayrıca yinelemeleri önlemek için bir bağlantı nesnesi ve bağlantıları başlatmak/durdurmak için iki yardımcı ekledim.
// 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, };
Artık sayfamız kod pasajındaki gibi görünebilir. Mesaj listesiyle data
alıp çıkarıyoruz ve bunları oluşturuyoruz. Ayrıca yukarıdaki useEffect
, NewMessage
olayını kaydeder, bir bağlantı oluşturur ve arka ucu dinler.
Olay tetiklendiğinde, kancadaki mutate
yöntemi mevcut listeyi yeni bir nesneyle günceller.
// 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> ); };
Bana güzel görünüyor, işe yarıyor ve yeni mesajların akışta nasıl göründüğünü görüyoruz. Temel örneği sohbetle seçtim çünkü açık ve anlaşılması kolay. Ve elbette bunu kendi mantığınıza göre uygularsınız.
Sürümlerden birini ( @microsoft/signalr
) kullanırken çoğaltmalarla ilgili bir sorunla karşılaştık. Bağımlılık dizisi olan useEffect
bağlandı. Bağımlılık her değiştirildiğinde, connection.on(event, callback);
geri aramayı önbelleğe aldı ve tekrar tekrar tetikledi.
useEffect(() => { // data equals [] by default (registerOnServerEvents 1 run), // but after initial data fetching it changes (registerOnServerEvents 2 run) // each event changes data and triggers runnning of registerOnServerEvents socketService.registerOnServerEvents( 'NewMessage', // callback cached (payload: Message) => { // mutate called multiple times on each data change mutate(() => [...data, payload], { revalidate: false }); } ); }, [data]); // after getting 3 messages events, we had got 4 messages rendered lol
Bulduğumuz en hızlı ve en güvenilir çözüm, verilerin bir kopyasını React ref
içinde tutmak ve bunu gelecekteki güncellemeler için useEffect
içinde kullanmaktı.
// 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> ); };
Şu anda @microsoft/signalr
gerekli düzeltmeleri içeren yeni bir sürümünü kullanıyoruz. Ama yine de birisi bu çözümü yararlı bulursa ve bu geçici çözümü kullanırsa mutlu olacağım. Sonuç olarak, SignalR ile olan deneyimimin oldukça olumlu olduğunu, kurulumun herhangi bir özel bağımlılık veya ayar gerektirmediğini, gayet iyi çalıştığını ve ihtiyaçlarımızı karşıladığını söylemek istiyorum.