بناء BFF GraphQL مع Apollo Server

تحسين واجهات برمجة التطبيقات الأمامية باستخدام BFF لـ GraphQL وApollo Server

Page content

الأن Backend for Frontend (BFF) نمط مع GraphQL و Apollo Server يخلق بنية قوية لتطبيقات الويب الحديثة.

هذا النهج يسمح لك ببناء واجهات برمجة تطبيقات محسّنة للعميل تجمع البيانات من مصادر متعددة مع الحفاظ على فصل واضح للمسؤوليات.

git-flow

فهم نمط BFF

ظهر نمط Backend for Frontend كحل لتحديات دعم تطبيقات frontend متعددة (الويب، الجوال، سطح المكتب) ذات متطلبات بيانات مختلفة. بدلًا من إجبار جميع العملاء على استخدام واجهة برمجة تطبيقات واحدة عامة، يخلق BFF خدمات backend مخصصة لكل عميل حسب احتياجاته الخاصة.

الفوائد الرئيسية لـ BFF

  • واجهات برمجة تطبيقات محسّنة للعميل: يحصل كل frontend بالضبط على البيانات التي يحتاجها بتنسيق يتوقعه
  • تقليل تعقيد العميل: تنتقل منطق تجميع البيانات وتغييرها إلى الخلفية
  • التطور المستقل: يمكن للواجهات الأمامية أن تتطور دون التأثير على العملاء الآخرين أو الخدمات الأساسية
  • أداء أفضل: تقليل عدد الاتصالات وحجم الحمولات يحسن سرعة التطبيق
  • الاعتماد الذاتي للفرق: يمكن لفرق الواجهات الأمامية أن تمتلك BFF الخاصة بها، مما يسمح بإجراء التكرارات بشكل أسرع

لماذا يناسب GraphQL BFF تمامًا

لغة الاستعلام المرنة لـ GraphQL تجعلها مثالية لتنفيذ BFF:

  1. استرجاع البيانات بدقة: يطلب العملاء فقط الحقول التي يحتاجونها
  2. طلب واحد: تجميع البيانات من مصادر متعددة في استعلام واحد
  3. النوعية القوية: يوفر المخطط عقدة واضحة بين الواجهة الأمامية والخلفية
  4. القدرات الحية: تسمح الاشتراكات بتحديث البيانات الحية
  5. تجربة المطور: تبسيط التطوير عبر التحقق الذاتي وغرفة لعب GraphQL

إعداد Apollo Server لـ BFF

يوفر Apollo Server أساسًا قويًا لبناء طبقات BFF لـ GraphQL. دعنا نمر عبر إنشاء تنفيذ جاهز للإنتاج.

التثبيت والإعداد الأساسي

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { buildSubgraphSchema } from '@apollo/subgraph';
import { gql } from 'graphql-tag';

// تعريف مخطط GraphQL الخاص بك
const typeDefs = gql`
  type User {
    id: ID!
    email: String!
    name: String!
    orders: [Order!]!
  }

  type Order {
    id: ID!
    status: String!
    total: Float!
    items: [OrderItem!]!
  }

  type OrderItem {
    id: ID!
    productId: ID!
    quantity: Int!
    price: Float!
  }

  type Query {
    me: User
    user(id: ID!): User
    orders(userId: ID!): [Order!]!
  }
`;

// تنفيذ المُحلِّلين
const resolvers = {
  Query: {
    me: async (_, __, { dataSources, user }) => {
      return dataSources.userAPI.getUser(user.id);
    },
    user: async (_, { id }, { dataSources }) => {
      return dataSources.userAPI.getUser(id);
    },
    orders: async (_, { userId }, { dataSources }) => {
      return dataSources.orderAPI.getOrdersByUser(userId);
    },
  },
  User: {
    orders: async (parent, _, { dataSources }) => {
      return dataSources.orderAPI.getOrdersByUser(parent.id);
    },
  },
};

// إنشاء مثيل Apollo Server
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

// بدء الخادم
const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => ({
    token: req.headers.authorization,
    dataSources: {
      userAPI: new UserAPI(),
      orderAPI: new OrderAPI(),
    },
  }),
  listen: { port: 4000 },
});

console.log(`🚀 الخادم جاهز في ${url}`);

تنفيذ مصادر البيانات

تُوفر مصادر البيانات تجريدًا نظيفًا لاسترجاع البيانات من مصادر خلفية متعددة:

import { RESTDataSource } from '@apollo/datasource-rest';

class UserAPI extends RESTDataSource {
  override baseURL = 'https://api.example.com/users/';

