Web Components:原生组件化开发新趋势

云端小木屋
2025-06-24 22:29
阅读 736

Web Components:在复杂前端生态中寻找“原生”的答案

几年前,我所在的团队接手了一个全新的项目——一个企业级的内部管理平台。这个平台需要集成多个系统模块,包括用户管理、权限配置、数据可视化、日志监控等。面对如此复杂的业务逻辑和UI需求,我们最初的选择是使用当时主流的 React 技术栈来搭建整套系统。

然而,在开发过程中我们逐渐发现了一些棘手的问题:

  • 组件复用困难:不同模块间存在大量重复 UI 元素(如输入框、按钮、弹窗),但由于它们处于不同的 React 项目中,无法直接共享组件。
  • 团队协作障碍:前端团队被拆分成了多个小组,各自维护独立的 React 应用,由于技术方案不统一,导致最终整合时问题频发。
  • 第三方库依赖过重:虽然 React 社区丰富,但引入过多第三方库导致构建体积臃肿,影响首屏加载速度。
  • 渲染性能瓶颈:随着组件层级加深,React 的虚拟 DOM 更新机制在某些情况下出现了卡顿现象。

这些问题让我们意识到:或许我们需要一种更轻量、更通用、更接近浏览器原生能力的方式来组织我们的组件结构。

于是,我们开始将目光投向 Web Components —— 这个早在 2011 年就被提出的技术标准,在现代浏览器中已经逐步成熟,并且得到了各大框架的支持。


转向 Web Components:一次大胆的尝试

转向 Web Components:一次大胆的尝试

Web Components 是一组浏览器原生 API 的统称,主要包括:

  • Custom Elements:可以定义自己的 HTML 标签
  • Shadow DOM:提供封装样式与结构的能力
  • HTML Templates:声明式的可复用模板
  • ES Modules:基于 JavaScript 模块化加载机制

这些特性让开发者可以创建真正的“原生”组件,而无需依赖任何框架。

我们决定在新开发的日志监控模块中进行一次小规模试点。目标很明确:用 Web Components 来封装一个可复用的表格组件,能够在不同的项目甚至不同技术栈中被引用。


实战中的设计与实现

实战中的设计与实现

我们在项目中创建了一个名为 <custom-data-table> 的自定义元素,其主要功能是渲染一个支持排序、筛选、分页的表格,并支持传入 JSON 数据动态渲染内容。

下面是简化版的核心代码示例:

class CustomDataTable extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // 创建模板内容
    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        table { width: 100%; border-collapse: collapse; }
        th, td { padding: 8px; border: 1px solid #ccc; text-align: left; }
        .sort-button { cursor: pointer; }
      </style>
      <table>
        <thead>
          <tr id="header-row"></tr>
        </thead>
        <tbody id="body"></tbody>
      </table>
    `;


![移动端适配方案-1](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025062422/12c2e9e1-8948-4594-823a-090bbc663b96.jpg)


    this.shadowRoot.appendChild(template.content.cloneNode(true));
    this.headers = [];
    this.rows = [];
  }

  connectedCallback() {
    this.render();
  }

  static get observedAttributes() {
    return ['data'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'data') {
      this.rows = JSON.parse(newValue);
      this.render();
    }
  }

  render() {
    const headerRow = this.shadowRoot.getElementById('header-row');
    const body = this.shadowRoot.getElementById('body');

    // 清空并重新渲染
    headerRow.innerHTML = '';
    body.innerHTML = '';

    // 生成表头
    if (this.rows.length > 0) {
      Object.keys(this.rows[0]).forEach(key => {
        const th = document.createElement('th');
        th.textContent = key;
        headerRow.appendChild(th);
      });
    }

    // 生成行数据
    this.rows.forEach(row => {
      const tr = document.createElement('tr');
      Object.values(row).forEach(value => {
        const td = document.createElement('td');
        td.textContent = value;
        tr.appendChild(td);
      });
      body.appendChild(tr);
    });
  }
}

customElements.define('custom-data-table', CustomDataTable);

接下来我们就可以在任意 HTML 文件中这样使用它:

<custom-data-table data='[{"name":"Alice","age":25},{"name":"Bob","age":30}]'></custom-data-table>

这简直太棒了!它不需要任何框架依赖,也不需要 npm 安装,开箱即用。


在实际开发中踩过的那些坑

在实际开发中踩过的那些坑

当然,理想很美好,现实也很骨感。我们在这个过程中也遇到了不少挑战。

1. 样式作用域控制

刚开始我们以为 Shadow DOM 天然隔离了样式,但实际上,有些全局 CSS 还是会通过继承影响组件表现。比如 a 标签的颜色设置、字体大小等。

✅ 解决办法: 我们在组件模板的 <style> 中主动重置了一些基础样式,类似:

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

同时在主应用中也避免使用 !important 或者全局覆盖性太强的样式类。

2. 事件通信机制

如何将组件内部事件(例如点击某一行)通知给外部呢?我们知道不能直接挂载 onclick 到子元素上,因为这些 DOM 是在 Shadow Root 内部的。

✅ 解决办法: 使用 dispatchEvent 主动触发事件。

const rowClickEvent = new CustomEvent('row-click', {
  detail: { rowData: row },
  bubbles: true,
  composed: true
});
tr.dispatchEvent(rowClickEvent);

然后外部监听:

<custom-data-table onrow-click="handleTableRowClick"></custom-data-table>

或者 JS 监听方式:

document.querySelector('custom-data-table').addEventListener('row-click', e => {
  console.log('Row clicked:', e.detail.rowData);
});

3. 兼容性和 Polyfill

并不是所有浏览器都完美支持 Web Components。尤其是一些老旧的内核浏览器(比如 IE11 及以下版本),在客户环境中仍有一定比例用户群体。

✅ 解决方案: 我们引入了 Google 提供的 webcomponents.js polyfill,并通过 Webpack 配置实现了按需加载。

import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js';
import '@webcomponents/webcomponentsjs/webcomponents-bundle.js';

同时我们在构建流程中添加了对老浏览器的降级处理逻辑。


收益与思考

经过三个月的实践与打磨,我们将 Web Components 逐步推广到了整个项目中。结果出乎意料的好:

  • 跨项目组件复用变得简单:一个组件只需要打包成单独的 JS 文件,即可在其他项目中以 <script> 引入;
  • 构建包体积显著下降:相比之前引入 React + 系列插件的方式,Web Components 组件本身的 JS 文件平均只在几 KB;
  • 团队协作更加顺畅:各个前端组不再受限于技术选型,都可以无缝接入相同的组件体系;
  • 性能提升明显:页面加载快了不少,组件渲染效率更高,尤其在低端设备或网络较差的情况下体验更好。

更重要的是,这种原生化的组件开发方式,让开发者回归到了“写 HTML/CSS/JS 的本质”,而不是沉迷于各种框架的语法糖和抽象层中。


如果你也想尝试,这里有些建议送给你

如果你正考虑是否要在项目中引入 Web Components,我的建议如下:

✅ 适用场景推荐

  • 需要跨项目复用组件
  • 希望减小构建体积
  • 不想绑定特定技术栈
  • 追求极致的性能表现

❗ 注意事项

  • 不要忽视样式隔离问题,尤其是在复杂项目中。
  • 对旧浏览器支持要做好 Polyfill 和降级处理。
  • 要有一套清晰的组件命名规范,避免冲突。
  • 可借助构建工具(如 Lit、Stencil、Open WC)提高开发效率。

🛠 开发工具推荐

  • LitElement / Lit:Google 推出的轻量级 Web Components 库,提供了良好的响应式编程模型;
  • Vue / React 渲染器:可以通过一些中间适配器,将 Web Components 嵌入到 Vue 或 React 项目中;
  • DevTools 调试技巧:Chrome DevTools 支持查看 Shadow DOM 结构,调试更方便;
  • Storybook for Web Components:可以为你的 Web Components 构建一个可视化的组件库文档。

结语:原生的力量从未过时

Web Components 让我重新认识了“原生”的价值。它不是对现有框架的替代,而是提供了一种更轻量、更自由的组件构建思路。在我参与的多个项目中,无论是从性能、可维护性还是团队协作角度来看,它都展现出了强大的生命力。

技术总是在不断演变,但我相信:真正好的架构,往往是对底层能力的合理抽象与组合。

如果你还没尝试 Web Components,不妨从一个小组件开始,感受一下“原生的力量”。它可能不会马上惊艳你,但它一定会让你走得更远。


评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