Building Custom Shopify Apps with Remix and Polaris

Complete developer guide to building custom Shopify apps using Remix, Shopify CLI 3, Polaris design system, and App Bridge for embedded admin experiences.

E
ECOSIRE Research and Development Team
|19 مارس 202611 دقائق قراءة2.4k كلمات|

إنشاء تطبيقات Shopify مخصصة باستخدام Remix وPolaris

يمثل ترحيل Shopify إلى Remix باعتباره إطار عمل التطبيق الافتراضي تحولًا أساسيًا في كيفية إنشاء تطبيقات الإنتاج على النظام الأساسي. يوفر قالب تطبيق Shopify القائم على Remix العرض من جانب الخادم والبث والتوجيه المتداخل وتكامل Shopify CLI الأصلي في حزمة واحدة متماسكة - لتحل محل أنماط Node/Express + React الأقدم التي هيمنت على تطوير التطبيق حتى عام 2023.

يستعرض هذا الدليل دورة حياة التطوير الكاملة: السقالات، والمصادقة، وجلب بيانات GraphQL، وتكامل مكونات Polaris، وخطافات الويب، والفوترة، والأنماط التي يستخدمها مطورو Shopify ذوو الخبرة لشحن تطبيقات مضمنة قابلة للصيانة وعالية الأداء.

الوجبات الرئيسية

  • يقوم Shopify CLI 3.x بإنشاء تطبيق Remix جاهز للإنتاج باستخدام OAuth وتخزين الجلسة وApp Bridge الذي تم تكوينه مسبقًا
  • يقوم المساعد authenticate.admin بمعالجة تدفق OAuth بأكمله — لا تقم بتنفيذ OAuth يدويًا
  • يتم تعيين نمط التحميل/الإجراء الخاص بـ Remix بشكل مثالي لدورة الطلب/الطفرة الخاصة بـ Shopify
  • مكونات Polaris مطلوبة للحصول على موافقة متجر التطبيقات - تفشل مراجعة واجهة المستخدم المخصصة التي تتجاهل معايير Polaris
  • يجب تسجيل خطافات الويب عبر تكوين التطبيق، وليس فقط استدعاءات واجهة برمجة التطبيقات (API)، لضمان الموثوقية
  • واجهة برمجة تطبيقات Shopify Admin GraphQL مقسمة إلى صفحات - تتعامل دائمًا مع ترقيم الصفحات المستند إلى المؤشر في أدوات تحميل البيانات
  • يوفر App Bridge تجربة مضمنة خالية من الكروم؛ استخدم الخطاف useAppBridge للتنقل
  • يلزم تكامل واجهة برمجة تطبيقات الفوترة لأي تطبيق يفرض رسوم اشتراك

إعداد المشروع والسقالات

ابدأ بـ Shopify CLI 3 لدعم تطبيق Remix الذي تم تكوينه بشكل صحيح:

npm install -g @shopify/cli@latest
shopify app create node --template remix
cd your-app-name

تتضمن السقالة التي تم إنشاؤها ما يلي:

  • remix.config.js بإعدادات متوافقة مع Shopify
  • shopify.app.toml — ملف تكوين تطبيقك (يحل محل .env لإعدادات التطبيق)
  • app/shopify.server.ts — المصادقة المركزية وتكوين عميل API
  • app/routes/app.tsx — تخطيط التطبيق المضمن الجذر
  • prisma/schema.prisma — تخزين جلسة SQLite (استبدله بـ PostgreSQL للإنتاج)

هيكل shopify.app.toml:

name = "your-app-name"
client_id = "your-api-key"
application_url = "https://your-app-url.com"
embedded = true

[access_scopes]
scopes = "read_products,write_products,read_orders"

[auth]
redirect_urls = ["https://your-app-url.com/auth/callback"]

[webhooks]
api_version = "2025-01"

  [[webhooks.subscriptions]]
  topics = ["app/uninstalled"]
  uri = "/webhooks"

المصادقة باستخدام authenticate.admin

يقوم الملف shopify.server.ts بتصدير كائن authenticate الذي يعالج كافة مشكلات المصادقة. لا تقم مطلقًا بتنفيذ OAuth يدويًا - يقوم المساعد authenticate.admin بإدارة التدفق بالكامل بما في ذلك:

  • تبادل رمز OAuth
  • ثبات الجلسة
  • تحديث الرمز المميز
  • أوضاع الوصول عبر الإنترنت مقابل وضع عدم الاتصال
  • جمع موافقة التاجر

استخدام المصادقة في أدوات تحميل المسار:

