关于 spree-storefront
面向开发者的 Spree Next.js 店面应用集成参考,说明如何通过 @spree/sdk 连接 Spree 后端 Store API。内容重点包括服务端数据请求、购物车与结账、多地区路由及可选的批发界面。
主要功能
- 01通过 @spree/sdk 调用 Store API
- 02服务端处理 API 请求与身份验证
- 03实现购物车、支付和订单流程
- 04按国家与语言组织店面路由
- 05配置批发渠道与 Webhook
适用场景
- 01为 Spree 后端构建 Next.js 店面
- 02实现购物车到订单的结账流程
- 03按市场提供多地区店面页面
- 04在批发渠道中配置专用购物体验
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-storefront安装后在 Claude Code 中输入/spree-storefront确认已加载。
详细介绍
Spree Storefront(Next.js)
Spree Next.js storefront 是一个独立应用,通过 v3 Store API(/api/v3/store/*)和 @spree/sdk 2.x 与 Spree 后端通信。create-spree-app 会将其放在 apps/storefront/ 中(与 server/ 和 apps/dashboard/ 并列);也可以 fork https://github.com/spree/storefront。使用 Next.js 16、React 19 和 Tailwind 4。
storefront 为可选项——任何前端(Astro、Remix、React Native 等)都可以使用相同的 Store API。本技能以官方 storefront 为前提;对于任何客户端,API 契约都相同。
架构——服务端优先
Browser → Server Action (src/lib/data/*) → @spree/sdk → Spree Store API
httpOnly cookies (JWT, refresh token, cart token, country/locale) via src/lib/spree helpers
- 所有 API 调用都在服务端运行(
src/lib/data/中的 Server Actions / Server Components)。浏览器不会直接调用 Spree;即使是可发布密钥也仅在服务端使用(SPREE_PUBLISHABLE_KEY,不使用NEXT_PUBLIC_)。 src/lib/spree/存放集成代码:getClient()、cookie helpers、withAuthRefresh()、getLocaleOptions()、getCartOptions()、middleware、surfaces(DTC 与批发)、webhooks。- 客户端组件(
CartContext、CheckoutContext等)负责保存 UI 状态并调用 Server Actions——不要在浏览器中添加通过 SWR/React Query 向 Store API 发起的请求。
// src/lib/data/customer.ts (pattern)
import { getClient, withAuthRefresh, setAccessToken, setRefreshToken } from '@/lib/spree'
export async function login(email: string, password: string) {
const { token, refresh_token, user } = await getClient().auth.login({ email, password })
await setAccessToken(token)
await setRefreshToken(refresh_token)
return { success: true, user }
}
export async function getCustomer() {
return withAuthRefresh((options) => getClient().customer.get(options))
}
必需的环境变量(.env.local):SPREE_API_URL、SPREE_PUBLISHABLE_KEY。可选项:NEXT_PUBLIC_DEFAULT_COUNTRY / NEXT_PUBLIC_DEFAULT_LOCALE、NEXT_PUBLIC_SITE_URL、SPREE_WHOLESALE_CHANNEL、SPREE_WHOLESALE_PUBLISHABLE_KEY、SPREE_WEBHOOK_SECRET、RESEND_API_KEY、EMAIL_FROM。NEXT_PUBLIC_* 仅用于浏览器确实需要的值(默认地区、Stripe/PayPal 可发布密钥)。
@spree/sdk 基础要点
import { createClient } from '@spree/sdk'
const spree = createClient({
baseUrl: process.env.SPREE_API_URL!,
publishableKey: process.env.SPREE_PUBLISHABLE_KEY!,
locale: 'en', // sent as x-spree-locale (also per request: { locale })
channel: 'online', // sent as X-Spree-Channel; omit for the store default
})
const { data, meta } = await spree.products.list({ expand: ['media', 'default_variant'] })
const product = await spree.products.get('cool-shirt', { expand: ['variants', 'media'] }) // slug or prod_ id
const cart = await spree.carts.create()
await spree.carts.items.create(cart.id, { variant_id: 'variant_k5nR8xLq', quantity: 1 }, { spreeToken: cart.token })
const orders = await spree.customer.orders.list({}, { token }) // logged-in customer (JWT)
- 响应为扁平结构(
product.name);关联关系通过*_id字段表示,或使用expand获取嵌套对象(product.default_variant、product.primary_media、product.media[])。价格以Price对象返回(product.price.display_amount)——切勿在客户端计算金额。 - 认证模式:仅使用可发布密钥(浏览)· +
spreeToken(访客购物车,x-spree-token)· +tokenJWT(客户)。类型定义在@spree/sdk(也可从@spree/sdk/zod获取),webhook 辅助函数在@spree/sdk/webhooks中。 - ID 是带前缀的字符串(
prod_、variant_、cart_、or_、cust_、ful_)。购物车使用cart.id——cart.number是id的弃用镜像字段,为 5.x 客户端保留(在 6.1 中移除)。
购物车 → 结账 → 订单
Spree 6 将可变的 购物车(cart_…)与购物车完成后创建的不可变 订单(or_…)分开。无需推进结账状态机:你可以按任意顺序更新购物车(邮箱、地址、配送费率、付款),购物车会报告仍缺少哪些内容。
const cart = await spree.carts.get(cartId, { spreeToken })
cart.requirements
// [{ step: 'address', field: 'ship_address', code: 'ship_address_required', message: '…' },
// { step: 'delivery', field: 'delivery_method', code: 'delivery_method_required', message: '…' }, …]
requirements[]条目格式为{ step, field, code, message }。代码包括line_items_required、email_required、ship_address_required、delivery_method_required、payment_required、po_number_required、guest_checkout_not_allowed、quantity_rule_violated、order_minimum_not_met、discontinued、out_of_stock。步骤(cart、address、delivery、payment)是用于 UI 分组的参考信息——将它们映射到结账各部分并显示message。只有完成操作才是硬性门槛。自定义要求来自后端(Spree::Checkout::Registry.add_requirement,参见spree-checkout)。- 配送:
spree.carts.fulfillments.update(cartId, fulfillmentId, { selected_delivery_rate_id }, opts)会返回重新计算后的购物车。 - 付款(基于会话的网关——Stripe、Adyen、PayPal):
spree.carts.paymentSessions.create(cartId, { payment_method_id }, opts)→ 提供商数据(客户端密钥、drop-in 令牌、重定向 URL)。- 渲染提供商 UI/执行重定向。
spree.carts.paymentSessions.complete(cartId, sessionId, { session_result }, opts)。spree.carts.complete(cartId, opts)→ 返回 订单(类型为Order | OrderGroup——多卖家商城购物车会返回ogrp_…分组;使用@spree/sdk中的isOrderGroup进行类型缩窄)。此操作具有幂等性:如果网关 webhook 已完成购物车,则会返回已有结果。 非会话方式(支票、货到付款、银行转账):spree.carts.payments.create(...)。
- 完成后,无法再通过
carts.get获取购物车——使用spree.orders.get(id, params, { spreeToken | token })读取结果。它接受or_…ID 或原始的cart_…ID,因此确认页面(order-placed/[id])可以继续使用购物车 ID 作为标识。订单的status为draft/placed/canceled,另有独立的payment_status和fulfillment_status。 - 参考流程:
src/lib/data/checkout.ts、payment.ts(completeCheckoutOrder也能处理 403/422 竞争情况)、components/checkout/*和网关 gem(例如spree_stripe)。
多区域 URL
每个路由都位于 src/app/[country]/[locale]/… 下(/us/en/products、/de/de/products),并映射到一个 Spree 市场(国家 + 货币 + 区域设置)。src/proxy.ts(Next 16 的中间件文件)会配置 @/lib/spree/middleware 中的 createSpreeMiddleware({ defaultCountry, defaultLocale, staticRoutes? }),它会:
- 将不带前缀的路径重定向到
/{country}/{locale}/…; - 从
spree_countrycookie →x-vercel-ip-country/cf-ipcountry→ 默认值检测国家,并从spree_locale→Accept-Language→ 默认值检测区域设置; - 将
spree_country/spree_localecookie 与 URL 保持同步。
数据函数不接受区域参数——它们会调用 getLocaleOptions(),读取这些 cookie 并将区域设置/国家传给 SDK。新页面应放在 src/app/[country]/[locale]/(storefront)/your-page/page.tsx 下,以继承布局。CountrySwitcher 通过导航到新的前缀来切换区域。
批发/渠道界面
/wholesale 是一个启用加入的 B2B 门户,使用独立的 渠道;仅当 SPREE_WHOLESALE_CHANNEL 设置为受限渠道的代码时才会启用(未设置 → 隐藏所有批发 UI,路由返回 404)。界面 = 与渠道绑定的 SDK 客户端(channel: 'wholesale' → X-Spree-Channel),拥有独立的购物车 cookie 和缓存标签;客户 JWT 由两者共享。API 会强制执行渠道的 storefront_access(login_required | prices_hidden | public)(返回 401,或将金额字段设为 null);买家获批条件 = 加入 Wholesale 客户组;批发价来自后端价格表。请将其视为可改造的参考实现(例如仅限会员的 DTC、仅面向 B2B 的商店)——参见 spree-b2b。
Webhook 和事务性邮件
店面可以负责发送客户邮件(src/lib/emails/ 中的 react-email 模板,通过 Resend 发送;开发环境下写入 .next/emails/;可在 /dev/emails 预览)。路由 src/app/api/webhooks/spree/route.ts 使用 @/lib/spree/webhooks 中的 createWebhookHandler,该函数会验证 X-Spree-Webhook-Signature(对 "{timestamp}.{body}" 计算 HMAC-SHA256,使用时序安全比较,并设置 5 分钟的重放保护窗口),然后按事件名称进行分发:
import { createWebhookHandler } from '@/lib/spree/webhooks'
const handler = createWebhookHandler({
secret: process.env.SPREE_WEBHOOK_SECRET!,
handlers: {
'order.placed': handleOrderPlaced,
'order.canceled': handleOrderCanceled,
'order.fulfilled': handleOrderFulfilled, // or 'fulfillment.fulfilled' for per-parcel emails
'customer.password_reset_requested': handlePasswordReset, // must be subscribed by name — `*` never receives it
},
// waitUntil, toleranceSeconds optional
})
export const POST = handler
- 事件名称采用 Spree 6 的命名:
order.placed(而非order.completed)、order.fulfilled/fulfillment.fulfilled(而非order.shipped)。已发布的店面代码仍注册了order.completed和order.shipped——这些是已弃用的别名,Spree 6.0 仍会与新名称一并发送,但从 6.1 起将停止发送。请将处理程序和端点订阅都改为新名称。 - 市场(拆分)结账:子订单会发布
order.placed,并带有notify_customer: false;购买确认事件属于order_group.completed——如果要发送订单邮件,请为其添加处理程序(参见spree-marketplace)。 - 如果由店面负责发送客户邮件,请通过商店偏好设置
send_consumer_transactional_emails(设为 false)关闭 Spree 自带的邮件,以免客户收到重复邮件。此开关(以及订单上的notify_customer: false)只会关闭客户邮件——只要设置了new_order_notifications_email地址,商店的新订单通知仍会发送。 - 在控制面板的 Settings → Developer → Webhooks 下创建端点并订阅事件,然后将其密钥复制到
SPREE_WEBHOOK_SECRET。如需本地测试,可公开店面(cloudflared tunnel --url http://localhost:3001)。 - 投递失败后会进行退避重试(约 6 分钟内尝试 5 次,事件
id相同);连续 15 次投递失败后,端点会被禁用——请尽快返回 2xx,按event.id去重,并通过waitUntil/队列执行耗时操作。未处理的事件会返回 200{ handled: false }。 - 在官方店面以外的项目中,请使用
@spree/sdk/webhooks中的verifyWebhookSignature(body, signature, timestamp, secret)和类型WebhookEvent<T>。完整事件列表:spree-events-webhooks。
测试
- **单元/集成测试:**Vitest —
pnpm test/pnpm test:watch(测试与代码放在一起,例如src/lib/data/__tests__)。 - **端到端测试:**使用 Playwright,通过 Docker 连接真实后端:导出匹配的一对 Stripe 测试密钥(
STRIPE_PUBLISHABLE_KEY、STRIPE_SECRET_KEY),运行pnpm e2e:up(启动 Postgres/Redis 和ghcr.io/spree/spree:latest,通过@spree/cli填充数据,并写入.env.e2e),运行pnpm test:e2e(交互模式使用:ui),最后运行pnpm e2e:down。 - 针对自己的后端测试:
SPREE_IMAGE=<image> pnpm e2e:up。在create-spree-app项目中,先在根目录运行docker build . -f server/Dockerfile -t project-spree:e2e进行构建;生成的 CI 会自动执行此操作。
商店前端还是后端——应该在哪里进行更改
| 想要…… | 应在……中进行 |
|---|---|
| 调整布局、文案、样式或新增内容页面 | 商店前端(src/app/[country]/[locale]/(storefront)/…,src/app/globals.css 中的 @theme 令牌) |
| 在商品/购物车响应中添加字段 | 后端——替换模型和序列化器(spree-resource、spree-api-v3) |
| 更改定价 | 后端——价格列表/规则(spree-pricing) |
| 更改购物车总额的计算方式 | 后端——Spree::Carts::RecalculateTotals(DI 键 cart_recalculate_totals_workflow)或工作流钩子(spree-order-totals、spree-workflows) |
| 添加结账要求(例如“接受条款”) | 后端的 Spree::Checkout::Registry.add_requirement;商店前端负责渲染新的 requirements[] 代码 |
| 添加国家/货币/语言 | 后端 Markets;商店前端只需遵循 URL 和 Cookie |
| 自定义结账 UI | 商店前端(components/checkout/*) |
| 对 PDP 进行 A/B 测试、客户端分析 | 商店前端(src/lib/analytics) |
| 将订单同步到 CRM/ERP | 后端订阅 order.placed 事件(spree-events-webhooks) |
| 客户邮件的外观和风格 | 商店前端邮件(webhooks)或后端 mailers——选择其中一方负责 |
规则:任何客户可见的内容都属于商店前端;任何涉及数据、资金或业务规则的内容都属于后端,这样每个前端才能获得一致的行为。
注意事项
- 密钥(
sk_…)绝不能放在商店前端——即使是服务端代码也不行;Store API 使用可发布密钥。管理任务应在控制面板中完成,或通过后端作业执行。 - 购物车令牌可授予购物车访问权限——请将其保存在 httpOnly、安全的 Cookie 中(辅助函数会处理)。
- 缓存商品目录,绝不缓存购物车/结账。 商店前端使用 Next 缓存标签区分不同表面(
updateTag(cartTag(surface)))——购物车发生变更后应使缓存失效。 - 不要假设结账步骤有固定顺序——根据
requirements[]驱动 UI;并处理carts.complete返回错误(代码cart_cannot_complete)的情况,重新读取购物车的requirements。 - 完成结账后不要调用
carts.get——使用购物车或订单 ID 调用orders.get。 - 本地化由请求头驱动(
x-spree-locale、X-Spree-Channel),而非Accept-Language;商店前端通过 Cookie 从 URL 路径段获取本地化信息。 - 跟进上游版本:将商店前端仓库添加为
upstream远程仓库,并定期合并;将自定义内容保存在你维护的组件/数据文件中。
延伸阅读
- 文档(已安装):
node_modules/@spree/docs/dist/developer/storefront/nextjs/{architecture,customization,multi-region,wholesale,emails,environment-variables,testing,deployment,wallet-payments}.md;SDK:developer/sdk/quickstart.md。在线文档:https://spreecommerce.org/docs/developer/storefront/nextjs/architecture - 教程:
developer/tutorial/storefront.md - 源码:https://github.com/spree/storefront(`src/lib/spree/`、`src/lib/data/`、`src/app/api/webhooks/spree/route.ts`)
- 相关技能:
spree-typescript-sdk、spree-api-v3、spree-checkout、spree-events-webhooks、spree-b2b、spree-i18n、spree-deployment。