Ajax 与 Liquid
Ajax 技术让您能够在不重新加载整个页面的情况下动态更新内容,提供更流畅的用户体验。结合 Liquid 模板,您可以构建高度交互的电商功能。
基础 Ajax 概念
Shopify Ajax API
<!-- Ajax API 端点 -->
<script>
// Shopify 提供的主要 Ajax 端点
const SHOPIFY_AJAX_ENDPOINTS = {
cart: '/cart.js', // 获取购物车信息
cartAdd: '/cart/add.js', // 添加商品到购物车
cartUpdate: '/cart/update.js', // 更新购物车
cartChange: '/cart/change.js', // 修改单个商品
cartClear: '/cart/clear.js', // 清空购物车
products: '/products/{handle}.js', // 获取产品信息
recommendations: '/recommendations/products.json', // 产品推荐
search: '/search/suggest.json' // 搜索建议
}
// 基础 Ajax 请求函数
function shopifyAjax(url, options = {}) {
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
}
return fetch(url, { ...defaultOptions, ...options })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.json()
})
}
</script>
购物车 Ajax 功能
动态添加商品
<!-- Ajax 添加商品表单 -->
<form class="ajax-product-form" data-product-id="{{ product.id }}">
<div class="variant-selector">
<select name="id" class="variant-select">
{% for variant in product.variants %}
<option value="{{ variant.id }}"
{% unless variant.available %}disabled{% endunless %}
data-price="{{ variant.price }}"
data-inventory="{{ variant.inventory_quantity }}">
{{ variant.title }}
{% unless variant.available %} - 缺货{% endunless %}
</option>
{% endfor %}
</select>
</div>
<div class="quantity-selector">
<input type="number" name="quantity" value="1" min="1" class="quantity-input">
</div>
<button type="submit" class="btn add-to-cart-ajax" data-loading-text="添加中...">
<span class="btn-text">加入购物车</span>
<span class="btn-loading" style="display: none;">
<svg class="spinner" width="20" height="20" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none"></circle>
</svg>
</span>
</button>
</form>
<script>
document.querySelectorAll('.ajax-product-form').forEach(form => {
form.addEventListener('submit', async function(e) {
e.preventDefault()
const formData = new FormData(this)
const button = this.querySelector('.add-to-cart-ajax')
const btnText = button.querySelector('.btn-text')
const btnLoading = button.querySelector('.btn-loading')
// 显示加载状态
button.disabled = true
btnText.style.display = 'none'
btnLoading.style.display = 'inline-block'
try {
const response = await fetch('/cart/add.js', {
method: 'POST',
body: formData
})
if (response.ok) {
const item = await response.json()
// 更新购物车UI
await updateCartUI()
// 显示成功消息
showNotification('商品已添加到购物车', 'success')
// 打开购物车抽屉
openCartDrawer()
} else {
throw new Error('添加失败')
}
} catch (error) {
showNotification('添加商品失败,请重试', 'error')
console.error('Add to cart error:', error)
} finally {
// 恢复按钮状态
button.disabled = false
btnText.style.display = 'inline-block'
btnLoading.style.display = 'none'
}
})
})
</script>
购物车实时更新
<!-- Ajax 购物车更新 -->
<div id="cart-drawer" class="cart-drawer">
<div class="cart-header">
<h3>购物车 (<span id="cart-count">{{ cart.item_count }}</span>)</h3>
<button class="close-cart" onclick="closeCartDrawer()">×</button>
</div>
<div class="cart-items" id="cart-items-container">
<!-- 购物车内容将通过 Ajax 加载 -->
</div>
<div class="cart-footer">
<div class="cart-total">
总计: <span id="cart-total">{{ cart.total_price | money }}</span>
</div>
<button class="checkout-btn" onclick="proceedToCheckout()">
立即结账
</button>
</div>
</div>
<script>
// 更新购物车UI
async function updateCartUI() {
try {
const cart = await fetch('/cart.js').then(res => res.json())
// 更新购物车计数
document.getElementById('cart-count').textContent = cart.item_count
document.querySelectorAll('.cart-count').forEach(el => {
el.textContent = cart.item_count
})
// 更新总价
document.getElementById('cart-total').textContent = formatMoney(cart.total_price)
// 更新购物车内容
const cartContainer = document.getElementById('cart-items-container')
if (cart.item_count > 0) {
cartContainer.innerHTML = await renderCartItems(cart.items)
} else {
cartContainer.innerHTML = '<p class="empty-cart">购物车为空</p>'
}
return cart
} catch (error) {
console.error('Failed to update cart:', error)
}
}
// 渲染购物车商品
async function renderCartItems(items) {
const itemsHTML = items.map(item => `
<div class="cart-item" data-variant-id="${item.variant_id}">
<img src="${item.image}" alt="${item.product_title}" class="item-image">
<div class="item-details">
<h4>${item.product_title}</h4>
${item.variant_title !== 'Default Title' ? `<p>${item.variant_title}</p>` : ''}
<div class="quantity-controls">
<button onclick="updateQuantity(${item.key}, ${item.quantity - 1})">-</button>
<span>${item.quantity}</span>
<button onclick="updateQuantity(${item.key}, ${item.quantity + 1})">+</button>
</div>
</div>
<div class="item-price">
${formatMoney(item.final_line_price)}
<button onclick="removeItem(${item.key})" class="remove-item">删除</button>
</div>
</div>
`).join('')
return itemsHTML
}
// 更新商品数量
async function updateQuantity(key, quantity) {
try {
await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: key, quantity: quantity })
})
await updateCartUI()
} catch (error) {
showNotification('更新失败', 'error')
}
}
// 删除商品
async function removeItem(key) {
await updateQuantity(key, 0)
}
// 格式化价格
function formatMoney(cents) {
return '¥' + (cents / 100).toFixed(2)
}
</script>
产品快速查看
模态框快速查看
<!-- 产品快速查看模态框 -->
<div id="quick-view-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="quick-view-title">产品详情</h3>
<button class="close-modal" onclick="closeQuickView()">×</button>
</div>
<div class="modal-body" id="quick-view-content">
<!-- 内容将通过 Ajax 加载 -->
</div>
</div>
</div>
<!-- 产品列表中的快速查看按钮 -->
<div class="product-grid">
{% for product in collection.products %}
<div class="product-card">
<img src="{{ product.featured_image | img_url: '300x300' }}"
alt="{{ product.title }}">
<h3>{{ product.title }}</h3>
<p>{{ product.price | money }}</p>
<div class="product-actions">
<button class="btn quick-view-btn"
data-product-handle="{{ product.handle }}"
onclick="openQuickView('{{ product.handle }}')">
快速查看
</button>
<button class="btn add-to-cart-btn"
data-variant-id="{{ product.variants.first.id }}"
onclick="quickAddToCart({{ product.variants.first.id }})">
快速添加
</button>
</div>
</div>
{% endfor %}
</div>
<script>
// 打开快速查看
async function openQuickView(productHandle) {
try {
// 显示加载状态
document.getElementById('quick-view-title').textContent = '加载中...'
document.getElementById('quick-view-content').innerHTML = '<div class="loading">正在加载产品信息...</div>'
document.getElementById('quick-view-modal').style.display = 'block'
// 获取产品数据
const product = await fetch(`/products/${productHandle}.js`).then(res => res.json())
// 渲染产品内容
const quickViewHTML = await renderQuickViewContent(product)
document.getElementById('quick-view-title').textContent = product.title
document.getElementById('quick-view-content').innerHTML = quickViewHTML
// 初始化快速查看表单
initQuickViewForm()
} catch (error) {
console.error('Failed to load product:', error)
document.getElementById('quick-view-content').innerHTML = '<div class="error">加载失败,请重试</div>'
}
}
// 渲染快速查看内容
async function renderQuickViewContent(product) {
const variantOptions = product.variants.map(variant =>
`<option value="${variant.id}" ${!variant.available ? 'disabled' : ''}
data-price="${variant.price}">
${variant.title} ${!variant.available ? '- 缺货' : ''}
</option>`
).join('')
return `
<div class="quick-view-layout">
<div class="product-images">
<img src="${product.featured_image}" alt="${product.title}" class="main-image">
</div>
<div class="product-info">
<div class="product-price">
<span id="current-price">${formatMoney(product.price)}</span>
${product.compare_at_price ? `<span class="compare-price">${formatMoney(product.compare_at_price)}</span>` : ''}
</div>
<div class="product-description">
${product.description}
</div>
<form class="quick-view-form">
<div class="variant-selector">
<select name="id" onchange="updateQuickViewPrice(this)">
${variantOptions}
</select>
</div>
<div class="quantity-selector">
<input type="number" name="quantity" value="1" min="1">
</div>
<button type="submit" class="btn btn-primary">加入购物车</button>
</form>
</div>
</div>
`
}
// 关闭快速查看
function closeQuickView() {
document.getElementById('quick-view-modal').style.display = 'none'
}
// 快速添加到购物车
async function quickAddToCart(variantId, quantity = 1) {
try {
await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: variantId, quantity: quantity })
})
await updateCartUI()
showNotification('商品已添加到购物车', 'success')
} catch (error) {
showNotification('添加失败', 'error')
}
}
</script>
动态搜索
实时搜索建议
<!-- 搜索框 -->
<div class="search-container">
<form class="search-form" action="{{ routes.search_url }}">
<input type="search"
name="q"
placeholder="搜索商品..."
class="search-input"
autocomplete="off"
oninput="handleSearchInput(this)"
onfocus="showSearchSuggestions()"
onblur="hideSearchSuggestions()">
<button type="submit" class="search-btn">
<svg class="search-icon" width="20" height="20" viewBox="0 0 24 24">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" fill="none"/>
</svg>
</button>
</form>
<!-- 搜索建议 -->
<div id="search-suggestions" class="search-suggestions" style="display: none;">
<div class="suggestions-content">
<!-- 建议内容将通过 Ajax 加载 -->
</div>
</div>
</div>
<script>
let searchTimeout
let searchCache = new Map()
// 处理搜索输入
function handleSearchInput(input) {
const query = input.value.trim()
// 清除之前的定时器
clearTimeout(searchTimeout)
if (query.length < 2) {
hideSearchSuggestions()
return
}
// 防抖:300ms 后执行搜索
searchTimeout = setTimeout(() => {
performSearch(query)
}, 300)
}
// 执行搜索
async function performSearch(query) {
try {
// 检查缓存
if (searchCache.has(query)) {
displaySearchResults(searchCache.get(query))
return
}
// 显示加载状态
document.querySelector('.suggestions-content').innerHTML = '<div class="loading">搜索中...</div>'
showSearchSuggestions()
// 搜索产品
const productsResponse = await fetch(`/search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=product&resources[limit]=5`)
const productsData = await productsResponse.json()
// 搜索集合
const collectionsResponse = await fetch(`/search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=collection&resources[limit]=3`)
const collectionsData = await collectionsResponse.json()
const results = {
products: productsData.resources.results.products || [],
collections: collectionsData.resources.results.collections || []
}
// 缓存结果
searchCache.set(query, results)
// 显示结果
displaySearchResults(results)
} catch (error) {
console.error('Search error:', error)
document.querySelector('.suggestions-content').innerHTML = '<div class="error">搜索失败</div>'
}
}
// 显示搜索结果
function displaySearchResults(results) {
const { products, collections } = results
let html = ''
// 产品建议
if (products.length > 0) {
html += '<div class="suggestion-section"><h4>产品</h4>'
products.forEach(product => {
html += `
<a href="${product.url}" class="suggestion-item product-suggestion">
<img src="${product.image}" alt="${product.title}" class="suggestion-image">
<div class="suggestion-info">
<div class="suggestion-title">${product.title}</div>
<div class="suggestion-price">${formatMoney(product.price)}</div>
</div>
</a>
`
})
html += '</div>'
}
// 集合建议
if (collections.length > 0) {
html += '<div class="suggestion-section"><h4>分类</h4>'
collections.forEach(collection => {
html += `
<a href="${collection.url}" class="suggestion-item collection-suggestion">
<div class="suggestion-info">
<div class="suggestion-title">${collection.title}</div>
</div>
</a>
`
})
html += '</div>'
}
if (html === '') {
html = '<div class="no-results">未找到相关商品</div>'
}
document.querySelector('.suggestions-content').innerHTML = html
showSearchSuggestions()
}
// 显示搜索建议
function showSearchSuggestions() {
document.getElementById('search-suggestions').style.display = 'block'
}
// 隐藏搜索建议
function hideSearchSuggestions() {
setTimeout(() => {
document.getElementById('search-suggestions').style.display = 'none'
}, 200) // 延迟隐藏,允许点击建议项
}
</script>
产品过滤
Ajax 过滤器
<!-- 产品过滤器 -->
<div class="collection-filters">
<div class="filter-group">
<h4>价格范围</h4>
<div class="price-filter">
<input type="range" id="price-min" min="0" max="{{ collection.products | map: 'price' | sort | last }}" value="0">
<input type="range" id="price-max" min="0" max="{{ collection.products | map: 'price' | sort | last }}" value="{{ collection.products | map: 'price' | sort | last }}">
</div>
</div>
<div class="filter-group">
<h4>品牌</h4>
{% assign vendors = collection.products | map: 'vendor' | uniq | sort %}
{% for vendor in vendors %}
<label class="filter-option">
<input type="checkbox" name="vendor" value="{{ vendor }}" onchange="applyFilters()">
{{ vendor }}
</label>
{% endfor %}
</div>
<div class="filter-group">
<h4>产品类型</h4>
{% assign types = collection.products | map: 'type' | uniq | sort %}
{% for type in types %}
<label class="filter-option">
<input type="checkbox" name="type" value="{{ type }}" onchange="applyFilters()">
{{ type }}
</label>
{% endfor %}
</div>
</div>
<!-- 产品列表 -->
<div id="products-container" class="products-grid">
{% for product in collection.products %}
<div class="product-card"
data-price="{{ product.price }}"
data-vendor="{{ product.vendor }}"
data-type="{{ product.type }}">
<img src="{{ product.featured_image | img_url: '300x300' }}" alt="{{ product.title }}">
<h3>{{ product.title }}</h3>
<p>{{ product.price | money }}</p>
</div>
{% endfor %}
</div>
<script>
// 应用过滤器
function applyFilters() {
const products = document.querySelectorAll('.product-card')
// 获取选中的过滤条件
const selectedVendors = Array.from(document.querySelectorAll('[name="vendor"]:checked')).map(cb => cb.value)
const selectedTypes = Array.from(document.querySelectorAll('[name="type"]:checked')).map(cb => cb.value)
const minPrice = parseInt(document.getElementById('price-min').value)
const maxPrice = parseInt(document.getElementById('price-max').value)
products.forEach(product => {
const productPrice = parseInt(product.dataset.price)
const productVendor = product.dataset.vendor
const productType = product.dataset.type
let showProduct = true
// 价格过滤
if (productPrice < minPrice || productPrice > maxPrice) {
showProduct = false
}
// 品牌过滤
if (selectedVendors.length > 0 && !selectedVendors.includes(productVendor)) {
showProduct = false
}
// 类型过滤
if (selectedTypes.length > 0 && !selectedTypes.includes(productType)) {
showProduct = false
}
product.style.display = showProduct ? 'block' : 'none'
})
// 更新 URL(可选)
updateFilterURL(selectedVendors, selectedTypes, minPrice, maxPrice)
}
// 更新 URL
function updateFilterURL(vendors, types, minPrice, maxPrice) {
const params = new URLSearchParams()
if (vendors.length > 0) params.set('vendor', vendors.join(','))
if (types.length > 0) params.set('type', types.join(','))
if (minPrice > 0) params.set('price_min', minPrice)
if (maxPrice < 999999) params.set('price_max', maxPrice)
const newUrl = window.location.pathname + '?' + params.toString()
window.history.replaceState({}, '', newUrl)
}
</script>
通知系统
全局通知组件
<!-- 通知容器 -->
<div id="notification-container" class="notification-container">
<!-- 通知将通过 JavaScript 动态添加 -->
</div>
<script>
// 通知系统
const NotificationSystem = {
container: null,
init() {
this.container = document.getElementById('notification-container')
if (!this.container) {
this.container = document.createElement('div')
this.container.id = 'notification-container'
this.container.className = 'notification-container'
document.body.appendChild(this.container)
}
},
show(message, type = 'info', duration = 5000) {
this.init()
const notification = document.createElement('div')
notification.className = `notification notification-${type}`
notification.innerHTML = `
<div class="notification-content">
<span class="notification-icon">${this.getIcon(type)}</span>
<span class="notification-message">${message}</span>
<button class="notification-close" onclick="NotificationSystem.close(this.parentElement.parentElement)">×</button>
</div>
`
this.container.appendChild(notification)
// 动画显示
setTimeout(() => notification.classList.add('show'), 10)
// 自动关闭
if (duration > 0) {
setTimeout(() => this.close(notification), duration)
}
return notification
},
close(notification) {
notification.classList.remove('show')
setTimeout(() => {
if (notification.parentElement) {
notification.parentElement.removeChild(notification)
}
}, 300)
},
getIcon(type) {
const icons = {
success: '✓',
error: '✕',
warning: '⚠',
info: 'ℹ'
}
return icons[type] || icons.info
}
}
// 全局通知函数
function showNotification(message, type = 'info', duration = 5000) {
return NotificationSystem.show(message, type, duration)
}
</script>
<style>
.notification-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
max-width: 400px;
}
.notification {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
margin-bottom: 10px;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
}
.notification.show {
opacity: 1;
transform: translateX(0);
}
.notification-content {
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.notification-success { border-left: 4px solid #10b981; }
.notification-error { border-left: 4px solid #ef4444; }
.notification-warning { border-left: 4px solid #f59e0b; }
.notification-info { border-left: 4px solid #3b82f6; }
</style>
下一步学习
现在您已经掌握了 Ajax 与 Liquid 的结合使用,建议继续学习:
- 性能优化 - 优化 Ajax 请求性能
- 高级 Liquid 技巧 - 学习更高级的模板技巧
- 最佳实践 - 了解 Ajax 开发最佳实践
- 测试和调试 - 学习调试 Ajax 功能
最后更新时间: