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
|2026年3月19日8 分钟阅读1.6k 字数|

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

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 集成、电商自动化和 AI 驱动商业解决方案的洞见。

通过 WhatsApp 聊天