Web Components:被低估的原生组件化新解法

接口字段消失术
2025-12-25 13:46
阅读 1848

上周五晚上十点半,我瘫在工位上盯着屏幕上那个祖传的 jQuery 项目,心里直冒火。产品经理又提了个“小需求”——要在我们基于 Spring Boot 的后台管理系统里嵌入一个可复用的日历组件,要求支持主题切换、国际化,还要能和现有 Vue 2 代码无缝协作。更要命的是,下周三就要上线。

作为实验室里唯一一个既懂后端(Spring Boot 写过不少)又被迫搞前端的“全栈民工”,这个锅自然落到了我头上。当时脑子里第一个想法是:“要不直接抄个 Element UI 的 Calendar 组件改改?”但转念一想,这玩意儿绑定 Vue 太深,以后要是换成 React 或者 Svelte 怎么办?而且团队里还有同学在搞微前端架构实验,组件复用成了老大难问题。

就在这时,我突然想起半年前在 GitHub 上刷到的一个仓库:shoelace-style/shoelace,一个纯 Web Components 实现的 UI 库。当时没太在意,觉得“这不就是老旧的 custom elements 吗?”。但眼下,它似乎成了破局的关键。


被遗忘的“原生”武器

很多人以为 Web Components 是新东西,其实它早在 2014 年就被 W3C 提案了。只不过那几年 React/Vue 风头正盛,大家忙着写 .jsx<template>,没人关心浏览器原生能干啥。再加上早期兼容性烂得一批,Polyfill 又重又慢,Web Components 就被打入冷宫。

但时代变了。现在 Chrome、Edge、Firefox、Safari 全面支持 Shadow DOM v1 和 Custom Elements v1,连 IE 都退休了,我们终于可以甩开膀子用原生能力了。

Web Components 的核心就三板斧

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

听起来是不是有点像“不用框架的组件化”?没错!它最大的优势就是框架无关。你可以在 Vue 里用,也可以在 React 里塞,甚至直接扔进 Spring Boot 渲染的 Thymeleaf 模板里——只要浏览器支持,就能跑。


实战:把日历组件做成 Web Component

说干就干。周六早上八点,我照例泡了杯速溶咖啡(实验室经费紧张,喝不起瑞幸),打开 VS Code 开始折腾。

第一步:创建基础结构

先建个 my-calendar.js

