Web Components:原生组件化开发新趋势?我踩完坑后有点话想说

采菊东篱下
2025-12-15 04:29
阅读 340

大家好,我是你们的老朋友小码(不是那个“码农”的码,是“代码”的码),一个在 Mac 上敲了快两年 JS 的 AI 编程工具评测博主。平时没事就爱折腾各种新出的开发工具,从 Vite 到 Bun,从 GitHub Copilot 到 Cursor,基本都玩过一圈。不过今天不聊 AI 工具——虽然上周五晚上我还被产品经理用 “能不能让 AI 自动把设计稿转成 Web Components?” 这种问题折磨到凌晨两点。

事情得从去年双11说起。我们团队接了个大活:给公司老后台系统做一次“微前端化”改造。领导的意思是:“别再搞一堆 jQuery 拼接页面了,现在都 2024 年了,要组件化、要复用、要现代化!” 可问题是,我们技术栈五花八门:有 Vue 2 的旧项目,有 React 16 的中台,还有几个纯 HTML+JS 的“古董级”运营页。更惨的是,运维那边死活不同意引入新打包工具,理由是“怕线上崩了背锅”。

就在我不知所措、差点准备提桶跑路时,组里一位资深前端大佬悠悠地说了一句:“你有没有试过 Web Components?原生支持,不用框架,兼容性还行。”

我当场愣住——Web Components?那不是十年前就被喊“未来”的东西吗?怎么现在又冒出来了?


原来,浏览器早就偷偷给我们造好了轮子

说实话,我对 Web Components 的第一印象还停留在 MDN 上那几行干巴巴的 API 文档。但这次被逼上梁山,只能硬着头皮去翻资料。结果发现,这玩意儿其实早就悄悄成熟了。

简单来说,Web Components 是一组浏览器原生支持的 API,让你能像写 Vue/React 组件一样,定义自己的 HTML 标签。比如你可以直接在页面里写:

<my-button variant="primary">点我啊</my-button>

而不用管它背后是用什么框架写的——甚至根本没用框架!

它的核心由三块组成:

  • Custom Elements:自定义 HTML 标签
  • Shadow DOM:隔离样式和 DOM,避免污染全局
  • HTML Templates:声明式模板,懒加载用

最爽的是,完全基于原生 JavaScript,不需要打包、不需要 Babel、不需要 npm install 一吨依赖。对于我们这种“运维不让动构建流程”的团队,简直是救命稻草。


实战:把一个按钮封装成 Web Component

说干就干。我先拿最简单的按钮开刀。目标:做一个带 loading 状态、可配置颜色的通用按钮,能在所有项目里直接用。

首先,在 GitHub 上建了个新仓库 wc-ui-kit(名字随便起的,反正没人 star 😅),然后开始写代码:

// my-button.js
class MyButton extends HTMLElement {
  constructor() {
    super();
    
    // 创建 Shadow DOM,隔离样式
    this.attachShadow({ mode: 'open' });
    
    // 获取属性
    const variant = this.getAttribute('variant') || 'default';
    const loading = this.hasAttribute('loading');
    
    // 构建模板
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: inline-block;
        }
        button {
          padding: 8px 16px;
          border: none;
          border-radius: 4px;
          cursor: pointer;
          background: ${variant === 'primary' ? '#007AFF' : '#f0f0f0'};
          color: ${variant === 'primary' ? 'white' : 'black'};
          opacity: ${loading ? 0.6 : 1};
        }
        .loading::after {
          content: " ⏳";
        }
      </style>
      <button ${loading ? 'disabled' : ''}>
        ${this.textContent}
        ${loading ? '<span class="loading"></span>' : ''}
      </button>
    `;
  }
}

// 注册自定义元素
customElements.define('my-button', MyButton);

然后在 HTML 里直接引入:

<script type="module" src="./my-button.js"></script>

<my-button variant="primary" loading>提交中...</my-button>

神奇的事情发生了:页面上真的出现了一个带 loading 的蓝色按钮!而且样式完全不会影响其他元素。

我当时差点激动得把 MacBook 合上亲一口——这不就是我们梦寐以求的“跨框架 UI 组件”吗?


踩坑实录:你以为的“原生”,其实处处是坑

当然,现实哪有这么美好。接下来几天,我陆续遇到了几个让人想砸键盘的问题。

坑 1:属性更新不响应

我一开始以为改个 variant 属性就能自动重绘,结果发现不行。查文档才知道,需要监听属性变化

static get observedAttributes() {
  return ['variant', 'loading'];
}

attributeChangedCallback(name, oldValue, newValue) {
  if (oldValue !== newValue) {
    this.render(); // 手动触发重绘
  }
}

还得自己写 render() 方法来更新 DOM。这时候真怀念 Vue 的响应式……

坑 2:事件怎么传出去?

用户点了按钮,父页面怎么知道?Web Components 没有 props 和 emit,但可以用原生事件:

// 在 button 的 click 里
this.dispatchEvent(new CustomEvent('my-click', {
  detail: { value: 'clicked!' },
  bubbles: true // 是否冒泡
}));

然后在 HTML 里监听:

<my-button @my-click="handleClick"></my-button>

……等等,@my-click 是 Vue 的语法!在原生 HTML 里得写成:

document.querySelector('my-button').addEventListener('my-click', (e) => {
  console.log(e.detail);
});

行吧,忍了。

坑 3:IE?不存在的

测试同学跑来问我:“IE11 支持吗?”
我:“……我们现在连 Edge 都只测 Chromium 版,你还惦记 IE?”
他:“产品说有些客户还在用。”
我:“那你让他们用 <button> 吧,这个组件叫 <modern-button>,专治复古情怀。”

好消息是,现代浏览器(Chrome/Firefox/Safari/Edge)基本全支持。坏消息是,如果你真要兼容 IE,得上 polyfill,体积不小,而且性能打折扣。建议直接放弃,省点头发。


性能 & 调试:比想象中友好

很多人担心 Shadow DOM 会影响性能,我自己也测了一波。用 Chrome DevTools 的 Performance 面板跑了 100 个 <my-button> 实例,渲染时间比普通 <button> 多了约 0.5ms——完全可以忽略。

调试方面,DevTools 对 Shadow DOM 的支持也很到位。你可以直接在 Elements 面板里展开 shadow-root,像操作普通 DOM 一样 inspect、修改样式、打断点。甚至还能用 $0.shadowRoot 在 Console 里快速访问。

另外,因为没有虚拟 DOM,内存占用更低。对我们这种嵌入老旧系统的场景,这点特别重要——之前用 React 写的组件,光 runtime 就占 40KB,现在原生 JS 只有 3KB。


和主流框架怎么共存?

这才是重点。我们不可能一夜之间把所有项目重写成 Web Components,但可以渐进式接入

我在 Vue 2 项目里这样用:

<template>
  <div>
    <!-- 直接当普通 HTML 用 -->
    <my-button variant="primary" @my-click="handleSubmit">
      提交
    </my-button>
  </div>
</template>

<script>
import './my-button.js'; // 注意要用 import 引入

export default {
  methods: {
    handleSubmit(e) {
      console.log('收到事件:', e.detail);
    }
  }
}
</script>

React 更简单,因为它本来就允许写自定义标签:

import './my-button.js';

function App() {
  const handleClick = (e) => {
    console.log('React 收到:', e.detail);
  };

  return (
    <my-button 
      variant="primary" 
      onMyClick={handleClick}
    >
      提交
    </my-button>
  );
}

注意:React 的事件系统会把 onMyClick 转成 my-click 事件监听,所以命名要对应。


最终效果:真的香

上线一个月后,我们成功把登录框、通知弹窗、数据表格这些公共组件全用 Web Components 重写了。现在:

  • 运营页可以直接 <login-modal></login-modal> 调用
  • Vue 项目和 React 项目共用同一套 UI 库
  • 新人入职不用学框架,看一眼 HTML 就会用
  • GitHub 仓库里 wc-ui-kit 虽然只有 23 个 star(大部分是我自己点的),但至少 CI/CD 跑得稳稳的
方案 体积 学习成本 跨框架 构建依赖
React Component ~40KB 必须 webpack/vite
Vue SFC ~30KB 必须 vue-cli/vite
Web Components ~3KB

写在最后:它不是银弹,但值得试试

Web Components 不是万能的。复杂状态管理、数据流、SSR 场景下,它确实不如 React/Vue 成熟。但如果你和我一样,身处一个多技术栈混杂、构建流程受限、又急需组件复用的团队——它可能是目前最务实的选择

而且别忘了,这可是浏览器原生支持的能力。这意味着未来十年它都不会“过时”,反而可能随着 Lit、FAST 等轻量库的成熟,成为微前端、Design System 的基础设施。

所以,别再觉得 Web Components 是“过气网红”了。打开你的终端,新建一个 .js 文件,写一行 customElements.define(...) —— 你会发现,原生的力量,其实一直都在。

对了,我把我做的那个 wc-ui-kit 开源了,虽然代码很糙,但至少能跑。GitHub 地址就不贴了(怕被喷),搜 “wc-ui-kit” 应该能找到。欢迎 PR,但别指望我及时合并——毕竟下个月又要双11了,产品经理已经在群里@我改需求了……

(完)

评论 0

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