Skip to Content
🎉 探索 Shopify 的无限可能 结构化知识 + 实战案例,持续更新中...
进阶教程Headless Commerce架构完整指南

Headless Commerce架构完整指南

Headless Commerce(无头电商)是现代电商架构的重要发展方向,它将前端展示层与后端商务逻辑分离,提供更大的灵活性和自定义能力。本指南将深入探讨Headless Commerce的概念、实现和最佳实践。

Headless Commerce概述

1. 传统电商 vs Headless 电商

传统电商架构:

Headless电商架构:

2. Headless Commerce的优势

技术优势:

  • 前后端分离:独立开发和部署
  • 技术栈自由:可选择最适合的前端技术
  • 性能优化:更好的加载速度和用户体验
  • 多渠道支持:统一后端支持多个前端

业务优势:

  • 快速迭代:前端可独立快速更新
  • 个性化定制:完全自定义用户界面
  • 全球化支持:更好的多语言和多地区支持
  • SEO优化:更好的搜索引擎优化能力

Shopify Storefront API

1. GraphQL基础查询

# 基础产品查询 query getProducts($first: Int!, $after: String) { products(first: $first, after: $after) { edges { node { id handle title description productType vendor tags createdAt updatedAt availableForSale priceRange { minVariantPrice { amount currencyCode } maxVariantPrice { amount currencyCode } } images(first: 10) { edges { node { id url altText width height } } } variants(first: 250) { edges { node { id title availableForSale selectedOptions { name value } price { amount currencyCode } compareAtPrice { amount currencyCode } image { url altText } } } } seo { title description } metafields(identifiers: [ {namespace: "custom", key: "specifications"}, {namespace: "custom", key: "features"} ]) { key value type } } cursor } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } }

2. 购物车操作

# 创建购物车 mutation cartCreate($input: CartInput!) { cartCreate(input: $input) { cart { id createdAt updatedAt lines(first: 250) { edges { node { id quantity merchandise { ... on ProductVariant { id title product { title handle } price { amount currencyCode } image { url altText } } } attributes { key value } } } } cost { totalAmount { amount currencyCode } subtotalAmount { amount currencyCode } totalTaxAmount { amount currencyCode } totalDutyAmount { amount currencyCode } } buyerIdentity { email phone customer { id } countryCode } } userErrors { field message } } } # 添加商品到购物车 mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) { cartLinesAdd(cartId: $cartId, lines: $lines) { cart { id lines(first: 250) { edges { node { id quantity merchandise { ... on ProductVariant { id title price { amount currencyCode } } } } } } cost { totalAmount { amount currencyCode } } } userErrors { field message } } } # 更新购物车商品数量 mutation cartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) { cartLinesUpdate(cartId: $cartId, lines: $lines) { cart { id lines(first: 250) { edges { node { id quantity } } } } userErrors { field message } } }

3. 结账流程

# 创建结账 mutation checkoutCreate($input: CheckoutCreateInput!) { checkoutCreate(input: $input) { checkout { id webUrl totalPrice { amount currencyCode } lineItems(first: 250) { edges { node { id title quantity variant { id title price { amount currencyCode } } } } } shippingAddress { firstName lastName address1 address2 city province country zip } shippingLine { handle price { amount currencyCode } title } availableShippingRates { ready shippingRates { handle price { amount currencyCode } title } } } userErrors { field message } } } # 更新结账配送地址 mutation checkoutShippingAddressUpdate($checkoutId: ID!, $shippingAddress: MailingAddressInput!) { checkoutShippingAddressUpdateV2(checkoutId: $checkoutId, shippingAddress: $shippingAddress) { checkout { id availableShippingRates { ready shippingRates { handle price { amount currencyCode } title } } } userErrors { field message } } }

前端框架实现

1. Next.js + TypeScript实现

项目结构:

headless-shopify/ ├── components/ │ ├── cart/ │ ├── product/ │ ├── layout/ │ └── ui/ ├── lib/ │ ├── shopify.ts │ ├── queries.ts │ └── types.ts ├── pages/ │ ├── api/ │ ├── products/ │ └── collections/ ├── styles/ └── public/

Shopify客户端配置:

// lib/shopify.ts import { createStorefrontApiClient } from '@shopify/storefront-api-client'; const client = createStorefrontApiClient({ storeDomain: process.env.NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN!, apiVersion: '2023-10', publicAccessToken: process.env.NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN!, }); export interface ShopifyProduct { id: string; handle: string; title: string; description: string; productType: string; vendor: string; tags: string[]; availableForSale: boolean; priceRange: { minVariantPrice: { amount: string; currencyCode: string; }; maxVariantPrice: { amount: string; currencyCode: string; }; }; images: Array<{ id: string; url: string; altText: string; width: number; height: number; }>; variants: Array<{ id: string; title: string; availableForSale: boolean; selectedOptions: Array<{ name: string; value: string; }>; price: { amount: string; currencyCode: string; }; compareAtPrice?: { amount: string; currencyCode: string; }; image?: { url: string; altText: string; }; }>; seo: { title: string; description: string; }; metafields: Array<{ key: string; value: string; type: string; }>; } export interface ShopifyCart { id: string; lines: Array<{ id: string; quantity: number; merchandise: { id: string; title: string; product: { title: string; handle: string; }; price: { amount: string; currencyCode: string; }; image?: { url: string; altText: string; }; }; attributes: Array<{ key: string; value: string; }>; }>; cost: { totalAmount: { amount: string; currencyCode: string; }; subtotalAmount: { amount: string; currencyCode: string; }; totalTaxAmount?: { amount: string; currencyCode: string; }; }; } export class ShopifyAPI { private client = client; async getProducts(first: number = 20, after?: string): Promise<{ products: ShopifyProduct[]; pageInfo: { hasNextPage: boolean; hasPreviousPage: boolean; startCursor: string; endCursor: string; }; }> { const query = ` query getProducts($first: Int!, $after: String) { products(first: $first, after: $after) { edges { node { id handle title description productType vendor tags availableForSale priceRange { minVariantPrice { amount currencyCode } maxVariantPrice { amount currencyCode } } images(first: 10) { edges { node { id url altText width height } } } variants(first: 250) { edges { node { id title availableForSale selectedOptions { name value } price { amount currencyCode } compareAtPrice { amount currencyCode } image { url altText } } } } seo { title description } metafields(identifiers: [ {namespace: "custom", key: "specifications"}, {namespace: "custom", key: "features"} ]) { key value type } } cursor } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } `; const variables = { first, after }; const response = await this.client.request(query, { variables }); return { products: response.data.products.edges.map((edge: any) => ({ ...edge.node, images: edge.node.images.edges.map((imgEdge: any) => imgEdge.node), variants: edge.node.variants.edges.map((varEdge: any) => varEdge.node) })), pageInfo: response.data.products.pageInfo }; } async getProduct(handle: string): Promise<ShopifyProduct | null> { const query = ` query getProduct($handle: String!) { productByHandle(handle: $handle) { id handle title description productType vendor tags availableForSale priceRange { minVariantPrice { amount currencyCode } maxVariantPrice { amount currencyCode } } images(first: 20) { edges { node { id url altText width height } } } variants(first: 250) { edges { node { id title availableForSale selectedOptions { name value } price { amount currencyCode } compareAtPrice { amount currencyCode } image { url altText } } } } seo { title description } metafields(identifiers: [ {namespace: "custom", key: "specifications"}, {namespace: "custom", key: "features"} ]) { key value type } } } `; const variables = { handle }; const response = await this.client.request(query, { variables }); if (!response.data.productByHandle) { return null; } const product = response.data.productByHandle; return { ...product, images: product.images.edges.map((edge: any) => edge.node), variants: product.variants.edges.map((edge: any) => edge.node) }; } async createCart(lines: Array<{ merchandiseId: string; quantity: number; attributes?: Array<{ key: string; value: string }>; }>): Promise<ShopifyCart> { const mutation = ` mutation cartCreate($input: CartInput!) { cartCreate(input: $input) { cart { id lines(first: 250) { edges { node { id quantity merchandise { ... on ProductVariant { id title product { title handle } price { amount currencyCode } image { url altText } } } attributes { key value } } } } cost { totalAmount { amount currencyCode } subtotalAmount { amount currencyCode } totalTaxAmount { amount currencyCode } } } userErrors { field message } } } `; const variables = { input: { lines: lines.map(line => ({ merchandiseId: line.merchandiseId, quantity: line.quantity, attributes: line.attributes || [] })) } }; const response = await this.client.request(mutation, { variables }); if (response.data.cartCreate.userErrors.length > 0) { throw new Error(response.data.cartCreate.userErrors[0].message); } const cart = response.data.cartCreate.cart; return { ...cart, lines: cart.lines.edges.map((edge: any) => edge.node) }; } async updateCart(cartId: string, lines: Array<{ id: string; quantity: number; }>): Promise<ShopifyCart> { const mutation = ` mutation cartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) { cartLinesUpdate(cartId: $cartId, lines: $lines) { cart { id lines(first: 250) { edges { node { id quantity merchandise { ... on ProductVariant { id title product { title handle } price { amount currencyCode } image { url altText } } } } } } cost { totalAmount { amount currencyCode } subtotalAmount { amount currencyCode } } } userErrors { field message } } } `; const variables = { cartId, lines }; const response = await this.client.request(mutation, { variables }); if (response.data.cartLinesUpdate.userErrors.length > 0) { throw new Error(response.data.cartLinesUpdate.userErrors[0].message); } const cart = response.data.cartLinesUpdate.cart; return { ...cart, lines: cart.lines.edges.map((edge: any) => edge.node) }; } } export const shopify = new ShopifyAPI();

2. React Hooks for State Management

// hooks/useCart.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { shopify, ShopifyCart } from '../lib/shopify'; interface CartState { cart: ShopifyCart | null; isLoading: boolean; error: string | null; // Actions addToCart: (merchandiseId: string, quantity: number, attributes?: Array<{ key: string; value: string }>) => Promise<void>; updateCartLine: (lineId: string, quantity: number) => Promise<void>; removeFromCart: (lineId: string) => Promise<void>; clearCart: () => void; refreshCart: () => Promise<void>; } export const useCart = create<CartState>()( persist( (set, get) => ({ cart: null, isLoading: false, error: null, addToCart: async (merchandiseId: string, quantity: number, attributes = []) => { set({ isLoading: true, error: null }); try { const { cart } = get(); if (!cart) { // Create new cart const newCart = await shopify.createCart([ { merchandiseId, quantity, attributes } ]); set({ cart: newCart, isLoading: false }); } else { // Add to existing cart const existingLine = cart.lines.find( line => line.merchandise.id === merchandiseId ); if (existingLine) { // Update existing line await get().updateCartLine(existingLine.id, existingLine.quantity + quantity); } else { // Add new line const updatedCart = await shopify.addToCart(cart.id, [ { merchandiseId, quantity, attributes } ]); set({ cart: updatedCart, isLoading: false }); } } } catch (error) { set({ error: (error as Error).message, isLoading: false }); } }, updateCartLine: async (lineId: string, quantity: number) => { const { cart } = get(); if (!cart) return; set({ isLoading: true, error: null }); try { const updatedCart = await shopify.updateCart(cart.id, [ { id: lineId, quantity } ]); set({ cart: updatedCart, isLoading: false }); } catch (error) { set({ error: (error as Error).message, isLoading: false }); } }, removeFromCart: async (lineId: string) => { await get().updateCartLine(lineId, 0); }, clearCart: () => { set({ cart: null }); }, refreshCart: async () => { const { cart } = get(); if (!cart) return; set({ isLoading: true }); try { const updatedCart = await shopify.getCart(cart.id); set({ cart: updatedCart, isLoading: false }); } catch (error) { set({ error: (error as Error).message, isLoading: false }); } } }), { name: 'cart-storage', partialize: (state) => ({ cart: state.cart }) } ) );

3. Product Components

