מנויי GraphQL: WebSocket, Redis, קנה מידה מוכן לשימוש

כאשר נתוני האתר חייבים להתעדכן באופן מיידי—בצ'אטים, במעקב הזמנות או בסטטיסטיקות חיות—סקרים מסורתיים יוצרים עומס ועיכובים נוספים. אנו מיישמים GraphQL Subscriptions באמצעות WebSocket כך שהממשק שלך מקבל שינויים בזמן אמת ללא בקשות חוזרות. הצוות שלנו מספק את הפרויקט במפתח מלא, מתכנון הארכיטקטורה ועד לתמיכה, ומבטיח פעילות יציבה גם בעומסי שיא.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
מנויי GraphQL: WebSocket, Redis, קנה מידה מוכן לשימוש
מורכב
~3-5 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1365
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1330
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1077
  • פיתוח CRM עבור Chasseurs
    פיתוח CRM עבור Chasseurs
    1113
  • פיתוח אתר עבור SBH Partners
    פיתוח אתר עבור SBH Partners
    1201
  • פיתוח אתר עבור Red Pear
    פיתוח אתר עבור Red Pear
    619

מינויים ל-GraphQL בזמן אמת (WebSocket + Redis)

באתר עם API של GraphQL, לעיתים קרובות יש צורך לעדכן נתונים ללא טעינת עמוד מחדש: צ'אט, מעקב אחר הזמנות, סטטיסטיקות חיות. הפתרון הרגיל הוא polling, אך הוא יוצר עומס נוסף על השרת ומגדיל את זמן ההשהיה. מינויים ל-GraphQL דרך WebSocket הם פתרון יעיל יותר. אנו מיישמים מינויים ל-GraphQL מאז 2019 והשלמנו למעלה מ-20 פרויקטים עם פונקציונליות בזמן אמת. מאמר זה מכסה פתרונות מעשיים, מלכודות ותצורות מוכחות.

בפרויקט אחד עם 10,000 משתמשי צ'אט בו-זמנית, החלפת polling במינויים הפחיתה את עומס השרת ב-50% וקיצצה את זמן ההשהיה בעדכונים מ-5 שניות ל-200 מילישניות. תוצאה כזו אפשרית רק עם ארכיטקטורת מינויים נכונה. אנו מבטיחים פעולה יציבה של המינויים גם בעומסי שיא—זה מאושר על ידי הניסיון שלנו.

מתי זה נחוץ

מינויים מכסים משימות שבהן ממשק המשתמש חייב להתעדכן ללא פעולת משתמש: צ'אט בזמן אמת, התראות אירועים, מעקב אחר סטטוס הזמנות, סטטיסטיקות חיות, עריכה שיתופית. אם כבר יש לך API של GraphQL, הוספת מינויים היא הדרך הקצרה ביותר לזמן אמת. בפרויקט אחד עם 10,000 משתמשי צ'אט בו-זמנית, החלפנו polling במינויים וחצינו את עומס השרת.

צד השרת: Node.js + graphql-ws

התקן המודרני הוא החבילה graphql-ws, המיישמת את פרוטוקול graphql-transport-ws המשופר. הנה דוגמה לשרת עם שני מינויים (צ'אט וסטטוס הזמנה):

import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

const typeDefs = `
  type Message {
    id: ID!
    roomId: String!
    authorId: String!
    text: String!
    createdAt: String!
  }

  type OrderStatus {
    orderId: ID!
    status: String!
    updatedAt: String!
  }

  type Query {
    messages(roomId: String!): [Message!]!
  }

  type Mutation {
    sendMessage(roomId: String!, text: String!): Message!
  }

  type Subscription {
    messageAdded(roomId: String!): Message!
    orderStatusChanged(orderId: ID!): OrderStatus!
  }
`;

const resolvers = {
  Mutation: {
    sendMessage: async (_, { roomId, text }, { userId }) => {
      const message = await MessageService.create({ roomId, text, authorId: userId });
      pubsub.publish(`MESSAGE_ADDED:${roomId}`, { messageAdded: message });
      return message;
    },
  },
  Subscription: {
    messageAdded: {
      subscribe: (_, { roomId }) => pubsub.asyncIterator(`MESSAGE_ADDED:${roomId}`)
    },
    orderStatusChanged: {
      subscribe: (_, { orderId }) => pubsub.asyncIterator(`ORDER_STATUS:${orderId}`)
    },
  },
};

const schema = makeExecutableSchema({ typeDefs, resolvers });

const httpServer = createServer();
const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' });

useServer({
  schema,
  context: async (ctx) => {
    const token = ctx.connectionParams?.authToken;
    const user = await verifyToken(token as string);
    return { userId: user?.id };
  },
  onConnect: async (ctx) => {
    const token = ctx.connectionParams?.authToken;
    if (!token) return false;
    return true;
  },
}, wsServer);

httpServer.listen(4000);
מידע נוסף על טיפול בשגיאות

כאשר החיבור נשבר, graphql-ws מנסה אוטומטית להתחבר מחדש. בצד השרת, חשוב לסגור כראוי איטרטורים: השתמש ב-finally ב-resolvers או הירשם לאירוע הסגירה.

סינון אירועים בצד השרת

לסינון אירועים, אנו משתמשים ב-import { createServer } from 'http'; import { WebSocketServer } from 'ws'; import { useServer } from 'graphql-ws/lib/use/ws'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { PubSub } from 'graphql-subscriptions'; const pubsub = new PubSub(); const typeDefs = ` type Message { id: ID! roomId: String! authorId: String! text: String! createdAt: String! } type OrderStatus { orderId: ID! status: String! updatedAt: String! } type Query { messages(roomId: String!): [Message!]! } type Mutation { sendMessage(roomId: String!, text: String!): Message! } type Subscription { messageAdded(roomId: String!): Message! orderStatusChanged(orderId: ID!): OrderStatus! } `; const resolvers = { Mutation: { sendMessage: async (_, { roomId, text }, { userId }) => { const message = await MessageService.create({ roomId, text, authorId: userId }); pubsub.publish(`MESSAGE_ADDED:${roomId}`, { messageAdded: message }); return message; }, }, Subscription: { messageAdded: { subscribe: (_, { roomId }) => pubsub.asyncIterator(`MESSAGE_ADDED:${roomId}`) }, orderStatusChanged: { subscribe: (_, { orderId }) => pubsub.asyncIterator(`ORDER_STATUS:${orderId}`) }, }, }; const schema = makeExecutableSchema({ typeDefs, resolvers }); const httpServer = createServer(); const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' }); useServer({ schema, context: async (ctx) => { const token = ctx.connectionParams?.authToken; const user = await verifyToken(token as string); return { userId: user?.id }; }, onConnect: async (ctx) => { const token = ctx.connectionParams?.authToken; if (!token) return false; return true; }, }, wsServer); httpServer.listen(4000); מ-withFilter. זה מאפשר שליחת אירועים רלוונטיים בלבד למנוי. לדוגמה, מינוי להודעות בחדר ספציפי: graphql-subscriptions. גישה זו חוסכת רוחב פס ומשאבים.

למה להשתמש ב-Redis PubSub לסקייל?

ה-PubSub המובנה מ-withFilter(() => pubsub.asyncIterator('MESSAGE_ADDED'), (payload, variables) => payload.messageAdded.roomId === variables.roomId) הוא בזיכרון ופועל רק בתוך תהליך יחיד. עם מספר מופעי יישום (סקייל אופקי), אירועים לא מגיעים למנויים בשרתים אחרים. Redis PubSub פותר בעיה זו. השוואה:

מאפיין PubSub בזיכרון Redis PubSub
סקייל תהליך יחיד בלבד כל מספר מופעים
ביצועים גבוהים (בזיכרון) גבוהים (החלפת רשת)
מורכבות הגדרה אין נמוכה (הפעלת Redis)
מתאים לעומס עד ~1000 מינויים אלפי מינויים ומעלה

דוגמה לחיבור Redis PubSub:

import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

const options = {
  host: process.env.REDIS_HOST,
  port: 6379,
};

const pubsub = new RedisPubSub({
  publisher: new Redis(options),
  subscriber: new Redis(options),
});

// Использование идентично — pubsub.publish() и pubsub.asyncIterator()

צד הלקוח: Apollo Client

הגדר את שכבת התעבורה כך ש-Query/Mutation יעברו דרך HTTP ומינויים דרך WebSocket:

import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { getMainDefinition } from '@apollo/client/utilities';

const httpLink = new HttpLink({ uri: '/graphql' });

const wsLink = new GraphQLWsLink(
  createClient({
    url: 'wss://example.com/graphql',
    connectionParams: () => ({
      authToken: localStorage.getItem('token'),
    }),
    shouldRetry: () => true,
    retryAttempts: 10,
  })
);

const splitLink = split(
  ({ query }) => {
    const def = getMainDefinition(query);
    return def.kind === 'OperationDefinition' && def.operation === 'subscription';
  },
  wsLink,
  httpLink
);

export const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

השתמש ב-hook graphql-subscriptions כדי להירשם לאירועים. סינון בצד השרת עם import { RedisPubSub } from 'graphql-redis-subscriptions'; import Redis from 'ioredis'; const options = { host: process.env.REDIS_HOST, port: 6379 }; const pubsub = new RedisPubSub({ publisher: new Redis(options), subscriber: new Redis(options), }); // Использование идентично — pubsub.publish() и pubsub.asyncIterator() מאפשר שימוש בערוץ יחיד במקום רבים.

איך להימנע מדליפות זיכרון עם מינויים?

כל חיבור יוצר איטרטור אסינכרוני. אם לא סוגרים אותו, הזיכרון גדל. import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { createClient } from 'graphql-ws'; import { getMainDefinition } from '@apollo/client/utilities'; const httpLink = new HttpLink({ uri: '/graphql' }); const wsLink = new GraphQLWsLink(createClient({ url: 'wss://example.com/graphql', connectionParams: () => ({ authToken: localStorage.getItem('token') }), shouldRetry: () => true, retryAttempts: 10, })); const splitLink = split( ({ query }) => { const def = getMainDefinition(query); return def.kind === 'OperationDefinition' && def.operation === 'subscription'; }, wsLink, httpLink ); export const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache() }); קורא אוטומטית ל-useSubscription בעת ביטול מינוי, אך הגנה נוספת היא withFilter מפורש ב-resolver. טעות אופיינית נוספת היא שכחת מחזור החיים של המינוי בצד הלקוח: בעת עזיבת עמוד, יש לבטל את המינוי.

