应用架构
构建 Shopify 应用需要理解你的应用如何在每一层与 Shopify 平台集成——从认证和会话管理到 UI 渲染和事件处理。本课程涵盖构建生产级 Shopify 应用所需的基础架构决策和模式。
应用类型
Shopify 支持三种主要的应用类型,每种适用于不同的用例:
公开应用
公开应用在 Shopify App Store 上架,任何商家都可以安装。它们需要通过应用审核流程,必须满足 Shopify 的质量、安全和性能要求。
- 通过 Shopify App Store 分发
- 与每个商家进行基于 OAuth 的认证
- 必须处理多租户数据隔离
- 与 Shopify 分享收入(Shopify 对前 100 万美元收取 0%,之后收取一定比例)
自定义应用
自定义应用是为单个商家或组织构建的。它们不在 App Store 上架,直接通过 Shopify 管理后台安装。
- 为特定商店或 Plus 组织构建
- 使用自定义应用访问令牌(无需 OAuth 流程)
- 由于服务单一租户,架构更简单
- 无需 App Store 审核
草稿应用
草稿应用是尚未提交审核的开发中应用。它们可以安装在开发商店上进行测试。
- 通过 Partners Dashboard 或 Shopify CLI 创建
- 通过安装 URL 安装在开发商店上
- 完整的测试和开发功能
- 准备好后转换为公开或自定义应用
如果你为特定客户构建,请从自定义应用开始。如果你为生态系统构建产品,请以草稿应用开始,准备好后转换为公开应用。本课程中的架构模式适用于所有三种类型。
Remix 应用模板
Shopify 的官方应用脚手架构建在 Remix 上,这是一个全栈 React 框架。该模板开箱即用地提供了你需要的一切:
# 使用 Remix 模板创建新的 Shopify 应用
shopify app init --template remix
# 脚手架后的项目结构
my-shopify-app/
├── app/
│ ├── routes/
│ │ ├── app._index.tsx # 应用主页
│ │ ├── app.tsx # 带导航的应用布局
│ │ ├── auth.$.tsx # OAuth 回调处理器
│ │ ├── auth.login/
│ │ │ └── route.tsx # 登录页面
│ │ └── webhooks.tsx # Webhook 处理器
│ ├── shopify.server.ts # Shopify API 配置
│ └── entry.server.tsx # 服务器入口点
├── extensions/ # 应用扩展
├── prisma/
│ └── schema.prisma # 数据库 Schema
├── shopify.app.toml # 应用配置
├── package.json
└── remix.config.js
关键文件说明
shopify.server.ts 文件是你的 Shopify 集成核心。它配置认证、API 访问和会话存储:
// app/shopify.server.ts
import "@shopify/shopify-app-remix/adapters/node";
import {
ApiVersion,
AppDistribution,
shopifyApp,
DeliveryMethod,
} from "@shopify/shopify-app-remix/server";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";
const shopify = shopifyApp({
apiKey: process.env.SHOPIFY_API_KEY!,
apiSecretKey: process.env.SHOPIFY_API_SECRET!,
apiVersion: ApiVersion.January25,
scopes: process.env.SCOPES?.split(","),
appUrl: process.env.SHOPIFY_APP_URL!,
authPathPrefix: "/auth",
sessionStorage: new PrismaSessionStorage(prisma),
distribution: AppDistribution.AppStore,
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
PRODUCTS_UPDATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},
hooks: {
afterAuth: async ({ session }) => {
shopify.registerWebhooks({ session });
},
},
});
export default shopify;
export const apiVersion = ApiVersion.January25;
export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
export const authenticate = shopify.authenticate;
export const unauthenticated = shopify.unauthenticated;
export const login = shopify.login;
export const registerWebhooks = shopify.registerWebhooks;
export const sessionStorage = shopify.sessionStorage;
App Bridge 4.x
App Bridge 是将你的应用前端连接到 Shopify 管理后台的 JavaScript 库。4.x 版本是一次重大改版,简化了集成模型。
App Bridge 的功能
- 导航:你的应用出现在 Shopify 管理后台的 iframe 中。App Bridge 处理导航、返回按钮和 URL 同步。
- 认证:管理会话令牌并确保 API 请求经过正确认证。
- UI 集成:提供对 Shopify 管理后台 UI 元素的访问,如 Toast 通知、模态窗口和上下文栏。
- 资源选择器:内置组件,供商家选择产品、系列和其他 Shopify 资源。
Remix 中的 App Bridge
使用 Remix 模板时,App Bridge 会自动配置。app.tsx 布局组件包裹你的应用:
// app/routes/app.tsx
import { Outlet } from "@remix-run/react";
import { AppProvider } from "@shopify/shopify-app-remix/react";
import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
export default function App() {
return (
<AppProvider isEmbeddedApp apiKey={window.ENV.SHOPIFY_API_KEY}>
<Outlet />
</AppProvider>
);
}
App Bridge 4.x 移除了早期版本中使用的 createApp() 函数模式。如果你从 App Bridge 3.x 迁移,你不再需要创建显式的应用实例。相反,当你的应用在 Shopify 管理后台中加载时,App Bridge 会自动初始化。直接的 window.shopify 调用取代了旧的 app.dispatch() 模式。
Polaris 设计系统
Polaris 是 Shopify 的设计系统,提供与 Shopify 管理后台外观和感觉匹配的 React 组件。使用 Polaris 确保你的应用对商家来说感觉是原生的。
// app/routes/app._index.tsx
import { json, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import {
Page, Layout, Card, BlockStack, Text,
DataTable, Badge, Button, InlineStack,
} from "@shopify/polaris";
import { authenticate } from "../shopify.server";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { admin } = await authenticate.admin(request);
const response = await admin.graphql(`
query {
orders(first: 10, sortKey: CREATED_AT, reverse: true) {
edges {
node {
id
name
totalPriceSet { shopMoney { amount currencyCode } }
displayFulfillmentStatus
createdAt
}
}
}
}
`);
const data = await response.json();
return json({ orders: data.data.orders.edges.map((e: any) => e.node) });
};
export default function Index() {
const { orders } = useLoaderData<typeof loader>();
const rows = orders.map((order: any) => [
order.name,
`${order.totalPriceSet.shopMoney.amount} ${order.totalPriceSet.shopMoney.currencyCode}`,
order.displayFulfillmentStatus,
new Date(order.createdAt).toLocaleDateString(),
]);
return (
<Page title="仪表板" primaryAction={{ content: "同步数据" }}>
<Layout>
<Layout.Section>
<Card>
<BlockStack gap="300">
<InlineStack align="space-between">
<Text as="h2" variant="headingMd">最近订单</Text>
<Badge tone="success">实时</Badge>
</InlineStack>
<DataTable
columnContentTypes={["text", "numeric", "text", "text"]}
headings={["订单", "总额", "履行状态", "日期"]}
rows={rows}
/>
</BlockStack>
</Card>
</Layout.Section>
</Layout>
</Page>
);
}
Remix 模板附带 Polaris 12.x+。在升级时请始终查看 Polaris 迁移指南,因为组件 API 在主要版本之间会有变化。BlockStack 和 InlineStack 组件在 Polaris 12 中取代了旧的 Stack 组件。
会话管理
Shopify 应用必须为经过认证的商家管理会话。会话包含商店域名、访问令牌和权限范围。Remix 模板通过 Prisma 支持的会话存储来处理这些。
会话流程
会话存储配置
// prisma/schema.prisma
model Session {
id String @id
shop String
state String
isOnline Boolean @default(false)
scope String?
expires DateTime?
accessToken String
userId BigInt?
@@index([shop])
}
永远不要记录、在客户端代码中暴露或将 Shopify 访问令牌存储在 Cookie 中。访问令牌在你的应用范围内授予对商家商店的完全 API 访问权限。将它们加密存储在数据库中,并且只在服务器端访问。
Webhook 和事件驱动架构
Webhook 是当商家商店中发生事件时,Shopify 发送到你的应用的 HTTP 回调。它们是响应式应用行为的基础。
注册 Webhook
Webhook 在 shopify.server.ts 中声明,并在 OAuth 后自动注册:
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
ORDERS_CREATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
PRODUCTS_UPDATE: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
CUSTOMERS_DATA_REQUEST: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
CUSTOMERS_REDACT: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
SHOP_REDACT: {
deliveryMethod: DeliveryMethod.Http,
callbackUrl: "/webhooks",
},
},
处理 Webhook
// app/routes/webhooks.tsx
import type { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import db from "../db.server";
export const action = async ({ request }: ActionFunctionArgs) => {
const { topic, shop, session, admin, payload } =
await authenticate.webhook(request);
switch (topic) {
case "APP_UNINSTALLED":
if (session) {
await db.session.deleteMany({ where: { shop } });
}
await db.appData.deleteMany({ where: { shop } });
break;
case "ORDERS_CREATE":
await processNewOrder(shop, payload);
break;
case "PRODUCTS_UPDATE":
await syncProductUpdate(shop, payload);
break;
case "CUSTOMERS_DATA_REQUEST":
await handleDataRequest(shop, payload);
break;
case "CUSTOMERS_REDACT":
await handleCustomerRedact(shop, payload);
break;
case "SHOP_REDACT":
await handleShopRedact(shop, payload);
break;
default:
console.warn(`Unhandled webhook topic: ${topic}`);
}
return new Response(null, { status: 200 });
};
每个 Shopify 应用必须处理三个 GDPR Webhook:CUSTOMERS_DATA_REQUEST、CUSTOMERS_REDACT 和 SHOP_REDACT。如果这些没有正确实现,你的应用将无法通过 App Store 审核。即使你的应用不存储客户数据,你也必须以 200 响应确认这些 Webhook。
Webhook 最佳实践
- 始终快速以 200 响应 —— 异步处理 Webhook 负载。Shopify 期望在 5 秒内获得响应。
- 处理幂等性 —— Shopify 可能多次发送相同的 Webhook。使用
X-Shopify-Webhook-Id头进行去重。 - 验证 HMAC 签名 ——
authenticate.webhook()调用会处理此事,但如果你构建自定义处理器,请始终验证X-Shopify-Hmac-SHA256头。 - 监控投递失败 —— Shopify 会在 48 小时内重试失败的 Webhook。如果你的端点持续失败,Shopify 将移除注册。
架构决策图
现在你已经理解了架构基础,继续学习管理后台 UI 扩展,了解如何使用自定义 UI 组件扩展 Shopify 管理后台。