Web Components:原生组件化开发新趋势,零基础也能上手

韩庆华
2026-03-10 18:27
阅读 1490

大家好,我是老张,一名985毕业的全栈工程师,平时在掘金写了不少前端入门教程。最近有位刚转行的朋友问我:“现在学前端,是不是必须用 React 或 Vue?”我笑了笑,说:“其实浏览器原生就支持组件化开发,只是很多人不知道。”

这让我想起自己刚入行时,也以为组件化是框架的“专利”。直到后来在项目中遇到性能瓶颈、打包体积过大等问题,才重新关注到 Web Components 这个被低估的原生能力。今天这篇教程,就是想带完全零基础的同学,从零开始掌握 Web Components —— 无需任何框架,就能写出可复用、可维护的组件。


为什么你应该了解 Web Components?

简单来说,Web Components 是一套浏览器原生支持的组件化技术标准,它让你像搭积木一样,把 UI 拆分成独立的小模块(比如一个按钮、一个卡片、一个日历),然后在任何页面中重复使用。

你可能在简历上看到过这个词,但觉得“太底层”不敢碰。其实不然!它比你想象的更简单,而且:

  • 不依赖任何框架:React、Vue 都要编译,而 Web Components 直接运行在浏览器里
  • 天然隔离:每个组件自带样式和逻辑,互不干扰(靠 Shadow DOM 实现)
  • 高度可复用:写一次,到处用,甚至能跨项目、跨团队共享
  • 未来趋势:Google、GitHub、Salesforce 等大厂都在用,OpenCode 开源项目中也越来越多见

我当初学的时候,就是被“原生”两个字吸引——既然浏览器自己支持,何必再套一层框架?


环境准备:5分钟搞定开发环境

好消息是:你不需要安装任何构建工具!Web Components 是原生 API,现代浏览器(Chrome、Edge、Firefox、Safari)都支持。

最小开发环境清单

工具 说明
浏览器 推荐 Chrome 最新版
代码编辑器 VS Code、Sublime、甚至记事本都行
本地服务器(可选) 用于避免 file:// 协议的 CORS 限制

💡 新手注意:直接双击 HTML 文件打开可能会报错(因为浏览器安全策略)。建议用 VS Code 安装 Live Server 插件,右键“Open with Live Server”即可启动本地服务。

验证环境是否 OK

新建一个 index.html,粘贴以下代码:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Web Components 测试</title>
</head>
<body>
  <my-greeting></my-greeting>

  <script>
    class MyGreeting extends HTMLElement {
      connectedCallback() {
        this.innerHTML = '<h1>Hello, Web Components!</h1>';
      }
    }
    customElements.define('my-greeting', MyGreeting);
  </script>
</body>
</html>

用 Live Server 打开,如果看到 “Hello, Web Components!”,恭喜你,环境搞定!


核心三件套:Custom Elements + Shadow DOM + HTML Templates

Web Components 由三个核心技术组成,我们逐个拆解。

1. Custom Elements(自定义元素)

这是最核心的部分——让你定义自己的 HTML 标签

规则很简单:

  • 标签名必须包含连字符 -(如 my-button,不能叫 button
  • 通过 customElements.define() 注册
// 定义一个组件类
class MyButton extends HTMLElement {
  constructor() {
    super(); // 必须调用 super()
  }

  // 元素被插入 DOM 时触发
  connectedCallback() {
    this.innerHTML = `<button>点我</button>`;
  }
}

// 注册组件
customElements.define('my-button', MyButton);

现在你就可以在 HTML 中直接写 <my-button></my-button> 了!

📌 避坑指南:很多人忘了调用 super(),导致报错。记住:只要继承 HTMLElement,构造函数第一行必须是 super()


2. Shadow DOM(影子 DOM)

解决了“样式污染”问题。普通组件的 CSS 会影响全局,但 Shadow DOM 让组件拥有私有作用域

改造上面的按钮:

class MyButton extends HTMLElement {
  constructor() {
    super();
    // 创建 Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });
    
    // 内部结构
    shadow.innerHTML = `
      <style>
        button {
          background: #4CAF50;
          color: white;
          border: none;
          padding: 10px 20px;
          border-radius: 4px;
          cursor: pointer;
        }
        button:hover {
          background: #45a049;
        }
      </style>
      <button>点我</button>
    `;
  }
}

现在,这个按钮的样式只在组件内部生效,不会影响页面其他按钮。

🔍 mode: 'open' vs 'closed'

  • open:外部可通过 element.shadowRoot 访问内部
  • closed:完全封闭,外部无法访问(但实际很少用)

3. HTML Templates(模板)

虽然不是必须,但 <template> 标签能让你预定义 HTML 结构,提升可读性。

<template id="my-card-template">
  <style>
    .card { border: 1px solid #ccc; padding: 16px; }
    .title { font-weight: bold; }
  </style>
  <div class="card">
    <div class="title"></div>
    <div class="content"></div>
  </div>
</template>

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

  connectedCallback() {
    // 从属性获取数据
    const title = this.getAttribute('title') || '默认标题';
    const content = this.getAttribute('content') || '默认内容';
    
    this.shadowRoot.querySelector('.title').textContent = title;
    this.shadowRoot.querySelector('.content').textContent = content;
  }
}

customElements.define('my-card', MyCard);
</script>

使用方式:

<my-card title="欢迎" content="这是卡片内容"></my-card>

优点:HTML 结构和 JS 逻辑分离,代码更清晰。


实战项目:做一个可复用的“待办事项”组件

现在,我们综合运用所学,做一个完整的 <todo-list> 组件。

步骤 1:规划功能

  • 显示任务列表
  • 输入框 + “添加”按钮
  • 点击任务可标记完成
  • 数据通过属性传入(JSON 字符串)

步骤 2:编写代码

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Todo List 组件</title>
</head>
<body>
  <todo-list tasks='[{"text":"学习Web Components","done":false}]'></todo-list>

  <script>
    class TodoList extends HTMLElement {
      constructor() {
        super();
        const shadow = this.attachShadow({ mode: 'open' });
        
        shadow.innerHTML = `
          <style>
            .container { max-width: 400px; margin: 20px auto; font-family: Arial; }
            input, button { padding: 8px; margin: 5px; }
            ul { list-style: none; padding: 0; }
            li { padding: 8px; border-bottom: 1px solid #eee; cursor: pointer; }
            .done { text-decoration: line-through; color: #999; }
          </style>
          <div class="container">
            <input type="text" id="new-task" placeholder="输入新任务">
            <button id="add-btn">添加</button>
            <ul id="task-list"></ul>
          </div>
        `;
      }

      connectedCallback() {
        this.tasks = JSON.parse(this.getAttribute('tasks') || '[]');
        this.render();
        this.bindEvents();
      }

      render() {
        const list = this.shadowRoot.querySelector('#task-list');
        list.innerHTML = '';
        this.tasks.forEach((task, index) => {
          const li = document.createElement('li');
          li.textContent = task.text;
          if (task.done) li.classList.add('done');
          li.dataset.index = index;
          list.appendChild(li);
        });
      }

      bindEvents() {
        const input = this.shadowRoot.querySelector('#new-task');
        const btn = this.shadowRoot.querySelector('#add-btn');
        const list = this.shadowRoot.querySelector('#task-list');

        btn.addEventListener('click', () => {
          if (input.value.trim()) {
            this.tasks.push({ text: input.value, done: false });
            input.value = '';
            this.render();
          }
        });

        list.addEventListener('click', (e) => {
          if (e.target.tagName === 'LI') {
            const index = e.target.dataset.index;
            this.tasks[index].done = !this.tasks[index].done;
            this.render();
          }
        });
      }
    }

    customExtensions.define('todo-list', TodoList); // 注意:这里故意写错!
  </script>
</body>
</html>

⚠️ 故意埋了个 Bug:最后一行应该是 customElements.define,不是 customExtensions。这是新手常犯的拼写错误!

修复后,刷新页面,你就能看到一个可交互的待办列表了!


新手常见问题 & 解答

Q1:组件之间怎么通信?

  • 父传子:通过 HTML 属性(如 <my-comp name="张三">
  • 子传父:派发自定义事件
// 子组件中
this.dispatchEvent(new CustomEvent('task-added', {
  detail: { task: '新任务' },
  bubbles: true // 允许冒泡
}));

// 父页面监听
document.querySelector('todo-list').addEventListener('task-added', (e) => {
  console.log('收到新任务:', e.detail.task);
});

Q2:能用 npm 包吗?

可以!虽然 Web Components 本身是原生的,但你可以:

  • 用 Lit、Stencil 等轻量库简化开发
  • 将 Web Components 发布到 npm,供他人使用

Q3:和 React/Vue 冲突吗?

完全不冲突!你可以在 React 项目中直接使用 Web Components,反之亦然。很多公司用它做微前端的边界组件。


学习建议:下一步怎么走?

  1. 动手改造现有项目
    把你页面中的重复 UI(如按钮、卡片、弹窗)抽成 Web Components。

  2. 阅读经典书籍
    推荐《Web Components in Action》(Manning 出版),讲解深入浅出。

  3. 参与开源
    在 GitHub 搜索 web-components,看看 OpenCode 项目是如何组织大型组件库的。

  4. 善用 AI 辅助
    遇到问题可以用 Kimi、Copilot 等工具生成示例代码,但务必理解原理。

  5. 更新简历
    掌握 Web Components 是加分项,尤其在强调“原生能力”或“微前端”的岗位中。


结语

Web Components 不是银弹,但它给了我们一种回归原生、轻量高效的开发思路。我当初学它,是为了减少对框架的依赖;现在用它,是为了构建更灵活的系统架构。

希望这篇教程能帮你迈出第一步。记住:所有复杂的概念,拆开看都不难。多写几遍代码,你就会发现,组件化开发,原来如此自然。

作者:老张,掘金专栏《前端入门不迷路》主理人
声明:本文代码均可直接运行,欢迎 fork 到你的 OpenCode 仓库!

评论 0

最热最新
暂无评论
韩庆华Lv.1
0
影响力
0
文章
0
粉丝