إطار Next.js
ميزات الوقت الفعلي
ميزات الوقت الفعلي
تمكّن ميزات الوقت الفعلي من تحديثات البيانات الفورية دون تحديث الصفحة، مما يخلق تجارب ديناميكية وتفاعلية. يغطي هذا الدرس WebSockets وأحداث الخادم المرسلة (SSE) ومكتبات الوقت الفعلي الشائعة لتطبيقات Next.js.
فهم الاتصال في الوقت الفعلي
ثلاثة أساليب رئيسية للاتصال في الوقت الفعلي في تطبيقات الويب:
/* 1. الاستطلاع - يطلب العميل التحديثات بشكل متكرر */
الإيجابيات: بسيط، يعمل في كل مكان
السلبيات: غير فعال، زمن استجابة عالٍ، حمل الخادم
/* 2. أحداث الخادم المرسلة (SSE) - يدفع الخادم إلى العميل */
الإيجابيات: دعم المتصفح المدمج، إعادة الاتصال التلقائي
السلبيات: أحادي الاتجاه (الخادم → العميل فقط)
/* 3. WebSockets - اتصال ثنائي الاتجاه */
الإيجابيات: ثنائي الاتجاه بالكامل، زمن استجابة منخفض، فعال
السلبيات: إعداد أكثر تعقيدًا، يتطلب خادم WebSocket
أحداث الخادم المرسلة (SSE)
توفر SSE طريقة بسيطة لدفع التحديثات في الوقت الفعلي من الخادم إلى العميل:
مسار SSE API
// app/api/events/route.ts
export const runtime = 'nodejs'; // تتطلب SSE بيئة تشغيل Node.js
export async function GET(request: Request) {
const encoder = new TextEncoder();
// إنشاء دفق قابل للقراءة مخصص
const stream = new ReadableStream({
async start(controller) {
// إرسال رسالة اتصال أولية
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ type: 'connected' })}\n\n`)
);
// إرسال التحديثات كل 3 ثوانٍ
const interval = setInterval(() => {
const data = {
type: 'update',
timestamp: new Date().toISOString(),
value: Math.random(),
};
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
);
}, 3000);
// التنظيف عند قطع الاتصال
request.signal.addEventListener('abort', () => {
clearInterval(interval);
controller.close();
});
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
مكون عميل SSE
// components/SSEClient.tsx
'use client';
import { useEffect, useState } from 'react';
export default function SSEClient() {
const [messages, setMessages] = useState<any[]>([]);
const [status, setStatus] = useState('disconnected');
useEffect(() => {
// إنشاء اتصال EventSource
const eventSource = new EventSource('/api/events');
eventSource.onopen = () => {
setStatus('connected');
};
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
setMessages((prev) => [...prev, data]);
};
eventSource.onerror = () => {
setStatus('error');
eventSource.close();
};
// التنظيف عند إلغاء التثبيت
return () => {
eventSource.close();
};
}, []);
return (
<div>
<p>الحالة: {status}</p>
<ul>
{messages.map((msg, i) => (
<li key={i}>{JSON.stringify(msg)}</li>
))}
</ul>
</div>
);
}
WebSockets في Next.js
توفر WebSockets اتصالاً ثنائي الاتجاه بالكامل لميزات الوقت الفعلي:
خيارات خادم WebSocket: لا تتضمن Next.js خادم WebSocket مدمجًا. تحتاج إلى استخدام خادم منفصل (خادم Node.js مخصص، خدمة طرف ثالث مثل Pusher/Ably، أو خادم WebSocket خارجي).
خادم WebSocket مخصص
// server.js (خادم Next.js مخصص)
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const { WebSocketServer } = require('ws');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = createServer((req, res) => {
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
});
// إنشاء خادم WebSocket
const wss = new WebSocketServer({ server, path: '/api/ws' });
wss.on('connection', (ws) => {
console.log('تم اتصال العميل');
// إرسال رسالة ترحيب
ws.send(JSON.stringify({ type: 'welcome', message: 'متصل!' }));
// معالجة الرسائل الواردة
ws.on('message', (data) => {
console.log('تم الاستلام:', data.toString());
// البث إلى جميع العملاء
wss.clients.forEach((client) => {
if (client.readyState === 1) { // OPEN
client.send(data);
}
});
});
// معالجة قطع الاتصال
ws.on('close', () => {
console.log('تم قطع اتصال العميل');
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`> جاهز على http://localhost:${PORT}`);
});
});
خطاف عميل WebSocket
// hooks/useWebSocket.ts
import { useEffect, useRef, useState } from 'react';
export function useWebSocket(url: string) {
const [isConnected, setIsConnected] = useState(false);
const [messages, setMessages] = useState<any[]>([]);
const ws = useRef<WebSocket | null>(null);
useEffect(() => {
// إنشاء اتصال WebSocket
ws.current = new WebSocket(url);
ws.current.onopen = () => {
setIsConnected(true);
console.log('تم اتصال WebSocket');
};
ws.current.onmessage = (event) => {
const data = JSON.parse(event.data);
setMessages((prev) => [...prev, data]);
};
ws.current.onerror = (error) => {
console.error('خطأ WebSocket:', error);
};
ws.current.onclose = () => {
setIsConnected(false);
console.log('تم قطع اتصال WebSocket');
};
// التنظيف عند إلغاء التثبيت
return () => {
ws.current?.close();
};
}, [url]);
const sendMessage = (message: any) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(message));
}
};
return { isConnected, messages, sendMessage };
}
استخدام خطاف WebSocket
// components/Chat.tsx
'use client';
import { useState } from 'react';
import { useWebSocket } from '@/hooks/useWebSocket';
export default function Chat() {
const [input, setInput] = useState('');
const { isConnected, messages, sendMessage } = useWebSocket(
'ws://localhost:3000/api/ws'
);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (input.trim()) {
sendMessage({ type: 'message', text: input });
setInput('');
}
};
return (
<div>
<div>الحالة: {isConnected ? 'متصل' : 'غير متصل'}</div>
<div style={{ height: '300px', overflow: 'auto' }}>
{messages.map((msg, i) => (
<div key={i}>{msg.text}</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="اكتب رسالة..."
/>
<button type="submit">إرسال</button>
</form>
</div>
);
}
تكامل Pusher
Pusher هي خدمة مستضافة في الوقت الفعلي تبسط تنفيذ WebSocket:
إعداد Pusher
# تثبيت Pusher SDK
npm install pusher pusher-js
# متغيرات البيئة
PUSHER_APP_ID=your_app_id
PUSHER_KEY=your_key
PUSHER_SECRET=your_secret
PUSHER_CLUSTER=your_cluster
NEXT_PUBLIC_PUSHER_KEY=your_key
NEXT_PUBLIC_PUSHER_CLUSTER=your_cluster
تكوين Pusher من جانب الخادم
// lib/pusher.ts
import Pusher from 'pusher';
export const pusherServer = new Pusher({
appId: process.env.PUSHER_APP_ID!,
key: process.env.PUSHER_KEY!,
secret: process.env.PUSHER_SECRET!,
cluster: process.env.PUSHER_CLUSTER!,
useTLS: true,
});
مسار Pusher API
// app/api/pusher/message/route.ts
import { pusherServer } from '@/lib/pusher';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const { message, channel } = await request.json();
await pusherServer.trigger(channel, 'new-message', {
message,
timestamp: new Date().toISOString(),
});
return Response.json({ success: true });
}
خطاف Pusher من جانب العميل
// hooks/usePusher.ts
import { useEffect, useState } from 'react';
import Pusher from 'pusher-js';
export function usePusher(channelName: string, eventName: string) {
const [messages, setMessages] = useState<any[]>([]);
useEffect(() => {
const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, {
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
});
const channel = pusher.subscribe(channelName);
channel.bind(eventName, (data: any) => {
setMessages((prev) => [...prev, data]);
});
return () => {
channel.unbind(eventName);
pusher.unsubscribe(channelName);
};
}, [channelName, eventName]);
return messages;
}
استخدام Pusher في المكونات
// components/PusherChat.tsx
'use client';
import { useState } from 'react';
import { usePusher } from '@/hooks/usePusher';
export default function PusherChat() {
const [input, setInput] = useState('');
const messages = usePusher('chat-room', 'new-message');
const sendMessage = async () => {
await fetch('/api/pusher/message', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channel: 'chat-room',
message: input,
}),
});
setInput('');
};
return (
<div>
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.message}</div>
))}
</div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
/>
<button onClick={sendMessage}>إرسال</button>
</div>
);
}
تكامل Socket.io
توفر Socket.io اتصالًا قويًا في الوقت الفعلي مع إعادة الاتصال التلقائي:
إعداد خادم Socket.io
# تثبيت Socket.io
npm install socket.io socket.io-client
// server.js
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const { Server } = require('socket.io');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = createServer((req, res) => {
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
});
const io = new Server(server, {
cors: {
origin: '*',
methods: ['GET', 'POST'],
},
});
io.on('connection', (socket) => {
console.log('تم اتصال العميل:', socket.id);
socket.on('message', (data) => {
io.emit('message', data);
});
socket.on('disconnect', () => {
console.log('تم قطع اتصال العميل:', socket.id);
});
});
server.listen(3000, () => {
console.log('> جاهز على http://localhost:3000');
});
});
خطاف عميل Socket.io
// hooks/useSocketIO.ts
import { useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
export function useSocketIO() {
const [socket, setSocket] = useState<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const socketInstance = io('http://localhost:3000');
socketInstance.on('connect', () => {
setIsConnected(true);
});
socketInstance.on('disconnect', () => {
setIsConnected(false);
});
setSocket(socketInstance);
return () => {
socketInstance.disconnect();
};
}, []);
return { socket, isConnected };
}
الإشعارات في الوقت الفعلي
قم بتنفيذ نظام إشعارات مع تحديثات في الوقت الفعلي:
// components/NotificationBell.tsx
'use client';
import { useEffect, useState } from 'react';
import { usePusher } from '@/hooks/usePusher';
export default function NotificationBell({ userId }: { userId: string }) {
const [count, setCount] = useState(0);
const [notifications, setNotifications] = useState<any[]>([]);
const [isOpen, setIsOpen] = useState(false);
const messages = usePusher(`user-${userId}`, 'notification');
useEffect(() => {
if (messages.length > 0) {
const latest = messages[messages.length - 1];
setNotifications((prev) => [latest, ...prev]);
setCount((prev) => prev + 1);
}
}, [messages]);
return (
<div style={{ position: 'relative' }}>
<button onClick={() => setIsOpen(!isOpen)}>
🔔
{count > 0 && <span>{count}</span>}
</button>
{isOpen && (
<div style={{
position: 'absolute',
top: '100%',
right: 0,
background: 'white',
border: '1px solid #ccc',
minWidth: '250px',
}}>
{notifications.length === 0 ? (
<p>لا توجد إشعارات</p>
) : (
notifications.map((notif, i) => (
<div key={i} style={{ padding: '10px', borderBottom: '1px solid #eee' }}>
{notif.message}
</div>
))
)}
</div>
)}
</div>
);
}
الوجود في الوقت الفعلي (المستخدمون المتصلون)
تتبع من هو متصل في الوقت الفعلي:
// components/OnlineUsers.tsx
'use client';
import { useEffect, useState } from 'react';
import Pusher from 'pusher-js';
export default function OnlineUsers() {
const [users, setUsers] = useState<string[]>([]);
useEffect(() => {
const pusher = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, {
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
});
const channel = pusher.subscribe('presence-room');
channel.bind('pusher:subscription_succeeded', (members: any) => {
const userList: string[] = [];
members.each((member: any) => {
userList.push(member.id);
});
setUsers(userList);
});
channel.bind('pusher:member_added', (member: any) => {
setUsers((prev) => [...prev, member.id]);
});
channel.bind('pusher:member_removed', (member: any) => {
setUsers((prev) => prev.filter((id) => id !== member.id));
});
return () => {
pusher.unsubscribe('presence-room');
};
}, []);
return (
<div>
<h3>المستخدمون المتصلون ({users.length})</h3>
<ul>
{users.map((user) => (
<li key={user}>{user}</li>
))}
</ul>
</div>
);
}
اختيار حل في الوقت الفعلي: استخدم SSE للتحديثات أحادية الاتجاه البسيطة (الإشعارات، الخلاصات الحية). استخدم WebSockets للاتصال ثنائي الاتجاه (الدردشة، التعاون). استخدم الخدمات المستضافة (Pusher، Ably) لتطبيقات الإنتاج لتجنب إدارة البنية التحتية.
تمرين: إنشاء ميزات الوقت الفعلي
- قم بإنشاء نقطة نهاية SSE تبث أسعار الأسهم الحية
- قم ببناء تطبيق دردشة في الوقت الفعلي باستخدام WebSockets
- قم بتنفيذ نظام إشعارات باستخدام Pusher
- قم بإنشاء مؤشر وجود عبر الإنترنت يعرض المستخدمين النشطين
- قم ببناء محرر نصوص تعاوني مع تحديثات في الوقت الفعلي
- قم بتنفيذ مؤشرات الكتابة في تطبيق الدردشة
- قم بإنشاء خلاصة نشاط حية يتم تحديثها تلقائيًا
- قم ببناء لوحة معلومات في الوقت الفعلي مع مقاييس حية
اعتبارات الإنتاج: قم بتنفيذ منطق إعادة الاتصال، وتعامل مع أعطال الشبكة بشكل رشيق، وحد معدل الاتصالات، وصادق على اتصالات WebSocket، وراقب عدد الاتصالات لمنع استنفاد الموارد.