Skip to Content
🎉 探索 Shopify 的无限可能 结构化知识 + 实战案例,持续更新中...
Liquid 开发自定义片段

自定义代码片段开发

代码片段 (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>

下一步学习

掌握自定义代码片段开发后,建议继续学习:

  1. 主题设置配置 - 全局主题配置
  2. 响应式设计实现 - 移动优先设计
  3. 电商功能实现 - 高级电商功能
  4. 第三方集成 - 外部服务集成
  5. 测试和部署 - 质量保证流程

代码片段是构建模块化、可维护主题的基础!

最后更新时间: