theme-shopify-javascript-standards

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Shopify JavaScript Standards

Shopify JavaScript 规范

JavaScript file structure, custom elements, and coding standards for Shopify theme development.
Shopify主题开发中的JavaScript文件结构、自定义元素及编码规范。

When to Use

适用场景

  • Writing JavaScript for Shopify theme sections
  • Creating interactive components
  • Setting up JavaScript file structure
  • Using custom HTML elements
  • 编写Shopify主题区块的JavaScript代码时
  • 创建交互式组件时
  • 搭建JavaScript文件结构时
  • 使用自定义HTML元素时

File Structure

文件结构

Separate JavaScript Files

独立JavaScript文件

  • JavaScript must live in a separate file in the
    assets/
    directory
  • Include it in the section using the
    asset_url
    filter
  • JavaScript文件必须放在
    assets/
    目录下的独立文件中
  • 使用
    asset_url
    过滤器将其引入区块中

Including JavaScript in Sections

在区块中引入JavaScript

liquid
<script src="{{ 'section-logic.js' | asset_url }}" defer="defer"></script>
Always use
defer
to ensure scripts load after HTML parsing.
liquid
<script src="{{ 'section-logic.js' | asset_url }}" defer="defer"></script>
务必使用
defer
,确保脚本在HTML解析完成后加载。

File Naming

文件命名

Match JavaScript file names to section names:
sections/
  └── product-quick-view.liquid

assets/
  └── product-quick-view.js
JavaScript文件名需与区块名称匹配:
sections/
  └── product-quick-view.liquid

assets/
  └── product-quick-view.js

JavaScript Rules

JavaScript规则

Variable Declarations

变量声明

  • Use only
    const
    and
    let
  • Never use
    var
  • Avoid global scope pollution
  • 仅使用
    const
    let
  • 禁止使用
    var
  • 避免全局作用域污染

Example

示例

javascript
// Good
const productId = document.querySelector('[data-product-id]').dataset.productId;
let isOpen = false;

// Bad
var productId = ...; // Never use var
window.myVariable = ...; // Avoid global pollution
javascript
// Good
const productId = document.querySelector('[data-product-id]').dataset.productId;
let isOpen = false;

// Bad
var productId = ...; // Never use var
window.myVariable = ...; // Avoid global pollution

Scope Management

作用域管理

Keep variables scoped to their usage:
javascript
// Good - scoped within function
function initProductCard() {
  const card = document.querySelector('.product-card');
  const button = card.querySelector('.product-card__button');
  // ...
}

// Bad - global variables
const card = document.querySelector('.product-card'); // Global scope
将变量限定在其使用的作用域内:
javascript
// Good - scoped within function
function initProductCard() {
  const card = document.querySelector('.product-card');
  const button = card.querySelector('.product-card__button');
  // ...
}

// Bad - global variables
const card = document.querySelector('.product-card'); // Global scope

Custom Elements

自定义元素

Use Custom HTML Elements

使用自定义HTML元素

Use custom HTML elements to encapsulate JavaScript logic and create reusable components.
使用自定义HTML元素封装JavaScript逻辑,创建可复用组件。

Custom Element Structure

自定义元素结构

javascript
class ProductQuickView extends HTMLElement {
  constructor() {
    super();
    this.init();
  }

  init() {
    this.button = this.querySelector('[data-trigger]');
    this.modal = this.querySelector('[data-modal]');
    this.closeButton = this.querySelector('[data-close]');
    
    this.button?.addEventListener('click', () => this.open());
    this.closeButton?.addEventListener('click', () => this.close());
  }

  open() {
    this.modal?.classList.add('is-open');
  }

  close() {
    this.modal?.classList.remove('is-open');
  }
}

customElements.define('product-quick-view', ProductQuickView);
javascript
class ProductQuickView extends HTMLElement {
  constructor() {
    super();
    this.init();
  }

  init() {
    this.button = this.querySelector('[data-trigger]');
    this.modal = this.querySelector('[data-modal]');
    this.closeButton = this.querySelector('[data-close]');
    
    this.button?.addEventListener('click', () => this.open());
    this.closeButton?.addEventListener('click', () => this.close());
  }

  open() {
    this.modal?.classList.add('is-open');
  }

  close() {
    this.modal?.classList.remove('is-open');
  }
}

customElements.define('product-quick-view', ProductQuickView);

Using Custom Elements in Liquid

在Liquid中使用自定义元素

liquid
<product-quick-view data-product-id="{{ product.id }}">
  <button data-trigger>Quick View</button>
  <div data-modal class="modal">
    <button data-close>Close</button>
    <!-- Modal content -->
  </div>
</product-quick-view>
liquid
<product-quick-view data-product-id="{{ product.id }}">
  <button data-trigger>Quick View</button>
  <div data-modal class="modal">
    <button data-close>Close</button>
    <!-- Modal content -->
  </div>
</product-quick-view>

Custom Element Benefits

自定义元素优势

  • Encapsulation - logic is self-contained
  • Reusability - use anywhere in the theme
  • Lifecycle hooks -
    connectedCallback
    ,
    disconnectedCallback
  • Data attributes - pass data via
    data-*
    attributes
  • 封装性——逻辑独立
  • 可复用性——可在主题中任意位置使用
  • 生命周期钩子——
    connectedCallback
    disconnectedCallback
  • 数据属性——通过
    data-*
    属性传递数据

Lifecycle Hooks

生命周期钩子

javascript
class MyComponent extends HTMLElement {
  connectedCallback() {
    // Element added to DOM
    this.init();
  }

  disconnectedCallback() {
    // Element removed from DOM
    this.cleanup();
  }

  init() {
    // Setup logic
  }

  cleanup() {
    // Cleanup logic (remove event listeners, etc.)
  }
}
javascript
class MyComponent extends HTMLElement {
  connectedCallback() {
    // Element added to DOM
    this.init();
  }

  disconnectedCallback() {
    // Element removed from DOM
    this.cleanup();
  }

  init() {
    // Setup logic
  }

  cleanup() {
    // Cleanup logic (remove event listeners, etc.)
  }
}

Data Attributes

数据属性

Passing Data to JavaScript

向JavaScript传递数据

  • Use custom HTML tags when appropriate
  • Pass dynamic data via
    data-*
    attributes
  • Access data via
    dataset
    property
  • 合适时使用自定义HTML标签
  • 通过
    data-*
    属性传递动态数据
  • 通过
    dataset
    属性访问数据

Example

示例

liquid
<product-card 
  data-product-id="{{ product.id }}"
  data-product-handle="{{ product.handle }}"
  data-variant-id="{{ product.selected_or_first_available_variant.id }}">
  <!-- Content -->
</product-card>
javascript
class ProductCard extends HTMLElement {
  constructor() {
    super();
    this.productId = this.dataset.productId;
    this.productHandle = this.dataset.productHandle;
    this.variantId = this.dataset.variantId;
  }
}
liquid
<product-card 
  data-product-id="{{ product.id }}"
  data-product-handle="{{ product.handle }}"
  data-variant-id="{{ product.selected_or_first_available_variant.id }}">
  <!-- Content -->
</product-card>
javascript
class ProductCard extends HTMLElement {
  constructor() {
    super();
    this.productId = this.dataset.productId;
    this.productHandle = this.dataset.productHandle;
    this.variantId = this.dataset.variantId;
  }
}

Observers

观察者

Use Observers Only When Explicitly Requested

仅在明确需要时使用观察者

Use observers (IntersectionObserver, MutationObserver, etc.) only if explicitly requested by the user.
仅在用户明确要求时使用观察者(IntersectionObserver、MutationObserver等)。

IntersectionObserver Example

IntersectionObserver示例

javascript
// Only use if explicitly needed
class LazyImage extends HTMLElement {
  constructor() {
    super();
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.loadImage();
          this.observer.unobserve(this);
        }
      });
    });
  }

  connectedCallback() {
    this.observer.observe(this);
  }

  loadImage() {
    const img = this.querySelector('img');
    img.src = img.dataset.src;
  }
}
javascript
// Only use if explicitly needed
class LazyImage extends HTMLElement {
  constructor() {
    super();
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          this.loadImage();
          this.observer.unobserve(this);
        }
      });
    });
  }

  connectedCallback() {
    this.observer.observe(this);
  }

  loadImage() {
    const img = this.querySelector('img');
    img.src = img.dataset.src;
  }
}

Best Practices

最佳实践

Event Handling

事件处理

javascript
class ProductForm extends HTMLElement {
  constructor() {
    super();
    this.form = this.querySelector('form');
    this.form?.addEventListener('submit', this.handleSubmit.bind(this));
  }

  handleSubmit(event) {
    event.preventDefault();
    // Handle form submission
  }

  disconnectedCallback() {
    // Clean up event listeners
    this.form?.removeEventListener('submit', this.handleSubmit);
  }
}
javascript
class ProductForm extends HTMLElement {
  constructor() {
    super();
    this.form = this.querySelector('form');
    this.form?.addEventListener('submit', this.handleSubmit.bind(this));
  }

  handleSubmit(event) {
    event.preventDefault();
    // Handle form submission
  }

  disconnectedCallback() {
    // Clean up event listeners
    this.form?.removeEventListener('submit', this.handleSubmit);
  }
}

Error Handling

错误处理

javascript
class ProductCard extends HTMLElement {
  init() {
    try {
      const button = this.querySelector('[data-add-to-cart]');
      if (!button) {
        console.warn('Add to cart button not found');
        return;
      }
      button.addEventListener('click', this.handleAddToCart.bind(this));
    } catch (error) {
      console.error('Error initializing product card:', error);
    }
  }
}
javascript
class ProductCard extends HTMLElement {
  init() {
    try {
      const button = this.querySelector('[data-add-to-cart]');
      if (!button) {
        console.warn('Add to cart button not found');
        return;
      }
      button.addEventListener('click', this.handleAddToCart.bind(this));
    } catch (error) {
      console.error('Error initializing product card:', error);
    }
  }
}

Shopify Theme Documentation

Shopify主题文档

Complete Example

完整示例

Section File

区块文件

liquid
{{ 'product-card.css' | asset_url | stylesheet_tag }}

<product-card 
  data-product-id="{{ product.id }}"
  data-product-handle="{{ product.handle }}">
  <div class="product-card">
    {{ image | image_tag: widths: '360, 720, 1080', loading: 'lazy' }}
    <h3>{{ product.title }}</h3>
    <button data-add-to-cart>Add to Cart</button>
  </div>
</product-card>

<script src="{{ 'product-card.js' | asset_url }}" defer="defer"></script>
liquid
{{ 'product-card.css' | asset_url | stylesheet_tag }}

<product-card 
  data-product-id="{{ product.id }}"
  data-product-handle="{{ product.handle }}">
  <div class="product-card">
    {{ image | image_tag: widths: '360, 720, 1080', loading: 'lazy' }}
    <h3>{{ product.title }}</h3>
    <button data-add-to-cart>Add to Cart</button>
  </div>
</product-card>

<script src="{{ 'product-card.js' | asset_url }}" defer="defer"></script>

JavaScript File

JavaScript文件

javascript
class ProductCard extends HTMLElement {
  constructor() {
    super();
    this.productId = this.dataset.productId;
    this.init();
  }

  init() {
    const button = this.querySelector('[data-add-to-cart]');
    button?.addEventListener('click', () => this.addToCart());
  }

  async addToCart() {
    try {
      const response = await fetch('/cart/add.js', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          id: this.productId,
          quantity: 1
        })
      });
      // Handle response
    } catch (error) {
      console.error('Error adding to cart:', error);
    }
  }
}

customElements.define('product-card', ProductCard);
javascript
class ProductCard extends HTMLElement {
  constructor() {
    super();
    this.productId = this.dataset.productId;
    this.init();
  }

  init() {
    const button = this.querySelector('[data-add-to-cart]');
    button?.addEventListener('click', () => this.addToCart());
  }

  async addToCart() {
    try {
      const response = await fetch('/cart/add.js', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          id: this.productId,
          quantity: 1
        })
      });
      // Handle response
    } catch (error) {
      console.error('Error adding to cart:', error);
    }
  }
}

customElements.define('product-card', ProductCard);

Instructions

操作指南

  1. Separate JS files - one file per section in
    assets/
    directory
  2. Use
    defer
    when including scripts
  3. Use
    const
    and
    let
    - never
    var
  4. Use custom elements to encapsulate logic
  5. Pass data via
    data-*
    attributes
  6. Avoid global scope pollution
  7. Use observers only when explicitly requested
  8. Clean up event listeners in
    disconnectedCallback
  1. 独立JS文件——每个区块对应
    assets/
    目录下的一个文件
  2. **使用
    defer
    **引入脚本
  3. 使用
    const
    let
    ——禁止使用
    var
  4. 使用自定义元素封装逻辑
  5. 通过
    data-*
    属性传递数据
  6. 避免全局作用域污染
  7. 仅在明确需要时使用观察者
  8. disconnectedCallback
    中清理事件监听器