Một số ít, nhưng vẫn là một số lượng lớn các dự án, yêu cầu tích hợp ổ cắm web để cung cấp phản ứng tức thời của giao diện đối với các thay đổi mà không cần tìm nạp lại dữ liệu.
Đó là một điều thiết yếu và chúng tôi sẽ không nói về chúng hoặc so sánh giữa các thư viện của bên thứ 3 cung cấp API để có trải nghiệm tốt hơn cho nhà phát triển.
Mục tiêu của tôi là trình bày cách tích hợp nhanh chóng @microsoft/signalr
với NextJs. Và cách giải quyết những vấn đề chúng tôi gặp phải trong quá trình phát triển.
Tôi hy vọng mọi người đã cài đặt và triển khai dự án NextJS tại địa phương. Trong trường hợp của tôi, phiên bản là 13.2.4
. Hãy thêm một số thư viện quan trọng hơn: swr
(phiên bản 2.1.5
) để tìm nạp dữ liệu và làm việc thêm với bộ đệm cục bộ và @microsoft/signalr
(phiên bản 7.0.5
) - API cho ổ cắm web.
npm install --save @microsoft/signalr swr
Hãy bắt đầu bằng việc tạo một hàm fetcher
đơn giản và một hook mới có tên useChatData
để lấy dữ liệu ban đầu từ API REST của chúng tôi. Nó trả về danh sách các tin nhắn dành cho cuộc trò chuyện, các trường phát hiện lỗi và trạng thái tải cũng như phương thức mutate
cho phép thay đổi dữ liệu được lưu trong bộ nhớ đệm.
// 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, }; };
Để kiểm tra xem nó có hoạt động như mong đợi hay không, hãy cập nhật thành phần trang của chúng tôi. Nhập hook của chúng tôi ở trên cùng và trích xuất dữ liệu từ nó như trong đoạn mã bên dưới. Nếu nó hoạt động, bạn sẽ thấy dữ liệu được hiển thị. Như bạn thấy, nó khá đơn giản.
// 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> ); };
Bước tiếp theo yêu cầu kết nối trang tương lai của chúng tôi với các ổ cắm web, nắm bắt các sự kiện NewMessage
và cập nhật bộ đệm bằng một tin nhắn mới. Tôi đề xuất bắt đầu bằng việc xây dựng dịch vụ socket trong một tệp riêng biệt.
Theo các ví dụ trong tài liệu SignalR, chúng tôi phải tạo một phiên bản kết nối để nghe thêm các sự kiện. Tôi cũng đã thêm một đối tượng kết nối để ngăn chặn sự trùng lặp và hai đối tượng trợ giúp để bắt đầu/dừng kết nối.
// 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, };
Bây giờ, trang của chúng ta có thể trông giống như đoạn mã. Chúng tôi tìm nạp và trích xuất data
cùng với danh sách tin nhắn và hiển thị chúng. Ngoài ra, useEffect
ở trên sẽ đăng ký sự kiện NewMessage
, tạo kết nối và lắng nghe phần phụ trợ.
Khi sự kiện kích hoạt, phương thức mutate
từ hook sẽ cập nhật danh sách hiện có bằng một đối tượng mới.
// 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> ); };
Với tôi thì có vẻ ổn, nó hoạt động và chúng tôi thấy các tin nhắn mới xuất hiện như thế nào trong nguồn cấp dữ liệu. Tôi chọn ví dụ cơ bản về trò chuyện vì nó rõ ràng và dễ hiểu. Và tất nhiên, bạn áp dụng nó theo logic của riêng mình.
Khi sử dụng một trong các phiên bản ( @microsoft/signalr
), chúng tôi gặp phải sự cố trùng lặp. Nó được kết nối với useEffect
, mảng phụ thuộc. Mỗi lần phần phụ thuộc được thay đổi, connection.on(event, callback);
gọi lại được lưu trong bộ nhớ cache và kích hoạt nó nhiều lần.
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
Giải pháp nhanh nhất và đáng tin cậy nhất mà chúng tôi tìm thấy là giữ một bản sao dữ liệu bên trong ref
React và sử dụng nó bên trong useEffect
cho các bản cập nhật trong tương lai.
// 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> ); };
Hiện tại, chúng tôi sử dụng phiên bản mới của @microsoft/signalr
và có vẻ như phiên bản này đã có các bản sửa lỗi cần thiết. Nhưng dù sao đi nữa, nếu ai đó thấy giải pháp này hữu ích và sử dụng cách giải quyết này thì tôi sẽ rất vui. Để kết luận, tôi muốn nói rằng trải nghiệm của tôi với SignalR khá tích cực, việc cài đặt không yêu cầu bất kỳ sự phụ thuộc hoặc cài đặt cụ thể nào và nó hoạt động tốt và đáp ứng nhu cầu của chúng tôi.