轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • Vue.js
  • 前端工程化
  • 浏览器与Web API
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • JavaScript
  • TypeScript
  • Node.js
  • Vue.js
  • 前端工程化
  • 浏览器与Web API
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaScript

  • TypeScript

  • Node.js

  • Vue.js

  • 工程化

  • 浏览器与Web API

    • HTML基础-语义化标签与文档结构
    • HTML基础-文本与排版标签
    • HTML基础-列表与表格
    • HTML表单-Input类型详解
    • HTML表单-表单元素与验证
    • HTML交互-多媒体元素
    • HTML工程化-模板与组件化
      • 一、Template元素基础
        • 1.1 Template元素特性
        • 1.2 Template的使用
      • 二、Web Components核心技术
        • 2.1 Custom Elements(自定义元素)
        • 2.2 Shadow DOM(影子DOM)
        • 2.3 HTML Templates与Slots
      • 三、组件生命周期
        • 3.1 生命周期回调
        • 3.2 最佳实践
      • 四、组件通信
        • 4.1 属性传递(Props)
        • 4.2 自定义事件(Events)
        • 4.3 状态管理
      • 五、组件样式封装
        • 5.1 Shadow DOM样式隔离
        • 5.2 CSS变量与主题
      • 六、模板引擎模式
        • 6.1 字符串模板
        • 6.2 Tagged Template Literals
      • 七、综合示例
      • 八、最佳实践
        • 8.1 组件设计原则
        • 8.2 命名规范
        • 8.3 属性与特性同步
        • 8.4 错误处理
        • 8.5 性能优化
      • 九、浏览器兼容性
        • 9.1 特性检测
        • 9.2 Polyfills
      • 十、总结
    • HTML工程化-性能优化
    • CSS基础-选择器与优先级
    • CSS基础-盒模型与布局基础
    • CSS基础-单位与颜色系统
    • CSS基础-文本与字体
    • CSS基础-背景、列表与表格样式
    • CSS布局-Flexbox完全指南
    • CSS布局-Grid网格布局
    • CSS布局-响应式设计实践
    • CSS进阶-动画与过渡
    • CSS进阶-渐变与阴影效果
    • CSS进阶-Transform与3D变换
    • CSS进阶-滤镜与混合模式
    • 现代CSS-CSS预处理器对比
    • 现代CSS-CSS-in-JS方案
    • 现代CSS-原子化CSS与Tailwind
    • CSS工程化-架构与规范
    • CSS工程化-性能优化
    • CSS工程化-PostCSS实战指南
    • Web API基础-DOM操作完全指南
    • Web API基础-事件处理与委托
    • Web API基础-BOM与浏览器环境
    • Web API存储-客户端存储方案
    • Web API网络-HTTP请求详解
    • Web API网络-实时通信方案
    • Web API交互-用户体验增强
    • HTML&CSS历代版本新特性
  • 前端
  • 浏览器与Web API
轩辕李
2019-06-24
目录

HTML工程化-模板与组件化

# HTML工程化-模板与组件化

在现代Web开发中,组件化已成为主流的开发模式。HTML提供了原生的<template>元素和Web Components API,让我们能够创建可复用的组件。本文将深入探讨HTML模板与组件化的实践方法。

# 一、Template元素基础

# 1.1 Template元素特性

<template>元素用于声明HTML片段,这些片段在页面加载时不会被渲染,只有在JavaScript中被激活时才会显示。

核心特性:

  • 内容在页面加载时不渲染
  • 可以放置在文档的任何位置
  • 可以包含任何HTML内容
  • 通过content属性访问DocumentFragment
<template id="card-template">
  <div class="card">
    <h3 class="card-title"></h3>
    <p class="card-description"></p>
  </div>
</template>

# 1.2 Template的使用

通过JavaScript获取模板并克隆内容:

const template = document.getElementById('card-template');
const clone = template.content.cloneNode(true);

clone.querySelector('.card-title').textContent = '标题';
clone.querySelector('.card-description').textContent = '描述';

document.body.appendChild(clone);

# 二、Web Components核心技术

Web Components由四个主要技术组成:

# 2.1 Custom Elements(自定义元素)

自定义元素允许我们创建自己的HTML标签。

定义自定义元素:

class UserCard extends HTMLElement {
  constructor() {
    super();
    // 初始化组件
  }

  connectedCallback() {
    // 元素插入DOM时调用
    this.render();
  }

  disconnectedCallback() {
    // 元素从DOM移除时调用
  }

  attributeChangedCallback(name, oldValue, newValue) {
    // 属性变化时调用
    this.render();
  }

  static get observedAttributes() {
    return ['name', 'email'];
  }

  render() {
    this.innerHTML = `
      <div class="user-card">
        <h3>${this.getAttribute('name')}</h3>
        <p>${this.getAttribute('email')}</p>
      </div>
    `;
  }
}

customElements.define('user-card', UserCard);

使用自定义元素:

<user-card name="张三" email="zhangsan@example.com"></user-card>

# 2.2 Shadow DOM(影子DOM)

Shadow DOM提供了封装机制,使组件的样式和结构与外部隔离。

class ShadowCard extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      <style>
        .card {
          border: 1px solid #ddd;
          padding: 20px;
          border-radius: 8px;
        }
        
        .card h3 {
          margin: 0 0 10px 0;
          color: #333;
        }
      </style>
      
      <div class="card">
        <h3><slot name="title">默认标题</slot></h3>
        <slot>默认内容</slot>
      </div>
    `;
  }
}

customElements.define('shadow-card', ShadowCard);

使用带Shadow DOM的组件:

<shadow-card>
  <span slot="title">自定义标题</span>
  <p>这是卡片内容</p>
</shadow-card>

# 2.3 HTML Templates与Slots

Slots允许在组件内部定义插槽,用户可以插入自定义内容。

class FlexibleCard extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ mode: 'open' });
    const template = document.getElementById('flexible-card-template');
    shadow.appendChild(template.content.cloneNode(true));
  }
}

customElements.define('flexible-card', FlexibleCard);
<template id="flexible-card-template">
  <style>
    .card {
      border: 1px solid #e0e0e0;
      border-radius: 8px;
      padding: 16px;
      margin: 16px 0;
    }
    
    .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 12px;
    }
    
    .actions {
      display: flex;
      gap: 8px;
    }
  </style>
  
  <div class="card">
    <div class="header">
      <slot name="header">默认标题</slot>
      <div class="actions">
        <slot name="actions"></slot>
      </div>
    </div>
    <slot>默认内容</slot>
    <slot name="footer"></slot>
  </div>
</template>

# 三、组件生命周期

自定义元素有明确的生命周期钩子:

# 3.1 生命周期回调

class LifecycleComponent extends HTMLElement {
  constructor() {
    super();
    console.log('1. constructor: 组件被创建');
  }

  connectedCallback() {
    console.log('2. connectedCallback: 组件被插入DOM');
    this.render();
  }

  disconnectedCallback() {
    console.log('3. disconnectedCallback: 组件从DOM移除');
    this.cleanup();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`4. attributeChangedCallback: ${name} 从 ${oldValue} 变为 ${newValue}`);
    this.render();
  }

  adoptedCallback() {
    console.log('5. adoptedCallback: 组件被移动到新文档');
  }

  static get observedAttributes() {
    return ['status', 'count'];
  }

  render() {
    // 渲染逻辑
  }

  cleanup() {
    // 清理事件监听器、定时器等
  }
}

customElements.define('lifecycle-component', LifecycleComponent);

# 3.2 最佳实践

  1. 在constructor中初始化状态,但不要访问属性或子元素
  2. 在connectedCallback中执行DOM操作和添加事件监听器
  3. 在disconnectedCallback中清理资源
  4. 使用observedAttributes监听属性变化

# 四、组件通信

# 4.1 属性传递(Props)

通过HTML属性向组件传递数据:

class DataCard extends HTMLElement {
  static get observedAttributes() {
    return ['title', 'count', 'status'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    this.render();
  }

  get title() {
    return this.getAttribute('title');
  }

  set title(value) {
    this.setAttribute('title', value);
  }

  get count() {
    return Number(this.getAttribute('count')) || 0;
  }

  set count(value) {
    this.setAttribute('count', value);
  }

  render() {
    this.innerHTML = `
      <div class="data-card ${this.getAttribute('status')}">
        <h3>${this.title}</h3>
        <p>数量: ${this.count}</p>
      </div>
    `;
  }
}

# 4.2 自定义事件(Events)

组件通过自定义事件向外部通信:

class CounterButton extends HTMLElement {
  connectedCallback() {
    this.count = 0;
    this.render();
    
    this.querySelector('button').addEventListener('click', () => {
      this.count++;
      this.dispatchEvent(new CustomEvent('count-changed', {
        detail: { count: this.count },
        bubbles: true,
        composed: true
      }));
      this.render();
    });
  }

  render() {
    this.innerHTML = `
      <button>点击次数: ${this.count}</button>
    `;
  }
}

customElements.define('counter-button', CounterButton);

监听自定义事件:

document.addEventListener('count-changed', (e) => {
  console.log('新的计数:', e.detail.count);
});

# 4.3 状态管理

对于复杂应用,可以使用简单的状态管理:

class Store {
  constructor(initialState) {
    this.state = initialState;
    this.listeners = [];
  }

  getState() {
    return this.state;
  }

  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.listeners.forEach(listener => listener(this.state));
  }

  subscribe(listener) {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }
}

const store = new Store({ count: 0, theme: 'light' });

class StoreConnectedComponent extends HTMLElement {
  connectedCallback() {
    this.unsubscribe = store.subscribe((state) => {
      this.render(state);
    });
    this.render(store.getState());
  }

  disconnectedCallback() {
    this.unsubscribe();
  }

  render(state) {
    this.innerHTML = `
      <div class="theme-${state.theme}">
        Count: ${state.count}
      </div>
    `;
  }
}

# 五、组件样式封装

# 5.1 Shadow DOM样式隔离

class StyledComponent extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      <style>
        :host {
          display: block;
          padding: 16px;
        }
        
        :host([disabled]) {
          opacity: 0.5;
          pointer-events: none;
        }
        
        :host-context(.dark-theme) {
          background: #333;
          color: white;
        }
        
        ::slotted(h3) {
          color: #2196F3;
        }
      </style>
      
      <div class="wrapper">
        <slot></slot>
      </div>
    `;
  }
}

