使用 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 Research and Development Team
在 ECOSIRE 构建企业级数字产品。分享关于 Odoo 集成、电商自动化和 AI 驱动商业解决方案的洞见。
相关文章
Case Study: eCommerce Migration to Shopify with Odoo Backend
How a fashion retailer migrated from WooCommerce to Shopify and connected it to Odoo ERP, cutting order fulfillment time by 71% and growing revenue 43%.
Integrating GoHighLevel CRM with eCommerce Stores
Step-by-step guide to integrating GoHighLevel CRM with Shopify and WooCommerce. Sync orders, automate post-purchase flows, and recover abandoned carts at scale.
Odoo + Shopify Sync: Products, Orders, and Inventory
Complete guide to syncing Odoo 19 with Shopify. Covers product sync, real-time order import, bidirectional inventory, financial reconciliation, and multi-store management.