// app/routes/app.products.tsx
import { json } from "@remix-run/node";
import { authenticate } from "../shopify.server";

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { admin, session } = await authenticate.admin(request);

  const response = await admin.graphql(`
    #graphql
    query GetProducts($first: Int!) {
      products(first: $first) {
        nodes {
          id
          title
          status
          priceRangeV2 {
            minVariantPrice {
              amount
              currencyCode
            }
          }
          totalInventory
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
  `, {
    variables: { first: 50 }
  });

  const data = await response.json();
  return json({ products: data.data.products });
};

جلسات عبر الإنترنت مقابل جلسات دون اتصال:

  • الوصول دون اتصال: الإعداد الافتراضي لمعظم التطبيقات. تستمر الجلسة بشكل مستقل عن تسجيل دخول التاجر. يستخدم للوظائف الخلفية وخطافات الويب والمهام المجدولة.
  • الوصول عبر الإنترنت: الجلسة مرتبطة بمستخدم التاجر الذي قام بتسجيل الدخول. مطلوب عندما تحتاج إلى التصرف نيابة عن مستخدم معين (على سبيل المثال، تتبع الموظف الذي قام بإجراء ما).

التكوين في shopify.server.ts:

const shopify = shopifyApp({
  apiKey: process.env.SHOPIFY_API_KEY!,
  apiSecretKey: process.env.SHOPIFY_API_SECRET!,
  scopes: process.env.SCOPES!.split(","),
  appUrl: process.env.SHOPIFY_APP_URL!,
  authPathPrefix: "/auth",
  sessionStorage: new PrismaSessionStorage(prisma),
  distribution: AppDistribution.AppStore,
  future: {
    unstable_newEmbeddedAuthStrategy: true, // Token Exchange (2024+)
  },
});

** تبادل الرموز (موصى به لعام 2026) **

تعمل إستراتيجية مصادقة Token Exchange الأحدث من Shopify على التخلص من عمليات إعادة توجيه OAuth للتطبيقات المضمنة. لا يحتاج المستخدمون داخل مسؤول Shopify إلى المغادرة والعودة - يتم تبادل رمز الجلسة من App Bridge مباشرةً من جانب الخادم لرمز وصول API. قم بتمكين هذا باستخدام unstable_newEmbeddedAuthStrategy: true.


أنماط جلب البيانات GraphQL

واجهة برمجة تطبيقات Shopify Admin API هي GraphQL أولاً. أتقن هذه الأنماط لجلب بيانات جودة الإنتاج.

ترقيم الصفحات المستند إلى المؤشر:

export async function getAllProducts(admin: AdminApiContext) {
  let hasNextPage = true;
  let cursor: string | null = null;
  const allProducts = [];

  while (hasNextPage) {
    const response = await admin.graphql(`
      #graphql
      query GetProducts($first: Int!, $after: String) {
        products(first: $first, after: $after) {
          nodes {
            id
            title
            handle
          }
          pageInfo {
            hasNextPage
            endCursor
          }
        }
      }
    `, {
      variables: { first: 250, after: cursor }
    });

    const data = await response.json();
    const { nodes, pageInfo } = data.data.products;

    allProducts.push(...nodes);
    hasNextPage = pageInfo.hasNextPage;
    cursor = pageInfo.endCursor;
  }

  return allProducts;
}

** الطفرات مع معالجة الأخطاء **:

export const action = async ({ request }: ActionFunctionArgs) => {
  const { admin } = await authenticate.admin(request);
  const formData = await request.formData();

  const title = formData.get("title") as string;
  const price = formData.get("price") as string;

  const response = await admin.graphql(`
    #graphql
    mutation CreateProduct($input: ProductInput!) {
      productCreate(input: $input) {
        product {
          id
          title
        }
        userErrors {
          field
          message
        }
      }
    }
  `, {
    variables: {
      input: {
        title,
        variants: [{ price }]
      }
    }
  });

  const data = await response.json();

  if (data.data.productCreate.userErrors.length > 0) {
    return json({
      errors: data.data.productCreate.userErrors
    }, { status: 422 });
  }

  return json({ product: data.data.productCreate.product });
};

** العمليات المجمعة لمجموعات البيانات الكبيرة **:

بالنسبة للعمليات على آلاف السجلات، استخدم Bulk Operations API بدلاً من الاستعلامات المقسمة إلى صفحات. يتم تشغيل الاستعلامات المجمعة بشكل غير متزامن وإرجاع ملف JSONL:

const bulkQuery = await admin.graphql(`
  mutation {
    bulkOperationRunQuery(
      query: """
        {
          products {
            edges {
              node {
                id
                title
                variants {
                  edges {
                    node {
                      id
                      price
                      inventoryQuantity
                    }
                  }
                }
              }
            }
          }
        }
      """
    ) {
      bulkOperation {
        id
        status
      }
      userErrors {
        field
        message
      }
    }
  }
`);

