WebSockets والتطبيقات الفورية

الوقت الفعلي مع اشتراكات GraphQL

17 دقيقة الدرس 24 من 35

مقدمة في اشتراكات GraphQL

توفر اشتراكات GraphQL طريقة لدفع التحديثات في الوقت الفعلي من الخادم إلى العملاء عندما تحدث أحداث معينة. على عكس الاستعلامات (طلب البيانات) والطفرات (تعديل البيانات)، تحافظ الاشتراكات على اتصال نشط وتدفق التحديثات.

ما هي اشتراكات GraphQL؟

الاشتراكات هي ميزة GraphQL تسمح بـ:

  • تحديثات الوقت الفعلي: دفع البيانات إلى العملاء عندما تحدث الأحداث
  • موجهة بالأحداث: يشترك العملاء في أحداث معينة
  • قائمة على WebSocket: يستخدم بروتوكول WebSocket للاتصال ثنائي الاتجاه
  • آمن من النوع: دعم كامل لنظام نوع GraphQL
مفهوم أساسي: تكمل الاشتراكات ثلاثية GraphQL: الاستعلامات (قراءة)، الطفرات (كتابة)، الاشتراكات (تحديثات الوقت الفعلي).

لماذا نستخدم اشتراكات GraphQL؟

الفوائد على REST WebSockets التقليدية:

  • بيانات وقت فعلي آمنة من النوع مع مخطط GraphQL
  • طلب البيانات التي تحتاجها بالضبط (لا جلب زائد)
  • التصفية والوسيطات المدمجة
  • سطح API موحد (الاستعلامات والطفرات والاشتراكات في نقطة نهاية واحدة)
  • أدوات قوية ودعم العميل (Apollo، Relay، URQL)

مخطط اشتراك GraphQL

حدد الاشتراكات في مخطط GraphQL الخاص بك:

// schema.graphql type Message { id: ID! content: String! author: User! createdAt: String! } type User { id: ID! username: String! avatar: String } type Query { messages: [Message!]! } type Mutation { sendMessage(content: String!): Message! } type Subscription { # الاشتراك في الرسائل الجديدة في غرفة messageAdded(roomId: ID!): Message! # الاشتراك في مؤشرات الكتابة userTyping(roomId: ID!): User! # الاشتراك في تغييرات حالة المستخدم userStatusChanged(userId: ID!): UserStatus! } enum UserStatus { ONLINE OFFLINE AWAY }

إعداد خادم Apollo

قم بإعداد خادم Apollo مع دعم الاشتراك:

// تثبيت التبعيات // npm install apollo-server-express graphql subscriptions-transport-ws const { ApolloServer, gql } = require('apollo-server-express'); const { PubSub } = require('graphql-subscriptions'); const { createServer } = require('http'); const express = require('express'); const { ApolloServerPluginDrainHttpServer } = require('apollo-server-core'); const { makeExecutableSchema } = require('@graphql-tools/schema'); const { WebSocketServer } = require('ws'); const { useServer } = require('graphql-ws/lib/use/ws'); const app = express(); const httpServer = createServer(app); // مثيل PubSub للنشر/الاشتراك في الأحداث const pubsub = new PubSub(); // مخطط GraphQL const typeDefs = gql` type Message { id: ID! content: String! author: String! timestamp: String! } type Query { messages: [Message!]! } type Mutation { sendMessage(content: String!, author: String!): Message! } type Subscription { messageAdded: Message! } `; // المحللون const resolvers = { Query: { messages: () => messages }, Mutation: { sendMessage: (_, { content, author }) => { const message = { id: String(Date.now()), content, author, timestamp: new Date().toISOString() }; messages.push(message); // نشر حدث للمشتركين pubsub.publish('MESSAGE_ADDED', { messageAdded: message }); return message; } }, Subscription: { messageAdded: { subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED']) } } }; // إنشاء المخطط const schema = makeExecutableSchema({ typeDefs, resolvers }); // خادم WebSocket للاشتراكات const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' }); const serverCleanup = useServer({ schema }, wsServer); // خادم Apollo const server = new ApolloServer({ schema, plugins: [ ApolloServerPluginDrainHttpServer({ httpServer }), { async serverWillStart() { return { async drainServer() { await serverCleanup.dispose(); } }; } } ] }); await server.start(); server.applyMiddleware({ app }); httpServer.listen(4000, () => { console.log(`الخادم جاهز على http://localhost:4000${server.graphqlPath}`); console.log(`الاشتراكات جاهزة على ws://localhost:4000${server.graphqlPath}`); });
مهم: تتطلب الاشتراكات كلاً من خادم HTTP (للاستعلامات/الطفرات) وخادم WebSocket (للاشتراكات) يعملان على نفس المنفذ.

