前端组件化

理解组件化思想及其在前端开发中的应用

问题

什么是前端组件化?为什么需要组件化?如何实现组件化?

解答

什么是组件化

组件化是将页面拆分成多个独立、可复用的组件,每个组件包含自己的结构(HTML)、样式(CSS)和逻辑(JS)。

组件化的好处

  1. 复用性 - 同一组件可在多处使用
  2. 可维护性 - 修改只影响单个组件
  3. 可测试性 - 组件可独立测试
  4. 协作效率 - 团队成员可并行开发不同组件

组件化实现示例

原生 Web Components

// 定义一个自定义按钮组件
class MyButton extends HTMLElement {
  constructor() {
    super();
    // 创建 Shadow DOM,实现样式隔离
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    // 组件挂载时执行
    this.render();
    this.bindEvents();
  }

  render() {
    const text = this.getAttribute('text') || '按钮';
    const type = this.getAttribute('type') || 'default';

    this.shadowRoot.innerHTML = `
      <style>
        .btn {
          padding: 8px 16px;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }
        .btn-default { background: #e0e0e0; }
        .btn-primary { background: #1890ff; color: white; }
      </style>
      <button class="btn btn-${type}">${text}</button>
    `;
  }

  bindEvents() {
    this.shadowRoot.querySelector('button').addEventListener('click', () => {
      // 触发自定义事件
      this.dispatchEvent(new CustomEvent('btn-click', {
        bubbles: true,
        detail: { text: this.getAttribute('text') }
      }));
    });
  }

  // 监听属性变化
  static get observedAttributes() {
    return ['text', 'type'];
  }

  attributeChangedCallback() {
    if (this.shadowRoot.innerHTML) {
      this.render();
    }
  }
}

// 注册组件
customElements.define('my-button', MyButton);
<!-- 使用组件 -->
<my-button text="提交" type="primary"></my-button>
<my-button text="取消"></my-button>

React 组件示例

// Button.jsx - 函数组件
import { useState } from 'react';
import './Button.css';

function Button({ text = '按钮', type = 'default', onClick }) {
  const [loading, setLoading] = useState(false);

  const handleClick = async () => {
    if (loading) return;
    setLoading(true);
    await onClick?.();
    setLoading(false);
  };

  return (
    <button 
      className={`btn btn-${type}`} 
      onClick={handleClick}
      disabled={loading}
    >
      {loading ? '加载中...' : text}
    </button>
  );
}

export default Button;

Vue 组件示例

<!-- Button.vue - 单文件组件 -->
<template>
  <button 
    :class="['btn', `btn-${type}`]" 
    :disabled="loading"
    @click="handleClick"
  >
    {{ loading ? '加载中...' : text }}
  </button>
</template>

<script setup>
import { ref, defineProps, defineEmits } from 'vue';

const props = defineProps({
  text: { type: String, default: '按钮' },
  type: { type: String, default: 'default' }
});

const emit = defineEmits(['click']);
const loading = ref(false);

const handleClick = async () => {
  if (loading.value) return;
  loading.value = true;
  emit('click');
  loading.value = false;
};
</script>

<style scoped>
.btn { padding: 8px 16px; border-radius: 4px; }
.btn-primary { background: #1890ff; color: white; }
</style>

组件设计原则

// 1. 单一职责 - 一个组件只做一件事
// ❌ 不好:一个组件又展示列表又处理表单
// ✅ 好:拆分成 List 组件和 Form 组件

// 2. 高内聚低耦合 - 组件内部紧密,组件之间松散
// 通过 props 传入数据,通过事件传出数据
function List({ items, onSelect }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onSelect(item)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

// 3. 可配置性 - 通过 props 控制组件行为
function Modal({ 
  visible,      // 是否显示
  title,        // 标题
  width = 500,  // 宽度,有默认值
  onClose,      // 关闭回调
  children      // 内容插槽
}) {
  if (!visible) return null;
  return (
    <div className="modal" style={{ width }}>
      <header>{title}</header>
      <main>{children}</main>
      <button onClick={onClose}>关闭</button>
    </div>
  );
}

关键点

  • 组件是独立的 UI 单元,包含结构、样式和逻辑
  • 组件通过 props 接收数据,通过事件向外通信
  • 遵循单一职责原则,一个组件只做一件事
  • 样式隔离可通过 Shadow DOM、CSS Modules、Scoped CSS 实现
  • 组件应该是可复用、可测试、可组合的