自定义代码片段开发
代码片段 (Snippets) 是 Shopify 主题开发中创建可复用组件的核心机制。本指南将详细介绍如何开发高质量、可维护的自定义代码片段。
基础组件开发
1. 产品卡片组件
<!-- snippets/product-card.liquid -->
{% comment %}
产品卡片组件
参数:
- product: 产品对象 (必需)
- card_style: 卡片样式 (可选: 'default', 'minimal', 'detailed')
- image_ratio: 图片比例 (可选: 'square', 'portrait', 'landscape')
- show_vendor: 是否显示品牌 (可选: true/false)
- show_badge: 是否显示标签 (可选: true/false)
- lazy_load: 是否懒加载 (可选: true/false)
{% endcomment %}
{% assign card_style = card_style | default: 'default' %}
{% assign image_ratio = image_ratio | default: 'square' %}
{% assign show_vendor = show_vendor | default: false %}
{% assign show_badge = show_badge | default: true %}
{% assign lazy_load = lazy_load | default: true %}
<article class="product-card product-card--{{ card_style }}" data-product-id="{{ product.id }}">
<div class="product-card__image-wrapper">
<a href="{{ product.url }}" class="product-card__link" aria-label="{{ product.title }}">
{% if product.featured_image %}
<div class="product-card__image product-card__image--{{ image_ratio }}">
{% if lazy_load %}
<img data-src="{{ product.featured_image | img_url: '400x400' }}"
alt="{{ product.featured_image.alt | default: product.title }}"
class="product-card__img lazyload">
{% else %}
<img src="{{ product.featured_image | img_url: '400x400' }}"
alt="{{ product.featured_image.alt | default: product.title }}"
class="product-card__img">
{% endif %}
<!-- 悬停图片 -->
{% if product.images[1] and card_style != 'minimal' %}
<img src="{{ product.images[1] | img_url: '400x400' }}"
alt="{{ product.title }}"
class="product-card__img product-card__img--hover">
{% endif %}
</div>
{% else %}
<div class="product-card__placeholder">
{% render 'icon-placeholder' %}
</div>
{% endif %}
<!-- 产品标签 -->
{% if show_badge %}
{% render 'product-badges', product: product %}
{% endif %}
</a>
<!-- 快速操作 -->
{% unless card_style == 'minimal' %}
<div class="product-card__actions">
{% if product.available and product.variants.size == 1 %}
<button type="button"
class="quick-add-btn"
data-variant-id="{{ product.first_available_variant.id }}">
{% render 'icon-cart' %}
</button>
{% endif %}
<button type="button"
class="wishlist-btn"
data-product-id="{{ product.id }}">
{% render 'icon-heart' %}
</button>
</div>
{% endunless %}
</div>
<div class="product-card__info">
{% if show_vendor and product.vendor %}
<p class="product-card__vendor">{{ product.vendor }}</p>
{% endif %}
<h3 class="product-card__title">
<a href="{{ product.url }}">{{ product.title }}</a>
</h3>
<!-- 价格显示 -->
<div class="product-card__price">
{% render 'price', product: product %}
</div>
<!-- 变体选项 -->
{% if card_style == 'detailed' and product.variants.size > 1 %}
{% render 'product-variant-pills', product: product %}
{% endif %}
<!-- 评分 -->
{% if product.metafields.reviews.rating and card_style != 'minimal' %}
{% render 'star-rating', rating: product.metafields.reviews.rating %}
{% endif %}
</div>
</article>
2. 价格显示组件
<!-- snippets/price.liquid -->
{% comment %}
价格显示组件
参数:
- product: 产品对象 (必需)
- variant: 变体对象 (可选,优先于产品价格)
- size: 显示尺寸 (可选: 'small', 'medium', 'large')
- show_currency: 是否显示货币符号 (可选: true/false)
{% endcomment %}
{% assign target = variant | default: product %}
{% assign size = size | default: 'medium' %}
{% assign show_currency = show_currency | default: true %}
<div class="price price--{{ size }}" data-price-container>
{% if target.compare_at_price > target.price %}
<!-- 促销价格 -->
<span class="price__sale">
{% if show_currency %}
{{ target.price | money }}
{% else %}
{{ target.price | money_without_currency }}
{% endif %}
</span>
<span class="price__compare">
{% if show_currency %}
{{ target.compare_at_price | money }}
{% else %}
{{ target.compare_at_price | money_without_currency }}
{% endif %}
</span>
<!-- 折扣百分比 -->
{% assign discount = target.compare_at_price | minus: target.price %}
{% assign discount_percent = discount | times: 100 | divided_by: target.compare_at_price | round %}
<span class="price__badge">-{{ discount_percent }}%</span>
{% else %}
<!-- 常规价格 -->
<span class="price__regular">
{% if show_currency %}
{{ target.price | money }}
{% else %}
{{ target.price | money_without_currency }}
{% endif %}
</span>
{% endif %}
<!-- 单位价格 -->
{% if target.unit_price_measurement %}
<div class="price__unit">
<span class="price__unit-price">
{{ target.unit_price | money }}
</span>
<span class="price__unit-measure">
/ {{ target.unit_price_measurement.reference_value }}{{ target.unit_price_measurement.reference_unit }}
</span>
</div>
{% endif %}
</div>
3. 星级评分组件
<!-- snippets/star-rating.liquid -->
{% comment %}
星级评分组件
参数:
- rating: 评分值 (必需, 0-5)
- max_rating: 最大评分 (可选, 默认: 5)
- show_text: 是否显示文字 (可选: true/false)
- size: 显示尺寸 (可选: 'small', 'medium', 'large')
- color: 星星颜色 (可选: 'gold', 'red', 'blue')
{% endcomment %}
{% assign rating = rating | default: 0 %}
{% assign max_rating = max_rating | default: 5 %}
{% assign show_text = show_text | default: false %}
{% assign size = size | default: 'medium' %}
{% assign color = color | default: 'gold' %}
<div class="star-rating star-rating--{{ size }} star-rating--{{ color }}"
role="img"
aria-label="评分 {{ rating }} 星,满分 {{ max_rating }} 星">
<div class="star-rating__stars" aria-hidden="true">
{% for i in (1..max_rating) %}
{% if rating >= i %}
<span class="star star--filled">★</span>
{% elsif rating > i | minus: 1 %}
<span class="star star--half">★</span>
{% else %}
<span class="star star--empty">☆</span>
{% endif %}
{% endfor %}
</div>
{% if show_text %}
<span class="star-rating__text">
{{ rating | round: 1 }}/{{ max_rating }}
</span>
{% endif %}
</div>
交互式组件
1. 变体选择器
<!-- snippets/variant-selector.liquid -->
{% comment %}
产品变体选择器
参数:
- product: 产品对象 (必需)
- option: 选项对象 (必需)
- option_index: 选项索引 (必需)
- selector_type: 选择器类型 (可选: 'dropdown', 'buttons', 'swatches')
{% endcomment %}
{% assign selector_type = selector_type | default: 'buttons' %}
<div class="variant-selector" data-option-index="{{ option_index }}">
<label class="variant-selector__label">{{ option.name }}</label>
{% case selector_type %}
{% when 'dropdown' %}
<select class="variant-selector__dropdown" data-option-selector>
{% for value in option.values %}
<option value="{{ value }}"
{% if option.selected_value == value %}selected{% endif %}>
{{ value }}
</option>
{% endfor %}
</select>
{% when 'buttons' %}
<div class="variant-selector__buttons">
{% for value in option.values %}
<button type="button"
class="variant-selector__button {% if option.selected_value == value %}active{% endif %}"
data-option-value="{{ value }}"
data-option-selector>
{{ value }}
</button>
{% endfor %}
</div>
{% when 'swatches' %}
<div class="variant-selector__swatches">
{% for value in option.values %}
{% assign color_image = value | handle | append: '.png' %}
<label class="variant-selector__swatch">
<input type="radio"
name="option-{{ option_index }}"
value="{{ value }}"
{% if option.selected_value == value %}checked{% endif %}
data-option-selector>
<span class="variant-selector__swatch-display"
style="background-color: {{ value | handle }}; background-image: url('{{ color_image | asset_img_url: '50x50' }}');"
title="{{ value }}">
<span class="visually-hidden">{{ value }}</span>
</span>
</label>
{% endfor %}
</div>
{% endcase %}
</div>
2. 数量选择器
<!-- snippets/quantity-selector.liquid -->
{% comment %}
数量选择器组件
参数:
- quantity: 初始数量 (可选, 默认: 1)
- min: 最小数量 (可选, 默认: 1)
- max: 最大数量 (可选)
- step: 步长 (可选, 默认: 1)
- variant_id: 变体ID (可选,用于库存检查)
{% endcomment %}
{% assign quantity = quantity | default: 1 %}
{% assign min = min | default: 1 %}
{% assign step = step | default: 1 %}
<div class="quantity-selector" data-quantity-selector>
<label for="quantity-{{ section.id }}" class="quantity-selector__label">
数量
</label>
<div class="quantity-selector__input-wrapper">
<button type="button"
class="quantity-selector__button quantity-selector__button--minus"
data-quantity-change="decrease"
aria-label="减少数量">
{% render 'icon-minus' %}
</button>
<input type="number"
id="quantity-{{ section.id }}"
class="quantity-selector__input"
name="quantity"
value="{{ quantity }}"
min="{{ min }}"
{% if max %}max="{{ max }}"{% endif %}
step="{{ step }}"
data-quantity-input
aria-label="商品数量">
<button type="button"
class="quantity-selector__button quantity-selector__button--plus"
data-quantity-change="increase"
aria-label="增加数量">
{% render 'icon-plus' %}
</button>
</div>
{% if variant_id %}
<div class="quantity-selector__stock" data-stock-info="{{ variant_id }}"></div>
{% endif %}
</div>
<script>
// 数量选择器功能
class QuantitySelector {
constructor(element) {
this.container = element
this.input = element.querySelector('[data-quantity-input]')
this.decreaseBtn = element.querySelector('[data-quantity-change="decrease"]')
this.increaseBtn = element.querySelector('[data-quantity-change="increase"]')
this.init()
}
init() {
this.decreaseBtn?.addEventListener('click', () => this.decrease())
this.increaseBtn?.addEventListener('click', () => this.increase())
this.input?.addEventListener('change', () => this.validate())
}
decrease() {
const currentValue = parseInt(this.input.value)
const minValue = parseInt(this.input.min)
const step = parseInt(this.input.step) || 1
if (currentValue > minValue) {
this.input.value = Math.max(currentValue - step, minValue)
this.input.dispatchEvent(new Event('change'))
}
}
increase() {
const currentValue = parseInt(this.input.value)
const maxValue = this.input.max ? parseInt(this.input.max) : Infinity
const step = parseInt(this.input.step) || 1
if (currentValue < maxValue) {
this.input.value = Math.min(currentValue + step, maxValue)
this.input.dispatchEvent(new Event('change'))
}
}
validate() {
const value = parseInt(this.input.value)
const min = parseInt(this.input.min)
const max = this.input.max ? parseInt(this.input.max) : Infinity
if (value < min) {
this.input.value = min
} else if (value > max) {
this.input.value = max
}
this.updateButtons()
}
updateButtons() {
const value = parseInt(this.input.value)
const min = parseInt(this.input.min)
const max = this.input.max ? parseInt(this.input.max) : Infinity
this.decreaseBtn.disabled = value <= min
this.increaseBtn.disabled = value >= max
}
}
// 初始化所有数量选择器
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-quantity-selector]').forEach(selector => {
new QuantitySelector(selector)
})
})
</script>
3. 图片画廊组件
<!-- snippets/product-gallery.liquid -->
{% comment %}
产品图片画廊组件
参数:
- product: 产品对象 (必需)
- featured_media: 特色媒体 (可选)
- enable_zoom: 是否启用放大 (可选: true/false)
- thumbnail_position: 缩略图位置 (可选: 'bottom', 'left', 'right')
{% endcomment %}
{% assign enable_zoom = enable_zoom | default: true %}
{% assign thumbnail_position = thumbnail_position | default: 'bottom' %}
<div class="product-gallery" data-product-gallery>
<!-- 主图显示区域 -->
<div class="product-gallery__main">
{% if product.featured_media %}
<div class="product-gallery__media" data-media-container>
{% case product.featured_media.media_type %}
{% when 'image' %}
<img src="{{ product.featured_media | img_url: '800x800' }}"
alt="{{ product.featured_media.alt | default: product.title }}"
class="product-gallery__image"
data-main-image
{% if enable_zoom %}data-zoom="{{ product.featured_media | img_url: '1600x1600' }}"{% endif %}>
{% when 'video' %}
<video class="product-gallery__video"
controls
data-main-video>
<source src="{{ product.featured_media.sources[0].url }}" type="{{ product.featured_media.sources[0].mime_type }}">
</video>
{% when 'external_video' %}
<div class="product-gallery__external-video">
{{ product.featured_media | external_video_tag }}
</div>
{% when 'model' %}
<div class="product-gallery__model">
{{ product.featured_media | model_viewer_tag }}
</div>
{% endcase %}
</div>
{% endif %}
<!-- 放大镜功能 -->
{% if enable_zoom %}
<div class="product-gallery__zoom-container" data-zoom-container style="display: none;"></div>
{% endif %}
</div>
<!-- 缩略图 -->
{% if product.media.size > 1 %}
<div class="product-gallery__thumbnails product-gallery__thumbnails--{{ thumbnail_position }}">
{% for media in product.media %}
<button type="button"
class="product-gallery__thumbnail {% if forloop.first %}active{% endif %}"
data-media-index="{{ forloop.index0 }}"
data-media-type="{{ media.media_type }}"
aria-label="查看 {{ media.alt | default: product.title }}">
{% case media.media_type %}
{% when 'image' %}
<img src="{{ media.preview_image | img_url: '100x100' }}"
alt="{{ media.alt | default: product.title }}"
class="product-gallery__thumbnail-image">
{% when 'video' %}
<img src="{{ media.preview_image | img_url: '100x100' }}"
alt="{{ media.alt | default: product.title }}"
class="product-gallery__thumbnail-image">
<span class="product-gallery__media-icon">{% render 'icon-play' %}</span>
{% when 'external_video' %}
<img src="{{ media.preview_image | img_url: '100x100' }}"
alt="{{ media.alt | default: product.title }}"
class="product-gallery__thumbnail-image">
<span class="product-gallery__media-icon">{% render 'icon-play' %}</span>
{% when 'model' %}
<img src="{{ media.preview_image | img_url: '100x100' }}"
alt="{{ media.alt | default: product.title }}"
class="product-gallery__thumbnail-image">
<span class="product-gallery__media-icon">{% render 'icon-3d' %}</span>
{% endcase %}
</button>
{% endfor %}
</div>
{% endif %}
</div>
<script>
// 产品画廊功能
class ProductGallery {
constructor(element) {
this.gallery = element
this.mediaContainer = element.querySelector('[data-media-container]')
this.thumbnails = element.querySelectorAll('[data-media-index]')
this.currentMediaIndex = 0
this.init()
}
init() {
this.thumbnails.forEach((thumbnail, index) => {
thumbnail.addEventListener('click', () => this.switchMedia(index))
})
// 键盘导航
this.gallery.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
this.previousMedia()
} else if (e.key === 'ArrowRight') {
this.nextMedia()
}
})
}
switchMedia(index) {
if (index === this.currentMediaIndex) return
const thumbnail = this.thumbnails[index]
const mediaType = thumbnail.dataset.mediaType
// 更新活跃缩略图
this.thumbnails.forEach(thumb => thumb.classList.remove('active'))
thumbnail.classList.add('active')
// 更新主媒体
this.updateMainMedia(index, mediaType)
this.currentMediaIndex = index
}
updateMainMedia(index, mediaType) {
// 这里需要根据媒体类型更新主显示区域
// 实际实现会更复杂,包括图片切换动画等
const mediaData = window.productMedia[index]
switch(mediaType) {
case 'image':
this.displayImage(mediaData)
break
case 'video':
this.displayVideo(mediaData)
break
case 'external_video':
this.displayExternalVideo(mediaData)
break
case 'model':
this.displayModel(mediaData)
break
}
}
previousMedia() {
const newIndex = this.currentMediaIndex > 0 ?
this.currentMediaIndex - 1 :
this.thumbnails.length - 1
this.switchMedia(newIndex)
}
nextMedia() {
const newIndex = this.currentMediaIndex < this.thumbnails.length - 1 ?
this.currentMediaIndex + 1 :
0
this.switchMedia(newIndex)
}
}
// 初始化产品画廊
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-product-gallery]').forEach(gallery => {
new ProductGallery(gallery)
})
})
</script>
功能性组件
1. 搜索建议组件
<!-- snippets/search-suggestions.liquid -->
{% comment %}
搜索建议组件
参数:
- max_suggestions: 最大建议数量 (可选, 默认: 6)
- show_products: 是否显示产品建议 (可选: true/false)
- show_collections: 是否显示集合建议 (可选: true/false)
- show_pages: 是否显示页面建议 (可选: true/false)
{% endcomment %}
{% assign max_suggestions = max_suggestions | default: 6 %}
{% assign show_products = show_products | default: true %}
{% assign show_collections = show_collections | default: true %}
{% assign show_pages = show_pages | default: false %}
<div class="search-suggestions" data-search-suggestions>
<div class="search-suggestions__content" data-suggestions-content>
<!-- 搜索建议内容将通过 JavaScript 动态加载 -->
</div>
</div>
<script>
class SearchSuggestions {
constructor(element) {
this.container = element
this.content = element.querySelector('[data-suggestions-content]')
this.searchInput = null
this.debounceTimer = null
this.init()
}
init() {
// 查找关联的搜索输入框
this.searchInput = document.querySelector('[data-search-input]')
if (this.searchInput) {
this.searchInput.addEventListener('input', (e) => {
this.handleInput(e.target.value)
})
this.searchInput.addEventListener('focus', () => {
this.show()
})
this.searchInput.addEventListener('blur', () => {
setTimeout(() => this.hide(), 200)
})
}
}
handleInput(query) {
clearTimeout(this.debounceTimer)
if (query.length < 2) {
this.hide()
return
}
this.debounceTimer = setTimeout(() => {
this.fetchSuggestions(query)
}, 300)
}
async fetchSuggestions(query) {
try {
const response = await fetch(`/search/suggest.json?q=${encodeURIComponent(query)}&resources[type]=product,collection,page&resources[limit]={{ max_suggestions }}`)
const data = await response.json()
this.renderSuggestions(data.resources.results)
this.show()
} catch (error) {
console.error('获取搜索建议失败:', error)
}
}
renderSuggestions(results) {
let html = ''
{% if show_products %}
if (results.products && results.products.length > 0) {
html += '<div class="suggestions-section">'
html += '<h4 class="suggestions-title">产品</h4>'
html += '<ul class="suggestions-list">'
results.products.forEach(product => {
html += `
<li class="suggestions-item">
<a href="${product.url}" class="suggestions-link">
${product.featured_image ? `<img src="${product.featured_image.url}&width=50" alt="${product.title}" class="suggestions-image">` : ''}
<div class="suggestions-info">
<span class="suggestions-text">${product.title}</span>
<span class="suggestions-price">${this.formatPrice(product.price)}</span>
</div>
</a>
</li>
`
})
html += '</ul></div>'
}
{% endif %}
{% if show_collections %}
if (results.collections && results.collections.length > 0) {
html += '<div class="suggestions-section">'
html += '<h4 class="suggestions-title">分类</h4>'
html += '<ul class="suggestions-list">'
results.collections.forEach(collection => {
html += `
<li class="suggestions-item">
<a href="${collection.url}" class="suggestions-link">
${collection.featured_image ? `<img src="${collection.featured_image.url}&width=50" alt="${collection.title}" class="suggestions-image">` : ''}
<span class="suggestions-text">${collection.title}</span>
</a>
</li>
`
})
html += '</ul></div>'
}
{% endif %}
{% if show_pages %}
if (results.pages && results.pages.length > 0) {
html += '<div class="suggestions-section">'
html += '<h4 class="suggestions-title">页面</h4>'
html += '<ul class="suggestions-list">'
results.pages.forEach(page => {
html += `
<li class="suggestions-item">
<a href="${page.url}" class="suggestions-link">
<span class="suggestions-text">${page.title}</span>
</a>
</li>
`
})
html += '</ul></div>'
}
{% endif %}
this.content.innerHTML = html
}
formatPrice(priceInCents) {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(priceInCents / 100)
}
show() {
this.container.classList.add('active')
}
hide() {
this.container.classList.remove('active')
}
}
// 初始化搜索建议
document.addEventListener('DOMContentLoaded', () => {
const suggestions = document.querySelector('[data-search-suggestions]')
if (suggestions) {
new SearchSuggestions(suggestions)
}
})
</script>
2. 愿望清单组件
<!-- snippets/wishlist-button.liquid -->
{% comment %}
愿望清单按钮组件
参数:
- product: 产品对象 (必需)
- style: 按钮样式 (可选: 'icon', 'text', 'both')
- size: 按钮尺寸 (可选: 'small', 'medium', 'large')
{% endcomment %}
{% assign style = style | default: 'icon' %}
{% assign size = size | default: 'medium' %}
<button type="button"
class="wishlist-btn wishlist-btn--{{ style }} wishlist-btn--{{ size }}"
data-wishlist-toggle="{{ product.id }}"
data-product-handle="{{ product.handle }}"
aria-label="{% if style == 'icon' %}添加到愿望清单{% endif %}">
<span class="wishlist-btn__icon" data-wishlist-icon>
{% render 'icon-heart' %}
</span>
{% if style == 'text' or style == 'both' %}
<span class="wishlist-btn__text" data-wishlist-text>
收藏
</span>
{% endif %}
</button>
<script>
class WishlistButton {
constructor(element) {
this.button = element
this.productId = element.dataset.wishlistToggle
this.productHandle = element.dataset.productHandle
this.icon = element.querySelector('[data-wishlist-icon]')
this.text = element.querySelector('[data-wishlist-text]')
this.init()
}
init() {
this.button.addEventListener('click', () => {
this.toggle()
})
// 检查初始状态
this.updateState()
}
toggle() {
const wishlist = this.getWishlist()
const isInWishlist = wishlist.includes(this.productId)
if (isInWishlist) {
this.removeFromWishlist()
} else {
this.addToWishlist()
}
}
addToWishlist() {
const wishlist = this.getWishlist()
wishlist.push(this.productId)
this.saveWishlist(wishlist)
this.updateState()
// 触发自定义事件
this.dispatchEvent('wishlist:added', {
productId: this.productId,
productHandle: this.productHandle
})
this.showNotification('已添加到愿望清单')
}
removeFromWishlist() {
let wishlist = this.getWishlist()
wishlist = wishlist.filter(id => id !== this.productId)
this.saveWishlist(wishlist)
this.updateState()
// 触发自定义事件
this.dispatchEvent('wishlist:removed', {
productId: this.productId,
productHandle: this.productHandle
})
this.showNotification('已从愿望清单移除')
}
updateState() {
const wishlist = this.getWishlist()
const isInWishlist = wishlist.includes(this.productId)
this.button.classList.toggle('active', isInWishlist)
this.button.setAttribute('aria-pressed', isInWishlist)
if (this.text) {
this.text.textContent = isInWishlist ? '已收藏' : '收藏'
}
}
getWishlist() {
try {
return JSON.parse(localStorage.getItem('shopify_wishlist') || '[]')
} catch {
return []
}
}
saveWishlist(wishlist) {
localStorage.setItem('shopify_wishlist', JSON.stringify(wishlist))
}
dispatchEvent(eventName, detail) {
const event = new CustomEvent(eventName, { detail })
document.dispatchEvent(event)
}
showNotification(message) {
// 这里可以集成您的通知系统
console.log(message)
}
}
// 初始化所有愿望清单按钮
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-wishlist-toggle]').forEach(button => {
new WishlistButton(button)
})
})
// 监听愿望清单事件
document.addEventListener('wishlist:added', (e) => {
console.log('产品已添加到愿望清单:', e.detail)
})
document.addEventListener('wishlist:removed', (e) => {
console.log('产品已从愿望清单移除:', e.detail)
})
</script>
下一步学习
掌握自定义代码片段开发后,建议继续学习:
代码片段是构建模块化、可维护主题的基础!
最后更新时间: