跳至主要内容

使用 Remix 和 Polaris 构建自定义 Shopify 应用程序

使用 Remix、Shopify CLI 3、Polaris 设计系统和 App Bridge 构建自定义 Shopify 应用程序以实现嵌入式管理体验的完整开发人员指南。

E
ECOSIRE Research and Development Team
|2026年3月19日9 分钟阅读1.6k 字数|

Shopify 迁移到 Remix 作为默认应用程序框架,标志着在该平台上构建生产应用程序的方式发生了根本性转变。基于 Remix 的 Shopify 应用程序模板在单个连贯堆栈中提供服务器端渲染、流式传输、嵌套路由和本机 Shopify CLI 集成 - 取代了 2023 年主导应用程序开发的旧 Node/Express + React 模式。

本指南介绍了完整的开发生命周期:脚手架、身份验证、GraphQL 数据获取、Polaris 组件集成、webhooks、计费以及经验丰富的 Shopify 开发人员用来发布可维护、高性能嵌入式应用程序的模式。

要点

  • Shopify CLI 3.x 生成一个可用于生产的 Remix 应用程序,其中包含 OAuth、会话存储和预配置的 App Bridge
  • authenticate.admin 帮助程序处理整个 OAuth 流程 — 不要手动实现 OAuth
  • Remix 的加载器/操作模式完美映射到 Shopify 的 GraphQL 请求/突变周期
  • App Store 批准需要 Polaris 组件 - 忽略 Polaris 标准的自定义 UI 无法通过审核
  • 为了可靠性,Webhook 必须通过应用程序配置注册,而不仅仅是 API 调用
  • Shopify Admin GraphQL API 已分页 - 始终在数据加载器中处理基于游标的分页
  • App Bridge 提供无 chrome 的嵌入式体验;使用 useAppBridge 钩子进行导航
  • 任何收取订阅费的应用程序都需要集成计费 API

项目设置和脚手架

从 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 年)

Shopify 较新的令牌交换身份验证策略消除了嵌入式应用程序的 OAuth 重定向。 Shopify 后台中的用户不需要离开和返回 - 来自 App Bridge 的会话令牌直接交换为服务器端的 API 访问令牌。使用 unstable_newEmbeddedAuthStrategy: true 启用此功能。


GraphQL 数据获取模式

Shopify 的管理 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 });
};

大型数据集的批量操作:

对于数千条记录的操作,请使用批量操作 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",然后从 url 下载并解析 JSONL。


Polaris 设计系统集成

Polaris 是 Shopify 的嵌入式应用程序设计系统。 App Store 提交需要使用 Polaris - 非 Polaris UI 将导致审核被拒绝。

在 Remix 中设置 Polaris:

// 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>
  );
}

Shopify 应用程序的常见 Polaris 组件模式:

// 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>
  );
}

Webhooks:注册和处理

可靠的 Webhook 处理对于响应商家商店事件的应用程序至关重要。

通过 shopify.app.toml 注册 webhook:

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

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

在 Remix 路由中处理 Webhooks:

// 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 });
};

Webhook 可靠性最佳实践:

  1. 立即返回 200 响应 — 如果操作缓慢,则通过后台队列异步处理
  2. 使用 webhook 的 X-Shopify-Webhook-Id 标头实现幂等性 3.验证HMAC签名(由authenticate.webhook处理)
  3. 处理重试逻辑 — Shopify 在 48 小时内重试失败的 Webhooks 最多 19 次
  4. 记录所有 Webhook 有效负载以进行调试(在记录之前剥离 PII)

计费 API:订阅和使用费用

任何应用收费都必须使用 Shopify 的 Billing API。绕过它违反了 App Store 政策。

一次性应用程序购买:

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 库,用于管理 Shopify 后台中的嵌入式 iframe 体验。

关键应用桥模式:

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");
  });
});

常见问题

我应该为新的 Shopify 应用使用 Remix 模板还是 Node/Express 模板?

对所有新应用程序使用 Remix 模板。 Shopify 已对其应用程序开发工具进行了 Remix 标准化 - Shopify CLI、文档和 App Bridge React 均针对 Remix 进行了优化。 The Node/Express template is no longer actively developed and lacks several modern authentication features including Token Exchange. Existing Node/Express apps do not need to migrate immediately, but all new apps should start with Remix.

Can I use a different frontend framework instead of Polaris?

您可以将自己的设计系统用于应用程序的面向公众的部分(您自己的域上的设置页面、登陆页面等),但 Shopify 后台中的嵌入部分必须使用 Polaris。应用程序审查指南明确要求使用 Polaris 进行嵌入式 UI。尝试使用自定义组件以视觉方式复制 Polaris 通常会因行为和可访问性不一致而导致审核被拒绝。

How do I handle Shopify API rate limits in my app?

Shopify 的 GraphQL 管理 API 使用“桶”速率限制模型。每个商店为您的应用程序提供每个存储桶 1,000 个成本点,每秒重新生成 50 个点。 Monitor the extensions.cost.throttleStatus field in GraphQL responses.对于批量操作(影响数千条记录),请使用批量操作 API,它具有单独的速率限制。当您收到 429 响应时,实施带抖动的指数退避。

What database should I use for my Shopify app in production?

Replace the default SQLite with PostgreSQL for production. Prisma 会话存储适配器可与 PostgreSQL 配合使用 - 更改环境变量中的连接字符串并更新 prisma/schema.prisma 以使用 postgresql 提供程序。对于会话量较大的应用程序,请考虑使用 Redis 而不是 PostgreSQL 进行会话存储,以减少热身份验证路径上的数据库负载。

How do I deploy a Shopify app built with Remix?

Shopify recommends deploying on platforms that support Node.js runtime: Fly.io, Render, Railway, AWS (EC2/ECS), or Google Cloud Run. Vercel and Netlify work for the frontend but cannot run the persistent Node.js server that Shopify's session storage requires. Ensure your deployment platform supports long-running processes for webhook processing and background jobs.


后续步骤

构建正确处理身份验证、GraphQL 数据管理、Webhooks 和计费的自定义 Shopify 应用程序需要深厚的平台专业知识。 A misconfigured app fails App Store review, breaks merchant stores, or exposes security vulnerabilities.

ECOSIRE 的 Shopify 应用开发服务 涵盖整个开发生命周期:架构设计、Remix/Polaris 实施、GraphQL API 集成、webhook 基础设施、计费设置、App Store 提交和发布后支持。

与我们的开发团队讨论您的自定义 Shopify 应用要求。

E

作者

ECOSIRE Research and Development Team

技术写作

ECOSIRE 技术写作团队的内容涵盖 Odoo ERP、Shopify 电子商务、AI 智能体、Power BI 分析、GoHighLevel 自动化以及企业软件最佳实践。我们的指南帮助企业做出明智的技术决策。

ECOSIRE

扩展您的 Shopify 商店

针对高增长电子商务的定制开发、优化和迁移服务。