Vendure في الإنتاج: نقاط القوة، الثغرات، ومتى تختاره
تقييم معماري صريح لـ Vendure في بيئة الإنتاج. نظام الـ Plugins، الـ EventBus، خدمة الـ Worker، واجهة الإدارة، النشر متعدد الـ Pods، والمقارنة مع Medusa و Saleor.
ليش اخترنا Vendure (ومتى ما بنختاره)
نحنا شغالين على Vendure من أيام نسخه الأولى، وبصراحة نحبه كمنصة. تجربة المطور فيه ممتازة. بتكتب TypeScript بكل مكان، الـ NestJS dependency injection بتخلي الـ services نظيفة وقابلة للاختبار، نظام الـ plugins بيخليك توسّع أي شي بدون ما تعدّل الكود الأصلي، والـ GraphQL API مصممة صح من أول يوم. كفريق معماريين ومهندسين، كتير مهتمين بتجربة المطور، و Vendure بيقدمها أحسن من أي منصة تجارة إلكترونية اشتغلنا عليها.
هاد مش كلام تسويقي. هاد اللي بيصير لمّا تبني plugin-ين enterprise على إطار عمل وكل شي يشتغل متل ما بتتوقع. واحد منهم نظام ETL pipeline كامل (Vendure Data Hub Plugin) فيه 9 extractors و 61 transform operator و 24 entity loader. والتاني مجموعة customer intelligence من 6 modules بتغطي wishlists، reviews، برامج ولاء، استرجاع سلال مهجورة، تنبيهات توفر المنتج، والمنتجات المشاهدة مؤخرا. اخترنا Vendure لأنو نظام الـ plugins أعطانا المرونة نبني أنظمة معقدة بدون ما نحارب الإطار. الكود بيضل نظيف، الأنماط بتضل ثابتة، وإعادة الهيكلة آمنة لأنو TypeScript بيمسك كل شي وقت الـ compile.
بس Vendure مش مثالي لكل حالة استخدام. هالمقال تقييم صريح من منظور معمارية الأنظمة. إذا إنت CTO بتقيّم منصات تجارة، أو مهندس قائد بتخطط لمشروع، أو معماري بتصمم نظام متعدد القنوات، هاد اللي لازم تعرفه. لسياق أوسع عن كيف بنتعامل مع قرارات منصات التجارة الإلكترونية، ذاك الدليل بيغطي المشهد الكامل.
نظرة عامة على المعمارية
Vendure إطار عمل تجارة إلكترونية headless مبني على NestJS (Node.js) و TypeORM و GraphQL. النواة بتوفر المنتجات، الطلبات، العملاء، المدفوعات، الشحن، العروض الترويجية، ونظام plugins لتوسيع كل شي.
┌─────────────────────────────────────────────────────────────┐
│ CLIENT APPLICATIONS │
│ Storefront (Next.js, Nuxt, etc.) │
│ Mobile App (React Native, Flutter) │
│ Admin Dashboard (React, built-in) │
│ POS System, Marketplace, Kiosk │
└────────────────────────┬────────────────────────────────────┘
│ GraphQL (Shop API + Admin API)
▼
┌─────────────────────────────────────────────────────────────┐
│ VENDURE SERVER │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Shop API │ │ Admin API │ │ Plugin APIs │ │
│ │ (customer) │ │ (backoffice)│ │ (extensions) │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ ┌──────▼─────────────────▼────────────────────▼─────────┐ │
│ │ NestJS Core │ │
│ │ Services │ Resolvers │ Guards │ Interceptors │ │
│ └──────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────▼────────────────────────────────┐ │
│ │ EventBus │ │
│ │ ProductEvent │ OrderEvent │ CustomerEvent │ Custom │ │
│ └──────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────▼────────────────────────────────┐ │
│ │ TypeORM + Database │ │
│ │ PostgreSQL (prod) / SQLite (dev) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Worker Service (BullMQ) │ │
│ │ Job Queues │ Email │ Search Index │ Custom Jobs │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
شو اللي Vendure بيسويه بشكل ممتاز
1. نظام الـ Plugins
هاد أقوى ميزة تميز Vendure. الـ plugin يقدر يوسّع كل طبقة بالنظام: entities، services، resolvers، GraphQL schema، واجهة الإدارة، workers، ومعالجات الأحداث. كل هاد بدون تعديل الكود الأصلي.
import { PluginCommonModule, VendurePlugin } from '@vendure/core';
import { WishlistService } from './services/wishlist.service';
import { WishlistShopResolver } from './resolvers/wishlist-shop.resolver';
import { WishlistAdminResolver } from './resolvers/wishlist-admin.resolver';
import { CiWishlist } from './entities/wishlist.entity';
import { CiWishlistItem } from './entities/wishlist-item.entity';
import { wishlistShopSchema, wishlistAdminSchema } from './schemas';
@VendurePlugin({
imports: [PluginCommonModule],
entities: [CiWishlist, CiWishlistItem],
providers: [WishlistService],
shopApiExtensions: {
schema: wishlistShopSchema,
resolvers: [WishlistShopResolver],
},
adminApiExtensions: {
schema: wishlistAdminSchema,
resolvers: [WishlistAdminResolver],
},
configuration: (config) => {
// تعديل إعدادات النواة إذا لزم الأمر
return config;
},
})
export class WishlistPlugin {}
شو اللي بيخليه ممتاز فعلا:
- توسيع الـ Entities: أضف TypeORM entities جديدة وبتحصل على جداول خاصة فيها، migrations، وعلاقات
- توسيع الـ Schema: وسّع الـ Shop API والـ Admin API GraphQL schemas بشكل مستقل
- فصل الـ Resolvers: الـ Shop API resolvers (للعملاء) والـ Admin API resolvers (للإدارة) دايما classes منفصلة. هاد قيد تصميمي بيمنع كشف عمليات الإدارة للعملاء بالغلط
- حقن الـ Services: NestJS dependency injection كامل. الـ plugin services تبعك تقدر تحقن Vendure core services
- Custom fields: أضف حقول على أي core entity (Product, Customer, Order) بدون ما تلمس كود Vendure الأصلي
بنينا plugin-ين enterprise باستخدام هالنظام. الـ Data Hub Plugin بيضيف محرك ETL pipeline كامل مع 9 data extractors و 61 transform operator و 24 entity loader. والـ Customer Intelligence Plugin بيضيف 6 modules لتفاعل العملاء مع entities خاصة فيهم، GraphQL APIs، لوحات إدارة، ومهام خلفية. الاثنين شغالين بالإنتاج بدون تعديل ولا سطر واحد من كود Vendure الأصلي.
2. الـ EventBus
الـ EventBus تبع Vendure هو العمود الفقري للربط المرن بين الـ modules. كل عملية مهمة بالنظام بتطلق حدث:
// Vendure core بيطلق أحداث تلقائيا
ProductEvent // إنشاء/تحديث/حذف منتج
OrderStateTransitionEvent // تغييرات حالة الطلب
CustomerEvent // إنشاء/تحديث عميل
OrderPlacedEvent // طلب تم بنجاح
RefundStateTransitionEvent // تغييرات حالة الاسترداد
// الـ plugins تبعك بتطلق أحداث مخصصة
export class CiWishlistItemAddedEvent extends VendureEvent {
constructor(
public ctx: RequestContext,
public wishlistId: string,
public productVariantId: string,
) {
super();
}
}
// modules ثانية بتشترك بدون استيراد الـ module المرسل
@Injectable()
export class StockSubscriber {
constructor(private eventBus: EventBus) {
this.eventBus.ofType(OrderPlacedEvent).subscribe(event => {
// تحديث المخزون، إشعار المستودع، إلخ
});
}
}
القاعدة المعمارية الأساسية: الـ modules بتتواصل من خلال الـ EventBus بس. ما في استيراد services بين الـ modules. هاد بيمنع الـ circular dependencies وبيخلي الـ modules قابلة للنشر بشكل مستقل. لمّا بنينا الـ Customer Intelligence Plugin بـ 6 modules (wishlist, reviews, loyalty, cart recovery, back-in-stock, recently viewed)، كل module بيعرف بس عن الـ services الخاصة فيه. التنسيق بين الـ modules (مثلا نقاط ولاء مقابل review) بيصير عن طريق الأحداث.
3. TypeScript من البداية للنهاية (تجربة المطور اللي بتخلينا هون)
هون Vendure بيتألق فعلا كتجربة مطور. كل الـ stack مكتوب بـ TypeScript: السيرفر، الـ plugins، واجهة الإدارة، GraphQL code generation. كل شي typed، كل شي بيعمل compile، كل شي بيمسك الأخطاء قبل وقت التشغيل.
- عقود API type-safe من الـ database entity للـ GraphQL schema للـ frontend component
- الـ GraphQL schema بيولّد TypeScript types تلقائي مع codegen
- غيّر اسم حقل بالـ entity، والـ compiler بيوريك كل مكان لازم يتحدث
- لغة وحدة للـ backend services، تطوير الـ plugins، لوحة الإدارة، ومجموعات الاختبار
- NestJS decorators (
@Injectable(),@Transaction(),@Allow()) بتخلي النية واضحة بالكود RequestContextبيتدفق عبر كل method، حامل معه المصادقة، القناة، والـ locale
هاد مش شي بسيط. بمنصات التجارة المبنية على PHP (Magento, Sylius, وحتى طبقة التجارة في Pimcore)، بتخسر الـ type safety عند كل حدود. بـ Vendure، تغيير جذري بالـ product entity بيظهر كخطأ compile بالـ storefront query. دورة الملاحظات هاي هي الفرق بين دقيقتين لتصليح type mismatch وساعتين لتصحيح bug بالإنتاج.
المرونة كمان مذهلة. بدك checkout flow مخصص؟ اكتب service. بدك تعدّل كيف العروض الترويجية بتنطبق؟ عدّل الـ promotion strategy. بدك entity جديد كليا مع GraphQL API خاص فيه، صفحة بلوحة الإدارة، ومهمة خلفية؟ نظام الـ plugins بيتعامل مع كل هاد بأنماط نظيفة بتضل ثابتة مع نمو النظام. ما حسينا يوم إنو الإطار بيقيدنا، وهاد نادر بمنصات التجارة الإلكترونية.
4. خدمة الـ Worker (BullMQ)
Vendure بيفصل العمليات الطويلة بخدمة Worker مخصصة مدعومة بـ BullMQ (Redis):
// تعريف job queue بالـ plugin
const myJobQueue = new JobQueue<{ productId: string }>({
name: 'generate-feed',
process: async (ctx, job) => {
const product = await this.productService.findOne(ctx, job.data.productId);
await this.feedGenerator.generate(product);
},
});
// إرسال المهمة من أي مكان
await this.myJobQueue.add({ productId: '123' }, { ctx });
خدمة الـ Worker بتشتغل كـ process منفصل. بـ Kubernetes، هي deployment منفصل مع scaling مستقل. هاد بيفصل بشكل نظيف بين معالجة الطلبات (web pods) والمعالجة الخلفية (worker pods).
لطريقة تفكيرنا بالـ أنماط event-driven ومعماريات المهام الخلفية، دليل الهندسة تبعنا بيغطي المبادئ الأوسع.
5. تصميم الـ GraphQL API
Vendure بيوفر GraphQL API-ين منفصلين:
- Shop API: العمليات الموجهة للعملاء (تصفح المنتجات، إنشاء الطلبات، إدارة الحساب)
- Admin API: عمليات الإدارة (إدارة المنتجات، معالجة الطلبات، إعداد النظام)
الاثنين بيدعمو:
- Pagination تلقائي مع
ListQueryBuilder - فلترة وترتيب مدمجين
- صلاحيات على مستوى الحقل
- Custom fields على أي entity
// ListQueryBuilder بيولّد SQL فعال مع pagination, sorting, filtering
async findByCustomer(ctx: RequestContext, options?: ListQueryOptions<CiWishlist>) {
return this.listQueryBuilder
.build(CiWishlist, options ?? {}, { ctx })
.andWhere('entity.customerId = :customerId', { customerId: ctx.activeUserId })
.getManyAndCount()
.then(([items, totalItems]) => ({ items, totalItems }));
}
6. واجهة الإدارة React (Vendure 3)
Vendure 3 بيجي مع واجهة إدارة React كاملة، مصممة للتوسعة من الأساس. هاي بدّلت واجهة Angular القديمة من Vendure 1/2 وهي خطوة كبيرة للأمام.
مطورو الـ Plugins يقدرو:
- يضيفو صفحات ومسارات مخصصة للوحة الإدارة
- يوسّعو العروض الحالية بمكونات مخصصة
- يبنو تجارب dashboard كاملة باستخدام React و TanStack Query و TanStack Router
- يستخدمو مكونات Vendure Dashboard SDK (مثل Page, PageTitle, DetailFormGrid, ListPage)
- يسجلو عناصر قائمة مخصصة، widgets، وتبويبات تفاصيل
بنينا dashboards الـ plugins تبعنا بهالـ stack. تجربة المطور قوية وطبيعية لأي مطور React.
7. الـ Strategies ونقاط التوسعة
Vendure بيوفر واجهات strategy لتخصيص سلوك التجارة الأساسي بدون تعديل الكود المصدري:
// شرط عرض ترويجي مخصص
class MinimumOrderAmountCondition implements PromotionCondition {
code = 'minimum_order_amount';
description = [{ languageCode: LanguageCode.en, value: 'Minimum order amount' }];
args = {
amount: { type: 'int' },
};
check(ctx: RequestContext, order: Order, args: { amount: number }) {
return order.subTotal >= args.amount;
}
}
// حاسبة شحن مخصصة
class WeightBasedShippingCalculator implements ShippingCalculator {
calculate(ctx: RequestContext, order: Order, args: any) {
const totalWeight = order.lines.reduce((sum, line) =>
sum + (line.productVariant.customFields.weight || 0) * line.quantity, 0
);
return { price: totalWeight * args.pricePerKg, priceIncludesTax: false };
}
}
حساب الضرائب، معالجة المدفوعات، تنفيذ الطلبات، تخزين الأصول، وفهرسة البحث كلها عندها واجهات strategy. يعني تقدر تغيّر كيف الضريبة بتتحسب لدولة معينة، كيف المدفوعات بتتسجل، أو كيف الطلبات بتتنفذ بدون ما تلمس الكود الأصلي. الإطار بيوفر الـ hooks، وإنت بتوفر منطق العمل.
8. الـ Custom Fields على الـ Core Entities
من أكتر ميزات Vendure العملية. أضف حقول على أي core entity بدون migrations أو تعديلات على النواة:
// بإعدادات الـ plugin
customFields: {
Product: [
{ name: 'weight', type: 'float', defaultValue: 0, label: [{ languageCode: LanguageCode.en, value: 'Weight (kg)' }] },
{ name: 'erpId', type: 'string', unique: true, label: [{ languageCode: LanguageCode.en, value: 'ERP ID' }] },
{ name: 'countryOfOrigin', type: 'string', label: [{ languageCode: LanguageCode.en, value: 'Country of Origin' }] },
{ name: 'harmonizedCode', type: 'string', label: [{ languageCode: LanguageCode.en, value: 'HS Code' }] },
],
ProductVariant: [
{ name: 'supplierSku', type: 'string', label: [{ languageCode: LanguageCode.en, value: 'Supplier SKU' }] },
{ name: 'minOrderQty', type: 'int', defaultValue: 1, label: [{ languageCode: LanguageCode.en, value: 'Min Order Quantity' }] },
{ name: 'leadTimeDays', type: 'int', nullable: true, label: [{ languageCode: LanguageCode.en, value: 'Lead Time (days)' }] },
],
Customer: [
{ name: 'loyaltyTier', type: 'string', options: [{ value: 'bronze' }, { value: 'silver' }, { value: 'gold' }] },
{ name: 'erpCustomerId', type: 'string', unique: true, label: [{ languageCode: LanguageCode.en, value: 'ERP Customer ID' }] },
{ name: 'creditLimit', type: 'int', nullable: true, label: [{ languageCode: LanguageCode.en, value: 'Credit Limit (cents)' }] },
],
Order: [
{ name: 'erpOrderId', type: 'string', nullable: true, label: [{ languageCode: LanguageCode.en, value: 'ERP Order ID' }] },
{ name: 'exportedAt', type: 'datetime', nullable: true, label: [{ languageCode: LanguageCode.en, value: 'Exported to ERP' }] },
],
}
الـ Custom fields بتظهر تلقائي بواجهة الإدارة، بتندمج بـ GraphQL schemas، وبتقدر تستعلم عنها عبر الـ API. بتتخزن بنفس جدول الـ core entity، فالـ joins مجانية. تقدر تفلتر وترتب حسب الـ custom fields باستخدام ListQueryBuilder القياسي تبع Vendure. هيك بتربط Vendure مع الأنظمة الخارجية: الـ erpId على Product بيصير مفتاح الربط لمزامنة الـ ERP، والـ erpCustomerId على Customer بيربط بالـ CRM، والـ erpOrderId على Order بيتتبع شو الطلبات اللي انتقلت للـ ERP.
الـ Custom fields كمان بتدعم العلاقات مع entities ثانية، نصوص مترجمة (حسب اللغة)، و validation hooks. هي الآلية الرئيسية لتكييف Vendure مع مجالك بدون جداول توسعة منفصلة.
9. استراتيجيات التسعير المخصصة
نظام التسعير بـ Vendure مبني على strategies. الافتراضي بيحسب السعر من السعر المخزّن للـ product variant. بس بالتجارة المؤسسية، التسعير نادرا ما يكون بهالبساطة. ممكن تحتاج أسعار من ERP، خصومات خاصة بالعميل، مستويات حسب الكمية، أو حسابات سعر بالوقت الحقيقي.
// استراتيجية تسعير مخصصة بتجيب الأسعار من ERP خارجي
class ErpPricingStrategy implements OrderItemPriceCalculationStrategy {
private erpClient: ErpApiClient;
init(injector: Injector) {
this.erpClient = injector.get(ErpApiClient);
}
async calculateUnitPrice(
ctx: RequestContext,
productVariant: ProductVariant,
orderLineCustomFields: { [key: string]: any },
order: Order,
): Promise<PriceCalculationResult> {
// تحقق إذا الـ variant عنده ERP SKU
const erpSku = productVariant.customFields?.supplierSku;
if (!erpSku) {
// ارجع للسعر المخزّن بـ Vendure
return {
price: productVariant.listPrice,
priceIncludesTax: productVariant.listPriceIncludesTax,
};
}
// جيب السعر بالوقت الحقيقي من الـ ERP
const erpCustomerId = order.customer?.customFields?.erpCustomerId;
const erpPrice = await this.erpClient.getPrice({
sku: erpSku,
customerId: erpCustomerId,
quantity: 1,
currency: ctx.currencyCode,
});
return {
price: erpPrice.unitPriceCents,
priceIncludesTax: false,
};
}
}
سجّل الـ strategy بإعدادات Vendure:
orderOptions: {
orderItemPriceCalculationStrategy: new ErpPricingStrategy(),
}
هيك بتتعامل عمليات Vendure المؤسسية مع التسعير الخاص بالعملاء، أسعار العقود، وحسابات الأسعار الديناميكية. الـ strategy بتستلم كل سياق الطلب (عميل، كمية، عملة) فتقدر تطبّق أي منطق تسعير عملك بيحتاجه. وتقدر كمان تدمج هاد مع نظام العروض الترويجية المدمج بـ Vendure لتكديس الخصومات فوق الأسعار الأساسية من الـ ERP.
10. استعلامات GraphQL مخصصة لتكامل الأنظمة الخارجية
من أقوى أنماط Vendure للتكامل المؤسسي: تقدر تضيف GraphQL queries و mutations مخصصة بتجيب بيانات من أي نظام خارجي وبتعرضها عبر API تبع Vendure. هاد بيحوّل Vendure لطبقة API تجارة موحدة بتجمّع البيانات من ERPs و PIMs والمستودعات وأنظمة خلفية ثانية.
// توسيع الـ Schema: أضف query بتجيب المخزون بالوقت الحقيقي من ERP
const shopApiExtensions = gql`
type ErpStockInfo {
sku: String!
warehouseCode: String!
availableQty: Int!
nextDeliveryDate: DateTime
leadTimeDays: Int
}
type ErpPriceInfo {
sku: String!
unitPrice: Int!
currency: String!
priceListName: String
validUntil: DateTime
}
extend type Query {
erpStockAvailability(sku: String!): [ErpStockInfo!]!
erpCustomerPrice(sku: String!, quantity: Int): ErpPriceInfo
}
`;
// Resolver: جيب من الـ ERP API
@Resolver()
export class ErpIntegrationShopResolver {
constructor(private erpService: ErpIntegrationService) {}
@Query()
@Allow(Permission.Public)
async erpStockAvailability(
@Ctx() ctx: RequestContext,
@Args() args: { sku: string },
): Promise<ErpStockInfo[]> {
return this.erpService.getStockLevels(ctx, args.sku);
}
@Query()
@Allow(Permission.Owner)
async erpCustomerPrice(
@Ctx() ctx: RequestContext,
@Args() args: { sku: string; quantity?: number },
): Promise<ErpPriceInfo | null> {
const customerId = ctx.activeUser?.customFields?.erpCustomerId;
if (!customerId) return null;
return this.erpService.getCustomerPrice(ctx, customerId, args.sku, args.quantity ?? 1);
}
}
// Service: بيتعامل مع استدعاءات ERP API الفعلية مع caching ومعالجة الأخطاء
@Injectable()
export class ErpIntegrationService {
constructor(
private httpService: HttpService,
private cacheService: CacheService,
) {}
async getStockLevels(ctx: RequestContext, sku: string): Promise<ErpStockInfo[]> {
const cacheKey = `erp:stock:${sku}`;
const cached = await this.cacheService.get(cacheKey);
if (cached) return cached;
const response = await this.httpService.get(
`${process.env.ERP_API_URL}/stock/${sku}`,
{ headers: { 'Authorization': `Bearer ${process.env.ERP_API_TOKEN}` } },
);
const result = response.data.warehouses.map((w: any) => ({
sku,
warehouseCode: w.code,
availableQty: w.available,
nextDeliveryDate: w.nextDelivery,
leadTimeDays: w.leadTime,
}));
await this.cacheService.set(cacheKey, result, { ttl: 300 }); // كاش 5 دقائق
return result;
}
async getCustomerPrice(
ctx: RequestContext, customerId: string, sku: string, quantity: number,
): Promise<ErpPriceInfo | null> {
try {
const response = await this.httpService.post(
`${process.env.ERP_API_URL}/pricing`,
{ customerId, sku, quantity, currency: ctx.currencyCode },
);
return {
sku,
unitPrice: Math.round(response.data.unitPrice * 100), // تحويل لسنتات
currency: ctx.currencyCode,
priceListName: response.data.priceList,
validUntil: response.data.validUntil,
};
} catch (error) {
// الـ ERP مش متاح: رجّع null، خلي الـ frontend يرجع للسعر من الكتالوج
return null;
}
}
}
الـ storefront بعدين بيستعلم من Vendure لبيانات الكتالوج وبيانات الـ ERP من خلال GraphQL endpoint واحد:
query ProductWithErpData($slug: String!, $sku: String!) {
product(slug: $slug) {
id
name
description
variants {
id
sku
price
customFields {
supplierSku
minOrderQty
leadTimeDays
}
}
}
erpStockAvailability(sku: $sku) {
warehouseCode
availableQty
nextDeliveryDate
}
erpCustomerPrice(sku: $sku, quantity: 1) {
unitPrice
priceListName
validUntil
}
}
طلب واحد. بيانات الكتالوج من قاعدة بيانات Vendure، المخزون من الـ ERP، التسعير الخاص بالعميل من الـ ERP. الـ storefront ما بيحتاج يعرف عن الـ ERP. بس بيستعلم من Vendure. هالنمط بيشتغل مع أي نظام خارجي: REST APIs, SOAP services, GraphQL endpoints, gRPC backends. يصير Vendure طبقة التجميع.
لكيفية بناء pipelines التكامل هاي على نطاق واسع (مزامنة مجدولة، استقبال webhooks، استيراد بالجملة)، الـ Data Hub Plugin بيتعامل مع جانب الـ ETL الكامل. والـ GraphQL queries المخصصة بتتعامل مع الجانب الفوري لكل طلب.
وين Vendure عنده ثغرات
1. ما في Multi-Warehouse مدمج
Vendure عنده نموذج موقع مخزون واحد. إذا بدك مخزون متعدد المستودعات (مخزون بالمستودع A، مخزون مختلف بالمستودع B، توجيه التنفيذ حسب الموقع)، لازم تبنيه بنفسك أو تستخدم plugin من طرف ثالث.
هاي ثغرة كبيرة لأي عملية تجارية عندها أكتر من موقع فعلي واحد. الحل البديل (custom fields + منطق تخصيص مخزون مخصص) هش وما بيتكامل مع نظام تنفيذ الطلبات المدمج بـ Vendure.
2. ميزات B2B محدودة
Vendure مصمم بالأساس للتجارة B2C. متطلبات B2B مثل:
- حسابات شركات مع مشترين متعددين
- سير عمل موافقة للطلبات
- تسعير خاص بالعميل مع قواعد معقدة
- إدارة عروض الأسعار
- أوامر الشراء
- ضوابط ميزانية لكل مشتري
كلها بتحتاج تطوير مخصص. نظام الـ Channels بيوفر بعض إمكانية الـ multi-tenant، بس مش مجموعة ميزات B2B.
3. البحث أساسي
البحث المدمج بـ Vendure بيستخدم فهرس نص كامل مدعوم بقاعدة البيانات. بيشتغل لكتالوجات صغيرة بس ما بيتوسع لـ:
- البحث الموجّه بفلاتر معقدة
- تحمّل الأخطاء الإملائية
- معالجة المرادفات
- البحث متعدد اللغات مع محللات خاصة بكل لغة
- تحديثات الفهرس بالوقت الحقيقي
للبحث بالإنتاج، بتحتاج محرك خارجي (MeiliSearch, Elasticsearch, Algolia). الـ Data Hub Plugin تبعنا بيتضمن search sinks لـ MeiliSearch و Elasticsearch و OpenSearch و Algolia و Typesense مع فهرسة بالوقت الحقيقي عبر أحداث Vendure. لتفاصيل أكتر عن معمارية البحث بالتجارة الإلكترونية، ذاك الدليل بيغطي التفاصيل التقنية.
4. أدوات الـ Migration
TypeORM migrations بـ Vendure ممكن تكون هشة. لمّا عدة plugins بتعرّف entities، ترتيب توليد الـ migrations بيصير غير متوقع. واجهنا:
- Migrations بتشير لجداول لسا ما انشأت من migrations تبع plugins ثانية
- اختلافات بأنواع الأعمدة بين SQLite (تطوير) و PostgreSQL (إنتاج)
- تعارضات migrations لمّا plugin-ين بيعدلو نفس الـ core entity عبر custom fields
الحل: ولّد migrations لكل plugin، اختبر مع PostgreSQL (مش SQLite)، وحافظ على ترتيب صارم للـ migrations بسكربتات النشر. بنتعامل مع تحديات migration مشابهة بـ دليل ترقية Pimcore، وين إدارة الـ database schema أعقد.
أنماط معمارية الـ Entities
بعد بناء plugin-ين enterprise، هالأنماط طلعت ضرورية لجودة الإنتاج.
اتفاقية بادئة الـ Entity
كل entity بالـ plugin بيستخدم بادئة لمنع تصادم الأسماء:
// أسماء الجداول معرّفة بـ constants (ما بتنكتب يدويا أبدا)
export const TABLE_NAMES = {
WISHLIST: 'ci_wishlist',
WISHLIST_ITEM: 'ci_wishlist_item',
REVIEW: 'ci_review',
REVIEW_VOTE: 'ci_review_vote',
LOYALTY_ACCOUNT: 'ci_loyalty_account',
LOYALTY_TRANSACTION: 'ci_loyalty_transaction',
// ...
};
// الـ Entity بيستخدم الـ constant
@Entity(TABLE_NAMES.WISHLIST)
@Index(['customerId', 'channelId'])
export class CiWishlist extends VendureEntity {
constructor(input?: DeepPartial<CiWishlist>) {
super(input);
}
@Column({ type: 'varchar', length: 255 })
name!: string;
@Column()
customerId!: number;
@Column()
channelId!: number;
}
كل entity بيوسّع VendureEntity (بيوفر id, createdAt, updatedAt). كل entity بيتضمن channelId لعزل القنوات المتعددة. كل اسم جدول بييجي من constant مشترك، ما في strings مكتوبة يدويا أبدا.
الحذف الناعم
للـ entities اللي بتحتاج سجل مراجعة، استخدم عمود deletedAt بدل الحذف الفعلي:
@Column({ type: 'datetime', nullable: true })
deletedAt?: Date;
هاد بيحافظ على التكامل المرجعي وبيسمح بالاستعادة. فلتر السجلات المحذوفة بكل الاستعلامات بشكل افتراضي.
تحديد نطاق القناة
كل استعلام لازم يكون محدود بالقناة النشطة. الـ RequestContext تبع Vendure بيحمل معلومات القناة:
async findByCustomer(ctx: RequestContext, customerId: number) {
return this.connection.getRepository(ctx, CiWishlist).find({
where: {
customerId,
channelId: ctx.channelId,
},
});
}
بدون تحديد نطاق القناة، البيانات من واجهة متجر بتتسرب لواجهة ثانية. هاد أكتر bug أمني شائع بعمليات Vendure متعددة القنوات. بنغطي أنماط عزل البيانات المشابهة للـ multi-tenant بـ دليل حوكمة الذكاء الاصطناعي وبـ صفحة الثقة تبعنا.
أنماط الاختبار
Vendure بيدعم اختبارات unit و e2e للـ plugins. إعداد الاختبار من نقاط القوة اللي ما بتنذكر كتير بالإطار.
اختبارات Unit
اختبر methods الـ service مع repositories مزيفة:
import { describe, it, expect, vi } from 'vitest';
describe('WishlistService', () => {
it('should add item to wishlist', async () => {
const mockRepo = {
save: vi.fn().mockResolvedValue({ id: '1', productVariantId: '42' }),
findOne: vi.fn().mockResolvedValue(null),
};
const service = new WishlistService(mockRepo as any);
const result = await service.addItem(mockCtx, { productVariantId: '42' });
expect(result.productVariantId).toBe('42');
expect(mockRepo.save).toHaveBeenCalledOnce();
});
});
اختبارات E2E
اختبر الـ GraphQL API الكاملة مع سيرفر Vendure حقيقي:
import { createTestEnvironment } from '@vendure/testing';
import { testConfig } from './test-config';
describe('Wishlist Shop API', () => {
const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
beforeAll(async () => {
await server.init({ initialData, productsCsvPath: './test-data/products.csv' });
await shopClient.asUserWithCredentials('sara.mustermann@beispiel.de', 'test');
});
afterAll(async () => {
await server.destroy();
});
it('should create a wishlist', async () => {
const { createWishlist } = await shopClient.query(CREATE_WISHLIST, {
input: { name: 'My Favorites' },
});
expect(createWishlist.name).toBe('My Favorites');
});
it('should enforce permissions', async () => {
await shopClient.asAnonymousUser();
const result = await shopClient.query(CREATE_WISHLIST, {
input: { name: 'Should Fail' },
});
expect(result.errors?.[0]?.extensions?.code).toBe('FORBIDDEN');
});
});
createTestEnvironment تبع Vendure بتشغّل سيرفر حقيقي مع SQLite، بتشغّل migrations، بتزرع بيانات اختبار، وبتوفر GraphQL clients مصادقين للـ Shop API والـ Admin API. الاختبارات بتشتغل على سطح الـ API الفعلي، مش mocks. هاد بيمسك مشاكل الصلاحيات، عدم تطابق الـ schema، و bugs التحقق من البيانات اللي اختبارات unit ما بتلاقيها.
نحنا بنستخدم Vitest مع SWC لاختبارات unit سريعة و Vitest مع forked processes لاختبارات e2e (ضروري لأنو lifecycle سيرفر Vendure بيحتاج عزل process بين مجموعات الاختبار). لطريقة تعاملنا مع الاختبار والجودة بممارسة الهندسة الأوسع، شوف صفحة المنهجية تبعنا.
التنسيق متعدد الـ Pods
تشغيل Vendure على عدة pods بيقدّم تحديات تنسيق مش موجودة بعمليات نشر instance واحد.
انتخاب القائد للمجدولين
المهام المجدولة (اكتشاف السلال كل 15 دقيقة، انتهاء الولاء يوميا، فحص انخفاض الأسعار كل ساعة) لازم تشتغل على instance واحد بس. إذا كل الـ pods شغّلت المجدول، بتحصل معالجة مكررة.
// خدمات الجدولة بتستخدم انتخاب القائد عبر JobQueue تبع Vendure
// instance واحد بس بيعالج المهمة
@Injectable()
export class CartDetectionService {
private jobQueue: JobQueue<{}>;
async onModuleInit() {
this.jobQueue = await this.jobQueueService.createQueue({
name: 'cart-detection',
process: async (ctx) => {
await this.detectAbandonedCarts(ctx);
},
});
// جدولة: بتشتغل كل 15 دقيقة، بس على pod واحد
await this.jobQueue.add({}, { ctx: RequestContext.empty() });
}
}
إزالة التكرار بالمفتاح التجاري لمستهلكي الأحداث
مستهلكو الأحداث (إشعارات انخفاض السعر، تنبيهات المخزون، طلبات المراجعة) بيشتغلو على كل الـ pods. كل اشتراك EventBus على كل pod بيستلم الحدث. إزالة التكرار بتمنع إرسال إيميلات مكررة:
| نوع الخدمة | السلوك مع عدة Instances |
|---|---|
| المجدولين (اكتشاف السلال، انتهاء الولاء) | انتخاب قائد عبر قفل DB. instance واحد بس بيشتغل. |
| مستهلكو الأحداث (انخفاض الأسعار، تنبيهات المخزون) | كل الـ instances بتستهلك. Idempotent عبر إزالة تكرار بالمفتاح التجاري. |
// إزالة التكرار بالمفتاح التجاري للإشعارات
const dedupeKey = `${recipientEmail}:${category}:${entityRef}:${dayBucket}`;
const existing = await this.notificationRepo.findOne({ where: { dedupeKey } });
if (existing) {
return; // تم الإرسال اليوم
}
مفتاح إزالة التكرار بيتضمن dayBucket (تاريخ UTC بصيغة YYYY-MM-DD) فنفس الإشعار يقدر ينرسل مرة ثانية اليوم التالي، بس أبدا مرتين بنفس اليوم.
الـ Idempotency لعمليات الـ API
العمليات الموجهة للعملاء (إضافة لقائمة الرغبات، إرسال مراجعة، استبدال نقاط ولاء) بتحتاج idempotency للتعامل مع إعادة المحاولات وفشل الشبكة:
@Entity(TABLE_NAMES.IDEMPOTENCY_KEY)
export class CiIdempotencyKey extends VendureEntity {
@Column({ type: 'varchar', length: 255 })
@Index()
key!: string;
@Column({ type: 'varchar', length: 50 })
scope!: string; // 'wishlist_add', 'review_submit', إلخ
@Column({ type: 'varchar', length: 64 })
requestHash!: string; // SHA-256 للمدخلات المعيارية
@Column({ type: 'varchar', length: 20 })
status!: string; // PENDING, COMPLETED, FAILED
}
// قيد فريد: UNIQUE(scope, key)
نموذجين مختلفين للـ idempotency:
- Idempotency الـ API (CiIdempotencyKey): للعمليات اللي بيبدأها المستخدم. المفتاح بيوفره العميل أو بيتولّد من hash المدخلات. بيعيد الرد المخزّن عند التكرار.
- Idempotency المهام (على مستوى الـ queue): للمعالجة الخلفية. مفتاح إزالة التكرار بحمولة المهمة. الـ worker بيتحقق قبل التنفيذ. بيستخدم قيود DB أو علامات الإتمام.
ما تخلط بين هالنموذجين أبدا. عندهم دورات حياة مختلفة ودلالات فشل مختلفة.
لطريقة تعاملنا مع أنماط التزامن المشابهة عبر أنظمتنا، شوف دليلنا عن تصميم سير عمل الذكاء الاصطناعي اللي بيغطي تحديات التنسيق المرتبطة.
Data Hub: الـ ETL لـ Vendure
من أكبر الثغرات بالتجارة الإلكترونية headless هو تكامل البيانات. كيف بتجيب المنتجات من الـ ERP لـ Vendure؟ كيف بتزامن المخزون عبر القنوات؟ كيف بتولّد feeds المنتجات لـ Google Merchant Center؟
بنينا Vendure Data Hub Plugin لحل هاد. هو محرك ETL pipeline كامل بيشتغل جوا Vendure:
| المكوّن | العدد | أمثلة |
|---|---|---|
| Extractors | 9 | HTTP/REST, GraphQL, Database (SQL), File (CSV/JSON/XML), S3, FTP/SFTP, Webhook, CDC |
| Transform Operators | 61 | String (12), Date (5), Numeric (9), Logic (4), JSON (4), Data (8), Enrichment (5), Aggregation (8), Validation (2) |
| Entity Loaders | 24 | Products, Variants, Customers, Collections, Orders, Promotions, Assets, Facets |
| Feed Generators | 4 | Google Merchant Center, Meta Catalog, Amazon Seller Central, Custom |
| Search Sinks | 7 | Elasticsearch, OpenSearch, MeiliSearch, Algolia, Typesense, Queue Producers, Webhooks |
| Triggers | 6 | Manual, Scheduled (cron), Webhook, Vendure Events, File Watch, Message Queue |
الـ plugin بيستخدم نفس أنماط Vendure core: TypeORM entities, NestJS services, GraphQL API, EventBus integration. بيتضمن محرر pipelines بصري بلوحة الإدارة، سجلات تنفيذ بالوقت الحقيقي، استعادة من نقاط تفتيش (استكمال من آخر سجل ناجح)، وأقفال موزعة لسلامة الـ multi-pod.
هاد نوع بنية هندسة البيانات التحتية اللي التجارة المؤسسية بتحتاجها بس نادرا ما بتحصل عليها من منصة التجارة نفسها.
Vendure مقابل Medusa مقابل Saleor (2026)
مقارنة صريحة للمعماريين اللي بيقيّمو المنصات:
| المعيار | Vendure | Medusa v2 | Saleor |
|---|---|---|---|
| اللغة | TypeScript (NestJS) | TypeScript (إطار عمل مخصص) | Python (Django + GraphQL) |
| الـ API | GraphQL (Shop + Admin) | REST + JS SDK + Admin API | GraphQL |
| نظام الـ Plugins | ممتاز (entities, schema, resolvers, admin) | جيد (modules, workflows) | محدود (apps عبر webhooks) |
| قاعدة البيانات | PostgreSQL, MySQL, SQLite (TypeORM) | PostgreSQL (MikroORM) | PostgreSQL (Django ORM) |
| واجهة الإدارة | React (مدمجة، قابلة للتخصيص بالكامل) | React (مدمجة) | React (مدمجة، Dashboard) |
| Worker/Jobs | BullMQ (مدمج) | نظام مهام مدمج | Celery (Python) |
| متعدد القنوات | Channels (مدمج) | Sales Channels | Channels |
| ميزات B2B | محدودة (تحتاج تطوير مخصص) | بتنمو (بعضها مدمج) | بتنمو (بعضها مدمج) |
| البحث | أساسي (مدعوم بـ DB) | أساسي (يحتاج تكامل) | أساسي (يحتاج تكامل) |
| متعدد المستودعات | غير مدمج | مدمج (v2) | مدمج |
| المجتمع | متوسط (بينمو) | كبير (بينمو بسرعة) | متوسط |
| الاستضافة | استضافة ذاتية | استضافة ذاتية + Medusa Cloud | Saleor Cloud + استضافة ذاتية |
| النضج | مستقر، مثبت بالإنتاج | v2 أحدث، تغييرات API محتملة | مستقر، مثبت بالإنتاج |
| تجربة المطور لفرق TypeScript | ممتازة | جيدة | بتحتاج معرفة Python |
| Plugins مؤسسية | نظام بيئي قوي | بينمو | مبني على webhooks (محدود) |
متى تختار Vendure
- فريقك بيكتب TypeScript. ميزة تجربة المطور مش هامشية، هي تحويلية. الـ Type safety عبر كامل الـ commerce stack بيغيّر سرعة الشحن وثقة إعادة الهيكلة.
- بتحتاج توسعة plugins عميقة. ما في منصة ثانية بتخليك تضيف entities، توسّع GraphQL schema، تبني صفحات لوحة إدارة، وتسجل مهام خلفية من تعريف plugin واحد.
- بتبني B2C أو تجارة هجينة بأغلبية B2C مع منطق عمل مخصص. Vendure بيعطيك نواة تجارة قوية وبيوقف جنب.
- بدك تملك البنية التحتية. Vendure بيشتغل بأي مكان Node.js بيشتغل فيه.
- بتحتاج تكامل وثيق مع NestJS microservices أو TypeScript backends موجودة. Vendure أصلا NestJS، فالـ services بتحكي نفس اللغة.
- بتقدّر المعمارية النظيفة والصيانة الطويلة المدى. الأنماط (EventBus, RequestContext, فصل Shop/Admin resolvers) بتتوسع لـ codebases كبيرة بدون تدهور.
متى تختار Medusa
- بتحتاج multi-warehouse جاهز من الصندوق.
- بتفضّل REST على GraphQL للـ storefront.
- بدك خيار cloud مُدار مع مرونة المصدر المفتوح.
- بتبدأ من الصفر وبدك أحدث أنماط الأطر.
متى تختار Saleor
- فريقك Python/Django.
- بتحتاج عرض cloud مُدار (Saleor Cloud).
- بدك internationalization قوي مدمج ومتعدد العملات.
- بتحتاج ميزات marketplace.
متى تختار Shopify
- ما بدك تدير البنية التحتية.
- احتياجاتك التجارية قياسية (كتالوج، checkout، مدفوعات).
- بدك أكبر نظام بيئي للتطبيقات.
- مستعد تقبل قيود المنصة مقابل وقت أسرع للسوق.
لمنظورنا الأوسع عن منصات التجارة الإلكترونية headless ووين الصناعة رايحة، ذاك الدليل بيغطي المشهد الكامل.
معمارية النشر للإنتاج
الـ Stack الموصى فيه
| المكوّن | التقنية | الغرض |
|---|---|---|
| السيرفر | Vendure (NestJS) | واجهة التجارة API |
| قاعدة البيانات | PostgreSQL 15+ | مخزن البيانات الأساسي |
| الكاش | Redis 7+ | Sessions، كاش، قوائم المهام |
| البحث | MeiliSearch أو OpenSearch | بحث المنتجات، فلترة موجّهة |
| التخزين | S3 / Azure Blob / محلي | تخزين الأصول |
| الـ CDN | CloudFront / Cloudflare | توصيل الأصول |
| وسيط الرسائل | Redis (BullMQ) أو RabbitMQ | قوائم المهام، معالجة الأحداث |
| المراقبة | OpenTelemetry + Grafana | المراقبة والرصد |
معمارية Kubernetes Pods
| الـ Pod | الغرض | عدد النسخ |
|---|---|---|
vendure-server | Shop API + Admin API | 2-4 |
vendure-worker | معالجة مهام BullMQ | 1-3 |
postgres | قاعدة البيانات (أو مُدارة) | 1+ |
redis | كاش + قوائم مهام | 1+ |
meilisearch | محرك البحث | 1-2 |
إعدادات البيئة
// vendure-config.ts (إنتاج)
export const config: VendureConfig = {
apiOptions: {
port: 3000,
adminApiPath: 'admin-api',
shopApiPath: 'shop-api',
cors: {
origin: process.env.CORS_ORIGIN?.split(',') || [],
},
},
dbConnectionOptions: {
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
synchronize: false, // أبدا true بالإنتاج
},
workerOptions: {
runInForkedProcess: false, // deployment منفصل بـ K8s
},
jobQueueOptions: {
activeQueues: process.env.WORKER === 'true'
? undefined // معالجة كل القوائم
: [], // ما تعالج شي (pod سيرفر فقط)
},
plugins: [
AssetServerPlugin.init({
storageStrategyFactory: configureAssetStorage(),
}),
DefaultSearchPlugin.init({ bufferUpdates: true }),
EmailPlugin.init({ /* ... */ }),
// الـ plugins تبعك
DataHubPlugin,
CustomerIntelligencePlugin.init({ /* ... */ }),
],
};
الإعداد الحرج: jobQueueOptions.activeQueues. على pods السيرفر، حطها [] (مصفوفة فارغة) عشان ما تعالج مهام خلفية. على pods الـ worker، خليها undefined عشان تعالج كل القوائم. هاد بيفصل معالجة الطلبات عن المعالجة الخلفية.
لأكتر عن طريقة تفكيرنا بـ النشر السحابي ومعمارية البنية التحتية، صفحة الخدمة بتغطي نهجنا.
اعتبارات الأداء
فهرسة قاعدة البيانات
Vendure بيعمل فهارس أساسية على الـ core entities. للإنتاج، أضف فهارس على:
- الـ Custom fields اللي بتستعلم عنها كتير
- أعمدة الـ plugin entities المستخدمة بعبارات WHERE
- المفاتيح الأجنبية على الجداول الكبيرة
- فهارس مركبة لأنماط الاستعلام الشائعة
@Entity(TABLE_NAMES.REVIEW)
@Index(['productId', 'status', 'channelId']) // استعلام شائع: المراجعات الموافق عليها لمنتج
@Index(['customerId', 'createdAt']) // استعلام شائع: سجل مراجعات العميل
export class CiReview extends VendureEntity {
// ...
}
تجميع الاتصالات
لـ PostgreSQL بالإنتاج:
dbConnectionOptions: {
type: 'postgres',
extra: {
max: 20, // أقصى اتصالات لكل pod
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
},
}
مع 4 pods سيرفر و 2 pods عمال، هاد 120 اتصال إجمالي. تأكد إنو إعداد max_connections تبع PostgreSQL يقدر يتحمل هاد (الافتراضي 100، وهاد قليل جدا).
تعقيد استعلامات GraphQL
Vendure ما بيحد عمق أو تعقيد استعلامات GraphQL بشكل افتراضي. بالإنتاج، أضف حدود لمنع الاستعلامات المكلفة من تحميل قاعدة البيانات زيادة:
apiOptions: {
shopApiPlayground: false, // عطّله بالإنتاج
adminApiPlayground: false,
middleware: [{
handler: depthLimit(10), // أقصى عمق استعلام
route: '*',
}],
},
مستقبل Vendure
Vendure بيتطور بنشاط. المجالات الرئيسية للمتابعة:
- نضج لوحة الإدارة: واجهة React الإدارية هي الافتراضية حاليا. توقع مكونات مدمجة أكتر، نقاط توسعة أفضل للـ plugins، وميزات dashboard أغنى جاهزة من الصندوق.
- تحسين B2B: حسابات الشركات وميزات المشترين على خارطة الطريق.
- بحث أفضل: نقاط تكامل بحث أكتر مرونة.
- Vendure Cloud: خيار استضافة مُدار (أُعلن عنه، مش متاح على نطاق واسع بعد).
الفريق الأساسي متجاوب والمشروع مدعوم تجاريا (Vendure Ltd). نظام TypeScript البيئي حوله (NestJS, TypeORM, BullMQ) ناضج ومُصان كويس.
أخطاء شائعة
-
استخدام SQLite بالإنتاج. SQLite كويس للتطوير. بالإنتاج، استخدم PostgreSQL. اختلافات سلوك TypeORM بين قاعدتي البيانات بتسبب bugs ما بتشوفها بالتطوير أبدا.
-
تشغيل الـ workers بنفس process السيرفر. افصل deployment الـ worker. مهمة طويلة بنفس process سيرفر الـ API بتحجب معالجة الطلبات.
-
ما في تحديد نطاق قناة. كل استعلام لازم يفلتر حسب
ctx.channelId. تجاهل هاد بيسرّب البيانات بين واجهات المتاجر. -
استيراد services بين الـ modules. استخدم الـ EventBus للتواصل بين الـ modules. الاستيراد المباشر بيعمل circular dependencies اللي بتتعطل مع نمو النظام.
-
ما في idempotency على العمليات. إعادة محاولات الشبكة وإعادة تشغيل الـ webhooks بتعمل طلبات مكررة، مراجعات مكررة، عناصر wishlist مكررة. ابني الـ idempotency من أول يوم.
-
أسماء جداول مكتوبة يدويا. استخدم constants. لمّا عندك 30+ entity عبر 3 plugins، النصوص المكتوبة يدويا بتصير كابوس صيانة.
-
Synchronize: true بالإنتاج. الـ auto-sync تبع TypeORM بيحذف أعمدة وبيخسر بيانات. استخدم migrations. دايما.
-
عدم الاختبار مع PostgreSQL. إذا بتطوّر على SQLite وبتنشر على PostgreSQL، رح تكتشف اختلافات أنواع الأعمدة، اختلافات سلوك الـ transactions، و bugs معالجة JSON بالإنتاج.
-
تجاهل خدمة الـ Worker. إرسال الإيميلات، فهرسة البحث، ومعالجة الأصول كلها لازم تمر عبر الـ job queue. تنفيذها بشكل متزامن بمعالجات الطلبات بيخلي الـ API بطيء وغير موثوق.
-
بناء كل شي من الصفر. تحقق من نظام الـ plugins البيئي أولا. لـ ETL/تكامل البيانات، فهرسة البحث، وميزات التجارة الشائعة، في plugins موجودة. البناء من الصفر بياخد 10 أضعاف الوقت.
النقاط الرئيسية
-
نظام plugins تبع Vendure هو الأفضل بالتجارة الإلكترونية headless. توسيع entities، توسيع schema، فصل Shop/Admin resolvers، NestJS DI كامل. ما في منصة ثانية بتقترب لفرق TypeScript. تجربة المطور هي اللي بتخلينا نكمل البناء عليه.
-
الـ EventBus بيمكّن الربط المرن الحقيقي. الـ modules بتتواصل عبر الأحداث بس. هاد بيتوسع لـ 6+ modules بـ plugin واحد بدون circular dependencies. هو النمط اللي بيخلي plugins المؤسسية ممكنة.
-
النشر متعدد الـ pods بيحتاج تنسيق صريح. انتخاب القائد للمجدولين، إزالة التكرار بالمفتاح التجاري لمستهلكي الأحداث، idempotency لعمليات الـ API. ما في شي من هاد أوتوماتيكي.
-
واجهة الإدارة React قابلة للتخصيص بالكامل. واجهة Vendure 3 المبنية على React بتدعم صفحات مخصصة، مسارات، مكونات، و dashboard widgets. مطورو الـ Plugins بيحصلو على تجربة مطور حديثة مع React 18 و TanStack Query و Vendure Dashboard SDK.
-
Multi-warehouse و B2B ثغرات حقيقية. إذا هاي متطلبات أساسية، قيّم Medusa v2 أو Saleor. بناءها مخصص على Vendure مكلف.
-
الإنتاج بيحتاج PostgreSQL و Redis و deployment عمال منفصل. SQLite للتطوير. BullMQ للمهام الخلفية. افصل pods السيرفر والعمال.
نحنا فعلا بنستمتع بالبناء على Vendure. تجربة المطور، الأنماط المعمارية، ومرونة نظام الـ plugins بتخليه منصة بننصح فيها بثقة لفرق TypeScript اللي بتبني تجارة إلكترونية. الكود بيضل نظيف حتى مع النمو، الإطار ما بيقف بطريقك، والمجتمع متجاوب وبينمو. إذا بتحتاج multi-warehouse أو B2B ثقيل من الصندوق، قيّم البدائل. لكل شي ثاني بالتجارة الإلكترونية headless، Vendure هو خيارنا الأول.
استكشف دليل Vendure للتجارة الإلكترونية headless لأكتر عن ممارسة Vendure تبعنا، أو شوف حالات استخدام واقعية وين نشرنا Vendure بالإنتاج. إذا بتقيّم منصات تجارة لمشروعك القادم، حكي مع فريقنا أو اطلب عرض سعر.
المواضيع المغطاة
أدلة ذات صلة
بناء إضافات Vendure للمؤسسات: أنماط تنجح في بيئة Multi-Pod
أنماط إنتاجية لتطوير إضافات Vendure. اصطلاحات الكيانات، فصل الـ Resolvers، تواصل EventBus، انتخاب القائد، منع تكرار الإشعارات، والاختبار على سيرفرات حقيقية.
اقرأ الدليلالدليل الشامل لأنظمة الذكاء الاصطناعي الوكيلي
دليل تقني لأنظمة الذكاء الاصطناعي الوكيلي في بيئات الأعمال. تعرف على البنية والقدرات والتطبيقات العملية للوكلاء المستقلين.
اقرأ الدليلالتجارة الوكيلية: كيف تخلي وكلاء الذكاء الاصطناعي يشترون بأمان
كيف تصمم تجارة وكيلية محكومة. محركات السياسات، بوابات الموافقة البشرية، إيصالات HMAC، الـ idempotency، عزل المستأجرين، وبروتوكول الدفع الوكيلي الكامل.
اقرأ الدليلجاهز لبناء أنظمة ذكاء اصطناعي جاهزة للإنتاج؟
فريقنا متخصص في بناء أنظمة ذكاء اصطناعي جاهزة للإنتاج. خلينا نحكي كيف نقدر نساعد.
ابدأ محادثة