// components/product/ProductCard.tsx import React from 'react'; import Link from 'next/link'; import Image from 'next/image'; import { ShopifyProduct } from '../../lib/shopify'; import { useCart } from '../../hooks/useCart'; interface ProductCardProps { product: ShopifyProduct; } export const ProductCard: React.FC<ProductCardProps> = ({ product }) => { const { addToCart, isLoading } = useCart(); const primaryImage = product.images[0]; const minPrice = parseFloat(product.priceRange.minVariantPrice.amount); const maxPrice = parseFloat(product.priceRange.maxVariantPrice.amount); const currency = product.priceRange.minVariantPrice.currencyCode; const handleAddToCart = async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); const firstAvailableVariant = product.variants.find(v => v.availableForSale); if (firstAvailableVariant) { await addToCart(firstAvailableVariant.id, 1); } }; const formatPrice = (amount: number, currencyCode: string) => { return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: currencyCode }).format(amount); }; return ( <div className="group relative bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-lg transition-shadow duration-300"> <Link href={`/products/${product.handle}`}> <div className="aspect-square overflow-hidden"> {primaryImage && ( <Image src={primaryImage.url} alt={primaryImage.altText || product.title} width={400} height={400} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 25vw" /> )} </div> <div className="p-4"> <h3 className="text-lg font-semibold text-gray-900 line-clamp-2 mb-2"> {product.title} </h3> <p className="text-sm text-gray-600 line-clamp-2 mb-3"> {product.description} </p> <div className="flex items-center justify-between"> <div className="flex flex-col"> {minPrice === maxPrice ? ( <span className="text-lg font-bold text-gray-900"> {formatPrice(minPrice, currency)} </span> ) : ( <span className="text-lg font-bold text-gray-900"> {formatPrice(minPrice, currency)} - {formatPrice(maxPrice, currency)} </span> )} {product.vendor && ( <span className="text-xs text-gray-500"> {product.vendor} </span> )} </div> {product.availableForSale && ( <button onClick={handleAddToCart} disabled={isLoading} className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200" > {isLoading ? '添加中...' : '添加到购物车'} </button> )} </div> {!product.availableForSale && ( <div className="mt-2"> <span className="inline-block px-3 py-1 text-xs font-medium text-red-800 bg-red-100 rounded-full"> 暂时缺货 </span> </div> )} {product.tags.length > 0 && ( <div className="mt-3 flex flex-wrap gap-1"> {product.tags.slice(0, 3).map((tag) => ( <span key={tag} className="inline-block px-2 py-1 text-xs text-gray-600 bg-gray-100 rounded-md" > {tag} </span> ))} </div> )} </div> </Link> </div> ); };
// components/product/ProductDetail.tsx import React, { useState } from 'react'; import Image from 'next/image'; import { ShopifyProduct } from '../../lib/shopify'; import { useCart } from '../../hooks/useCart'; interface ProductDetailProps { product: ShopifyProduct; } export const ProductDetail: React.FC<ProductDetailProps> = ({ product }) => { const { addToCart, isLoading } = useCart(); const [selectedVariant, setSelectedVariant] = useState(product.variants[0]); const [selectedOptions, setSelectedOptions] = useState<Record<string, string>>(() => { const options: Record<string, string> = {}; selectedVariant.selectedOptions.forEach(option => { options[option.name] = option.value; }); return options; }); const [selectedImageIndex, setSelectedImageIndex] = useState(0); const [quantity, setQuantity] = useState(1); // 获取所有选项名称 const optionNames = Array.from( new Set(product.variants.flatMap(variant => variant.selectedOptions.map(option => option.name) )) ); // 获取特定选项的所有值 const getOptionValues = (optionName: string) => { return Array.from( new Set(product.variants .filter(variant => variant.availableForSale) .flatMap(variant => variant.selectedOptions .filter(option => option.name === optionName) .map(option => option.value) ) ) ); }; // 处理选项变更 const handleOptionChange = (optionName: string, optionValue: string) => { const newSelectedOptions = { ...selectedOptions, [optionName]: optionValue }; setSelectedOptions(newSelectedOptions); // 查找匹配的变体 const matchingVariant = product.variants.find(variant => variant.selectedOptions.every(option => newSelectedOptions[option.name] === option.value ) ); if (matchingVariant) { setSelectedVariant(matchingVariant); } }; const handleAddToCart = async () => { if (selectedVariant && selectedVariant.availableForSale) { await addToCart(selectedVariant.id, quantity); } }; const formatPrice = (amount: string, currencyCode: string) => { return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: currencyCode }).format(parseFloat(amount)); }; return ( <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> {/* 产品图片 */} <div className="space-y-4"> <div className="aspect-square overflow-hidden rounded-lg"> <Image src={product.images[selectedImageIndex]?.url || product.images[0]?.url} alt={product.images[selectedImageIndex]?.altText || product.title} width={600} height={600} className="w-full h-full object-cover" priority /> </div> {product.images.length > 1 && ( <div className="grid grid-cols-4 gap-2"> {product.images.map((image, index) => ( <button key={image.id} onClick={() => setSelectedImageIndex(index)} className={`aspect-square overflow-hidden rounded-md border-2 ${ index === selectedImageIndex ? 'border-blue-500' : 'border-gray-200' }`} > <Image src={image.url} alt={image.altText || product.title} width={150} height={150} className="w-full h-full object-cover" /> </button> ))} </div> )} </div> {/* 产品信息 */} <div className="space-y-6"> <div> <h1 className="text-3xl font-bold text-gray-900">{product.title}</h1> {product.vendor && ( <p className="text-lg text-gray-600 mt-2">品牌:{product.vendor}</p> )} </div> <div className="space-y-2"> <div className="flex items-center space-x-4"> <span className="text-3xl font-bold text-gray-900"> {formatPrice(selectedVariant.price.amount, selectedVariant.price.currencyCode)} </span> {selectedVariant.compareAtPrice && ( <span className="text-xl text-gray-500 line-through"> {formatPrice(selectedVariant.compareAtPrice.amount, selectedVariant.compareAtPrice.currencyCode)} </span> )} </div> {selectedVariant.availableForSale ? ( <span className="inline-block px-3 py-1 text-sm font-medium text-green-800 bg-green-100 rounded-full"> 现货供应 </span> ) : ( <span className="inline-block px-3 py-1 text-sm font-medium text-red-800 bg-red-100 rounded-full"> 暂时缺货 </span> )} </div> {/* 产品选项 */} {optionNames.map(optionName => ( <div key={optionName} className="space-y-2"> <h3 className="text-lg font-medium text-gray-900">{optionName}</h3> <div className="flex flex-wrap gap-2"> {getOptionValues(optionName).map(optionValue => ( <button key={optionValue} onClick={() => handleOptionChange(optionName, optionValue)} className={`px-4 py-2 border rounded-md text-sm font-medium transition-colors ${ selectedOptions[optionName] === optionValue ? 'border-blue-500 bg-blue-50 text-blue-900' : 'border-gray-300 bg-white text-gray-900 hover:bg-gray-50' }`} > {optionValue} </button> ))} </div> </div> ))} {/* 数量选择 */} <div className="space-y-2"> <h3 className="text-lg font-medium text-gray-900">数量</h3> <div className="flex items-center space-x-3"> <button onClick={() => setQuantity(Math.max(1, quantity - 1))} className="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:bg-gray-50" > - </button> <span className="text-lg font-medium">{quantity}</span> <button onClick={() => setQuantity(quantity + 1)} className="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:bg-gray-50" > + </button> </div> </div> {/* 添加到购物车按钮 */} <button onClick={handleAddToCart} disabled={!selectedVariant.availableForSale || isLoading} className="w-full py-3 px-6 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200" > {isLoading ? '添加中...' : '添加到购物车'} </button> {/* 产品描述 */} <div className="space-y-4"> <h3 className="text-lg font-medium text-gray-900">产品描述</h3> <div className="text-gray-600 prose max-w-none" dangerouslySetInnerHTML={{ __html: product.description }} /> </div> {/* 产品标签 */} {product.tags.length > 0 && ( <div className="space-y-2"> <h3 className="text-lg font-medium text-gray-900">标签</h3> <div className="flex flex-wrap gap-2"> {product.tags.map(tag => ( <span key={tag} className="inline-block px-3 py-1 text-sm text-gray-600 bg-gray-100 rounded-md" > {tag} </span> ))} </div> </div> )} {/* 元字段 */} {product.metafields.length > 0 && ( <div className="space-y-4"> <h3 className="text-lg font-medium text-gray-900">详细信息</h3> <div className="space-y-2"> {product.metafields.map(metafield => ( <div key={metafield.key} className="border-b border-gray-200 pb-2"> <dt className="text-sm font-medium text-gray-900 capitalize"> {metafield.key.replace(/_/g, ' ')} </dt> <dd className="text-sm text-gray-600 mt-1"> {metafield.value} </dd> </div> ))} </div> </div> )} </div> </div> </div> ); };

性能优化策略

1. 图片优化

// components/OptimizedImage.tsx import React from 'react'; import Image from 'next/image'; interface OptimizedImageProps { src: string; alt: string; width: number; height: number; priority?: boolean; sizes?: string; className?: string; } export const OptimizedImage: React.FC<OptimizedImageProps> = ({ src, alt, width, height, priority = false, sizes = '100vw', className = '' }) => { // Shopify图片URL优化 const optimizeShopifyImage = (url: string, width: number, height: number) => { if (!url.includes('cdn.shopify.com')) { return url; } // 移除现有的尺寸参数 const baseUrl = url.split('?')[0]; // 添加优化参数 return `${baseUrl}?width=${width}&height=${height}&crop=center&format=webp`; }; const optimizedSrc = optimizeShopifyImage(src, width, height); return ( <Image src={optimizedSrc} alt={alt} width={width} height={height} priority={priority} sizes={sizes} className={className} quality={85} placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWGRkqGx0f/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HaH9bcfaSXWGaRmknyJckliyjqTzSlT54b6bk+h0R//2Q==" /> ); };

2. 数据预取和缓存

// lib/cache.ts interface CacheEntry<T> { data: T; timestamp: number; expiry: number; } class Cache { private cache = new Map<string, CacheEntry<any>>(); private defaultTTL = 5 * 60 * 1000; // 5分钟 set<T>(key: string, data: T, ttl: number = this.defaultTTL): void { this.cache.set(key, { data, timestamp: Date.now(), expiry: Date.now() + ttl }); } get<T>(key: string): T | null { const entry = this.cache.get(key); if (!entry) { return null; } if (Date.now() > entry.expiry) { this.cache.delete(key); return null; } return entry.data; } delete(key: string): void { this.cache.delete(key); } clear(): void { this.cache.clear(); } // 缓存包装器 async getOrFetch<T>( key: string, fetchFn: () => Promise<T>, ttl: number = this.defaultTTL ): Promise<T> { const cached = this.get<T>(key); if (cached !== null) { return cached; } const data = await fetchFn(); this.set(key, data, ttl); return data; } } export const cache = new Cache(); // 增强的Shopify API类 export class CachedShopifyAPI extends ShopifyAPI { async getProducts(first: number = 20, after?: string) { const cacheKey = `products_${first}_${after || 'first'}`; return await cache.getOrFetch( cacheKey, () => super.getProducts(first, after), 10 * 60 * 1000 // 10分钟缓存 ); } async getProduct(handle: string) { const cacheKey = `product_${handle}`; return await cache.getOrFetch( cacheKey, () => super.getProduct(handle), 30 * 60 * 1000 // 30分钟缓存 ); } } export const cachedShopify = new CachedShopifyAPI();

3. 代码分割和懒加载

// pages/products/[handle].tsx import dynamic from 'next/dynamic'; import { GetStaticProps, GetStaticPaths } from 'next'; import { cachedShopify, ShopifyProduct } from '../../lib/shopify'; // 懒加载组件 const ProductDetail = dynamic(() => import('../../components/product/ProductDetail').then(mod => ({ default: mod.ProductDetail })), { loading: () => <div className="animate-pulse">加载中...</div> }); const ProductReviews = dynamic(() => import('../../components/product/ProductReviews'), { loading: () => <div>加载评论中...</div> }); const RelatedProducts = dynamic(() => import('../../components/product/RelatedProducts'), { loading: () => <div>加载相关产品中...</div> }); interface ProductPageProps { product: ShopifyProduct; } export default function ProductPage({ product }: ProductPageProps) { return ( <div> <ProductDetail product={product} /> <ProductReviews productId={product.id} /> <RelatedProducts productType={product.productType} excludeId={product.id} /> </div> ); } export const getStaticPaths: GetStaticPaths = async () => { // 只预生成热门产品页面 const { products } = await cachedShopify.getProducts(20); const paths = products.map(product => ({ params: { handle: product.handle } })); return { paths, fallback: 'blocking' // 使用ISR for non-pregenerated pages }; }; export const getStaticProps: GetStaticProps = async ({ params }) => { const handle = params?.handle as string; const product = await cachedShopify.getProduct(handle); if (!product) { return { notFound: true }; } return { props: { product }, revalidate: 3600 // 1小时重新验证 }; };

SEO优化

1. 结构化数据

// components/SEO/StructuredData.tsx import React from 'react'; import Head from 'next/head'; import { ShopifyProduct } from '../../lib/shopify'; interface ProductStructuredDataProps { product: ShopifyProduct; } export const ProductStructuredData: React.FC<ProductStructuredDataProps> = ({ product }) => { const structuredData = { '@context': 'https://schema.org', '@type': 'Product', name: product.title, description: product.description, brand: { '@type': 'Brand', name: product.vendor }, image: product.images.map(img => img.url), sku: product.variants[0]?.id, offers: { '@type': 'AggregateOffer', lowPrice: product.priceRange.minVariantPrice.amount, highPrice: product.priceRange.maxVariantPrice.amount, priceCurrency: product.priceRange.minVariantPrice.currencyCode, availability: product.availableForSale ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock', offerCount: product.variants.filter(v => v.availableForSale).length, offers: product.variants.filter(v => v.availableForSale).map(variant => ({ '@type': 'Offer', price: variant.price.amount, priceCurrency: variant.price.currencyCode, availability: 'https://schema.org/InStock', sku: variant.id })) }, category: product.productType, url: `${process.env.NEXT_PUBLIC_SITE_URL}/products/${product.handle}` }; return ( <Head> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }} /> </Head> ); };

2. 动态Meta标签

// components/SEO/ProductSEO.tsx import React from 'react'; import Head from 'next/head'; import { ShopifyProduct } from '../../lib/shopify'; interface ProductSEOProps { product: ShopifyProduct; } export const ProductSEO: React.FC<ProductSEOProps> = ({ product }) => { const title = product.seo.title || `${product.title} | Your Store Name`; const description = product.seo.description || product.description.substring(0, 160); const canonical = `${process.env.NEXT_PUBLIC_SITE_URL}/products/${product.handle}`; const ogImage = product.images[0]?.url; return ( <Head> <title>{title}</title> <meta name="description" content={description} /> <link rel="canonical" href={canonical} /> {/* Open Graph */} <meta property="og:type" content="product" /> <meta property="og:title" content={title} /> <meta property="og:description" content={description} /> <meta property="og:url" content={canonical} /> {ogImage && <meta property="og:image" content={ogImage} />} <meta property="og:site_name" content="Your Store Name" /> {/* Twitter Card */} <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content={title} /> <meta name="twitter:description" content={description} /> {ogImage && <meta name="twitter:image" content={ogImage} />} {/* Product specific meta */} <meta property="product:price:amount" content={product.priceRange.minVariantPrice.amount} /> <meta property="product:price:currency" content={product.priceRange.minVariantPrice.currencyCode} /> <meta property="product:availability" content={product.availableForSale ? 'instock' : 'oos'} /> <meta property="product:brand" content={product.vendor} /> <meta property="product:category" content={product.productType} /> {/* Additional SEO tags */} <meta name="robots" content="index, follow" /> <meta name="googlebot" content="index, follow" /> </Head> ); };

最佳实践总结

架构设计原则

  1. API First:以API为中心的设计理念
  2. 组件化:构建可复用的UI组件
  3. 类型安全:使用TypeScript确保类型安全
  4. 性能优先:优化加载速度和用户体验

开发最佳实践

  1. 缓存策略:合理使用缓存减少API调用
  2. 错误处理:完善的错误处理和用户反馈
  3. 测试覆盖:编写全面的单元测试和集成测试
  4. 监控日志:建立完善的监控和日志系统

部署和运维

  1. CI/CD:自动化构建和部署流程
  2. 环境管理:区分开发、测试、生产环境
  3. 性能监控:持续监控应用性能
  4. 安全防护:实施安全最佳实践

SEO和用户体验

  1. Core Web Vitals:优化核心性能指标
  2. 移动优先:确保移动端体验优秀
  3. 可访问性:遵循WCAG无障碍指南
  4. 国际化:支持多语言和多地区

总结

Headless Commerce架构为电商网站提供了前所未有的灵活性和性能优势。通过合理使用Shopify Storefront API、现代前端框架和最佳实践,您可以构建出功能强大、性能优异的电商解决方案。

成功的Headless实施需要综合考虑技术架构、用户体验、SEO优化和性能监控等多个方面。随着技术的不断发展,Headless Commerce将成为电商行业的主流趋势。

最后更新时间: