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>
);
};
最佳实践总结
架构设计原则
- API First:以API为中心的设计理念
- 组件化:构建可复用的UI组件
- 类型安全:使用TypeScript确保类型安全
- 性能优先:优化加载速度和用户体验
开发最佳实践
- 缓存策略:合理使用缓存减少API调用
- 错误处理:完善的错误处理和用户反馈
- 测试覆盖:编写全面的单元测试和集成测试
- 监控日志:建立完善的监控和日志系统
部署和运维
- CI/CD:自动化构建和部署流程
- 环境管理:区分开发、测试、生产环境
- 性能监控:持续监控应用性能
- 安全防护:实施安全最佳实践
SEO和用户体验
- Core Web Vitals:优化核心性能指标
- 移动优先:确保移动端体验优秀
- 可访问性:遵循WCAG无障碍指南
- 国际化:支持多语言和多地区
总结
Headless Commerce架构为电商网站提供了前所未有的灵活性和性能优势。通过合理使用Shopify Storefront API、现代前端框架和最佳实践,您可以构建出功能强大、性能优异的电商解决方案。
成功的Headless实施需要综合考虑技术架构、用户体验、SEO优化和性能监控等多个方面。随着技术的不断发展,Headless Commerce将成为电商行业的主流趋势。
最后更新时间: