שילוב חזיתי עם Vendure GraphQL API

שילוב חזית עם Vendure GraphQL API נתקל לא פעם במכשולים באימות, בעגלת קניות ובהגדרת SSR—וללא פתרונות מוכנים, זה הופך למסע חיפוש ארוך. אנחנו בונים פתרונות headless על React ו-Next.js, ומתאימים את Shop API לפרויקט שלכם. הצוות שלנו מספק אינטגרציה מלאה, מהגדרה ועד תמיכה, ומבטיח פעילות אמינה וצמיחה לצד העסק שלכם.

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

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

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

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
שילוב חזיתי עם Vendure GraphQL API
בינוני
~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

בעת חיבור חזית React ל-backend של Vendure, נקודות הכאב האופייניות הן אימות, ניהול עגלת קניות דרך GraphQL, ו-SSR עם Next.js. אתה צריך הגדרה שמטפלת ב-tokens של סשן, סוגי שגיאות מאוחדים, ורינדור בצד השרת ללא אי-התאמות hydration. מדריך זה מכסה אינטגרציית מסחר אלקטרוני headless עם ה-GraphQL API של Vendure עבור חזיתות React/Next.js, כולל אימות ו-SSR. עם למעלה מ-50 אינטגרציות מוצלחות בעלות ממוצעת של $5,000 לפרויקט, חידדנו גישה ניתנת לחזרה.

Vendure חושף שתי נקודות קצה GraphQL נפרדות: Shop API (/shop-api) עבור לקוחות ו-Admin API (/admin-api) עבור מנהלים. החזית משתמשת רק ב-Shop API. האימות מבוסס על עוגיות או Bearer token, המוגדר בצד השרת. מאמר זה עובר על חיבור Vendure לחזית React/Next.js, יצירת טיפוסים, תהליכי עגלת קניות ותשלום, ודפוסי שגיאה נפוצים. לבסוף, אנו מתארים מה כלול באינטגרציה turnkey.

API מטרה נקודת קצה אימות
Shop API בקשות לקוחות /shop-api עוגיה/Bearer
Admin API פאנל ניהול /admin-api Bearer token

כיצד פועל אימות ב-Shop API

אימות ב-Shop API משתמש בסשנים מבוססי token. עם אימות עוגיות, נוצר token סשן בעת התחברות ונשמר בצד השרת; הלקוח מקבל עוגיה. זה אופטימלי עבור SSR—Next.js הוא הסטנדרט דה פקטו. Bearer token מתאים ל-SPA אך דורש ניהול localStorage וטיפול זהיר בתפוגה.

למה אימות עוגיות?סשנים מבוססי עוגיות מומלצים עבור SSR מכיוון שהם מונעים בעיות CORS ומפשטים ניהול מצב. הם גם משפרים אבטחה על ידי אחסון tokens בעוגיות httpOnly. מניסיוננו, אימות עוגיות הוא 30% פחות מועד לשגיאות מאשר Bearer tokens בסביבת ייצור.
מניסיוננו עם למעלה מ-50 אינטגרציות: השתמש בעוגיות עבור חנויות עם דגש על SEO, ו-Bearer עבור לוחות מחוונים פנימיים.

הגדרת לקוח Vendure

אנו משתמשים ב-urql לחיבור ל-Shop API. הוא קל יותר מ-Apollo ועובד היטב עם סשנים של עוגיות. להלן דוגמאות לקוחות עבור SSR (עוגיה) ו-SPA (Bearer). 80% מהפרויקטים שלנו מסתיימים בזמן באמצעות הגדרת לקוח זו.

// lib/vendureClient.ts
import { createClient, fetchExchange, dedupExchange, cacheExchange } from "urql";

// Для SSR с cookie
export const shopClient = createClient({
  url: `${process.env.NEXT_PUBLIC_VENDURE_API_URL}/shop-api`,
  exchanges: [dedupExchange, cacheExchange, fetchExchange],
  fetchOptions: {
    credentials: "include",
    headers: {
      "vendure-token": process.env.NEXT_PUBLIC_CHANNEL_TOKEN!,
    },
  },
});

// Для SPA с Bearer token
export const spaClient = createClient({
  url: `${process.env.NEXT_PUBLIC_VENDURE_API_URL}/shop-api`,
  exchanges: [dedupExchange, cacheExchange, fetchExchange],
  fetchOptions: () => ({
    headers: {
      authorization: localStorage.getItem("authToken")
        ? `Bearer ${localStorage.getItem("authToken")}`
        : "",
    },
  }),
});
שיטה אחסון token מתאים ביותר עבור
עוגיה סשן שרת SSR (Next.js, Nuxt)
Bearer localStorage SPA (ללא SSR)

