واجهات GraphQL

الواجهات وأنواع الاتحاد في GraphQL

20 دقيقة الدرس 25 من 35

فهم الأنواع المجردة

يوفر GraphQL ميزتين قويتين للتعامل مع البيانات متعددة الأشكال: الواجهات وأنواع الاتحاد. تسمح هذه الأنواع المجردة بتحديد حقول يمكن أن ترجع أنواع كائنات مختلفة متعددة، مما يجعل مخططك أكثر مرونة وتعبيرًا. إنها ضرورية لنمذجة سيناريوهات العالم الحقيقي حيث تشترك الكيانات في خصائص مشتركة أو يمكن أن تكون من أنواع مختلفة.

واجهات GraphQL

الواجهة هي نوع مجرد يحدد مجموعة من الحقول التي يجب أن تتضمنها الأنواع المنفذة:

interface Node { id: ID! createdAt: DateTime! updatedAt: DateTime! } type User implements Node { # الحقول المطلوبة من واجهة Node id: ID! createdAt: DateTime! updatedAt: DateTime! # حقول إضافية خاصة بـ User name: String! email: String! posts: [Post!]! } type Post implements Node { # الحقول المطلوبة من واجهة Node id: ID! createdAt: DateTime! updatedAt: DateTime! # حقول إضافية خاصة بـ Post title: String! content: String! author: User! } type Comment implements Node { # الحقول المطلوبة من واجهة Node id: ID! createdAt: DateTime! updatedAt: DateTime! # حقول إضافية خاصة بـ Comment text: String! author: User! post: Post! } type Query { # إرجاع أي نوع ينفذ Node node(id: ID!): Node # البحث عبر جميع أنواع Node search(query: String!): [Node!]! }
ملاحظة: يجب أن يتضمن أي نوع ينفذ واجهة جميع الحقول المحددة في الواجهة بنفس الأنواع. يمكن للأنواع المنفذة إضافة حقول إضافية بما يتجاوز ما تتطلبه الواجهة.

الاستعلام عن أنواع الواجهة

استخدم الأجزاء المضمنة للاستعلام عن الحقول الخاصة بالنوع:

query SearchContent { search(query: "GraphQL") { # الحقول المشتركة المتاحة على جميع أنواع Node id createdAt updatedAt # استخدم __typename لتحديد النوع المحدد __typename # أجزاء مضمنة للحقول الخاصة بالنوع ... on User { name email } ... on Post { title content author { name } } ... on Comment { text author { name } post { title } } } } # الاستجابة: { "data": { "search": [ { "id": "1", "createdAt": "2026-01-15T10:00:00Z", "updatedAt": "2026-01-15T10:00:00Z", "__typename": "Post", "title": "مقدمة إلى GraphQL", "content": "GraphQL هي لغة استعلام...", "author": { "name": "جون دو" } }, { "id": "2", "createdAt": "2026-01-16T11:30:00Z", "updatedAt": "2026-01-16T11:30:00Z", "__typename": "User", "name": "جين سميث", "email": "jane@example.com" } ] } }

تنفيذ واجهات متعددة

يمكن للنوع تنفيذ واجهات متعددة:

interface Node { id: ID! } interface Timestamped { createdAt: DateTime! updatedAt: DateTime! } interface Authored { author: User! publishedAt: DateTime } # Post ينفذ جميع الواجهات الثلاث type Post implements Node & Timestamped & Authored { # من Node id: ID! # من Timestamped createdAt: DateTime! updatedAt: DateTime! # من Authored author: User! publishedAt: DateTime # حقول خاصة بـ Post title: String! content: String! tags: [String!]! } # Comment ينفذ Node و Timestamped type Comment implements Node & Timestamped { id: ID! createdAt: DateTime! updatedAt: DateTime! text: String! author: User! } type Query { # إرجاع أي محتوى مؤلف authoredContent(authorId: ID!): [Authored!]! # إرجاع أي كيان موقوت recentActivity: [Timestamped!]! }
نصيحة: استخدم واجهات متعددة لتكوين السلوكيات المشتركة. هذا يتبع مبدأ فصل الواجهة ويجعل مخططك أكثر قابلية للصيانة.

أنواع الاتحاد

يمثل نوع الاتحاد كائنًا يمكن أن يكون أحد عدة أنواع، دون الحاجة إلى حقول مشتركة:

type User { id: ID! name: String! email: String! } type Post { id: ID! title: String! content: String! } type Comment { id: ID! text: String! } # SearchResult يمكن أن يكون User أو Post أو Comment union SearchResult = User | Post | Comment type Query { search(query: String!): [SearchResult!]! } # المحللات const resolvers = { Query: { search: async (parent, { query }, context) => { // البحث عبر مجموعات متعددة const users = await context.db.users.find({ name: query }); const posts = await context.db.posts.find({ title: query }); const comments = await context.db.comments.find({ text: query }); // إرجاع مصفوفة مختلطة من أنواع مختلفة return [...users, ...posts, ...comments]; } }, SearchResult: { __resolveType(obj) { // تحديد النوع بناءً على خصائص الكائن if (obj.email) return 'User'; if (obj.title) return 'Post'; if (obj.text) return 'Comment'; return null; } } };

الاستعلام عن أنواع الاتحاد

استخدم الأجزاء المضمنة للوصول إلى الحقول في أنواع أعضاء الاتحاد المحددة:

query SearchAll { search(query: "GraphQL") { __typename ... on User { id name email } ... on Post { id title content } ... on Comment { id text } } } # استجابة مع أنواع مختلطة: { "data": { "search": [ { "__typename": "User", "id": "1", "name": "خبير GraphQL", "email": "expert@example.com" }, { "__typename": "Post", "id": "10", "title": "أفضل ممارسات GraphQL", "content": "إليك بعض النصائح..." }, { "__typename": "Comment", "id": "50", "text": "شرح رائع لـ GraphQL!" } ] } }
تحذير: لا تشترك أنواع الاتحاد في أي حقول مشتركة. يجب عليك استخدام أجزاء مضمنة للوصول إلى أي حقول. إذا كان يجب أن تشترك الأنواع في حقول، فكر في استخدام الواجهات بدلاً من ذلك.

الواجهات مقابل أنواع الاتحاد

اختر النوع المجرد المناسب لحالتك الاستخدامية:

# ✅ استخدم الواجهة عندما تشترك الأنواع في حقول مشتركة interface Media { id: ID! title: String! url: String! uploadedAt: DateTime! } type Image implements Media { id: ID! title: String! url: String! uploadedAt: DateTime! width: Int! height: Int! format: String! } type Video implements Media { id: ID! title: String! url: String! uploadedAt: DateTime! duration: Int! resolution: String! } type Query { media(id: ID!): Media allMedia: [Media!]! } # يمكن للاستعلام الوصول إلى الحقول المشتركة بدون أجزاء query GetAllMedia { allMedia { id title url uploadedAt # الحقول الخاصة بالنوع تحتاج إلى أجزاء ... on Image { width height } ... on Video { duration } } } # ✅ استخدم الاتحاد عندما ترتبط الأنواع مفاهيميًا لكنها لا تشترك في حقول type TextPost { id: ID! text: String! } type ImagePost { id: ID! imageUrl: String! caption: String! } type VideoPost { id: ID! videoUrl: String! thumbnail: String! } union FeedItem = TextPost | ImagePost | VideoPost type Query { feed: [FeedItem!]! } # يتطلب الاستعلام أجزاء لجميع الحقول query GetFeed { feed { __typename ... on TextPost { id text } ... on ImagePost { id imageUrl caption } ... on VideoPost { id videoUrl thumbnail } } }

حقل __typename

يُرجع حقل __typename الوصفي اسم نوع الكائن:

query GetContent { search(query: "tutorial") { __typename # متاح دائمًا على أي نوع كائن ... on Post { id title } ... on Video { id duration } } } # المعالجة من جانب العميل بناءً على __typename function renderSearchResult(result) { switch (result.__typename) { case 'Post': return <PostCard post={result} />; case 'Video': return <VideoPlayer video={result} />; case 'User': return <UserProfile user={result} />; default: return null; } } // Apollo Client يتضمن تلقائيًا __typename // يتم استخدامه لتطبيع التخزين المؤقت وتحديد النوع const { data } = useQuery(SEARCH_QUERY); data.search.forEach(result => { console.log(result.__typename); // 'Post' أو 'Video' أو 'User' });

حل النوع

نفذ __resolveType لتحديد الأنواع الملموسة:

// للواجهات const resolvers = { Node: { __resolveType(obj, context, info) { // تحقق من حقل المميز if (obj.type === 'USER') return 'User'; if (obj.type === 'POST') return 'Post'; if (obj.type === 'COMMENT') return 'Comment'; // أو تحقق من الحقول الفريدة if (obj.email) return 'User'; if (obj.title && obj.content) return 'Post'; if (obj.text && obj.postId) return 'Comment'; // أو استخدم instanceof (إذا كنت تستخدم فئات) if (obj instanceof User) return 'User'; if (obj instanceof Post) return 'Post'; return null; // نوع غير معروف } } }; // لأنواع الاتحاد const resolvers = { SearchResult: { __resolveType(obj) { // استخدم حقل النوع الصريح إذا كان متاحًا if (obj.__typename) return obj.__typename; // خلاف ذلك استنتج من الهيكل if (obj.email && obj.posts) return 'User'; if (obj.title && obj.author) return 'Post'; if (obj.text && obj.post) return 'Comment'; throw new Error('تعذر حل النوع'); } } }; // بديل: إرجاع كائنات مع __typename const resolvers = { Query: { search: async (parent, { query }, context) => { const users = await context.db.users.find({ name: query }); const posts = await context.db.posts.find({ title: query }); return [ ...users.map(u => ({ ...u, __typename: 'User' })), ...posts.map(p => ({ ...p, __typename: 'Post' })) ]; } } };
أفضل ممارسة: قم بتضمين حقل مميز __typename أو type في نماذج قاعدة البيانات الخاصة بك. هذا يجعل حل النوع بسيطًا وصريحًا.

الاستعلامات متعددة الأشكال

مثال من العالم الحقيقي: موجز النشاط مع أنواع محتوى متعددة:

interface Activity { id: ID! timestamp: DateTime! actor: User! } type PostCreated implements Activity { id: ID! timestamp: DateTime! actor: User! post: Post! } type CommentAdded implements Activity { id: ID! timestamp: DateTime! actor: User! comment: Comment! post: Post! } type UserFollowed implements Activity { id: ID! timestamp: DateTime! actor: User! followedUser: User! } type LikeReceived implements Activity { id: ID! timestamp: DateTime! actor: User! likedContent: LikeableContent! } union LikeableContent = Post | Comment type Query { # موجز النشاط يعرض جميع الأنواع activityFeed(limit: Int = 20): [Activity!]! } # الاستعلام مع الأجزاء query GetActivityFeed { activityFeed(limit: 10) { # الحقول المشتركة id timestamp actor { name avatar } # الحقول الخاصة بالنوع ... on PostCreated { post { title excerpt } } ... on CommentAdded { comment { text } post { title } } ... on UserFollowed { followedUser { name avatar } } ... on LikeReceived { likedContent { __typename ... on Post { title } ... on Comment { text } } } } } # المحللات const resolvers = { Query: { activityFeed: async (parent, { limit }, { userId, db }) => { // جلب ودمج الأنشطة من جداول مختلفة const activities = await db.activities .find({ userId }) .sort({ timestamp: -1 }) .limit(limit); return activities; // يتضمن بالفعل __typename من قاعدة البيانات } }, Activity: { __resolveType(obj) { return obj.__typename; // 'PostCreated', 'CommentAdded', إلخ. } }, LikeableContent: { __resolveType(obj) { return obj.__typename; // 'Post' أو 'Comment' } } };

تركيب الأجزاء

أعد استخدام الأجزاء لاستعلامات أنظف:

# حدد أجزاء قابلة لإعادة الاستخدام fragment UserInfo on User { id name avatar email } fragment PostPreview on Post { id title excerpt publishedAt } fragment CommentPreview on Comment { id text createdAt } # الاستخدام في استعلام متعدد الأشكال query GetSearchResults { search(query: "GraphQL") { __typename ... on User { ...UserInfo posts { ...PostPreview } } ... on Post { ...PostPreview author { ...UserInfo } comments { ...CommentPreview } } ... on Comment { ...CommentPreview author { ...UserInfo } } } }
نصيحة: استخدم تركيب الأجزاء للحفاظ على قابلية قراءة وصيانة الاستعلامات متعددة الأشكال. حدد الأجزاء مرة واحدة وأعد استخدامها عبر استعلامات متعددة.

امتدادات الواجهة

قم بتوسيع الواجهات لإضافة أنواع منفذة جديدة:

# المخطط الأساسي interface Node { id: ID! } type User implements Node { id: ID! name: String! } # الامتداد في وحدة أخرى extend interface Node { createdAt: DateTime! # إضافة حقل إلى الواجهة } # يجب أن تتضمن جميع الأنواع المنفذة الآن createdAt type User implements Node { id: ID! name: String! createdAt: DateTime! # مطلوب بعد امتداد الواجهة } # إضافة نوع جديد ينفذ واجهة موجودة type Organization implements Node { id: ID! createdAt: DateTime! name: String! members: [User!]! }
تمرين: صمم مخطط نظام إدارة محتوى باستخدام الواجهات والاتحادات:
  1. أنشئ واجهة Content مع حقول مشتركة (id, title, createdAt, author)
  2. نفذ أنواع Article وVideo وPodcast مع حقول خاصة بالنوع
  3. أنشئ اتحاد MediaAsset لأنواع Image وAudio وFile
  4. صمم واجهة Notification لـ ContentPublished وCommentReceived وMentionReceived
  5. نفذ محللات __resolveType لجميع الأنواع المجردة
  6. اكتب استعلامات باستخدام أجزاء مضمنة و__typename
  7. أنشئ أجزاء قابلة لإعادة الاستخدام للأنماط الشائعة
اختبر مخططك باستعلامات متعددة الأشكال معقدة عبر مستويات متعددة من التجريد.