Liquid 开发 · 整理与归档

代码组织和结构

代码组织和结构 良好的代码组织是成功的 Shopify 主题开发项目的基础。本指南将介绍如何构建清晰、可维护、可扩展的主题架构。 npm 脚本、 scripts/ 目录与 Theme ID / 店铺信息脱敏 的工程化约定见:主题工程

代码组织和结构

良好的代码组织是成功的 Shopify 主题开发项目的基础。本指南将介绍如何构建清晰、可维护、可扩展的主题架构。

npm 脚本、scripts/ 目录与 Theme ID / 店铺信息脱敏 的工程化约定见:主题工程化:目录与 npm 脚本

项目目录结构

1. 标准主题目录结构

my-shopify-theme/
├── assets/                 # 静态资源
│   ├── css/
│   │   ├── base/          # 基础样式
│   │   ├── components/    # 组件样式
│   │   ├── layout/        # 布局样式
│   │   ├── pages/         # 页面特定样式
│   │   └── vendors/       # 第三方库样式
│   ├── js/
│   │   ├── components/    # 组件脚本
│   │   ├── modules/       # 功能模块
│   │   ├── utils/         # 工具函数
│   │   └── vendors/       # 第三方库
│   ├── images/            # 图片资源
│   └── fonts/             # 字体文件
├── config/                # 配置文件
│   ├── settings_schema.json
│   └── settings_data.json
├── layout/                # 布局模板
│   ├── theme.liquid
│   ├── password.liquid
│   └── checkout.liquid
├── locales/               # 多语言文件
│   ├── en.default.json
│   ├── zh-CN.json
│   └── ja.json
├── sections/              # 可重用分区
│   ├── header.liquid
│   ├── footer.liquid
│   ├── product-form.liquid
│   └── featured-collection.liquid
├── snippets/              # 代码片段
│   ├── components/        # 组件片段
│   ├── helpers/           # 辅助函数
│   ├── icons/             # 图标
│   └── shared/            # 共享片段
├── templates/             # 页面模板
│   ├── 404.liquid
│   ├── article.liquid
│   ├── blog.liquid
│   ├── cart.liquid
│   ├── collection.liquid
│   ├── index.liquid
│   ├── page.liquid
│   ├── product.liquid
│   └── search.liquid
├── docs/                  # 文档
│   ├── README.md
│   ├── CHANGELOG.md
│   └── DEVELOPMENT.md
├── scripts/               # 构建脚本
│   ├── build.js
│   ├── deploy.js
│   └── watch.js
├── tests/                 # 测试文件
│   ├── unit/
│   ├── integration/
│   └── e2e/
├── .env                   # 环境变量
├── .gitignore
├── package.json
├── shopify.theme.toml
└── README.md

2. 模块化CSS架构

// assets/css/main.scss
// 主样式入口文件
 
// 1. 变量和配置
@import 'base/variables';
@import 'base/functions';
@import 'base/mixins';
 
// 2. 重置和基础样式
@import 'base/normalize';
@import 'base/typography';
@import 'base/base';
 
// 3. 布局
@import 'layout/grid';
@import 'layout/header';
@import 'layout/footer';
@import 'layout/navigation';
 
// 4. 组件
@import 'components/buttons';
@import 'components/forms';
@import 'components/cards';
@import 'components/modals';
@import 'components/product-card';
@import 'components/cart-drawer';
 
// 5. 页面特定样式
@import 'pages/home';
@import 'pages/product';
@import 'pages/collection';
@import 'pages/cart';
 
// 6. 第三方库
@import 'vendors/swiper';
@import 'vendors/lightbox';
 
// 7. 工具类
@import 'utilities/spacing';
@import 'utilities/visibility';
@import 'utilities/text';

3. JavaScript模块组织

// assets/js/main.js
// 主脚本入口文件
 
// 核心模块
import { App } from './modules/App.js';
import { CartManager } from './modules/CartManager.js';
import { ProductForm } from './modules/ProductForm.js';
import { Navigation } from './modules/Navigation.js';
 
// 组件
import { Modal } from './components/Modal.js';
import { Accordion } from './components/Accordion.js';
import { Tabs } from './components/Tabs.js';
 
// 工具函数
import { debounce, throttle } from './utils/helpers.js';
import { api } from './utils/api.js';
 
// 初始化应用
document.addEventListener('DOMContentLoaded', () => {
  const app = new App();
  app.init();
});

组件化开发

1. Liquid组件模式

<!-- snippets/components/product-card.liquid -->
{% comment %}
  产品卡片组件
  
  参数:
  - product: 产品对象 (必需)
  - image_size: 图片尺寸 (可选,默认: '300x300')
  - show_vendor: 是否显示品牌 (可选,默认: false)
  - show_price: 是否显示价格 (可选,默认: true)
  - card_style: 卡片样式 (可选: 'standard', 'minimal', 'detailed')
{% endcomment %}
 
{%- liquid
  assign image_size = image_size | default: '300x300'
  assign show_vendor = show_vendor | default: false
  assign show_price = show_price | default: true
  assign card_style = card_style | default: 'standard'
  assign card_class = 'product-card product-card--' | append: card_style
-%}
 
<div class="{{ card_class }}" data-product-id="{{ product.id }}">
  <div class="product-card__image-wrapper">
    <a href="{{ product.url }}" class="product-card__image-link">
      {% if product.featured_image %}
        {% render 'responsive-image',
          image: product.featured_image,
          alt: product.featured_image.alt | default: product.title,
          sizes: image_size,
          loading: 'lazy'
        %}
      {% else %}
        {% render 'placeholder-image', type: 'product' %}
      {% endif %}
    </a>
    
    {% if product.compare_at_price > product.price %}
      <span class="product-card__badge product-card__badge--sale">
        {{ 'products.general.sale' | t }}
      </span>
    {% endif %}
    
    {% unless product.available %}
      <span class="product-card__badge product-card__badge--sold-out">
        {{ 'products.general.sold_out' | t }}
      </span>
    {% endunless %}
  </div>
  
  <div class="product-card__content">
    {% if show_vendor and product.vendor != blank %}
      <p class="product-card__vendor">{{ product.vendor }}</p>
    {% endif %}
    
    <h3 class="product-card__title">
      <a href="{{ product.url }}">{{ product.title }}</a>
    </h3>
    
    {% if show_price %}
      <div class="product-card__price">
        {% render 'price', product: product %}
      </div>
    {% endif %}
    
    {% if card_style == 'detailed' %}
      {% if product.description != blank %}
        <p class="product-card__description">
          {{ product.description | strip_html | truncate: 100 }}
        </p>
      {% endif %}
      
      <div class="product-card__actions">
        {% render 'quick-add-button', product: product %}
        {% render 'wishlist-button', product: product %}
      </div>
    {% endif %}
  </div>
</div>

2. JavaScript组件基类

// assets/js/components/BaseComponent.js
export class BaseComponent {
  constructor(element, options = {}) {
    this.element = element;
    this.options = { ...this.defaultOptions, ...options };
    this.isInitialized = false;
    
    this.init();
  }
  
  get defaultOptions() {
    return {};
  }
  
  init() {
    if (this.isInitialized) return;
    
    this.bindEvents();
    this.render();
    this.isInitialized = true;
    
    this.element.setAttribute('data-component-initialized', 'true');
  }
  
  bindEvents() {
    // 子类重写此方法
  }
  
  render() {
    // 子类重写此方法
  }
  
  destroy() {
    this.unbindEvents();
    this.element.removeAttribute('data-component-initialized');
    this.isInitialized = false;
  }
  
  unbindEvents() {
    // 子类重写此方法
  }
  
  emit(eventName, detail = {}) {
    const event = new CustomEvent(eventName, {
      detail,
      bubbles: true,
      cancelable: true
    });
    
    this.element.dispatchEvent(event);
  }
  
  on(eventName, handler) {
    this.element.addEventListener(eventName, handler);
  }
  
  off(eventName, handler) {
    this.element.removeEventListener(eventName, handler);
  }
}

3. 具体组件实现

// assets/js/components/ProductForm.js
import { BaseComponent } from './BaseComponent.js';
import { formatMoney } from '../utils/helpers.js';
 
export class ProductForm extends BaseComponent {
  get defaultOptions() {
    return {
      enableHistory: true,
      enableAjax: true,
      selectors: {
        variantSelect: '[data-variant-select]',
        priceElement: '[data-price]',
        addButton: '[data-add-to-cart]',
        form: 'form[action*="/cart/add"]'
      }
    };
  }
  
  bindEvents() {
    this.variantSelects = this.element.querySelectorAll(this.options.selectors.variantSelect);
    this.priceElement = this.element.querySelector(this.options.selectors.priceElement);
    this.addButton = this.element.querySelector(this.options.selectors.addButton);
    this.form = this.element.querySelector(this.options.selectors.form);
    
    this.variantSelects.forEach(select => {
      select.addEventListener('change', this.handleVariantChange.bind(this));
    });
    
    if (this.options.enableAjax && this.form) {
      this.form.addEventListener('submit', this.handleAddToCart.bind(this));
    }
  }
  
  handleVariantChange() {
    const selectedOptions = Array.from(this.variantSelects).map(select => select.value);
    const variant = this.findVariant(selectedOptions);
    
    this.updatePrice(variant);
    this.updateAddButton(variant);
    this.updateUrl(variant);
    
    this.emit('variant:changed', { variant });
  }
  
  findVariant(selectedOptions) {
    return window.productVariants?.find(variant => {
      return variant.options.every((option, index) => {
        return option === selectedOptions[index];
      });
    });
  }
  
  updatePrice(variant) {
    if (!this.priceElement || !variant) return;
    
    let priceHtml = formatMoney(variant.price);
    
    if (variant.compare_at_price > variant.price) {
      priceHtml = `
        <span class="price--sale">${formatMoney(variant.price)}</span>
        <span class="price--compare">${formatMoney(variant.compare_at_price)}</span>
      `;
    }
    
    this.priceElement.innerHTML = priceHtml;
  }
  
  updateAddButton(variant) {
    if (!this.addButton) return;
    
    if (!variant) {
      this.addButton.disabled = true;
      this.addButton.textContent = '无此组合';
    } else if (!variant.available) {
      this.addButton.disabled = true;
      this.addButton.textContent = '缺货';
    } else {
      this.addButton.disabled = false;
      this.addButton.textContent = '加入购物车';
    }
  }
  
  updateUrl(variant) {
    if (!this.options.enableHistory || !variant) return;
    
    const url = new URL(window.location);
    url.searchParams.set('variant', variant.id);
    
    window.history.replaceState(null, '', url);
  }
  
  async handleAddToCart(event) {
    event.preventDefault();
    
    const formData = new FormData(this.form);
    
    try {
      this.setLoading(true);
      
      const response = await fetch('/cart/add.js', {
        method: 'POST',
        body: formData
      });
      
      if (response.ok) {
        const item = await response.json();
        this.emit('cart:added', { item });
      } else {
        throw new Error('添加到购物车失败');
      }
      
    } catch (error) {
      this.emit('cart:error', { error: error.message });
    } finally {
      this.setLoading(false);
    }
  }
  
  setLoading(loading) {
    this.addButton.disabled = loading;
    this.addButton.classList.toggle('loading', loading);
  }
}

配置管理

1. 主题设置架构

// config/settings_schema.json
[
  {
    "name": "theme_info",
    "theme_name": "Custom Theme",
    "theme_version": "1.0.0",
    "theme_author": "Your Name",
    "theme_documentation_url": "https://docs.example.com",
    "theme_support_url": "https://support.example.com"
  },
  {
    "name": "颜色",
    "settings": [
      {
        "type": "header",
        "content": "主要颜色"
      },
      {
        "type": "color",
        "id": "color_primary",
        "label": "主色调",
        "default": "#1a73e8"
      },
      {
        "type": "color",
        "id": "color_secondary", 
        "label": "副色调",
        "default": "#34a853"
      },
      {
        "type": "color",
        "id": "color_accent",
        "label": "强调色",
        "default": "#ea4335"
      }
    ]
  },
  {
    "name": "字体",
    "settings": [
      {
        "type": "font_picker",
        "id": "type_header_font",
        "label": "标题字体",
        "default": "helvetica_n4"
      },
      {
        "type": "font_picker",
        "id": "type_body_font",
        "label": "正文字体", 
        "default": "helvetica_n4"
      },
      {
        "type": "range",
        "id": "type_body_size",
        "label": "正文字体大小",
        "min": 12,
        "max": 24,
        "step": 1,
        "unit": "px",
        "default": 16
      }
    ]
  }
]

2. 环境配置管理

// scripts/config.js
const environments = {
  development: {
    store: 'your-dev-store.myshopify.com',
    theme: 'development',
    debug: true,
    watch: true
  },
  
  staging: {
    store: 'your-staging-store.myshopify.com', 
    theme: 'staging',
    debug: false,
    watch: false
  },
  
  production: {
    store: 'your-store.myshopify.com',
    theme: 'live',
    debug: false,
    watch: false
  }
};
 
export function getConfig(env = 'development') {
  return environments[env] || environments.development;
}

命名约定

1. CSS命名规范 (BEM)

// 组件样式命名
.product-card {                          // 块 (Block)
  &__image {                            // 元素 (Element)
    display: block;
  }
  
  &__title {
    font-size: 1.2rem;
  }
  
  &--featured {                         // 修饰符 (Modifier)
    border: 2px solid gold;
  }
  
  &--sale {
    position: relative;
    
    &::after {
      content: 'SALE';
      position: absolute;
      top: 0;
      right: 0;
    }
  }
}
 
// 状态类
.is-active { }
.is-hidden { }
.is-loading { }
 
// JavaScript钩子类
.js-toggle { }
.js-modal-trigger { }
.js-cart-form { }

2. JavaScript命名规范

// 类名使用PascalCase
class ProductRecommendations { }
class CartDrawer { }
 
// 方法和变量使用camelCase
const productData = {};
const isVisible = true;
 
function calculatePrice() { }
function updateCartCount() { }
 
// 常量使用UPPER_SNAKE_CASE
const API_ENDPOINT = '/cart.js';
const DEFAULT_TIMEOUT = 5000;
 
// 事件名使用kebab-case
element.dispatchEvent(new CustomEvent('cart:updated'));
element.dispatchEvent(new CustomEvent('product:variant-changed'));

3. Liquid文件命名

// 模板文件
templates/
├── index.liquid              # 首页
├── product.liquid            # 产品页
├── collection.liquid         # 集合页
├── page.contact.liquid       # 联系页面 (特定页面)
└── customers/
    ├── login.liquid          # 客户登录
    └── register.liquid       # 客户注册

// 分区文件
sections/
├── header.liquid             # 头部
├── footer.liquid             # 页脚
├── product-form.liquid       # 产品表单
├── featured-collection.liquid # 特色集合
└── newsletter-signup.liquid  # 邮件订阅

// 代码片段
snippets/
├── product-card.liquid       # 产品卡片
├── price.liquid              # 价格显示
├── icon-cart.liquid          # 购物车图标
├── responsive-image.liquid   # 响应式图片
└── components/               # 组件目录
    ├── modal.liquid
    ├── accordion.liquid
    └── tabs.liquid

文档和注释

1. 代码注释规范

{% comment %}
  产品推荐分区
  
  功能:显示相关产品推荐
  
  设置:
  - heading: 标题文本
  - product_limit: 显示产品数量 (1-12)
  - show_vendor: 是否显示品牌
  - image_ratio: 图片比例 ('square', 'portrait', 'landscape')
  
  依赖:
  - snippets/product-card.liquid
  - assets/recommendations.js
  
  更新记录:
  - 2024-01-15: 初始创建
  - 2024-01-20: 添加图片比例选项
{% endcomment %}
 
{%- liquid
  assign heading = section.settings.heading | default: '相关推荐'
  assign product_limit = section.settings.product_limit | default: 4
  assign show_vendor = section.settings.show_vendor | default: false
-%}

2. JavaScript文档注释

/**
 * 购物车抽屉组件
 * 
 * @class CartDrawer
 * @extends BaseComponent
 * 
 * @example
 * const cartDrawer = new CartDrawer(document.querySelector('[data-cart-drawer]'), {
 *   autoOpen: true,
 *   closeOnOverlay: true
 * });
 * 
 * @since 1.0.0
 * @author Your Name
 */
export class CartDrawer extends BaseComponent {
  /**
   * 创建CartDrawer实例
   * 
   * @param {HTMLElement} element - 购物车抽屉元素
   * @param {Object} options - 配置选项
   * @param {boolean} options.autoOpen - 是否自动打开
   * @param {boolean} options.closeOnOverlay - 点击遮罩是否关闭
   */
  constructor(element, options = {}) {
    super(element, options);
  }
  
  /**
   * 打开购物车抽屉
   * 
   * @fires CartDrawer#drawer:opened
   * @returns {Promise<void>}
   */
  async open() {
    // 实现...
  }
}

版本控制策略

1. Git工作流程

# 分支命名规范
feature/product-recommendations    # 新功能
bugfix/cart-total-calculation     # 错误修复
hotfix/critical-checkout-issue    # 紧急修复
release/v1.2.0                   # 发布版本
 
# 提交信息规范
feat: 添加产品推荐功能
fix: 修复购物车总价计算错误
docs: 更新README文档
style: 调整按钮样式
refactor: 重构产品卡片组件
test: 添加购物车功能测试

2. 版本发布流程

// package.json
{
  "name": "custom-shopify-theme",
  "version": "1.2.0",
  "scripts": {
    "dev": "shopify theme dev",
    "build": "node scripts/build.js",
    "deploy:staging": "node scripts/deploy.js --env=staging",
    "deploy:production": "node scripts/deploy.js --env=production",
    "test": "jest",
    "lint": "eslint assets/js/**/*.js"
  }
}

通过遵循这些代码组织和结构最佳实践,您可以创建易于维护、扩展和协作的 Shopify 主题项目!

下一步学习

掌握代码组织后,建议继续学习:

  1. 安全最佳实践 - 主题安全规范
  2. 实际应用示例 - 实战案例分析