בעת חיבור חזית 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 בסביבת ייצור.
הגדרת לקוח 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
}
}
} יישום תשלום
ליישום תשלום, בצע את השלבים הבאים:
- הגדר כתובת משלוח באמצעות 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". - בחר שיטת משלוח דרך
VENDURE_API_URL=http://localhost:3000 CHANNEL_TOKEN=my-token npx graphql-codegen. - העבר הזמנה למצב
# Каталог: список товаров 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 } } }. - הוסף תשלום עם
// 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); }. - השלם הזמנה (מועבר אוטומטית למצב
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 יציבים, סקלביליות, ועמידה בלוחות זמנים. צור קשר לייעוץ והערכת פרויקט.







