الاشتراكات
التحديثات في الوقت الفعلي مع اشتراكات GraphQL
تتيح الاشتراكات الاتصال القائم على الأحداث في الوقت الفعلي بين الخادم والعميل. على عكس الاستعلامات والتحويرات، تحافظ الاشتراكات على اتصال مستمر وتدفع البيانات عند حدوث الأحداث.
ما هي الاشتراكات؟
تسمح الاشتراكات للعملاء بالاستماع إلى التحديثات في الوقت الفعلي من الخادم. إنها مثالية لتطبيقات الدردشة والإشعارات المباشرة ولوحات المعلومات في الوقت الفعلي والتحرير التعاوني.
type Subscription {
messageAdded(channelId: ID!): Message
userStatusChanged(userId: ID!): User
commentAdded(postId: ID!): Comment
notificationReceived: Notification
}
type Message {
id: ID!
content: String!
author: User!
channel: Channel!
createdAt: DateTime!
}
نمط النشر والاشتراك (PubSub)
تستخدم اشتراكات GraphQL عادةً نمط النشر والاشتراك. ينشر الخادم الأحداث إلى المواضيع، ويتلقى العملاء المشتركون التحديثات.
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
// تحوير ينشر الحدث
const resolvers = {
Mutation: {
sendMessage: async (parent, { channelId, content }, { user }) => {
const message = await Message.create({
channelId,
content,
authorId: user.id
});
// نشر الحدث للمشتركين
pubsub.publish('MESSAGE_ADDED', {
messageAdded: message,
channelId
});
return message;
}
},
Subscription: {
messageAdded: {
subscribe: (parent, { channelId }) => {
return pubsub.asyncIterator(['MESSAGE_ADDED']);
},
resolve: (payload, { channelId }) => {
// تصفية حسب القناة
if (payload.channelId === channelId) {
return payload.messageAdded;
}
return null;
}
}
}
};
محللات الاشتراكات
محللات الاشتراكات لها وظيفتان: subscribe (إعداد الاشتراك) و resolve (تحويل البيانات المنشورة).
const resolvers = {
Subscription: {
commentAdded: {
// إعداد الاشتراك
subscribe: withFilter(
() => pubsub.asyncIterator(['COMMENT_ADDED']),
(payload, variables) => {
// تصفية: إرسال فقط إذا كان التعليق لهذا المنشور
return payload.commentAdded.postId === variables.postId;
}
),
// تحويل الحمولة
resolve: (payload) => {
return payload.commentAdded;
}
},
userStatusChanged: {
subscribe: withFilter(
() => pubsub.asyncIterator(['USER_STATUS_CHANGED']),
(payload, variables, context) => {
// فحص المصادقة
if (!context.user) return false;
// إخطار الأصدقاء فقط
return payload.userId === variables.userId &&
context.user.friends.includes(payload.userId);
}
)
}
}
};
نقل WebSocket
تتطلب الاشتراكات اتصالات WebSocket للاتصال ثنائي الاتجاه. يستخدم Apollo Server بروتوكول graphql-ws.
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import express from 'express';
const app = express();
const httpServer = createServer(app);
const schema = makeExecutableSchema({
typeDefs,
resolvers
});
// خادم WebSocket للاشتراكات
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
const serverCleanup = useServer({ schema }, wsServer);
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
}
};
}
}
]
});
await server.start();
app.use('/graphql', express.json(), expressMiddleware(server));
httpServer.listen(4000);
تصفية الاشتراكات
تسمح وظيفة withFilter بتصفية المشتركين الذين يتلقون أي أحداث.
import { withFilter } from 'graphql-subscriptions';
const resolvers = {
Subscription: {
// تصفية حسب معرف المستخدم
notificationReceived: {
subscribe: withFilter(
() => pubsub.asyncIterator(['NOTIFICATION']),
(payload, variables) => {
return payload.userId === variables.userId;
}
)
},
// تصفية حسب معايير متعددة
orderUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator(['ORDER_UPDATED']),
(payload, variables, context) => {
const { orderId, status } = variables;
const order = payload.orderUpdated;
// تصفية حسب معرف الطلب
if (orderId && order.id !== orderId) return false;
// تصفية حسب الحالة
if (status && order.status !== status) return false;
// المصادقة: يجب أن يمتلك المستخدم الطلب
return order.userId === context.user.id;
}
)
}
}
};
دورة حياة الاشتراك
فهم دورة حياة الاشتراك يساعد في تصحيح الأخطاء وتحسين الميزات في الوقت الفعلي.
const resolvers = {
Subscription: {
messageAdded: {
subscribe: async (parent, args, context) => {
// 1. إنشاء الاتصال
console.log('المشترك متصل:', context.user.id);
// 2. التحقق من الأذونات
const channel = await Channel.findById(args.channelId);
if (!channel.hasAccess(context.user)) {
throw new Error('الوصول مرفوض');
}
// 3. إعداد المكرر
const iterator = pubsub.asyncIterator(['MESSAGE_ADDED']);
// 4. التنظيف عند قطع الاتصال
const cleanup = () => {
console.log('المشترك قطع الاتصال:', context.user.id);
// تنظيف الموارد
};
iterator.return = async () => {
cleanup();
return { value: undefined, done: true };
};
return iterator;
}
}
}
};
اشتراك جانب العميل (React)
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_SUBSCRIPTION = gql`
subscription MessageAdded($channelId: ID!) {
messageAdded(channelId: $channelId) {
id
content
author {
id
name
}
createdAt
}
}
`;
function ChatRoom({ channelId }) {
const { data, loading, error } = useSubscription(
MESSAGE_SUBSCRIPTION,
{ variables: { channelId } }
);
if (loading) return <p>جارٍ الاتصال...</p>;
if (error) return <p>خطأ: {error.message}</p>;
return (
<div>
{data && (
<div className="new-message">
<strong>{data.messageAdded.author.name}:</strong>
{data.messageAdded.content}
</div>
)}
</div>
);
}
- نفذ اشتراكًا لتحديثات التعليقات المباشرة على منشور مدونة
- أضف تصفية لإرسال التعليقات فقط للمستخدمين الذين لديهم وصول إلى المنشور
- أنشئ تحويرًا ينشر حدث التعليق
- قم ببناء مكون React يشترك ويعرض التعليقات الجديدة في الوقت الفعلي
- أضف المصادقة إلى سياق الاشتراك