关于 shopify-storefront-api
面向构建自定义 Shopify 店面的开发者,介绍用于前端访问产品、系列、购物车和结账的 GraphQL API。内容包括令牌设置、查询与购物车管理,适用于无头店面及移动应用等场景。
主要功能
- 01创建 Storefront Access Token
- 02通过 GraphQL 查询产品和系列
- 03创建购物车并管理商品行
- 04保存购物车并跳转结账
- 05实现预测搜索和变体选择
适用场景
- 01构建 Next.js 或 Remix 无头店面
- 02为移动应用读取产品与购物车数据
- 03在非 Shopify 网站嵌入购买组件
- 04实现跨会话购物车持久化
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ finsilabs/ awesome-ecommerce-skills --skill shopify-storefront-api安装后在 Claude Code 中输入/shopify-storefront-api确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
tile.jsonevals/cart-persistence-usererrors-handling-var/criteria.jsontask.mdevals/cursor-pagination-incontext-directive-fr/criteria.jsontask.mdevals/storefront-client-setup-with-public-and-/criteria.jsontask.md详细介绍
Shopify Storefront API
概述
Shopify Storefront API 是面向公众的 GraphQL API,可从任何前端读写商店的产品、系列、购物车和结账信息。它使用 Storefront Access Token(与 Admin API 令牌不同),可安全地公开在客户端 JavaScript 中。可用它搭建无头店面,搭配 Next.js、Remix/Hydrogen 或任何 JS 框架。
何时使用此技能
- 使用自定义前端框架构建无头 Shopify 店面时
- 创建需要产品和购物车数据的 React Native 或 Flutter 移动应用时
- 在非 Shopify 网站中嵌入 Shopify 购买按钮或产品组件时
- 使用 Shopify Hydrogen(基于 Remix)打造完全自定义的店面体验时
- 需要实时获取产品库存情况或价格,且不想承担 Admin API 的开销时
- 使用 Shopify 托管的购物车实现跨会话购物车持久化时
核心说明
-
创建 Storefront Access Token
在 Shopify Admin → Apps → Develop apps → Your App → API credentials → Storefront API access token 中创建。也可以通过 Admin API 创建:
// Via Admin API (one-time setup) const token = await admin.graphql(` mutation { storefrontAccessTokenCreate(input: { title: "Headless Frontend" }) { storefrontAccessToken { accessToken title } userErrors { field message } } } `);Storefront Access Token 不使用
shpat_前缀(该前缀用于 Admin API 令牌)。Storefront Token 是不透明字符串,可安全地用于浏览器代码中;它们仅允许执行 Storefront 范围内的操作。 -
设置 Storefront API 客户端
使用官方的
@shopify/storefront-api-client:npm install @shopify/storefront-api-client// lib/shopify.ts import { createStorefrontApiClient } from "@shopify/storefront-api-client"; export const storefront = createStorefrontApiClient({ storeDomain: process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN!, // e.g. "mystore.myshopify.com" apiVersion: "2025-01", publicAccessToken: process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_TOKEN!, });使用私有访问令牌进行服务端调用(速率限制更高):
export const storefrontServer = createStorefrontApiClient({ storeDomain: process.env.SHOPIFY_STORE_DOMAIN!, apiVersion: "2025-01", privateAccessToken: process.env.SHOPIFY_STOREFRONT_PRIVATE_TOKEN!, }); -
查询产品和系列
// lib/products.ts export async function getProducts(first = 20, after?: string) { const { data, errors } = await storefront.request(` query GetProducts($first: Int!, $after: String) { products(first: $first, after: $after, sortKey: BEST_SELLING) { pageInfo { hasNextPage endCursor } edges { node { id title handle availableForSale priceRange { minVariantPrice { amount currencyCode } maxVariantPrice { amount currencyCode } } images(first: 1) { edges { node { url altText width height } } } variants(first: 10) { edges { node { id title availableForSale selectedOptions { name value } price { amount currencyCode } } } } } } } } `, { variables: { first, after } }); if (errors) throw new Error(errors.message); return data.products; } -
创建和管理购物车
// lib/cart.ts // Create a new cart export async function cartCreate(lines: { merchandiseId: string; quantity: number }[]) { const { data } = await storefront.request(` mutation CartCreate($lines: [CartLineInput!]) { cartCreate(input: { lines: $lines }) { cart { id checkoutUrl lines(first: 50) { edges { node { id quantity merchandise { ... on ProductVariant { id title price { amount currencyCode } product { title handle } } } } } } cost { subtotalAmount { amount currencyCode } totalAmount { amount currencyCode } } } userErrors { field message } } } `, { variables: { lines } }); return data.cartCreate; } // Add lines to existing cart export async function cartLinesAdd(cartId: string, lines: { merchandiseId: string; quantity: number }[]) { const { data } = await storefront.request(` mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) { cartLinesAdd(cartId: $cartId, lines: $lines) { cart { id checkoutUrl } userErrors { field message } } } `, { variables: { cartId, lines } }); return data.cartLinesAdd; } -
持久化购物车 ID 并跳转到结账页面
// hooks/useCart.ts import { useState, useEffect } from "react"; import { cartCreate, cartLinesAdd } from "../lib/cart"; const CART_ID_KEY = "shopify_cart_id"; export function useCart() { const [cartId, setCartId] = useState<string | null>(null); const [checkoutUrl, setCheckoutUrl] = useState<string | null>(null); useEffect(() => { setCartId(localStorage.getItem(CART_ID_KEY)); }, []); const addToCart = async (variantId: string, quantity = 1) => { const lines = [{ merchandiseId: variantId, quantity }]; if (cartId) { const result = await cartLinesAdd(cartId, lines); setCheckoutUrl(result.cart.checkoutUrl); } else { const result = await cartCreate(lines); const newCartId = result.cart.id; localStorage.setItem(CART_ID_KEY, newCartId); setCartId(newCartId); setCheckoutUrl(result.cart.checkoutUrl); } }; const goToCheckout = () => { if (checkoutUrl) window.location.href = checkoutUrl; }; return { addToCart, goToCheckout, cartId }; }
示例
带有变体选择的产品详情页(Next.js)
// app/products/[handle]/page.tsx
import { storefront } from "@/lib/shopify";
async function getProduct(handle: string) {
const { data } = await storefront.request(`
query GetProduct($handle: String!) {
product(handle: $handle) {
id
title
descriptionHtml
seo { title description }
images(first: 10) {
edges { node { url altText } }
}
options {
id name values
}
variants(first: 100) {
edges {
node {
id
availableForSale
selectedOptions { name value }
price { amount currencyCode }
compareAtPrice { amount currencyCode }
}
}
}
}
}
`, { variables: { handle } });
return data.product;
}
export default async function ProductPage({ params }: { params: { handle: string } }) {
const product = await getProduct(params.handle);
// Render product with client-side variant picker
return <ProductDetail product={product} />;
}
// Generate static params for all products
export async function generateStaticParams() {
const { data } = await storefront.request(`
query { products(first: 200) { edges { node { handle } } } }
`);
return data.products.edges.map(({ node }: { node: { handle: string } }) => ({
handle: node.handle,
}));
}
预测性搜索
export async function predictiveSearch(query: string) {
const { data } = await storefront.request(`
query PredictiveSearch($query: String!) {
predictiveSearch(query: $query, limit: 5, types: [PRODUCT, COLLECTION, ARTICLE]) {
products {
id title handle
featuredImage { url altText }
priceRange { minVariantPrice { amount currencyCode } }
}
collections {
id title handle
image { url altText }
}
}
}
`, { variables: { query } });
return data.predictiveSearch;
}
最佳实践
- 在服务端使用私有令牌——私有 Storefront Access Token 的速率限制更高(1000 req/s,而非 100 req/s),且绝不可暴露给浏览器
- 尽可能在构建时获取产品数据(ISR 或 SSG)——Storefront API 的速率限制按商店计算,而非按客户计算
- 在显示“加入购物车”之前,始终检查产品和变体的
availableForSale——产品可能有货,但个别变体可能已售罄 - 使用
after游标分页,不要使用偏移量——Storefront API 使用基于游标的分页;保存endCursor以便查询下一页 - 使用 Next.js
fetch缓存标签或 React cache 缓存系列和产品查询——产品数据很少实时变化 - 使用
@inContext指令处理国际定价——@inContext(country: CA, language: EN)会以买家使用的货币返回价格 - 重复使用片段——定义 GraphQL 片段(例如
ProductFragment),避免在多个查询中重复选择字段 - 处理所有变更操作中的
userErrors——购物车变更操作会返回userErrors数组;更新本地状态前请先检查该数组
常见问题
| 问题 | 解决方案 |
|---|---|
| 速率限制错误(429) | 在服务端使用私有访问令牌并实现请求批处理;避免 N+1 产品查询 |
| 页面重新加载后购物车 ID 丢失 | 将 cartId 持久化到 localStorage 或 cookie 中;仅在购物车不存在时创建新购物车 |
| 产品价格显示的货币不正确 | 添加 @inContext(country: $country) 指令,并通过地理位置传入买家所在国家/地区 |
product(handle:) 返回 null | 正确处理 slug 化的 handle——Shopify handle 使用小写字母和连字符;请检查 slug 是否完全匹配 |
| 在移动版 Safari 中跳转结账失败 | 在用户手势处理函数中使用 window.location.href = checkoutUrl,不要在异步回调中使用 |
| 选择选项时找不到变体 | 在客户端筛选 variants.edges,匹配所有 selectedOptions,而不只是一个 |
相关技能
- @shopify-admin-api
- @shopify-app-development
- @shopify-checkout-extensions
- @headless-commerce-architecture
- @graphql-api-design