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

代码里的小宇宙
2025-12-14 22:29
阅读 1653

写这篇文章的时候,我刚从产品经理转岗做前端开发满三年。说实话,一开始我以为自己会一辈子画原型、写 PRD、跟开发撕需求,结果某天凌晨三点改完第 18 版交互稿后,突然觉得——“要不我自己上手写代码算了?”于是真就转了。现在白天写 React 组件,晚上研究 V8 引擎,周末还刷 LeetCode 准备跳槽……典型的斜杠青年(自封)。


上周五晚上十点半,办公室只剩我和运维老张在加班。他一边喝着红牛一边问我:“你们前端又搞啥新花样?怎么 CI 流水线里突然多了个 web-components 分支?”

我苦笑:“这不是被逼的嘛。”

事情是这样的:我们团队最近接到一个“战略性项目”——把公司内部多个业务系统的 UI 组件统一起来。听起来很美好,但现实很骨感:A 系统用 Vue 2,B 系统用 AngularJS(对,你没看错,2024 年还有人在维护 AngularJS),C 系统倒是用了 React,但版本是 16.x,而且没人敢升级。

更离谱的是,老板要求“所有系统下周上线新主题”,而设计团队给了我们一套包含 30+ 原子组件的设计规范。产品经理(没错,就是我以前的角色)拍胸脯说:“技术上肯定没问题!”

我当时真的想砸电脑。

为什么不用 React?

作为前产品经理 + 现前端,我对 React 有感情。过去三年,我写的每一个页面几乎都离不开它。但这次,React 根本救不了我们。

  • 跨框架复用?不存在的。你总不能让 AngularJS 项目去装 React runtime 吧?
  • Bundle 体积爆炸。每个系统都要引入一整套 React + 我们的 UI 库,光基础组件就多出 200KB。
  • 版本冲突地狱。React 16 和 18 的 Context API 都不一样,更别说 hooks 了。

有一次我在测试环境部署了一个 React 组件,结果老 AngularJS 系统直接白屏,控制台报错:

Uncaught TypeError: Cannot read property 'memo' of undefined

——因为那个系统全局挂了个叫 React 的变量,但其实是他们自己魔改的伪 React。

那一刻我悟了:我们需要的是与框架无关的组件。

Web Components:被遗忘的原生方案

其实 Web Components 这玩意儿早在 2014 年就出来了,但一直不温不火。为啥?因为早期浏览器支持差、API 反人类、生态贫瘠。再加上 React/Vue 的崛起,大家更愿意用“高级”方案。

但今时不同往日。如今:

  • Chrome/Firefox/Safari/Edge 全面支持 Custom Elements v1 和 Shadow DOM v1
  • caniuse 显示全球覆盖率达 97%+
  • 工具链也成熟了(后面会细说)

最关键的是——它原生、轻量、无依赖。一个 Web Component 就是一个 HTML 标签,像 <my-button> 这样直接用,任何框架都能嵌入。

于是我开始调研。没想到,这一试就停不下来。

实战:从零构建一个按钮组件

先别急着喷“Web Components 写法太啰嗦”。确实,原生 API 是有点 verbose,但有了现代工具,体验完全不一样。

我用的是 Lit —— Google 出的轻量级 Web Components 库。它不是框架,只是一个语法糖 + 响应式更新的小工具,打包后只有 5KB。

// my-button.ts
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('my-button')
export class MyButton extends LitElement {
  @property({ type: String }) variant: 'primary' | 'secondary' = 'primary';
  @property({ type: Boolean, reflect: true }) disabled = false;

  static styles = css`
    :host {
      display: inline-block;
      padding: 8px 16px;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
    }
    :host([variant="primary"]) {
      background: #1890ff;
      color: white;
    }
    :host([disabled]) {
      opacity: 0.5;
      pointer-events: none;
    }
  `;

  render() {
    return html`<slot></slot>`;
  }

  // 处理点击事件
  connectedCallback() {
    super.connectedCallback();
    this.addEventListener('click', (e) => {
      if (!this.disabled) {
        this.dispatchEvent(new CustomEvent('my-click', { bubbles: true }));
      }
    });
  }
}

关键点解释:

  • @customElement 注册自定义标签
  • @property 定义属性,reflect: true 会让属性同步到 DOM attribute(方便 CSS 选择)
  • :host 是 Shadow DOM 的根元素样式
  • <slot> 支持内容分发(类似 React 的 children
  • 事件通过 CustomEvent 派发,bubbles: true 确保能被外部监听

然后在 HTML 里直接用:

<my-button variant="primary" @my-click="handleClick">
  点我!
</my-button>

甚至在 React 里也能无缝集成:

function App() {
  const handleClick = () => console.log('clicked!');
  return (
    <my-button variant="primary" onMyClick={handleClick}>
      在 React 里用 Web Component
    </my-button>
  );
}

不需要任何 wrapper,不需要额外配置。这就是原生的力量。

工具链:告别手写 boilerplate

早期 Web Components 开发痛苦,一大原因是工具链缺失。但现在,生态已经相当完善:

工具 作用 个人体验
Lit 轻量级开发库 写起来像 Svelte,但更小
Stencil 编译型 Web Components 框架 适合大型组件库,支持 TSX
Vite + @web/dev-server 开发服务器 HMR 快得飞起
Playwright E2E 测试 可以直接测 Shadow DOM 内部

我最终选了 Lit + Vite。Vite 的插件生态对 Web Components 友好得惊人。比如这个 vite-plugin-wc 插件,能自动注册组件,连 customElements.define 都不用写了。

开发时启动命令:

npm run dev  # 热更新快如闪电

构建命令:

npm run build  # 输出 esm + systemjs 双格式,兼容老旧系统

最爽的是调试。Chrome DevTools 现在直接支持 Shadow DOM 调试,点开 Elements 面板就能看到内部结构,还能实时修改 CSS。

性能与兼容性:真香警告

上线前我做了对比测试(数据基于 1000 个按钮实例):

方案 Bundle Size (gzip) 渲染时间 (ms) 内存占用 (MB)
React + UI Lib 180KB 120 45
Web Components (Lit) 25KB 85 28

Web Components 不仅体积小,渲染还更快!原因很简单:没有虚拟 DOM diff,直接操作真实 DOM;没有 React reconciler 开销;Shadow DOM 还隔离了样式,避免全局污染。

兼容性方面,我们通过 @webcomponents/webcomponentsjs polyfill 支持到 IE11(虽然老板说 IE 用户不到 0.1%,但合规部门非要支持)。实际只增加了 15KB 的 polyfill 体积。

踩过的坑 & 解决方案

当然,路不可能一帆风顺。分享几个血泪教训:

1. 事件处理的坑

Web Components 默认不会冒泡到外部框架。解决方法是在派发事件时加 { bubbles: true, composed: true }

this.dispatchEvent(new CustomEvent('change', {
  bubbles: true,
  composed: true, // 关键!允许穿越 Shadow DOM 边界
  detail: { value: this.value }
}));

2. 样式穿透问题

Shadow DOM 隔离了样式,但也导致外部无法定制。解决方案是使用 CSS Shadow Parts:

// 组件内部
render() {
  return html`<button part="base">...</button>`;
}
/* 外部定制 */
my-button::part(base) {
  background: hotpink;
}

3. SSR 支持

Web Components 本身不支持服务端渲染。但我们用 Stencil 生成的组件库自带 hydrate 功能,配合 Next.js 或 Nuxt 可以实现 SSR + CSR 混合。

效果如何?

上周双11大促,我们的新组件库正式上线。结果:

  • 所有系统 UI 一致性 100% 达成 ✅
  • Bundle 体积平均减少 60% ✅
  • 前端团队不再互相甩锅(Vue 组和 React 组终于握手言和)✅

最让我感动的是,那个 AngularJS 系统的老师傅跑来跟我说:“小伙子,你这按钮在我这儿跑得挺溜啊!”

最后:给想跳槽的你一点建议

写这篇文章时,我正投着简历。面试官问:“你为什么研究 Web Components?”

我说:“因为我受够了框架战争。用户不在乎你用 React 还是 Vue,他们只想要一个快、稳、好看的页面。”

如果你也在准备跳槽,我强烈建议你了解一下 Web Components。它不是要取代 React,而是提供一种更底层、更通用的组件化思路。大厂如 GitHub、Salesforce、Adobe 都在用它构建 Design System。

而且,懂 Web Components 的前端,在面试时真的能加分——毕竟,能讲清楚 Shadow DOM 和 Custom Elements 的人,不多。


PS:有人问我“产品经理转技术会不会吃亏”?我的回答是:恰恰相反。正因为当过 PM,我才更懂用户、更懂协作、更知道什么技术值得投入。技术只是手段,解决问题才是目的。

好了,我去改简历了。希望下一家公司,不要再让我凌晨三点改交互稿 😅

评论 0

最热最新
暂无评论
代码里的小宇宙Lv.1
0
影响力
0
文章
0
粉丝