class MyCalendar extends HTMLElement {
  constructor() {
    super();
    // 创建 Shadow DOM
    this.attachShadow({ mode: 'open' });
    
    // 定义内部模板
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          font-family: Arial, sans-serif;
        }
        .calendar {
          border: 1px solid #ddd;
          padding: 16px;
          background: white;
        }
        /* 更多样式... */
      </style>
      <div class="calendar">
        <h3 id="month-title">Loading...</h3>
        <div id="days-grid"></div>
      </div>
    `;
  }

  connectedCallback() {
    // 组件挂载时初始化
    this.render();
  }

  render() {
    const now = new Date();
    const month = now.toLocaleString('default', { month: 'long' });
    this.shadowRoot.getElementById('month-title').textContent = month;
    // 真实项目中这里会渲染日期网格...
  }
}

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

注意几个关键点:

  • :host 选择器用于设置组件根元素样式
  • 所有样式都封装在 Shadow DOM 内,不会影响外部页面
  • connectedCallback 相当于 React 的 useEffect(() => {}, []) 或 Vue 的 mounted

第二步:支持属性和事件

产品经理肯定要传参数,比如 locale="zh-CN"theme="dark"。Web Components 通过 attributesproperties 通信:

// 监听属性变化
static get observedAttributes() {
  return ['locale', 'theme'];
}

attributeChangedCallback(name, oldValue, newValue) {
  if (oldValue !== newValue) {
    this[name] = newValue; // 同步到内部属性
    this.render(); // 触发重绘
  }
}

对外暴露方法也很简单:

// 外部可通过 calendarRef.goToToday() 调用
goToToday() {
  this.currentDate = new Date();
  this.render();
}

// 触发自定义事件
dispatchEvent(new CustomEvent('date-selected', {
  detail: { date: selectedDate },
  bubbles: true,
  composed: true // 允许事件穿越 Shadow DOM
}));

⚠️ 注意 composed: true!否则事件无法被 Vue/React 父组件监听。

第三步:集成到现有项目

我们的后台系统是 Spring Boot + Thymeleaf + Vue 2 混合架构。在某个页面的 .html 文件里直接引入:

<!-- 引入 Web Component -->
<script type="module" src="/static/js/my-calendar.js"></script>

<!-- 在 Vue 模板中使用 -->
<template>
  <div>
    <my-calendar 
      locale="zh-CN" 
      theme="light"
      @date-selected="handleDateSelect">
    </my-calendar>
  </div>
</template>

<script>
export default {
  methods: {
    handleDateSelect(e) {
      console.log('选中日期:', e.detail.date);
    }
  }
}
</script>

神奇的是,Vue 自动把 @date-selected 绑定到了自定义事件上!完全不需要额外适配。


踩坑实录:那些文档没告诉你的事

当然,过程不可能一帆风顺。以下是几个让我掉头发的坑:

坑1:样式隔离太彻底,反而不好调试

Shadow DOM 把样式封得死死的,DevTools 里看不到内部结构?别慌,在 Chrome DevTools 设置里勾选 “Show user agent shadow DOM”,就能展开查看了。

另外,有些全局样式(比如字体)需要穿透进去。可以用 CSS 变量:

/* 外部页面定义 */
my-calendar {
  --font-family: "PingFang SC", sans-serif;
}

/* 组件内部使用 */
:host {
  font-family: var(--font-family, Arial);
}

坑2:SSR(服务端渲染)不支持

如果你的 Spring Boot 用 Thymeleaf 做服务端渲染,<my-calendar> 在服务端就是个空标签。解决办法有两个:

  1. 降级显示:在 <my-calendar> 内部放 fallback 内容,比如 <span>Loading...</span>
  2. 客户端激活:确保 JS 加载后再替换内容(我们目前采用这种方式)

坑3:属性类型只有字符串

Web Components 的 attribute 只能传字符串!想传对象?得自己序列化:

<!-- 不行 -->
<my-calendar config="{ theme: 'dark' }"></my-calendar>

<!-- 可以 -->
<my-calendar config='{"theme":"dark"}'></my-calendar>

然后在 JS 里 JSON.parse(this.getAttribute('config'))。虽然麻烦,但总比耦合框架强。


性能与兼容性:真的能用吗?

我用 Lighthouse 测了下,加载一个 8KB 的 Web Component(gzip 后),FCP(首次内容绘制)只增加了 12ms。对比引入整个 Vue 或 React 运行时(>30KB),优势明显。

兼容性方面,Can I Use 数据显示:

浏览器 支持情况
Chrome ✅ 54+
Firefox ✅ 63+
Safari ✅ 10.1+
Edge ✅ 79+
iOS Safari ✅ 10.3+

考虑到我们内部系统主要跑在现代浏览器,基本无压力。如果真要支持老古董,可以用 @webcomponents/webcomponentsjs 打个 Polyfill,不过体积会增加 15KB 左右。


为什么 Web Components 适合产品化?

最近在实验室做了一个内部工具平台,需要给不同业务线提供统一的 UI 组件(按钮、表单、图表等)。以前的做法是:

  • Vue 团队维护一套 .vue 组件
  • React 团队另起炉灶写 .tsx
  • 后台 Java 同学只能用 Bootstrap 凑合

结果就是重复造轮子 + 设计不一致 + 升级困难

现在我们把核心组件全部用 Web Components 重写,发布到私有 npm 仓库。各团队只需:

npm install @our-company/ui-components

然后在任意框架中直接使用 <oc-button variant="primary">。设计师改个色值,所有产品自动同步!

GitHub 上也有不少成功案例:

  • GitHub 自己:大量使用 Web Components 构建 UI(比如 <details-menu>
  • Ionic:从 Angular 专属转向 Web Components,实现框架无关
  • Shoelace:纯 Web Components UI 库,Star 数已超 15k

教程资源推荐

如果你也想试试,这里是我踩坑后整理的入门路径:

  1. 官方文档MDN Web Components Guide(中文友好)
  2. 实战教程:Google 的 Web Fundamentals - Custom Elements
  3. 脚手架:用 open-wc 快速生成项目(支持 TypeScript + Lit)
  4. UI 库参考

个人建议:别一上来就手写原生 API,先用 Lit(Google 出品)简化开发。它只是个轻量 wrapper,最终产物仍是标准 Web Components。


写在最后

回过头看,Web Components 并非要取代 React/Vue,而是填补了框架之间的空白地带。尤其在以下场景特别香:

  • 微前端架构中的共享组件
  • 跨技术栈团队协作
  • 需要嵌入第三方网站的 widget(比如客服聊天窗)
  • 内部工具平台标准化

上周三,那个日历组件顺利上线。产品经理居然夸“这次交互很流畅”,我差点以为他被盗号了。运维同学也松了口气——毕竟没引入新框架,打包体积没暴涨。

现在,我已经在推动团队把常用组件逐步迁移到 Web Components。虽然写起来比 Vue 单文件组件啰嗦一点,但换来的是长期的可维护性和技术自由度。

毕竟,作为一个研二狗,谁不想少加班、多摸鱼呢?用对工具,才能早点下班去吃公司楼下那家开了十年的生煎包啊。

P.S. 本文示例代码已上传 GitHub:github.com/yourname/web-components-demo(名字当然是假的,别真去搜)。欢迎 Star(不是)——欢迎提 Issue 讨论!

评论 0

最热最新
暂无评论
接口字段消失术Lv.1
0
影响力
0
文章
0
粉丝