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

吴刚_数据
2025-12-16 00:19
阅读 581

去年双11大促前夜,我正对着屏幕疯狂调试一个“神奇”的弹窗组件——它在我本地跑得飞起,在测试环境却直接白屏。运维甩锅说 CDN 有问题,测试小姐姐截图发来一堆红叉,而我这个刚从产品经理转岗没多久的“伪前端”,一边啃着泡面一边在 Chrome DevTools 里翻滚,突然意识到一个问题:

我们是不是把组件这事儿搞得太复杂了?

React、Vue、Svelte……每个框架都有一套自己的组件体系,甚至同一个团队里前后端分离项目用 Vue,内部工具又用 React,连个按钮都要写两遍。更别说和后端同学联调时,他们用 SpringBoot 写页面(是的,有些老系统还在 Thymeleaf 时代),根本没法复用我们的 UI 组件。

就在那一刻,我点开 MDN,看到了那个被很多人“遗忘”的技术:Web Components


从 PM 到码农的“叛逃”之路

先简单自我介绍下:我原本是个天天画原型、怼需求的产品经理,结果某天被自家 CTO 一句“你不是总说技术不懂产品吗?那你来写写看?”给“骗”进了开发组。现在远程在家撸代码,白天写 JS,晚上研究 DOM 渲染原理,周末还得帮老婆修打印机(程序员的宿命)。

但正因为这段 PM 经历,我对“可复用性”和“协作成本”特别敏感。以前开需求评审会,前端同学常吐槽:“这个弹窗和上个项目一模一样,能不能别让我再写一遍?” 而现在,轮到我写代码了,我才真正体会到那种重复造轮子的痛苦。

所以当我发现 Web Components 居然能用原生浏览器能力实现跨框架复用时,简直像发现了新大陆——虽然这“大陆”早在 2011 年就被 Google 提出来了,只是我们一直忙着卷框架,把它当古董供起来了。


为什么是现在?一场被迫的技术突围

契机其实很现实:公司接了个政府项目,要求前端必须支持 IE11(别笑,真的有)。但我们的主力技术栈是 Vue3 + Vite,IE11 直接原地爆炸。领导拍板:“要么降级到 Vue2 + Babel Polyfill,要么找兼容方案。”

降级?那等于放弃 Composition API 和响应式系统的优雅。Polyfill?打包体积直接翻倍,首屏加载慢到用户以为网站挂了。

于是我开始调研“不依赖框架”的组件方案。Web Components 进入视野——它基于 Custom Elements、Shadow DOM、HTML Templates 等原生 API,只要浏览器支持(现代浏览器基本全支持,IE11 需要 polyfill,但至少可控),就能运行。

更重要的是,它和任何框架无关。Vue 能用,React 能用,Angular 能用,甚至 SpringBoot 的 JSP 页面里也能直接塞一个 <my-button>


上手即踩坑:理想很丰满,现实很骨感

我兴致勃勃地写了第一个 Web Component:

class MyButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <style>
        button { background: #4CAF50; color: white; border: none; padding: 8px 16px; }
      </style>
      <button><slot></slot></button>
    `;
  }
}

customElements.define('my-button', MyButton);

本地测试完美!绿色按钮,文字居中,还能传 slot 内容。我美滋滋地提交 PR,结果第二天就被测试打回来了:

“在 React 项目里点击没反应,控制台报错:Cannot read property 'addEventListener' of null

啊?我写的组件根本没加事件监听啊!

后来才发现,React 在处理自定义元素时,不会自动将 props 转为 attributes,也不会绑定事件到原生 DOM 上。你得手动处理:

// React 中使用 Web Component 的正确姿势
<my-button onClick={(e) => console.log('clicked!')}>
  点我
</my-button>

不行!React 会忽略 onClick,因为它不是标准 HTML attribute。

正确的做法是:

const MyButtonWrapper = () => {
  const ref = useRef(null);
  useEffect(() => {
    ref.current?.addEventListener('click', handleClick);
    return () => ref.current?.removeEventListener('click', handleClick);
  }, []);

  return <my-button ref={ref}>点我</my-button>;
};

或者,让 Web Component 自己派发自定义事件:

// 在 MyButton 内部
this.shadowRoot.querySelector('button').addEventListener('click', () => {
  this.dispatchEvent(new CustomEvent('myclick', { bubbles: true }));
});

然后在 React 里监听 onMyclick

那一刻我真想砸键盘——跨框架集成比想象中麻烦得多。但转念一想,这不正是 Web Components 的价值所在吗?它强迫你思考“如何设计一个真正通用的接口”,而不是依赖某个框架的魔法。


架构设计思考:组件边界与通信模型

作为一个前 PM,我现在特别在意“契约”——组件暴露什么属性、触发什么事件、接收什么方法调用。Web Components 的设计哲学其实很“产品经理”:

  • 属性(attributes):用于传递简单、可序列化的数据(字符串、数字)
  • 属性(properties):用于传递复杂对象(需通过 JavaScript 设置)
  • 事件(events):用于向外通知状态变化
  • 方法(methods):提供主动控制能力(如 open(), close()

这种清晰的输入/输出模型,比框架内部的 $emitpropscontext 更“标准化”。

举个实际例子:我们有个通知中心组件,需要支持动态添加消息、清除、标记已读。用 Web Components 可以这样设计:

class NotificationCenter extends HTMLElement {
  connectedCallback() {
    this.render();
    // 暴露方法
    this.addMessage = (msg) => { /* ... */ };
    this.clear = () => { /* ... */ };
  }

  // 通过 attribute 控制初始状态
  static get observedAttributes() {
    return ['unread-count'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'unread-count') {
      this.updateUnreadCount(newValue);
    }
  }
}

前端框架只需这样调用:

<notification-center unread-count="5"></notification-center>

<script>
  const nc = document.querySelector('notification-center');
  nc.addMessage({ title: '新订单', content: '...' });
</script>

SpringBoot 后端同学甚至可以直接在 Thymeleaf 模板里用! 他们再也不用等我们出 Vue 组件包了——这对跨团队协作简直是降维打击。


性能与兼容性:别被“原生”二字骗了

很多人一听“原生”就觉得性能无敌。醒醒!Web Components 也有坑:

  1. Shadow DOM 的样式隔离是把双刃剑
    虽然避免了样式污染,但也导致无法全局覆盖样式。比如你想统一改所有按钮的圆角,传统 CSS 一句 button { border-radius: 4px } 就行,但 Shadow DOM 里的按钮你根本 touch 不到。解决方案是使用 CSS Parts 或 CSS Custom Properties:

    /* 定义可定制的 CSS 变量 */
    my-button {
      --button-bg: #4CAF50;
    }
    
    // 组件内部
    <style>
      button { background: var(--button-bg, #ccc); }
    </style>
    
  2. IE11 支持需要 polyfill
    @webcomponents/webcomponentsjs 这个包能搞定大部分问题,但体积不小(~30KB gzipped)。如果项目必须支持 IE,建议按需引入,或者干脆放弃——毕竟微软自己都 EOL 了。

  3. 生命周期管理比框架组件弱
    没有 mountedbeforeUpdate 这种钩子,你得自己监听 connectedCallback / disconnectedCallback。对于复杂逻辑,容易写出回调地狱。

不过好消息是,现代浏览器对 Web Components 的性能优化已经非常成熟。Chrome 的 DevTools 甚至专门有“Custom Elements”面板,可以查看注册状态、实例数量等。


工程化实践:如何融入现有项目?

光有理论不够,得落地。我在 GitHub 上建了个小仓库 wc-playground(名字随便起的),尝试把 Web Components 融入真实工作流:

  • Lit(Google 出的轻量库)简化开发,避免手写一堆 boilerplate
  • 通过 Rollup 打包,输出 ES Module 和 UMD 两种格式
  • 编写 Storybook 文档,方便非前端同事预览组件效果
  • 在 CI 中加入 Playwright 测试,确保跨浏览器兼容性

关键配置示例:

// rollup.config.js
export default {
  input: 'src/index.js',
  output: [
    { file: 'dist/wc-components.esm.js', format: 'es' },
    { file: 'dist/wc-components.umd.js', format: 'umd', name: 'WCComponents' }
  ],
  plugins: [litCss(), resolve(), commonjs()]
};

团队接入也很简单:

<!-- 任何项目,一行 script 引入 -->
<script type="module" src="/assets/wc-components.esm.js"></script>

<!-- 然后直接用 -->
<my-dialog open>
  <p>你好,世界!</p>
</my-dialog>

上周五晚上加班部署上线后,后端老哥发来微信:“卧槽,这按钮居然在我们的管理后台直接跑起来了?!” 那一刻,我觉得从 PM 转码农,值了。


未来展望:微前端的新可能?

最近团队在讨论微前端架构。主流方案如 qiankun、Module Federation 都不错,但子应用之间共享 UI 组件依然头疼——版本不一致、样式冲突、事件通信复杂。

而 Web Components 天然具备沙箱隔离 + 标准接口的特性,或许能成为微前端中 UI 层的“通用语言”。

想象一下:主应用提供一套 <header-bar>, <notification-center>, <user-avatar>,所有子应用直接使用,无需关心实现细节。子应用之间切换时,这些组件状态还能保持(因为是同一个 DOM 实例)。

当然,这需要更完善的工具链支持,比如:

  • 组件版本管理
  • 动态加载策略
  • 跨应用状态同步

但方向是对的——回归 Web 本质,用标准解决问题


写在最后:代码人生,少点套路,多点真诚

从画原型到写 Custom Elements,我越来越觉得:好的技术,应该降低协作成本,而不是制造壁垒

Web Components 不是银弹,它不适合构建整个 SPA 应用(缺乏状态管理、路由等),但在“可复用 UI 元素”这个细分领域,它可能是最接近“一次编写,到处运行”理想的方案。

而且,当你看到 SpringBoot 项目里直接渲染出你写的 <data-table>,那种跨技术栈的打通感,真的很爽。

所以,别再只盯着 React 19、Vue 5 的新特性了。有时候,回头看看那些“老”标准,反而能找到破局之道。

毕竟,代码人生,不是追逐风口,而是解决真实问题

P.S. 我把踩过的坑整理成了开源项目,欢迎 Star & PR:github.com/yourname/wc-playground
P.P.S. 前端组长说我再在 PR 里写“这个组件产品经理都能用”,就要把我调去写 Java 了……救命!


方案 跨框架复用 样式隔离 学习成本 IE11 支持 适合场景
React/Vue 组件 ⚠️ (CSS Modules) ✅ (配合 polyfill) 单框架项目
Web Components ✅ (Shadow DOM) ⚠️ (需 polyfill) 通用 UI 库、微前端共享
iframe 完全隔离的独立模块

(表格数据基于个人项目经验,仅供参考)

好了,泡面凉了,继续改 bug 去了。

评论 0

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