איזה תעבורה עדיפה: WebSocket או SSE?

WebSocket מהיר פי 3 מ-SSE עבור יישומים בזמן אמת מכיוון שהוא מספק תקשורת דו-כיוונית ללא תקורה של חיבורים חוזרים. עבור מינויים ל-GraphQL, WebSocket הוא התקן בשל תאימות מלאה ל-graphql-ws. SSE מתאים רק לתרחישים פשוטים של התראות push.

השוואת תעבורה: WebSocket מול SSE לזמן אמת

מאפיין WebSocket SSE (Server-Sent Events)
תקשורת דו-כיוונית כן לא (משרת ללקוח בלבד)
תמיכה טבעית רחבה בכל מקום מלבד IE
פרוטוקול graphql-ws כן דורש מתאם
ביצועים גבוהים בינוניים
מורכבות הגדרה בינונית נמוכה

עבור מינויים ל-GraphQL, אנו ממליצים על WebSocket מכיוון שהוא מספק תקשורת דו-כיוונית ותאימות מלאה ל-graphql-ws.

אינטגרציה עם Backend של Laravel

אם ה-API של GraphQL הוא ב-PHP (Lighthouse), ניתן לפרסם אירועים דרך Redis מ-Laravel ולעבד אותם בשרת WebSocket של Node.js:

// Node.js слушает Redis и форвардит в pubsub
const subscriber = new Redis({ host: process.env.REDIS_HOST });
subscriber.psubscribe('ORDER_STATUS:*');
subscriber.on('pmessage', (pattern, channel, message) => {
  const orderId = channel.split(':')[1];
  pubsub.publish(`ORDER_STATUS:${orderId}`, JSON.parse(message));
});

גישה זו מספקת גמישות: Laravel נשאר מקור הנתונים, בעוד Node.js מטפל בזמן אמת.

בדיקת מינויים

אנו מבצעים בדיקות עומס באמצעות graphql-ws וסקריפטים מותאמים אישית. אנו מדמים עד 10,000 מינויים בו-זמנית, ומודדים זמן השהיה ויציבות. הדוח כולל זמן השהיה p95 ומספר המסירות המוצלחות. זה מבטיח שהמינויים יעמדו בעומס ייצור.

מה כלול בעבודה

אנו מספקים:

  • תיעוד של סכמת המינויים והאירועים.
  • קוד מקור לחלקי השרת והלקוח במאגר שלך.
  • בדיקות עומס עם דוח (הדמיית 10,000 מינויים בו-זמנית).
  • הדרכת צוות.
  • תמיכה למשך שבועיים לאחר ההשקה.

אנו מומחי GraphQL מוסמכים ומבטיחים את איכות היישום. צור קשר כדי לקבל ייעוץ ממהנדס.

תהליך העבודה

  1. ניתוח דרישות ובחירת תרחישי מינויים.
  2. עיצוב סכמת המינויים.
  3. יישום צד השרת (Node.js או אינטגרציה עם Laravel).
  4. הגדרת Redis PubSub וסקייל.
  5. אינטגרציה בצד הלקוח.
  6. בדיקות עומס ואופטימיזציה.
  7. פריסה.

לוחות זמנים משוערים

מינויים בסיסיים עם סוג אירוע אחד—מ-3 ימים. יישום מלא עם Redis PubSub, אימות ובדיקות—מ-1 עד 2 שבועות. אינטגרציה עם Laravel/Lighthouse—הוסף 2–3 ימים. עלות הפרויקט מחושבת באופן אישי—חיסכון בפיתוח הודות לפתרונות מוכנים.

הזמן יישום מינויים—המהנדסים שלנו יעזרו בארכיטקטורה. קבל ייעוץ: כתוב לנו.