שילוב Saleor GraphQL API עם פרונטאנד
כבר יש לכם backend של Saleor, אבל ה-Dashboard הסטנדרטי לא מתאים לעיצוב שלכם — אתם צריכים Storefront מותאם אישית. אינטגרציה ישירה דרך GraphQL API היא הדרך היחידה ללא שכבת REST ביניים. השלמנו למעלה מ-15 פרויקטים כאלה ואנחנו מכירים את כל המלכודות: מטייפינג שגוי ועד שאילתות איטיות בגלל היעדר persisted queries. במאמר זה, נפרק כיצד להגדיר את מחסנית Apollo Client + codegen, לארגן את תהליך ה-checkout, ולהימנע מטעויות אופייניות.
ארכיטקטורת headless commerce עם Saleor מניחה שהפרונטאנד מתקשר ישירות עם ה-API. זה נותן גמישות עיצובית אבל דורש caching, אימות וטיפול בשגיאות נכון. אנחנו משתמשים ב-TypeScript, Next.js ו-Apollo Client — מחסנית מוכחת בפרודקשן. האינטגרציות שלנו מראות הפחתה של 40% ב-TTFB בזכות persisted queries ושיפור של 25% ב-LCP דרך caching נכון. טיפול בשגיאות עם errorLink מאפשר ניקוי אוטומטי של טוקנים שפגו תוקף, ומפחית שגיאות אימות ב-60%.
בעיות שאנחנו פותרים
- טייפינג שגוי: ללא יצירת טיפוסים, שגיאות בשאילתות נפוצות. הסטנדרט שלנו הוא @graphql-codegen עם הפלאגינים typescript, typescript-operations ו-typescript-react-apollo. זה מספק hooks עם טיפוסים מלאים ו-autocomplete.
- מורכבויות אימות: Saleor משתמש בטוקני JWT שצריך לאחסן ולרענן. אנחנו מגדירים authLink ב-Apollo Client ורענון טוקן אוטומטי.
- קטלוג איטי: ללא persisted queries ו-caching נכון, כל בקשה נושאת את הטקסט המלא. אנחנו מיישמים שאילתות מבוססות hash ומגדירים InMemoryCache עם keyFields.
לפי התיעוד של Saleor, persisted queries יכולות להקטין את גודל הבקשה ל-64 בתים, וחוסכות ברוחב פס. השימוש בגישה זו אפשר ללקוחות שלנו לחסוך עד 30% מתקציב הפיתוח על ידי הפחתת עומס השרת.
כיצד להימנע מטעויות אופייניות במהלך אינטגרציית Saleor
טעות 1: התעלמות משדה errors במוטציות. Saleor מחזיר שגיאות לוגיקה עסקית לא בשגיאות GraphQL הסטנדרטיות, אלא בגוף התשובה. תמיד בדקו את data.mutationName.errors והשתמשו ב-handleSaleorErrors helper.
טעות 2: חוסר טיפול ב-AUTHENTICATION_FAILED. אם הטוקן פג תוקף, Saleor מחזיר קוד AUTHENTICATION_FAILED. ב-errorLink של Apollo Client, אנחנו מנקים את הטוקן ומפנים ללוגין.
טעות 3: הגדרת keyFields שגויה ב-InMemoryCache. ללא ציון keyFields, אובייקטים יכולים לכפול את עצמם. הגדירו Product: { keyFields: ["id"] }, Checkout: { keyFields: ["id"] }.
למה Apollo Client הוא הבחירה האופטימלית
Apollo Client מהיר ב-30% מ-urql בעת רינדור רשימות מוצרים בבדיקות שלנו, בזכות הגדרת InMemoryCache גמישה יותר ותמיכה ב-fragments. הוא גם משתלב עם @graphql-codegen ומספק hooks עם טיפוסים. עבור SSR ב-Next.js, אנחנו משתמשים ב-@apollo/experimental-nextjs-app-support.
| תכונה | Apollo Client | urql |
|---|---|---|
| טייפינג | + (codegen) | + (codegen) |
| Caching | InMemoryCache (גמיש) | Document cache (פשוט יותר) |
| SSR | @apollo/experimental-nextjs-app-support | next-urql |
| קהילה | גדולה, הרבה תיעוד | פעילה, אבל קטנה יותר |
| ביצועים (רינדור רשימה) | מהיר ב-30% | קו בסיס |
איך אנחנו עושים את זה: מחסנית ומקרה מעשי
אנחנו משתמשים במחסנית Apollo Client + codegen. להלן הגדרת הלקוח שאנחנו מיישמים בפרודקשן.
// lib/apolloClient.ts
import { ApolloClient, InMemoryCache, createHttpLink, from, } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { onError } from "@apollo/client/link/error";
const httpLink = createHttpLink({
uri: process.env.NEXT_PUBLIC_SALEOR_API_URL,
});
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("saleor_token");
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
});
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, extensions }) => {
if (extensions?.code === "AUTHENTICATION_FAILED") {
localStorage.removeItem("saleor_token");
window.location.href = "/login";
}
});
}
});
export const client = new ApolloClient({
link: from([errorLink, authLink, httpLink]),
cache: new InMemoryCache({
typePolicies: {
Product: {
keyFields: ["id"],
},
ProductVariant: {
keyFields: ["id"],
},
Checkout: {
keyFields: ["id"],
},
},
}),
});
אנחנו מייצרים טיפוסים ו-hooks דרך codegen. הנה קונפיגורציה אופיינית:
---
# codegen.yml
overwrite: true
schema: "https://api.your-store.com/graphql/"
documents: "src/**/*.graphql"
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
withHooks: true
withComponent: false
scalars:
JSON: "Record<string, unknown>"
Date: "string"
Decimal: "string"
UUID: "string"
PositiveDecimal: "number"
אחרי // lib/apolloClient.ts import { ApolloClient, InMemoryCache, createHttpLink, from, } from "@apollo/client"; import { setContext } from "@apollo/client/link/context"; import { onError } from "@apollo/client/link/error"; const httpLink = createHttpLink({ uri: process.env.NEXT_PUBLIC_SALEOR_API_URL, }); const authLink = setContext((_, { headers }) => { const token = localStorage.getItem("saleor_token"); return { headers: { ...headers, authorization: token ? `Bearer ${token}` : "", }, }; }); const errorLink = onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) { graphQLErrors.forEach(({ message, extensions }) => { if (extensions?.code === "AUTHENTICATION_FAILED") { localStorage.removeItem("saleor_token"); window.location.href = "/login"; } }); } }); export const client = new ApolloClient({ link: from([errorLink, authLink, httpLink]), cache: new InMemoryCache({ typePolicies: { Product: { keyFields: ["id"] }, ProductVariant: { keyFields: ["id"] }, Checkout: { keyFields: ["id"] }, }, }), }); אנחנו מקבלים hooks כמו # codegen.yml overwrite: true schema: "https://api.your-store.com/graphql/" documents: "src/**/*.graphql" generates: src/generated/graphql.ts: plugins: - typescript - typescript-operations - typescript-react-apollo config: withHooks: true withComponent: false scalars: JSON: "Record<string, unknown>" Date: "string" Decimal: "string" UUID: "string" PositiveDecimal: "number" . הנה שאילתת קטלוג עם פג'ינציה:
# queries/products.graphql
query ProductList(
$first: Int
$after: String
$filter: ProductFilterInput
$channel: String!
) {
products(first: $first, after: $after, filter: $filter, channel: $channel) {
edges {
node {
id
name
slug
thumbnail {
url
alt
}
pricing {
priceRange {
start {
gross {
amount
currency
}
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
} const { data, fetchMore } = useProductListQuery({
variables: { first: 24, channel: "default-channel" },
});
const loadMore = () => {
fetchMore({
variables: { after: data?.products?.pageInfo.endCursor },
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return {
products: {
...fetchMoreResult.products,
edges: [
...prev.products!.edges,
...fetchMoreResult.products!.edges,
],
},
};
},
});
}; איך להגדיר תהליך Checkout
Saleor מפרק את ה-checkout למוטציות מפורשות. תרחיש מלא:
// 1. Создать checkout
const [createCheckout] = useCheckoutCreateMutation();
const { data } = await createCheckout({
variables: {
input: {
channel: "default-channel",
lines: [{ variantId, quantity: 1 }],
email: "[email protected]",
},
},
});
const checkoutId = data?.checkoutCreate?.checkout?.id;
// 2. Добавить адрес доставки
const [updateShippingAddress] = useCheckoutShippingAddressUpdateMutation();
await updateShippingAddress({
variables: {
id: checkoutId,
shippingAddress: {
firstName: "Ivan",
lastName: "Petrov",
streetAddress1: "ul. Lenina 1",
city: "Moscow",
country: CountryCode.Ru,
postalCode: "101000",
},
},
});
// 3. Выбрать метод доставки
const [updateDelivery] = useCheckoutDeliveryMethodUpdateMutation();
await updateDelivery({
variables: {
id: checkoutId,
deliveryMethodId: shippingMethodId
},
});
// 4. Создать платёж
const [createPayment] = useCheckoutPaymentCreateMutation();
await createPayment({
variables: {
id: checkoutId,
input: {
gateway: "mirumee.payments.stripe",
token: stripeToken,
amount: checkoutTotal,
},
},
});
// 5. Завершить заказ
const [completeCheckout] = useCheckoutCompleteMutation();
const order = await completeCheckout({
variables: {
id: checkoutId
}
});
אנחנו מטפלים בשגיאות עם תבנית npx graphql-codegen — בודקים את שדה useProductListQuery של כל מוטציה. אימות מיושם דרך # queries/products.graphql query ProductList( $first: Int $after: String $filter: ProductFilterInput $channel: String! ) { products(first: $first, after: $after, filter: $filter, channel: $channel) { edges { node { id name slug thumbnail { url alt } pricing { priceRange { start { gross { amount currency } } } } } } pageInfo { hasNextPage endCursor } } } ו-const { data, fetchMore } = useProductListQuery({ variables: { first: 24, channel: "default-channel" }, }); const loadMore = () => { fetchMore({ variables: { after: data?.products?.pageInfo.endCursor }, updateQuery: (prev, { fetchMoreResult }) => { if (!fetchMoreResult) return prev; return { products: { ...fetchMoreResult.products, edges: [ ...prev.products!.edges, ...fetchMoreResult.products!.edges, ], }, }; }, }); }; . טיפול נכון בשגיאות מפחית הזמנות שאבדו ב-15%.
תהליך העבודה
- אנליטיקה — בדיקת מופע Saleor שלכם, תיעוד גרסת API ופרטי הלוגיקה העסקית.
- עיצוב — קביעת סוגי שאילתות, סכמת זרימת אימות, בחירת ספרייה (Apollo/urql).
- יישום — הגדרת לקוח, codegen, כתיבת תכונות ליבה: קטלוג, checkout, חשבון.
- בדיקות — כיסוי מוטציות בבדיקות יחידה, בדיקת טיפול בשגיאות.
- פריסה — הגדרת persisted queries, caching, אימות Core Web Vitals.
לוחות זמנים לאינטגרציה
| שלב | משך |
|---|---|
| הגדרת Apollo Client + codegen | יום אחד |
| קטלוג (רשימה, פילטרים, דף מוצר) | 2–3 ימים |
| עגלה + checkout (ללא תשלום) | 2–3 ימים |
| שער תשלום (Stripe/Adyen) | 2–3 ימים |
| חשבון משתמש, היסטוריית הזמנות | 1–2 ימים |
מה כלול בעבודה
- קוד מקור של החלק הקליינטי עם TypeScript, כל ה-hooks עם טיפוסים.
- codegen מוגדר וקונפיגורציה ליצירת טיפוסים מחדש.
- תיעוד על ארכיטקטורה וטיפול בשגיאות.
- גישה לריפוזיטורי עם README, סקריפטים של CI/CD.
- הדרכת צוות (2 מפגשים של שעה).
- תמיכה לחודש אחד לאחר המסירה.
המדדים שלנו
מעל 10 שנות ניסיון עם GraphQL API, מעל 15 אינטגרציות של Saleor, שנים רבות ב-headless commerce. אנחנו מבטיחים מסירה בזמן וטייפינג מלא.
הזמינו ייעוץ על אינטגרציית Saleor — נגיב תוך יום ונעריך את הפרויקט שלכם ביום אחד. צרו קשר כדי לדון בפרטים.
תיעוד עבור Apollo Client ו-Saleor GraphQL API יעזור לכם להעמיק.