  async getUser(id: string) {
    return this.get(`${id}`);
  }

  async getUsersByIds(ids: string[]) {
    return Promise.all(ids.map(id => this.getUser(id)));
  }

  async updateUser(id: string, data: any) {
    return this.patch(`${id}`, { body: data });
  }
}

class OrderAPI extends RESTDataSource {
  override baseURL = 'https://api.example.com/orders/';

  async getOrdersByUser(userId: string) {
    return this.get('', {
      params: { userId },
    });
  }

  async getOrder(id: string) {
    return this.get(`${id}`);
  }
}

تحسين باستخدام DataLoader

يجمع DataLoader بين طلبات البيانات ويقوم بتخزينها مؤقتًا لتجنب مشكلة N+1 في الاستعلامات:

import DataLoader from 'dataloader';

const createUserLoader = (userAPI: UserAPI) => 
  new DataLoader(async (ids: readonly string[]) => {
    const users = await userAPI.getUsersByIds([...ids]);
    return ids.map(id => users.find(user => user.id === id));
  });

// استخدامه في السياق
const context = async ({ req }) => ({
  dataSources: {
    userAPI: new UserAPI(),
    orderAPI: new OrderAPI(),
  },
  loaders: {
    userLoader: createUserLoader(new UserAPI()),
  },
});

أنماط BFF المتقدمة

تجميع خدمات متعددة

من أبرز نقاط قوة BFF هي دمج البيانات من خدمات خلفية متعددة:

const resolvers = {
  Query: {
    dashboard: async (_, __, { dataSources, user }) => {
      // استرجاع البيانات من خدمات متعددة في وقت واحد
      const [userData, orders, recommendations, analytics] = await Promise.all([
        dataSources.userAPI.getUser(user.id),
        dataSources.orderAPI.getRecentOrders(user.id),
        dataSources.recommendationAPI.getRecommendations(user.id),
        dataSources.analyticsAPI.getUserStats(user.id),
      ]);

      return {
        user: userData,
        recentOrders: orders,
        recommendations,
        stats: analytics,
      };
    },
  },
};

معالجة الأخطاء والمتانة

قم بتنفيذ معالجة أخطاء قوية لـ BFF في الإنتاج:

import { GraphQLError } from 'graphql';

const resolvers = {
  Query: {
    user: async (_, { id }, { dataSources }) => {
      try {
        const user = await dataSources.userAPI.getUser(id);
        
        if (!user) {
          throw new GraphQLError('العميل غير موجود', {
            extensions: {
              code: 'NOT_FOUND',
              http: { status: 404 },
            },
          });
        }
        
        return user;
      } catch (error) {
        if (error instanceof GraphQLError) throw error;
        
        throw new GraphQLError('فشل في استرجاع العميل', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR',
            originalError: error.message,
          },
        });
      }
    },
  },
};

استراتيجيات التخزين المؤقت

قم بتنفيذ تخزين مؤقت فعّال لتحسين الأداء:

import { KeyValueCache } from '@apollo/utils.keyvaluecache';
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  cache: new InMemoryLRUCache({
    maxSize: Math.pow(2, 20) * 100, // 100 MB
    ttl: 300, // 5 دقائق
  }),
  plugins: [
    {
      async requestDidStart() {
        return {
          async willSendResponse({ response, contextValue }) {
            // تعيين رؤوس التخزين المؤقت
            response.http.headers.set(
              'Cache-Control',
              'max-age=300, public'
            );
          },
        };
      },
    },
  ],
});

المصادقة والتفويض

أمين BFF بطرق مصادقة وتفويض مناسبة:

import jwt from 'jsonwebtoken';

const context = async ({ req }) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  
  if (!token) {
    return { user: null };
  }
  
  try {
    const user = jwt.verify(token, process.env.JWT_SECRET);
    return { user };
  } catch (error) {
    throw new GraphQLError('رمز المصادقة غير صحيح', {
      extensions: { code: 'UNAUTHENTICATED' },
    });
  }
};

// حماية المُحلِّلين
const resolvers = {
  Query: {
    me: (_, __, { user }) => {
      if (!user) {
        throw new GraphQLError('يجب أن تكون مسجّلاً دخولك', {
          extensions: { code: 'UNAUTHENTICATED' },
        });
      }
      return user;
    },
  },
};

Apollo Federation لخدمات الميكرو

عند العمل مع فرق وخدمات متعددة، تمكن Apollo Federation من بنية GraphQL موزعة:

import { buildSubgraphSchema } from '@apollo/subgraph';

