Web Components:一场原生组件化的温柔革命

半夜部署日记
2025-06-15 19:37
阅读 785

背景:一次“重复造轮子”的项目引发的思考

背景:一次“重复造轮子”的项目引发的思考

大约在两年前,我参与了一个企业级内部系统的重构项目。这套系统原本是使用 React 构建的 SPA 应用,模块分散,多个前端团队各自为战,UI 风格和交互体验差异巨大。随着业务增长,代码复用率低、协作成本高这些问题逐渐暴露出来。

当时我们尝试引入一种统一的 UI 组件库方案来解决这个问题。但问题很快来了:

  • 多个项目依赖同一个 React 组件库,一旦版本升级或样式更改,各个项目都要重新构建部署。
  • 团队中有些项目还在用 Vue 2,React 组件根本无法直接复用。
  • 某些老项目甚至还是 jQuery 的时代产物,组件共享简直是天方夜谭。

我们开始寻找更“通用”的解决方案。这时候,Web Components 浮现在我的视野中——它不依赖任何框架,可以直接通过 HTML 标签调用,而且浏览器原生支持。

于是我决定在一个小功能模块上进行试水,尝试用 Web Components 实现一个可跨项目、跨技术栈使用的 UI 组件库原型。

这个过程让我从踩坑到深信其价值,也促使我写下这篇文章,分享我的实战经验和真实体会。


道阻且长:遇到的问题与挑战

道阻且长:遇到的问题与挑战

初次接触 Web Components 是兴奋中带着迷茫的。

一开始我只是想封装一个通用的 <custom-input> 组件,让它可以在多个项目中被复用。然而实际操作中却遇到了一连串问题:

样式隔离成了“噩梦”

我想当然地给组件添加了一些内联样式,结果发现这些样式很容易被父页面污染掉。于是我去查资料,了解到 Shadow DOM 可以实现样式隔离。但在实际应用中却发现:

  • 在某些旧版浏览器中表现不稳定(尤其 IE11);
  • 使用 CSS-in-JS 工具导入样式时,Shadow DOM 下的样式加载顺序容易出错;
  • 主题切换等动态样式处理不够灵活。

数据传递像在“猜谜”

刚开始写的时候,我试图用属性绑定传值,比如这样:

<custom-input value="hello"></custom-input>

但组件内部读取不到更新后的值,尤其是当值由 JavaScript 动态设置时:

const input = document.querySelector('custom-input');
input.value = 'world'; // 不生效?

这才意识到,需要手动定义属性反射机制,才能让 HTML 属性和对象属性同步变化。

生命周期管理有点“迷糊”

不像 React 或 Vue 有清晰的生命周期钩子,Web Components 中只能靠 connectedCallbackdisconnectedCallback 等几个方法去维护状态。刚开始我总是忘记做一些清理逻辑,导致内存泄漏或者状态混乱。

调试工具匮乏

那时候我在 Chrome DevTools 里调试 Shadow DOM,有时候看到一堆 #shadow-root 内容,点进去看样式又看不到关键信息,感觉像是在黑盒中摸索。


解决思路:用原生力量构建解耦的 UI

虽然初期困难重重,但我始终相信 Web Components 所代表的“原生组件化”是一条正确的路。于是我们逐步调整了开发策略。

选型:采用基础 Custom Elements + Shadow DOM 结合方式

我们没有一开始就用复杂的第三方封装库(比如 LitElement),而是选择从最基础的 Custom Element API 开始:

class CustomInput extends HTMLElement {
  constructor() {
    super();
    this._shadowRoot = this.attachShadow({ mode: 'open' });
    this._shadowRoot.innerHTML = `
      <style>
        :host {
          display: inline-block;
        }
        input {
          border: 1px solid #ddd;
          padding: 8px;
          font-size: 14px;
        }
      </style>
      <input type="text" />
    `;
  }

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


![CSS动画效果展示-2](https://code-guide.oss.shanghai.autogptai.club/common/file/download?name=date2025061519/7fcef85f-61b3-471e-b671-21930af75897.jpg)


  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'value') {
      this._shadowRoot.querySelector('input').value = newVal;
    }
  }

  connectedCallback() {
    const inputEl = this._shadowRoot.querySelector('input');
    inputEl.addEventListener('input', () => {
      this.setAttribute('value', inputEl.value);
      this.dispatchEvent(new Event('change'));
    });
  }
}

customElements.define('custom-input', CustomInput);

这样的结构虽然原始,但能帮助我们彻底理解组件运行机制,并具备良好的可拓展性。

封装通用基础库辅助开发

为了不让每个组件都重写相同逻辑,我们抽离了一个小型封装库,实现了以下功能:

  • 自动将 HTML 属性映射到 JS 属性
  • 支持监听属性变更并触发回调
  • 提供基于模板的 DOM 创建方式
  • 支持外部样式注入和主题变量控制

这大大提升了后续组件的开发效率。

引入兼容性和性能优化手段

针对旧浏览器的支持,我们采用了两个关键策略:

  • 通过 polyfill 支持 ES6 之前的环境
  • 对 Shadow DOM 做渐进降级处理(IE11 不支持时改用 light DOM)

同时,在渲染大型组件时注意避免频繁创建 DOM 节点,引入虚拟滚动、懒加载等方式提升性能。


踩过的坑:那些深夜 debug 的教训

🧠 Shadow DOM 样式作用域误解

一开始我误以为写在 <style> 标签里的所有样式都会自动隔离。结果测试后发现:

如果你不小心在外部写了类似 input { color: red; } 的全局样式,它们依然会影响到 Shadow DOM 内部的 input!

解决办法是使用 :host 和类名限定:

:host(input-blue) input {
  color: blue;
}

也可以考虑使用 CSS 自定义属性做主题控制:

input {
  color: var(--primary-color, #333);
}

🧾 属性类型处理不当导致的数据异常

我曾定义了一个布尔类型的属性 disabled,但在组件中判断时总失败:

<custom-button disabled></custom-button>

组件内部这样写:

if (this.disabled) {
  // never entered
}

后来才明白,HTML 属性是字符串,需要显式解析:

this.hasAttribute('disabled')

或者手动转换:

this.disabled = this.getAttribute('disabled') !== null;

🔁 数据双向绑定不好做

在 Vue/React 中非常自然的 v-model 和双向数据流,在 Web Components 中要自己实现:

input.addEventListener('input', () => {
  this.setAttribute('value', input.value);
  this.dispatchEvent(new CustomEvent('input', {
    detail: input.value,
    bubbles: true,
  }));
});

然后在父组件中监听事件并更新其他状态。

虽然略显繁琐,但好处是可以完全控制通信细节,避免过度隐式的逻辑。


成果显现:一套真正跨项目的 UI 库

经过几个月的打磨,我们最终建立了一套完整的 Web Components 组件库,覆盖表单控件、按钮、弹窗、表格等功能。

效果显著:

  • 多个项目的 UI 统一程度大幅提升
  • 组件库可以作为 npm 包发布,各项目按需安装
  • 升级更新只需重新发布 NPM 包,不影响业务项目本身构建流程
  • 新人上手门槛较低,无需熟悉具体框架语法即可使用组件
  • 开发者反馈组件“干净、透明、无侵入”,比以往的框架封装组件更好维护

更重要的是,我们在多个技术栈并存的环境中实现了真正的组件复用。


给你的建议:拥抱未来但脚踏实地

如果你也想试试 Web Components,下面几点是我这几年总结下来的建议:

✅ 从基础开始实践

别一上来就用 Lit、Stencil 或者 Angular Elements。先写几个简单的 Custom Element,看看浏览器怎么处理它们。你会对整个机制有一个真实的认识。

✅ 模块化组织组件代码

即使是原生写法,也要注意组件结构的组织。推荐使用 ES Module 方式导出组件构造函数,便于打包和维护。

✅ 合理利用 Shadow DOM

并不是所有组件都需要 Shadow DOM。对于一些轻量级 UI 元素,比如图标、按钮等,直接使用普通 DOM 更加简单高效。

✅ 注意性能边界

Web Components 的性能一般不错,但在渲染大数据列表、动画频繁变换的场景下还是要多加关注。必要时引入虚拟滚动、节流防抖、局部渲染等策略。

✅ 别忽视调试技巧

Chrome DevTools 的 Elements 面板下可以看到 Shadow DOM 的结构,你甚至可以用命令行访问 shadowRoot 内的内容:

document.querySelector('custom-element').shadowRoot.querySelector('.some-class')

还可以通过样式编辑器临时调试样式作用范围。

✅ 关注生态演进

Web Components 的标准一直在演进。例如:

  • Declarative Shadow DOM 让服务器端也能提前生成 Shadow DOM 内容
  • constructable stylesheets 允许高效复用样式对象
  • 第三方库如 Lit、Fast 等提供了更高阶的开发能力

你可以根据项目需要逐步引入这些现代特性。


最后的心得:回到 HTML 本身的纯粹

JavaScript框架对比-1

说实话,刚接触 Web Components 的时候,我对它有过怀疑。毕竟它不提供响应式机制、也没有框架那么多“魔法”。

但正是这种“原生感”,让我找回了最初写网页的快乐。你可以不用学太多概念,只靠基本的 HTML/CSS/JS,就能写出漂亮、稳定、可复用的 UI 组件。

Web Components 并不是替代框架的存在,而是一种补充性更强的底层抽象能力。它允许我们将“UI 组件”的定义回归到更本质的形式,从而真正实现跨技术栈、跨团队的协作可能。

我相信,随着浏览器厂商继续推进标准演进,越来越多开发者会像我一样,爱上这种看似朴素却充满力量的方式。


如果你也在做一个需要组件跨平台复用的项目,不妨从一个输入框、一个按钮开始试起。也许不久的将来,你会发现:原来,“原生”才是最强大、最稳定的那一层抽象。

评论 0

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