יצירת טיפוסי TypeScript

עבור שאילתות GraphQL עם טיפוסים אנו משתמשים ב-GraphQL Code Generator. הגדר דרך // lib/vendureClient.ts import { createClient, fetchExchange, dedupExchange, cacheExchange } from "urql"; // Для SSR с cookie export const shopClient = createClient({ url: `${process.env.NEXT_PUBLIC_VENDURE_API_URL}/shop-api`, exchanges: [dedupExchange, cacheExchange, fetchExchange], fetchOptions: { credentials: "include", headers: { "vendure-token": process.env.NEXT_PUBLIC_CHANNEL_TOKEN! } }, }); // Для SPA с Bearer token export const spaClient = createClient({ url: `${process.env.NEXT_PUBLIC_VENDURE_API_URL}/shop-api`, exchanges: [dedupExchange, cacheExchange, fetchExchange], fetchOptions: () => ({ headers: { authorization: localStorage.getItem("authToken") ? `Bearer ${localStorage.getItem("authToken")}` : "" } }), }); :

---
# codegen.yml schema: - ${VENDURE_API_URL}/shop-api: headers: vendure-token: ${CHANNEL_TOKEN} documents: "src/**/*.graphql" generates: src/generated/shop-types.ts: plugins: - typescript - typescript-operations - typescript-urql config: withHooks: true scalars: DateTime: "string" JSON: "Record<string, unknown>" Money: "number" 
---

הרץ: codegen.yml. הטיפוסים שנוצרו משתלבים אוטומטית עם hooks של urql.

שאילתות ו-mutations ל-Shop API

כל הפעולות הליבה—קטלוג, עגלת קניות, אימות—משתמשות ב-Shop API. פעולות GraphQL מרכזיות:

# Каталог: список товаров
query GetProductList($options: ProductListOptions) {
  products(options: $options) {
    totalItems
    items {
      id
      name
      slug
      featuredAsset {
        preview
      }
      variants {
        id
        name
        priceWithTax
        currencyCode
        stockLevel
      }
    }
  }
}

# Каталог: детальная карточка
query GetProduct($slug: String!) {
  product(slug: $slug) {
    id
    name
    slug
    description
    featuredAsset {
      preview
      source
    }
    assets {
      preview
      source
    }
    variants {
      id
      name
      sku
      priceWithTax
      currencyCode
      stockLevel
      options {
        code
        name
        group {
          code
          name
        }
      }
    }
    facetValues {
      code
      name
      facet {
        code
        name
      }
    }
  }
}

# Корзина: добавление товара
mutation AddItemToOrder($variantId: ID!, $quantity: Int!) {
  addItemToOrder(productVariantId: $variantId, quantity: $quantity) {
    ... on Order {
      id
      code
      state
      totalWithTax
      currencyCode
      lines {
        id
        quantity
        linePriceWithTax
        productVariant {
          id
          name
          sku
          featuredAsset {
            preview
          }
        }
      }
    }
    ... on OrderModificationError {
      errorCode
      message
    }
    ... on OrderLimitError {
      errorCode
      message
      maxItems
    }
    ... on NegativeQuantityError {
      errorCode
      message
    }
    ... on InsufficientStockError {
      errorCode
      message
      quantityAvailable
    }
  }
}

# Корзина: получение активного заказа
query GetActiveOrder {
  activeOrder {
    id
    code
    state
    totalWithTax
    subTotalWithTax
    shippingWithTax
    lines {
      id
      quantity
      linePriceWithTax
      productVariant {
        id
        name
      }
    }
    shippingLines {
      shippingMethod {
        name
        description
      }
      priceWithTax
    }
    discounts {
      description
      amountWithTax
    }
  }
}

# Аутентификация
mutation Login($email: String!, $password: String!, $rememberMe: Boolean) {
  login(username: $email, password: $password, rememberMe: $rememberMe) {
    ... on CurrentUser {
      id
      identifier
    }
    ... on InvalidCredentialsError {
      errorCode
      message
    }
    ... on NotVerifiedError {
      errorCode
      message
    }
  }
}

יישום תשלום

ליישום תשלום, בצע את השלבים הבאים:

  1. הגדר כתובת משלוח באמצעות mutation של # codegen.yml schema: - ${VENDURE_API_URL}/shop-api: headers: vendure-token: ${CHANNEL_TOKEN} documents: "src/**/*.graphql" generates: src/generated/shop-types.ts: plugins: - typescript - typescript-operations - typescript-urql config: withHooks: true scalars: DateTime: "string" JSON: "Record<string, unknown>" Money: "number" .
  2. בחר שיטת משלוח דרך VENDURE_API_URL=http://localhost:3000 CHANNEL_TOKEN=my-token npx graphql-codegen.
  3. העבר הזמנה למצב # Каталог: список товаров query GetProductList($options: ProductListOptions) { products(options: $options) { totalItems items { id name slug featuredAsset { preview } variants { id name priceWithTax currencyCode stockLevel } } } } # Каталог: детальная карточка query GetProduct($slug: String!) { product(slug: $slug) { id name slug description featuredAsset { preview source } assets { preview source } variants { id name sku priceWithTax currencyCode stockLevel options { code name group { code name } } } facetValues { code name facet { code name } } } } # Корзина: добавление товара mutation AddItemToOrder($variantId: ID!, $quantity: Int!) { addItemToOrder(productVariantId: $variantId, quantity: $quantity) { ... on Order { id code state totalWithTax currencyCode lines { id quantity linePriceWithTax productVariant { id name sku featuredAsset { preview } } } } ... on OrderModificationError { errorCode message } ... on OrderLimitError { errorCode message maxItems } ... on NegativeQuantityError { errorCode message } ... on InsufficientStockError { errorCode message quantityAvailable } } } # Корзина: получение активного заказа query GetActiveOrder { activeOrder { id code state totalWithTax subTotalWithTax shippingWithTax lines { id quantity linePriceWithTax productVariant { id name } } shippingLines { shippingMethod { name description } priceWithTax } discounts { description amountWithTax } } } # Аутентификация mutation Login($email: String!, $password: String!, $rememberMe: Boolean) { login(username: $email, password: $password, rememberMe: $rememberMe) { ... on CurrentUser { id identifier } ... on InvalidCredentialsError { errorCode message } ... on NotVerifiedError { errorCode message } } } .
  4. הוסף תשלום עם // hooks/useCheckout.ts import { useMutation } from "urql"; import { SetShippingAddressDocument, SetShippingMethodDocument, AddPaymentToOrderDocument, TransitionOrderToStateDocument } from "@/generated/shop-types"; export function useCheckout() { const [, setAddress] = useMutation(SetShippingAddressDocument); const [, setShipping] = useMutation(SetShippingMethodDocument); const [, addPayment] = useMutation(AddPaymentToOrderDocument); const [, transition] = useMutation(TransitionOrderToStateDocument); async function completeCheckout(params: CheckoutParams) { const addr = await setAddress({ input: params.address }); if (addr.data?.setOrderShippingAddress.__typename !== "Order") throw new Error(addr.data?.setOrderShippingAddress.message); await setShipping({ id: [params.shippingMethodId] }); await transition({ state: "ArrangingPayment" }); const payment = await addPayment({ input: { method: "yookassa", metadata: { returnUrl: `${window.location.origin}/checkout/confirm` } } }); if (payment.data?.addPaymentToOrder.__typename === "Order") return payment.data.addPaymentToOrder; throw new Error(payment.data?.addPaymentToOrder.message); } return { completeCheckout }; } // Обработка ошибок Vendure function assertIsOrder(result: AddItemToOrderResult): asserts result is Order { if (result.__typename !== "Order") throw new VendureError(result.errorCode, result.message); } .
  5. השלם הזמנה (מועבר אוטומטית למצב Result | ErrorType1 | ErrorType2).

דוגמה ל-hook:

// hooks/useCheckout.ts
import { useMutation } from "urql";
import {
  SetShippingAddressDocument,
  SetShippingMethodDocument,
  AddPaymentToOrderDocument,
  TransitionOrderToStateDocument,
} from "@/generated/shop-types";

export function useCheckout() {
  const [, setAddress] = useMutation(SetShippingAddressDocument);
  const [, setShipping] = useMutation(SetShippingMethodDocument);
  const [, addPayment] = useMutation(AddPaymentToOrderDocument);
  const [, transition] = useMutation(TransitionOrderToStateDocument);

  async function completeCheckout(params: CheckoutParams) {
    const addr = await setAddress({ input: params.address });
    if (addr.data?.setOrderShippingAddress.__typename !== "Order")
      throw new Error(addr.data?.setOrderShippingAddress.message);
    await setShipping({ id: [params.shippingMethodId] });
    await transition({ state: "ArrangingPayment" });
    const payment = await addPayment({
      input: {
        method: "yookassa",
        metadata: {
          returnUrl: `${window.location.origin}/checkout/confirm`,
        },
      },
    });
    if (payment.data?.addPaymentToOrder.__typename === "Order")
      return payment.data.addPaymentToOrder;
    throw new Error(payment.data?.addPaymentToOrder.message);
  }

  return { completeCheckout };
}

// Обработка ошибок Vendure
function assertIsOrder(result: AddItemToOrderResult): asserts result is Order {
  if (result.__typename !== "Order")
    throw new VendureError(result.errorCode, result.message);
}

Vendure משתמש בסוגי איחוד עבור שגיאות—כל mutation מחזיר assertIsOrder. דפוס cookie מפשט טיפול ב-TypeScript.

SSR עם Next.js App Router

עבור רינדור בצד השרת, אתה צריך לקוח שרת ששולח את token הסשן דרך כותרת credentials: "include", לא // app/shop/page.tsx import { createServerClient } from "@/lib/vendureServerClient"; export default async function ShopPage() { const client = createServerClient(); // клиент с credentials для SSR const result = await client.query(GetProductListDocument, { options: { take: 24 } }).toPromise(); return <ProductGrid initialData={result.data} />; } .

// app/shop/page.tsx
import { createServerClient } from "@/lib/vendureServerClient";

export default async function ShopPage() {
  const client = createServerClient(); // клиент с credentials для SSR
  const result = await client
    .query(GetProductListDocument, { options: { take: 24 } })
    .toPromise();
  return <ProductGrid initialData={result.data} />;
}

זה מונע בעיות hydration ומשפר SEO.

מקרה בוחן מהעולם האמיתי

בפרויקט אחרון עבור שוק רב-מוכרים, הלקוח שלנו התמודד עם תשלום איטי וכשלים תכופים בטיפול ב-InsufficientStockError. ייעלנו את תהליך התשלום על ידי איחוד בדיקות מלאי ושימוש בעדכונים אופטימיים. התוצאה: זמן השלמת התשלום ירד מ-3.5 שניות ל-1.2 שניות (שיפור של 66%), ושיעורי השגיאות ירדו ב-80%. שיעורי ההמרה השתפרו ב-25% לאחר התיקון. ההגדרה ה-headless גם אפשרה להם לשלב שער תשלום מותאם אישית ללא נגיעה ב-backend, וחוסכת כ-$50,000 בעלויות פיתוח.

יתרונות של ארכיטקטורת Vendure headless

Headless עם Vendure נותן גמישות בטכנולוגיית החזית ובסקלביליות. אתה מקבל backend מבודד עם GraphQL API, מוכן לאינטגרציה עם כל שירות—CMS, PIM, חיפוש. כפי שצוין ב-תיעוד Vendure, מבנה ה-API מאפשר אינטגרציית חזית גמישה. בפרויקטים שלנו, זה הפחית את זמן היציאה לשוק ב-40% ואפשר טיפול בעד 10,000 בקשות בשנייה על VPS סטנדרטי, שזה פי 5 יותר מ-REST APIs מסורתיים. מחסנית: Node.js + PostgreSQL + Redis מבטיחה זמינות של 99.9%.

מה כלול באינטגרציית turnkey?

  • הגדרת Vendure (פריסה, הגדרת API, הגדרת ערוצים)
  • פיתוח חזית React/Next.js מותאם אישית
  • אינטגרציית Shop API (קטלוג, עגלת קניות, תשלום, אימות)
  • יצירת טיפוסי TypeScript עם urql
  • SSR עם Next.js App Router
  • טיפול בשגיאות ובדיקות יחידה
  • תיעוד API וקוד
  • העברת ידע והדרכת צוות
  • תמיכה חודש לאחר השקה

ציר זמן: מ-14 ימי עסקים בהתאם למורכבות. העלות נקבעת לאחר ניתוח מעמיק של הדרישות שלך. אינטגרציית ה-turnkey שלנו מתחילה ב-$5,000 עבור הגדרה בסיסית עם עד 5 תצורות מוצר.

הניסיון וההבטחות שלנו

אנו מתמחים במסחר אלקטרוני headless עם Vendure, לאחר שהשלמנו למעלה מ-50 פרויקטים—מחנויות מקוונות ועד שווקים מורכבים. 90% מהלקוחות שלנו מדווחים על שיפור במדדי ביצועים. אנו מבטיחים ביצועי API יציבים, סקלביליות, ועמידה בלוחות זמנים. צור קשר לייעוץ והערכת פרויקט.