قم باستقصاء currentBulkOperation حتى status === "COMPLETED"، ثم قم بتنزيل JSONL من url وتحليله.


تكامل نظام تصميم بولاريس

Polaris هو نظام تصميم Shopify للتطبيقات المدمجة. يعد استخدام Polaris أمرًا مطلوبًا لعمليات الإرسال في App Store - وستؤدي واجهة المستخدم غير الخاصة بـ Polaris إلى رفض المراجعة.

** إعداد Polaris في Remix **:

// app/root.tsx
import { AppProvider } from "@shopify/polaris";
import "@shopify/polaris/build/esm/styles.css";
import translations from "@shopify/polaris/locales/en.json";

export default function App() {
  return (
    <AppProvider i18n={translations}>
      <Outlet />
    </AppProvider>
  );
}

أنماط مكونات Polaris الشائعة لتطبيقات Shopify:

// Product listing with DataTable
import {
  Page,
  Card,
  DataTable,
  Button,
  Badge,
  Filters,
  EmptyState
} from "@shopify/polaris";

export default function ProductsPage() {
  const { products } = useLoaderData<typeof loader>();

  const rows = products.nodes.map(product => [
    product.title,
    <Badge tone={product.status === 'ACTIVE' ? 'success' : 'warning'}>
      {product.status}
    </Badge>,
    product.totalInventory,
    `${product.priceRangeV2.minVariantPrice.currencyCode} ${product.priceRangeV2.minVariantPrice.amount}`,
    <Button url={`/app/products/${product.id}`}>Edit</Button>
  ]);

  return (
    <Page
      title="Products"
      primaryAction={{ content: "Add product", url: "/app/products/new" }}
    >
      <Card>
        <DataTable
          columnContentTypes={['text', 'text', 'numeric', 'numeric', 'text']}
          headings={['Title', 'Status', 'Inventory', 'Price', 'Actions']}
          rows={rows}
        />
      </Card>
    </Page>
  );
}

** التعامل مع النموذج باستخدام Polaris و Remix **:

import { Form, useNavigation, useActionData } from "@remix-run/react";
import { TextField, Select, FormLayout, Banner } from "@shopify/polaris";

export default function ProductForm() {
  const actionData = useActionData<typeof action>();
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <Form method="post">
      <FormLayout>
        {actionData?.errors && (
          <Banner tone="critical">
            {actionData.errors.map(e => <p key={e.field}>{e.message}</p>)}
          </Banner>
        )}
        <TextField
          label="Product title"
          name="title"
          autoComplete="off"
        />
        <TextField
          label="Price"
          name="price"
          type="number"
          prefix="$"
          autoComplete="off"
        />
        <Button submit loading={isSubmitting}>Save product</Button>
      </FormLayout>
    </Form>
  );
}

خطافات الويب: التسجيل والمعالجة

يعد التعامل الموثوق مع خطاف الويب أمرًا بالغ الأهمية للتطبيقات التي تتفاعل مع أحداث متجر التجار.

تسجيل الخطافات عبر الويب عبر shopify.app.toml:

[[webhooks.subscriptions]]
topics = ["products/create", "products/update", "products/delete"]
uri = "/webhooks"

[[webhooks.subscriptions]]
topics = ["orders/create"]
uri = "/webhooks/orders"

** معالجة خطافات الويب في مسار Remix **:

// app/routes/webhooks.tsx
import { authenticate } from "../shopify.server";
import db from "../db.server";

export const action = async ({ request }: ActionFunctionArgs) => {
  const { topic, shop, session, payload } = await authenticate.webhook(request);

  switch (topic) {
    case "PRODUCTS_CREATE":
      await db.product.create({
        data: {
          shopifyId: payload.id.toString(),
          title: payload.title,
          shop: shop,
        }
      });
      break;

    case "APP_UNINSTALLED":
      if (session) {
        await db.session.deleteMany({ where: { shop } });
      }
      break;

    default:
      throw new Response("Unhandled webhook topic", { status: 404 });
  }

  return new Response(null, { status: 200 });
};

أفضل ممارسات موثوقية الرد التلقائي على الويب:

  1. قم بإرجاع استجابة 200 على الفور - قم بالمعالجة بشكل غير متزامن عبر قائمة انتظار الخلفية إذا كانت العملية بطيئة
  2. قم بتنفيذ القصور الذاتي باستخدام رأس X-Shopify-Webhook-Id الخاص بخطاف الويب
  3. تحقق من توقيع HMAC (يتم التعامل معه بواسطة authenticate.webhook)
  4. التعامل مع منطق إعادة المحاولة — يقوم Shopify بإعادة محاولة خطافات الويب الفاشلة حتى 19 مرة خلال 48 ساعة
  5. قم بتسجيل جميع حمولات webhook لتصحيح الأخطاء (قم بإزالة معلومات تحديد الهوية الشخصية قبل التسجيل)

واجهة برمجة تطبيقات الفواتير: رسوم الاشتراك والاستخدام

يجب أن تستخدم أي رسوم لشحن التطبيق Shopify's Billing API. تجاوزه ينتهك سياسات متجر التطبيقات.

** شراء التطبيق لمرة واحدة **:

export const loader = async ({ request }: LoaderFunctionArgs) => {
  const { billing, session } = await authenticate.admin(request);

  const { hasActivePayment, appSubscription } = await billing.check({
    plans: ["Professional Plan"],
    isTest: process.env.NODE_ENV !== "production",
  });

  if (!hasActivePayment) {
    await billing.request({
      plan: "Professional Plan",
      isTest: process.env.NODE_ENV !== "production",
    });
  }

  return json({ session });
};

تكوين خطط الفوترة في shopify.server.ts:

const shopify = shopifyApp({
  // ...other config
  billing: {
    "Professional Plan": {
      amount: 29.99,
      currencyCode: "USD",
      interval: BillingInterval.Every30Days,
    },
    "Enterprise Plan": {
      amount: 99.99,
      currencyCode: "USD",
      interval: BillingInterval.Every30Days,
    }
  }
});

جسر التطبيقات والملاحة المضمنة

App Bridge هي مكتبة JavaScript التي تدير تجربة iframe المضمنة داخل مسؤول Shopify.

أنماط جسر التطبيقات الرئيسية:

import { useAppBridge } from "@shopify/app-bridge-react";
import { Redirect } from "@shopify/app-bridge/actions";

// Navigate to an external URL from embedded context
function ExternalLinkButton() {
  const app = useAppBridge();

  const handleClick = () => {
    const redirect = Redirect.create(app);
    redirect.dispatch(Redirect.Action.REMOTE, {
      url: "https://your-docs-site.com",
      newContext: true
    });
  };

  return <Button onClick={handleClick}>View documentation</Button>;
}

// Toast notifications
import { useToast } from "@shopify/app-bridge-react";

function SaveButton() {
  const { show } = useToast();
  const fetcher = useFetcher();

  useEffect(() => {
    if (fetcher.state === "idle" && fetcher.data?.success) {
      show("Settings saved");
    }
  }, [fetcher.state, fetcher.data]);

  return (
    <Button
      onClick={() => fetcher.submit(formData, { method: "post" })}
      loading={fetcher.state !== "idle"}
    >
      Save
    </Button>
  );
}

الاختبار والتنمية المحلية

# Start the app in development mode
shopify app dev

# The CLI handles:
# - ngrok tunnel creation for webhook delivery
# - App installation on your development store
# - Hot module replacement for Remix routes
# - Shopify Partner Dashboard app URL updates

اختبار استعلامات GraphQL:

استخدم تطبيق Shopify GraphiQL في مسؤول متجر التطوير الخاص بك لاختبار الاستعلامات قبل تنفيذها في التعليمات البرمجية. الوصول عبر: your-store.myshopify.com/admin/apps/graphiql.

** لوادر مسار اختبار الوحدة **:

import { describe, it, expect, vi } from "vitest";
import { loader } from "~/routes/app.products";

vi.mock("~/shopify.server", () => ({
  authenticate: {
    admin: vi.fn().mockResolvedValue({
      admin: {
        graphql: vi.fn().mockResolvedValue({
          json: () => Promise.resolve({
            data: {
              products: {
                nodes: [{ id: "gid://shopify/Product/1", title: "Test Product" }],
                pageInfo: { hasNextPage: false, endCursor: null }
              }
            }
          })
        })
      },
      session: { shop: "test-shop.myshopify.com" }
    })
  }
}));

describe("Products loader", () => {
  it("returns product list", async () => {
    const request = new Request("https://test.com/app/products");
    const response = await loader({ request, params: {}, context: {} });
    const data = await response.json();

    expect(data.products.nodes).toHaveLength(1);
    expect(data.products.nodes[0].title).toBe("Test Product");
  });
});

الأسئلة المتداولة

هل يجب علي استخدام قالب Remix أو قالب Node/Express لتطبيقات Shopify الجديدة؟

استخدم قالب Remix لجميع التطبيقات الجديدة. قام Shopify بتوحيد Remix لأدوات تطوير التطبيقات الخاصة به - تم تحسين كل من Shopify CLI والوثائق وApp Bridge React من أجل Remix. لم يعد قالب Node/Express قيد التطوير بشكل نشط ويفتقر إلى العديد من ميزات المصادقة الحديثة بما في ذلك Token Exchange. لا تحتاج تطبيقات Node/Express الحالية إلى الترحيل على الفور، ولكن يجب أن تبدأ جميع التطبيقات الجديدة بـ Remix.

هل يمكنني استخدام إطار عمل مختلف للواجهة الأمامية بدلاً من Polaris؟

يمكنك استخدام نظام التصميم الخاص بك للأجزاء العامة من تطبيقك (صفحات الإعدادات على نطاقك الخاص، والصفحات المقصودة، وما إلى ذلك)، ولكن الأقسام المضمنة داخل مسؤول Shopify يجب أن تستخدم Polaris. تتطلب إرشادات مراجعة التطبيق بشكل صريح وجود Polaris لواجهة المستخدم المضمنة. عادةً ما تؤدي محاولة نسخ Polaris بصريًا باستخدام مكونات مخصصة إلى رفض المراجعة بسبب عدم الاتساق في السلوك وإمكانية الوصول.

كيف أتعامل مع حدود أسعار Shopify API في تطبيقي؟

تستخدم واجهة برمجة تطبيقات GraphQL Admin الخاصة بـ Shopify نموذج تحديد معدل "الدلو". يمنح كل متجر تطبيقك 1000 نقطة تكلفة لكل مجموعة، والتي يتم تجديدها بمعدل 50 نقطة في الثانية. راقب الحقل extensions.cost.throttleStatus في استجابات GraphQL. بالنسبة للعمليات المجمعة (التي تؤثر على آلاف السجلات)، استخدم واجهة برمجة التطبيقات للعمليات المجمعة، التي لها حدود معدل منفصلة. قم بتنفيذ التراجع الأسي مع عدم الاستقرار عندما تتلقى 429 ردًا.

ما هي قاعدة البيانات التي يجب أن أستخدمها لتطبيق Shopify الخاص بي في الإنتاج؟

استبدل SQLite الافتراضي بـ PostgreSQL للإنتاج. يعمل محول تخزين جلسة Prisma مع PostgreSQL - قم بتغيير سلسلة الاتصال في متغيرات البيئة الخاصة بك وقم بتحديث prisma/schema.prisma لاستخدام موفر postgresql. بالنسبة للتطبيقات ذات أحجام الجلسات الكبيرة، فكر في استخدام Redis لتخزين الجلسات بدلاً من PostgreSQL لتقليل تحميل قاعدة البيانات على مسار المصادقة الساخن.

كيف يمكنني نشر تطبيق Shopify المصمم باستخدام Remix؟

يوصي Shopify بالنشر على الأنظمة الأساسية التي تدعم وقت تشغيل Node.js: Fly.io، أو Render، أو Railway، أو AWS (EC2/ECS)، أو Google Cloud Run. تعمل Vercel وNetlify للواجهة الأمامية ولكن لا يمكنها تشغيل خادم Node.js المستمر الذي يتطلبه تخزين جلسة Shopify. تأكد من أن نظام النشر الخاص بك يدعم العمليات طويلة الأمد لمعالجة خطاف الويب ومهام الخلفية.


الخطوات التالية

يتطلب إنشاء تطبيق Shopify مخصص يتعامل مع المصادقة وإدارة بيانات GraphQL وخطافات الويب والفوترة بشكل صحيح خبرة عميقة في النظام الأساسي. يفشل التطبيق الذي تمت تهيئته بشكل خاطئ في مراجعة App Store، أو يعطل المتاجر التجارية، أو يكشف عن ثغرات أمنية.

تغطي خدمات تطوير تطبيقات Shopify من ECOSIRE دورة حياة التطوير الكاملة: التصميم المعماري، وتنفيذ Remix/Polaris، وتكامل واجهة برمجة التطبيقات GraphQL، والبنية التحتية لخطاف الويب، وإعداد الفواتير، وتقديم App Store، ودعم ما بعد الإطلاق.

ناقش متطلبات تطبيق Shopify المخصصة مع فريق التطوير لدينا.

E

بقلم

ECOSIRE Research and Development Team

بناء منتجات رقمية بمستوى المؤسسات في ECOSIRE. مشاركة رؤى حول تكاملات Odoo وأتمتة التجارة الإلكترونية وحلول الأعمال المدعومة بالذكاء الاصطناعي.

الدردشة على الواتساب