# 5.2 CSS变量与主题

使用CSS变量实现可定制的组件:

class ThemeableCard extends HTMLElement {
  constructor() {
    super();
    
    const shadow = this.attachShadow({ mode: 'open' });
    
    shadow.innerHTML = `
      <style>
        :host {
          --card-bg: white;
          --card-text: #333;
          --card-border: #ddd;
          --card-radius: 8px;
          --card-padding: 16px;
        }
        
        .card {
          background: var(--card-bg);
          color: var(--card-text);
          border: 1px solid var(--card-border);
          border-radius: var(--card-radius);
          padding: var(--card-padding);
        }
      </style>
      
      <div class="card">
        <slot></slot>
      </div>
    `;
  }
}

使用CSS变量自定义样式:

<style>
  themeable-card {
    --card-bg: #f5f5f5;
    --card-text: #1a1a1a;
    --card-border: #2196F3;
  }
</style>

<themeable-card>
  <h3>自定义样式的卡片</h3>
  <p>使用CSS变量定制外观</p>
</themeable-card>

# 六、模板引擎模式

# 6.1 字符串模板

class TemplateComponent extends HTMLElement {
  constructor() {
    super();
    this.data = {
      title: '标题',
      items: ['项目1', '项目2', '项目3']
    };
  }

  template(data) {
    return `
      <div class="container">
        <h2>${data.title}</h2>
        <ul>
          ${data.items.map(item => `<li>${item}</li>`).join('')}
        </ul>
      </div>
    `;
  }

  connectedCallback() {
    this.innerHTML = this.template(this.data);
  }

  updateData(newData) {
    this.data = { ...this.data, ...newData };
    this.innerHTML = this.template(this.data);
  }
}

# 6.2 Tagged Template Literals

使用标记模板字符串实现更强大的模板功能:

function html(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = values[i] || '';
    return result + str + value;
  }, '');
}

class AdvancedTemplate extends HTMLElement {
  connectedCallback() {
    const name = '张三';
    const age = 25;
    
    this.innerHTML = html`
      <div class="user-info">
        <h3>${name}</h3>
        <p>年龄: ${age}</p>
      </div>
    `;
  }
}

# 七、综合示例

<html>
  <div id="app-n9o0p">
    <h3>Web Components 综合示例</h3>
    
    <template id="product-card-template-n9o0p">
      <style>
        :host {
          display: block;
          margin: 12px 0;
        }
        
        .product-card-n9o0p {
          border: 1px solid #e0e0e0;
          border-radius: 8px;
          padding: 16px;
          background: white;
          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
          transition: transform 0.2s, box-shadow 0.2s;
        }
        
        .product-card-n9o0p:hover {
          transform: translateY(-2px);
          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
        }
        
        .product-header-n9o0p {
          display: flex;
          justify-content: space-between;
          align-items: start;
          margin-bottom: 12px;
        }
        
        .product-title-n9o0p {
          margin: 0;
          font-size: 18px;
          color: #333;
        }
        
        .product-price-n9o0p {
          font-size: 20px;
          font-weight: bold;
          color: #ff5722;
        }
        
        .product-description-n9o0p {
          color: #666;
          line-height: 1.5;
          margin: 8px 0;
        }
        
        .product-footer-n9o0p {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-top: 12px;
          padding-top: 12px;
          border-top: 1px solid #f0f0f0;
        }
        
        .product-stock-n9o0p {
          font-size: 14px;
          color: #666;
        }
        
        .product-stock-n9o0p.low-n9o0p {
          color: #ff9800;
        }
        
        .product-stock-n9o0p.out-n9o0p {
          color: #f44336;
        }
        
        .btn-add-n9o0p {
          background: #2196F3;
          color: white;
          border: none;
          padding: 8px 16px;
          border-radius: 4px;
          cursor: pointer;
          font-size: 14px;
          transition: background 0.2s;
        }
        
        .btn-add-n9o0p:hover {
          background: #1976D2;
        }
        
        .btn-add-n9o0p:disabled {
          background: #ccc;
          cursor: not-allowed;
        }
      </style>
      
      <div class="product-card-n9o0p">
        <div class="product-header-n9o0p">
          <h4 class="product-title-n9o0p"></h4>
          <span class="product-price-n9o0p"></span>
        </div>
        <p class="product-description-n9o0p"></p>
        <div class="product-footer-n9o0p">
          <span class="product-stock-n9o0p"></span>
          <button class="btn-add-n9o0p">加入购物车</button>
        </div>
      </div>
    </template>
    
    <div id="product-list-n9o0p"></div>
    
    <div id="cart-summary-n9o0p" style="margin-top: 20px; padding: 16px; background: #f5f5f5; border-radius: 8px;">
      <h4 style="margin: 0 0 8px 0;">购物车</h4>
      <div id="cart-items-n9o0p"></div>
      <p id="cart-total-n9o0p" style="margin: 8px 0 0 0; font-weight: bold; color: #ff5722;"></p>
    </div>
  </div>
</html>

<script>
  class ProductCard extends HTMLElement {
    static get observedAttributes() {
      return ['product-name', 'price', 'description', 'stock'];
    }

    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      
      const template = document.getElementById('product-card-template-n9o0p');
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }

    connectedCallback() {
      this.render();
      
      const btn = this.shadowRoot.querySelector('.btn-add-n9o0p');
      btn.addEventListener('click', () => {
        this.addToCart();
      });
    }

    attributeChangedCallback(name, oldValue, newValue) {
      if (oldValue !== newValue) {
        this.render();
      }
    }

    render() {
      const name = this.getAttribute('product-name') || '';
      const price = this.getAttribute('price') || '0';
      const description = this.getAttribute('description') || '';
      const stock = Number(this.getAttribute('stock')) || 0;
      
      this.shadowRoot.querySelector('.product-title-n9o0p').textContent = name;
      this.shadowRoot.querySelector('.product-price-n9o0p').textContent = `¥${price}`;
      this.shadowRoot.querySelector('.product-description-n9o0p').textContent = description;
      
      const stockEl = this.shadowRoot.querySelector('.product-stock-n9o0p');
      stockEl.className = 'product-stock-n9o0p';
      
      if (stock === 0) {
        stockEl.textContent = '已售罄';
        stockEl.classList.add('out-n9o0p');
      } else if (stock < 10) {
        stockEl.textContent = `仅剩 ${stock} 件`;
        stockEl.classList.add('low-n9o0p');
      } else {
        stockEl.textContent = `库存: ${stock} 件`;
      }
      
      const btn = this.shadowRoot.querySelector('.btn-add-n9o0p');
      btn.disabled = stock === 0;
    }

    addToCart() {
      const stock = Number(this.getAttribute('stock'));
      if (stock <= 0) return;
      
      this.dispatchEvent(new CustomEvent('add-to-cart', {
        detail: {
          name: this.getAttribute('product-name'),
          price: Number(this.getAttribute('price'))
        },
        bubbles: true,
        composed: true
      }));
      
      this.setAttribute('stock', stock - 1);
    }
  }

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

  const products = [
    { name: 'JavaScript高级程序设计', price: 129, description: '前端开发必读经典书籍', stock: 15 },
    { name: 'Vue.js实战', price: 89, description: 'Vue框架深度解析与实践', stock: 8 },
    { name: 'CSS世界', price: 99, description: 'CSS核心技术详解', stock: 0 },
    { name: 'Node.js开发指南', price: 79, description: '后端JavaScript开发', stock: 20 }
  ];

  const cart = [];
  
  const productList = document.getElementById('product-list-n9o0p');
  products.forEach(product => {
    const card = document.createElement('product-card');
    card.setAttribute('product-name', product.name);
    card.setAttribute('price', product.price);
    card.setAttribute('description', product.description);
    card.setAttribute('stock', product.stock);
    productList.appendChild(card);
  });

  document.getElementById('app-n9o0p').addEventListener('add-to-cart', (e) => {
    cart.push(e.detail);
    updateCart();
  });

  function updateCart() {
    const cartItems = document.getElementById('cart-items-n9o0p');
    const cartTotal = document.getElementById('cart-total-n9o0p');
    
    if (cart.length === 0) {
      cartItems.innerHTML = '<p style="color: #999; margin: 0;">购物车为空</p>';
      cartTotal.textContent = '';
      return;
    }
    
    const itemCounts = {};
    cart.forEach(item => {
      itemCounts[item.name] = (itemCounts[item.name] || 0) + 1;
    });
    
    cartItems.innerHTML = Object.entries(itemCounts)
      .map(([name, count]) => {
        const item = cart.find(i => i.name === name);
        return `<div style="margin: 4px 0;">${name} x${count} - ¥${item.price * count}</div>`;
      })
      .join('');
    
    const total = cart.reduce((sum, item) => sum + item.price, 0);
    cartTotal.textContent = `总计: ¥${total}`;
  }

  updateCart();
</script>

# 八、最佳实践

# 8.1 组件设计原则

  1. 单一职责:每个组件只负责一个功能
  2. 可配置性:通过属性和插槽提供灵活性
  3. 封装性:使用Shadow DOM隔离样式和结构
  4. 可访问性:确保组件支持键盘操作和屏幕阅读器
  5. 性能优化:避免不必要的重渲染

# 8.2 命名规范

// 使用连字符命名(至少包含一个连字符)
customElements.define('user-profile', UserProfile);      // ✓
customElements.define('shopping-cart-item', CartItem);   // ✓
customElements.define('userprofile', UserProfile);       // ✗ 缺少连字符

# 8.3 属性与特性同步

class SyncedComponent extends HTMLElement {
  static get observedAttributes() {
    return ['name', 'age'];
  }

  get name() {
    return this.getAttribute('name');
  }

  set name(value) {
    if (value) {
      this.setAttribute('name', value);
    } else {
      this.removeAttribute('name');
    }
  }

  get age() {
    return Number(this.getAttribute('age'));
  }

  set age(value) {
    this.setAttribute('age', value);
  }
}

# 8.4 错误处理

class SafeComponent extends HTMLElement {
  connectedCallback() {
    try {
      this.render();
    } catch (error) {
      console.error('渲染错误:', error);
      this.innerHTML = '<p>组件加载失败</p>';
    }
  }

  render() {
    // 渲染逻辑
  }
}

# 8.5 性能优化

class OptimizedComponent extends HTMLElement {
  constructor() {
    super();
    this._updateScheduled = false;
  }

  attributeChangedCallback() {
    if (!this._updateScheduled) {
      this._updateScheduled = true;
      
      requestAnimationFrame(() => {
        this.render();
        this._updateScheduled = false;
      });
    }
  }

  render() {
    // 渲染逻辑
  }
}

# 九、浏览器兼容性

# 9.1 特性检测

if ('customElements' in window) {
  customElements.define('my-component', MyComponent);
} else {
  console.log('浏览器不支持Custom Elements');
}

if (document.head.attachShadow) {
  console.log('支持Shadow DOM');
} else {
  console.log('不支持Shadow DOM');
}

# 9.2 Polyfills

对于不支持Web Components的浏览器,可以使用polyfills:

<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2/webcomponents-loader.js"></script>
<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2/custom-elements-es5-adapter.js"></script>

# 十、总结

HTML模板与组件化技术为现代Web开发提供了强大的工具:

  1. Template元素提供了声明式的HTML片段管理
  2. Custom Elements允许创建自定义HTML标签
  3. Shadow DOM提供了样式和结构的封装
  4. Slots实现了灵活的内容分发
  5. 生命周期钩子让我们能够精确控制组件行为

Web Components是原生的、标准的组件化方案,不依赖任何框架,可以在任何项目中使用。掌握这些技术,能够帮助我们构建更加模块化、可维护的Web应用。

祝你变得更强!

编辑 (opens new window)
#HTML#工程化#模板#组件化#Web Components
上次更新: 2025/11/28
HTML交互-多媒体元素
HTML工程化-性能优化

← HTML交互-多媒体元素 HTML工程化-性能优化→

最近更新
01
AI编程时代的一些心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code 最佳实践(个人版)
08-01
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式