const typeDefs = gql`
  extend schema
    @link(url: "https://specs.apollo.dev/federation/v2.3",
          import: ["@key", "@shareable"])

  type User @key(fields: "id") {
    id: ID!
    email: String!
    name: String!
  }
`;

const resolvers = {
  User: {
    __resolveReference: async (reference, { dataSources }) => {
      return dataSources.userAPI.getUser(reference.id);
    },
  },
};

const server = new ApolloServer({
  schema: buildSubgraphSchema({ typeDefs, resolvers }),
});

نصائح تحسين الأداء

  1. استخدم DataLoader: قم دائمًا بتنفيذ DataLoader لجمع طلبات البيانات وتخزينها مؤقتًا
  2. التخزين المؤقت على مستوى الحقول: تخزين الحسابات المكلفة على مستوى الحقول
  3. تحليل تعقيد الاستعلامات: قم بتقليص عمق وتعقيد الاستعلامات لمنع الاستغلال
  4. الاستعلامات المستمرة: استخدم الاستعلامات المستمرة في الإنتاج لتقليل حجم الحمولات
  5. ضغط الاستجابات: قم بتفعيل ضغط gzip/brotli للردود
  6. مراقبة أداء الاستعلامات: تتبع الاستعلامات البطيئة وتحسين المُحلِّلين
  7. استخدم CDN للخطط الثابتة: تخزين استعلامات التحقق من التفاصيل في الحافة

اختبار BFF الخاص بك

اكتب اختبارات شاملة لـ BFF الخاص بك لـ GraphQL:

import { ApolloServer } from '@apollo/server';

describe('استعلامات المستخدم', () => {
  let server: ApolloServer;

  beforeAll(() => {
    server = new ApolloServer({
      typeDefs,
      resolvers,
    });
  });

  it('يستخرج المستخدم حسب المعرف', async () => {
    const result = await server.executeOperation({
      query: `
        query GetUser($id: ID!) {
          user(id: $id) {
            id
            email
            name
          }
        }
      `,
      variables: { id: '123' },
    });

    expect(result.body.kind).toBe('single');
    if (result.body.kind === 'single') {
      expect(result.body.singleResult.data?.user).toEqual({
        id: '123',
        email: 'test@example.com',
        name: 'مستخدم الاختبار',
      });
    }
  });
});

اعتبارات النشر

التعبئة

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 4000

CMD ["node", "dist/index.js"]

تكوين البيئة

استخدم المتغيرات البيئية لتكوين التطبيق:

const config = {
  port: process.env.PORT || 4000,
  userServiceUrl: process.env.USER_SERVICE_URL,
  orderServiceUrl: process.env.ORDER_SERVICE_URL,
  jwtSecret: process.env.JWT_SECRET,
  nodeEnv: process.env.NODE_ENV || 'development',
};

المراقبة والمراقبة

قم بتنفيذ مراقبة شاملة:

import { ApolloServerPluginInlineTrace } from '@apollo/server/plugin/inlineTrace';
import { ApolloServerPluginLandingPageDisabled } from '@apollo/server/plugin/disabled';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    ApolloServerPluginInlineTrace(),
    process.env.NODE_ENV === 'production'
      ? ApolloServerPluginLandingPageDisabled()
      : ApolloServerPluginLandingPageLocalDefault(),
    {
      async requestDidStart() {
        const start = Date.now();
        return {
          async willSendResponse({ operationName, contextValue }) {
            const duration = Date.now() - start;
            console.log(`العملية ${operationName} استغرقت ${duration} مللي ثانية`);
          },
        };
      },
    },
  ],
});

الأخطاء الشائعة التي يجب تجنبها

  1. مشكلة الاستعلام N+1: استخدم DataLoader دائمًا للبيانات المرتبطة
  2. استرجاع البيانات الزائدة من الخلفية: تحسين استعلامات الخلفية بناءً على مجموعات الاختيار في GraphQL
  3. عدم وجود معالجة الأخطاء: تنفيذ معالجة الأخطاء والتسجيل بشكل صحيح
  4. عدم وجود حد من الطلب: حماية BFF من الاستغلال باستخدام حد من الطلب
  5. تجاهل الأمان: التحقق من المدخلات، تنفيذ المصادقة، وتحديد حد تعقيد الاستعلام
  6. تصميم المخطط غير الجيد: تصميم المخططات بالتفكير في احتياجات العميل
  7. عدم وجود استراتيجية تخزين مؤقت: تنفيذ تخزين مؤقت على مستويات متعددة

روابط مفيدة