Web Components:原生组件化开发的新答案

掘金种树人
2026-01-06 00:13
阅读 1616

上周五晚上十点半,我正瘫在成都的出租屋里刷 LeetCode 准备跳槽面试——没错,最近被几道“手写 Web Components”的题狠狠教育了。说实话,以前我一直觉得这玩意儿是“前端考古学”范畴,直到我们团队接了个跨框架共享 UI 组件的需求,才真正意识到:Web Components 可能比我们想象中更实用

我是谁?一个在成都某中型互联网公司搬砖的前端,日常用 VSCode + 一堆插件(比如 Live Server、Auto Rename Tag、ES7+ React/Redux Snippets),喜欢折腾新技术但上线代码永远选最稳的方案。去年双11期间因为乱用新框架导致线上白屏,被产品经理追着问“你是不是故意的”,从此对“炫技”有了 PTSD。


被逼上梁山:为什么我们开始用 Web Components?

事情起因是我们要做一个“设计系统统一平台”。公司内部同时维护 React、Vue 2、Vue 3 三套前端项目,每次改个按钮样式,设计师要提三次需求,开发要改三套代码,测试要测三个环境。领导拍板:“搞一套通用组件库,一次开发,到处使用!”

最初的想法是封装成 npm 包,按需引入。但很快发现:框架耦合太深。React 的 JSX、Vue 的 SFC、状态管理方式完全不同,强行抽象反而增加维护成本。这时候,同事小王(就是那个总穿拖鞋来公司的后端转前端)弱弱地说了一句:“要不……试试 Web Components?”

全场沉默三秒。
“那不是 IE 时代的老古董吗?”
“Shadow DOM 性能是不是很烂?”
“浏览器兼容性咋办?”

但现实是:我们已经被 deadline 逼到墙角。于是,抱着“死马当活马医”的心态,我们开始了 Web Components 的实战之旅。


别被名字骗了:Web Components 不是“过时技术”

很多人一听 Web Components 就想到 2014 年的草案,觉得它已经凉透了。其实不然。现代 Web Components(基于 Custom Elements v1 + Shadow DOM v1)早在 2017 年就成为 W3C 标准,并且被所有现代浏览器原生支持(连 Edge 都投降了)。

简单来说,Web Components 由三部分组成:

  • Custom Elements:定义自己的 HTML 标签,比如 <my-button>
  • Shadow DOM:封装样式和 DOM,避免外部污染
  • HTML Templates:声明式模板,配合 JS 动态渲染

最关键的是:它不依赖任何框架。你可以把它当成“浏览器原生的组件系统”。

// 定义一个简单的自定义元素
class MyButton extends HTMLElement {
  constructor() {
    super();
    // 创建 Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });
    
    // 添加样式(隔离的!)
    const style = document.createElement('style');
    style.textContent = `
      button {
        background: #007aff;
        color: white;
        border: none;
        padding: 8px 16px;
        border-radius: 4px;
        cursor: pointer;
      }
      button:hover { opacity: 0.9; }
    `;
    
    // 创建按钮
    const btn = document.createElement('button');
    btn.textContent = this.getAttribute('label') || 'Click me';
    
    shadow.appendChild(style);
    shadow.appendChild(btn);
  }
}

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

然后在任意 HTML 中直接用:

<my-button label="提交订单"></my-button>

React、Vue、Svelte 甚至纯静态页面都能无缝使用。这就是我们想要的“跨框架”能力。


实战踩坑:那些文档不会告诉你的细节

理想很丰满,现实很骨感。我们在落地过程中踩了几个大坑,分享出来帮大家避雷。

坑 1:属性 vs 属性(Attribute vs Property)

Web Components 默认只监听 HTML attribute(字符串),但很多时候我们需要传对象、函数等复杂类型。

比如:

<user-card user="{ name: '张三', id: 123 }"></user-card>

这里 user 是字符串,不是对象!

解决办法:优先使用 JavaScript property 赋值,而不是 attribute。

const card = document.querySelector('user-card');
card.user = { name: '张三', id: 123 }; // 这样才是对象

或者在组件内部做解析(不推荐,性能差):

const userStr = this.getAttribute('user');
const user = JSON.parse(userStr); // 危险!可能抛错

坑 2:事件通信:怎么让父应用知道子组件发生了什么?

Web Components 不能直接用 Vue 的 $emit 或 React 的 onXXX。但我们可以通过 CustomEvent 实现。

// 在组件内部触发事件
this.dispatchEvent(new CustomEvent('submit', {
  detail: { value: '用户输入的内容' },
  bubbles: true,
  composed: true // 关键!允许事件穿透 Shadow DOM
}));

外部监听:

document.querySelector('my-form').addEventListener('submit', (e) => {
  console.log(e.detail.value);
});

💡 提醒:一定要加 composed: true,否则事件在 Shadow DOM 边界就被拦住了!

坑 3:样式定制:如何让外部能覆盖内部样式?

Shadow DOM 默认完全隔离样式,这既是优点也是痛点。比如设计师说:“这个按钮在暗色模式下要变白”,怎么办?

解决方案:使用 CSS custom properties(CSS 变量)暴露可配置项

// 组件内部
const style = document.createElement('style');
style.textContent = `
  :host {
    --btn-bg: #007aff;
    --btn-color: white;
  }
  button {
    background: var(--btn-bg);
    color: var(--btn-color);
  }
`;

外部使用时:

my-button {
  --btn-bg: #333;
  --btn-color: #fff;
}

这样既保持了封装性,又给了外部定制空间。这是 Web Components 最优雅的样式扩展方式


性能与兼容性:真的能上生产吗?

很多人担心 Shadow DOM 性能差。实测结果让人意外。

我们在一个包含 500 个 <data-table-row> 组件的页面做了对比:

方案 首屏渲染时间 内存占用 滚动流畅度
纯 DOM(无 Shadow) 320ms 45MB 60fps
Shadow DOM 340ms 48MB 58fps
React 虚拟列表 280ms 52MB 60fps

差异几乎可以忽略。现代浏览器对 Shadow DOM 优化得很好,除非极端场景(比如每帧更新上千个组件),否则不用过度担心。

至于兼容性?看这张表:

浏览器 支持情况
Chrome ✅ 54+ (2016)
Firefox ✅ 63+ (2018)
Safari ✅ 10.1+ (2017)
Edge ✅ 79+ (Chromium 版)
IE11 ❌(但可用 polyfill)

我们公司用户基本没有 IE,所以直接放弃兼容。如果你需要支持老浏览器,可以用 @webcomponents/webcomponentsjs 这个 polyfill,但它会带来 ~15KB 的额外体积和轻微性能损耗。


面试题挑战:Web Components 高频考点总结

既然我被面试题虐过,那就帮大家整理一下常见考点(附答案思路):

Q1:Web Components 和 React/Vue 组件有什么区别?

  • 核心差异:Web Components 是浏览器原生标准,无运行时依赖;框架组件依赖特定生态。
  • 适用场景:跨框架共享用 Web Components;复杂交互逻辑用框架。

Q2:Shadow DOM 的 mode: 'open' 和 'closed' 有什么区别?

  • 'open':可通过 element.shadowRoot 访问内部 DOM(调试友好)
  • 'closed':外部无法访问,彻底封闭(安全性更高,但调试困难)
  • 实践中几乎都用 'open'

Q3:如何在 Web Components 中实现响应式更新?

  • 监听 attribute 变化:通过 observedAttributes + attributeChangedCallback
  • 监听 property 变化:用 getter/setter 或 Proxy(需手动触发 render)
static get observedAttributes() {
  return ['label'];
}

attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'label') {
    this.render(); // 重新渲染
  }
}

Q4:Web Components 能用 TypeScript 吗?

当然能!而且体验不错。配合 @web/dev-server 或 Vite,开发体验接近现代框架。


工具链推荐:让开发不那么痛苦

虽然 Web Components 是原生 API,但纯手写还是太原始。这些工具能大幅提升体验:

  • Lit:Google 出的轻量库(~5KB),提供响应式、模板语法,类似“Web Components 的 React”
  • Stencil:Ionic 团队出品,支持 TSX、懒加载、SSR,适合构建组件库
  • Vite + @vite/plugin-react:即使你用 React,也可以把 Web Components 当普通标签用

我个人最爱 Lit,因为它最小巧,且不改变 Web Components 的本质:

import { LitElement, html, css } from 'lit';

class MyButton extends LitElement {
  static styles = css`
    button { background: var(--bg, #007aff); }
  `;

  render() {
    return html`<button @click=${this._onClick}>点我</button>`;
  }

  _onClick() {
    this.dispatchEvent(new CustomEvent('click'));
  }
}

最终效果:我们真的做到了“一次开发,到处使用”

经过两个月迭代,我们的设计系统组件库(基于 Lit + Web Components)成功接入了 React、Vue 2、Vue 3 三个项目。设计师改一次 Figma,我们发一次 npm 包,全平台自动更新

更重要的是:Bundle Size 降了 20%。因为不用再为每个框架打包一份 UI 逻辑。

上周上线后,产品经理居然请我们喝了奶茶(虽然只是蜜雪冰城)。那一刻我觉得,折腾 Web Components 是值得的。


写在最后:它不是银弹,但值得你了解

Web Components 不会取代 React 或 Vue。它的定位很清晰:解决跨框架复用、微前端集成、低侵入式嵌入等特定场景

如果你:

  • 正在维护多技术栈项目
  • 想构建可被任意前端使用的 UI 库
  • 对框架 lock-in 感到焦虑

那 Web Components 值得你花一周时间深入研究。别被“原生”两个字吓到——现代 Web Components 开发体验已经非常成熟

至于面试?现在大厂(尤其偏底层或基建的岗位)越来越爱考它。毕竟,理解浏览器原生能力,是高级前端的基本素养。

哦对了,我跳槽成功了。新东家第一周就让我用 Web Components 重构他们的插件系统。看来,这波技术债,还得继续还啊 😅


PS:如果你也在成都,欢迎约茶(最好是宽窄巷子那家竹叶青)。我们可以聊聊 Web Components,或者吐槽产品经理。反正,生活节奏舒服点,代码才能写得稳。

评论 0

最热最新
暂无评论
掘金种树人Lv.1
0
影响力
0
文章
0
粉丝