使用 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 可靠性最佳实践:
- 立即返回 200 响应 — 如果操作缓慢,则通过后台队列异步处理
- 使用 webhook 的
X-Shopify-Webhook-Id标头实现幂等性 3.验证HMAC签名(由authenticate.webhook处理) - 处理重试逻辑 — Shopify 在 48 小时内重试失败的 Webhooks 最多 19 次
- 记录所有 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 提交和发布后支持。
作者
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
相关文章
Shopify App Bridge 4 教程:在 2026 年构建嵌入式应用程序
使用 App Bridge 4 构建 Shopify 嵌入式管理应用程序:会话令牌、令牌交换、导航、模式、资源选择器和 Polaris React 13 设置。
Shopify 功能 2026:折扣、送货、支付定制
构建用于折扣、送货率定制、付款方式过滤和购物车验证的 Shopify 功能。 Rust + JavaScript 示例。
Shopify GraphQL 管理 API 2026:完整开发人员指南
掌握 Shopify Admin GraphQL API:查询、突变、OAuth、计算的查询成本、速率限制、批量操作和生产代码示例。