محللات الاشتراك

تستخدم محللات الاشتراك مكررات غير متزامنة:

const resolvers = { Subscription: { messageAdded: { // اشتراك بسيط subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED']) }, // اشتراك مع التصفية messageAddedInRoom: { subscribe: (_, { roomId }) => { return pubsub.asyncIterator([`MESSAGE_ADDED_${roomId}`]); } }, // اشتراك مع تصفية مخصصة messageAddedForUser: { subscribe: () => pubsub.asyncIterator(['MESSAGE_ADDED']), resolve: (payload, args, context) => { // تصفية الرسائل بناءً على أذونات المستخدم if (payload.messageAdded.private && payload.messageAdded.recipientId !== context.userId) { return null; // لا ترسل لهذا المشترك } return payload.messageAdded; } } } }; // نشر الأحداث const sendMessage = async (roomId, message) => { await db.messages.create(message); // النشر إلى قناة خاصة بالغرفة pubsub.publish(`MESSAGE_ADDED_${roomId}`, { messageAddedInRoom: message }); };

PubSub لنشر الأحداث

يربط نمط PubSub الطفرات بالاشتراكات:

const { PubSub } = require('graphql-subscriptions'); const pubsub = new PubSub(); // أسماء الأحداث كثوابت const EVENTS = { MESSAGE_ADDED: 'MESSAGE_ADDED', USER_TYPING: 'USER_TYPING', USER_STATUS_CHANGED: 'USER_STATUS_CHANGED' }; const resolvers = { Mutation: { sendMessage: async (_, { roomId, content }, { userId }) => { const message = await db.messages.create({ roomId, content, authorId: userId }); // النشر لجميع المشتركين await pubsub.publish(EVENTS.MESSAGE_ADDED, { messageAdded: message, roomId // يمكن استخدامه للتصفية }); return message; }, setTyping: async (_, { roomId, isTyping }, { userId }) => { await pubsub.publish(EVENTS.USER_TYPING, { userTyping: { userId, roomId, isTyping } }); return { success: true }; } }, Subscription: { messageAdded: { subscribe: (_, { roomId }) => { return pubsub.asyncIterator([EVENTS.MESSAGE_ADDED]); }, resolve: (payload, { roomId }) => { // التصفية حسب الغرفة if (payload.roomId === roomId) { return payload.messageAdded; } return null; } }, userTyping: { subscribe: (_, { roomId }) => { return pubsub.asyncIterator([EVENTS.USER_TYPING]); }, resolve: (payload, { roomId }) => { if (payload.userTyping.roomId === roomId) { return payload.userTyping; } return null; } } } };
أفضل ممارسة: استخدم ثوابت لأسماء الأحداث لتجنب الأخطاء المطبعية وتسهيل إعادة البناء.

تصفية الاشتراكات

تصفية المشتركين الذين يتلقون الأحداث:

// الطريقة 1: التصفية القائمة على القناة (الأكثر كفاءة) const resolvers = { Subscription: { messageAdded: { subscribe: (_, { roomId }) => { // لكل غرفة قناتها الخاصة return pubsub.asyncIterator([`ROOM_${roomId}_MESSAGES`]); } } }, Mutation: { sendMessage: async (_, { roomId, content }) => { const message = await saveMessage(roomId, content); // النشر إلى قناة خاصة بالغرفة pubsub.publish(`ROOM_${roomId}_MESSAGES`, { messageAdded: message }); return message; } } }; // الطريقة 2: التصفية القائمة على المحلل (أكثر مرونة) const resolvers = { Subscription: { messageAdded: { subscribe: () => pubsub.asyncIterator(['ALL_MESSAGES']), resolve: (payload, args, context) => { const { messageAdded } = payload; const { roomId } = args; const { user } = context; // إرسال فقط إذا كان المستخدم في الغرفة if (messageAdded.roomId !== roomId) return null; // إرسال فقط إذا كان المستخدم لديه إذن if (!user.hasAccessToRoom(roomId)) return null; return messageAdded; } } } }; // الطريقة 3: مساعد withFilter (يوفره Apollo) const { withFilter } = require('graphql-subscriptions'); const resolvers = { Subscription: { messageAdded: { subscribe: withFilter( () => pubsub.asyncIterator(['MESSAGE_ADDED']), (payload, variables, context) => { // إرجاع true للإرسال، false للتخطي return payload.messageAdded.roomId === variables.roomId && context.user.hasAccessToRoom(variables.roomId); } ) } } };

معالجة الاشتراك من جانب العميل

الاتصال والاشتراك من عميل React:

// تثبيت التبعيات // npm install @apollo/client graphql subscriptions-transport-ws import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { getMainDefinition } from '@apollo/client/utilities'; import { createClient } from 'graphql-ws'; // اتصال HTTP للاستعلامات والطفرات const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' }); // اتصال WebSocket للاشتراكات const wsLink = new GraphQLWsLink(createClient({ url: 'ws://localhost:4000/graphql' })); // تقسيم حركة المرور بين HTTP و WebSocket const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ); }, wsLink, httpLink ); // عميل Apollo const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache() });

استخدام الاشتراكات في React

import { useSubscription, gql, useMutation } from '@apollo/client'; const MESSAGE_ADDED = gql` subscription MessageAdded($roomId: ID!) { messageAdded(roomId: $roomId) { id content author { id username avatar } createdAt } } `; const SEND_MESSAGE = gql` mutation SendMessage($roomId: ID!, $content: String!) { sendMessage(roomId: $roomId, content: $content) { id content author { id username } createdAt } } `; function ChatRoom({ roomId }) { const [messages, setMessages] = useState([]); // الاشتراك في الرسائل الجديدة const { data, loading, error } = useSubscription(MESSAGE_ADDED, { variables: { roomId }, onSubscriptionData: ({ subscriptionData }) => { const newMessage = subscriptionData.data.messageAdded; setMessages(prev => [...prev, newMessage]); } }); // طفرة لإرسال الرسائل const [sendMessage] = useMutation(SEND_MESSAGE); const handleSend = async (content) => { await sendMessage({ variables: { roomId, content } }); }; return ( <div> {messages.map(msg => ( <div key={msg.id}> <strong>{msg.author.username}:</strong> {msg.content} </div> ))} <MessageInput onSend={handleSend} /> </div> ); }
تمرين:
  1. قم بإعداد خادم Apollo مع اشتراكات GraphQL
  2. أنشئ مخططاً مع:
    • استعلام لجلب البيانات الأولية
    • طفرة لإنشاء المنشورات
    • اشتراك لاستقبال المنشورات الجديدة
  3. نفذ التصفية بحيث يتلقى المستخدمون فقط المنشورات من المستخدمين المتابَعين
  4. أنشئ عميل React مع عميل Apollo
  5. اشترك في التحديثات في الوقت الفعلي واعرضها في واجهة المستخدم
  6. اختبر مع نوافذ متصفح متعددة للتحقق من المزامنة في الوقت الفعلي

ملخص

توفر اشتراكات GraphQL اتصالاً في الوقت الفعلي آمناً من النوع:

  • اتصال ثنائي الاتجاه قائم على WebSocket
  • آمن من النوع مع دعم مخطط GraphQL الكامل
  • طلب البيانات التي تحتاجها بالضبط (لا جلب زائد)
  • تصفية مدمجة مع الوسيطات والمحللات
  • API موحد مع الاستعلامات والطفرات
  • دعم عميل قوي مع Apollo و Relay و URQL