用 Web Components 做组件化,我做了一个可复用的 UI 库

SQL调音师
2025-06-23 16:04
阅读 406

引言:为什么是 Web Components?

引言:为什么是 Web Components?

作为一名在互联网公司干了四年多的前端开发,我经历过 jQuery 时代、Vue2 到 Vue3 的演变,也尝试过 React 和 Angular。在不同项目之间跳来跳去时,我总有一个痛点挥之不去——组件的复用性差,跨技术栈迁移成本高

去年我们部门接到一个任务:为多个业务线开发一套统一风格的 UI 组件库。这个组件库需要同时支持旧项目的 jQuery 页面,以及新上的 Vue3 + Vite 工程,并且未来可能还会接入 React。

起初我们打算基于 Vue 来封装一套公共组件,但这很快就被打脸了。因为老系统压根没有引入 Vue,连打包工具都没有,更别说通过 npm 安装依赖了。那怎么办?有没有一种“写一次,随处运行”的技术方案呢?

这时候我想起了之前听说过但没真正上手的 Web Components。它号称可以脱离框架存在,原生支持浏览器,是不是正好可以解决我们的需求?于是抱着试试看的心态,开启了这次实战之旅。


问题描述:组件库开发中的三大难题

问题描述:组件库开发中的三大难题

难题一:兼容性差、迁移难

我们要对接的页面从静态 HTML 文件(纯jQuery),到 Vue 项目,再到 React 甚至小程序都有。传统组件库必须绑定某一种框架,一旦换框架就得重写一遍。

难题二:样式污染严重

由于历史原因,很多老页面使用的是全局 CSS 变量和类名命名规则混乱。我们在封装组件的时候发现,只要类名稍有冲突,界面就面目全非,尤其是按钮样式和模态框位置错乱等问题频发。

难题三:性能敏感

部分用户场景下加载速度要求极高,像支付页这种对延迟非常敏感的地方,不允许引入太多额外 JS 或进行复杂的初始化过程。

这些现实问题让我们不得不思考:有没有一种既轻量又能独立运行的解决方案?


解决方案:使用 Web Components 构建跨技术栈组件库

解决方案:使用 Web Components 构建跨技术栈组件库

初识 Web Components

Web Components 是一组浏览器原生特性,包括:

  • customElements:自定义 HTML 元素
  • shadow DOM:创建隔离的 DOM 和 CSS 作用域
  • HTML templates:定义元素结构模板

它的优势在于不依赖任何框架,直接在浏览器中工作。这意味着我们可以把组件封装成类似 <my-button> 这样的标签,在任意页面上直接调用而无需关心底层实现。

技术选型:Lit + Vite + TypeScript

为了提高开发效率,我们最终选择使用 Lit,这是一个由谷歌开发的小而强大的 Web Components 框架。它基于标准 API,提供了模板语法、响应式更新机制等能力,但又不像 Polymer 那么重。

开发工具链方面,我们使用了 Vite + TypeScript + TailwindCSS:

  • Vite 提供极速构建体验
  • TypeScript 确保类型安全
  • TailwindCSS 提供统一设计语言

举个例子:封装一个 <ui-modal>

import { LitElement, html, css } from 'lit'
import { customElement, property } from 'lit/decorators.js'

@customElement('ui-modal')
export class UiModal extends LitElement {
  @property({ type: boolean }) open = false

  static styles = css`
    :host {
      display: block;
    }
    .modal {
      position: fixed;
      top: 0; left: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);
      display: none;
    }
    .modal.active {
      display: flex;
    }
    .content {
      margin: auto;
      background: white;
      padding: 16px;
      border-radius: 8px;
    }
  `

  render() {
    return html`
      <div class="modal ${this.open ? 'active' : ''}">
        <div class="content">
          <slot></slot>
          <button @click=${() => this.closeModal()}>关闭</button>
        </div>
      </div>
    `
  }

  closeModal() {
    this.open = false
    this.dispatchEvent(new CustomEvent('close', { bubbles: true }))
  }
}

上面是一个最简版的模态框组件,通过 Shadow DOM 实现了样式隔离,通过属性控制状态,还能对外派发事件。

如何集成到不同项目中?

这正是 Web Components 的妙处所在:

在 Vue 中使用:

<template>
  <div>
    <button @click="showModal">打开弹窗</button>
    <ui-modal :open.sync="isOpen" @close="isOpen = false">
      这是弹窗内容
    </ui-modal>
  </div>
</template>

<script>
import { defineCustomElement } from 'vue'
import UiModal from '../components/UiModal.ce.vue'

defineCustomElement(UiModal)

export default {
  data () {
    return {
      isOpen: false
    }
  },
  methods: {
    showModal () {
      this.isOpen = true
    }
  }
}
</script>

在 React 中使用:

import React, { useState } from 'react'

// 在 React 项目中注册组件
const UiModal = require('../components/UiModal.ce').default
customElements.define('ui-modal', UiModal)

function App() {
  const [open, setOpen] = useState(false)

  return (
    <div>
      <button onClick={() => setOpen(true)}>打开弹窗</button>
      <ui-modal
        open={open}
        onClose={() => setOpen(false)}
      >
        这是 React 中的弹窗内容
      </ui-modal>
    </div>
  )
}

在 jQuery 静态页面中使用:

<!-- HTML 页面 -->
<script src="/dist/bundle.js"></script> <!-- 注册 Web Components -->

<button onclick="document.querySelector('#myModal').open = true">打开弹窗</button>
<ui-modal id="myModal">
  这是一个静态页面中的弹窗
</ui-modal>

只需要一段 JS 注册组件即可,完全不需要打包工具或模块加载器。


效果总结:性能优化+维护成本降低+开发者体验提升

效果总结:性能优化+维护成本降低+开发者体验提升

在实际落地后,我们获得了以下几方面的显著提升:

✅ 性能优化明显

  • Web Components 自身体积小,平均每个组件不到 3KB
  • 不依赖框架启动逻辑,首次渲染更快
  • 使用 Shadow DOM 后避免样式冲突,减少了重复样式注入

✅ 维护成本降低

  • 所有组件统一开发、测试、发布流程,减少冗余代码
  • 修改某个基础组件只需更新一次,各项目同步受益
  • 文档体系统一建设,团队成员更容易上手

✅ 开发者体验改善

  • 支持热更新(借助 Vite)
  • 支持 Storybook 编写组件文档和示例
  • 支持 TypeScript 类型提示
  • 可以结合 Tailwind 快速开发视觉组件

经验分享:实战建议与避坑指南

如果你也有类似的跨技术栈组件复用需求,不妨考虑采用 Web Components,下面是我踩过的坑,供你参考:

🔍 需要关注浏览器兼容性

虽然主流浏览器都支持 Web Components v1 标准,但像 IE11 就完全不支持。如果你的项目需要兼容老旧浏览器,需要加上 polyfill(如 webcomponentsjs)。

不过在我们这边,IE 已经被淘汰,所以没有这个问题。

📦 注意打包方式的选择

推荐使用 ESM + Rollup/Vite 打包,保持模块化输出。如果需要兼容不支持动态导入的环境,可以使用打包配置生成 UMD 版本。

另外注意不要把所有组件打包成一个大 bundle,最好是按需引入或分块加载。

⚠️ Shadow DOM 并非万能

虽然 Shadow DOM 很适合封装内部结构和样式,但也有一些限制需要注意:

  • 外部无法直接访问 shadow root 内部的节点
  • 伪元素(如 ::placeholder)在 Shadow DOM 内部行为会有所不同
  • SEO 友好度略低于普通 DOM

如果是用于交互组件,这些都不是大问题;但如果用于 SEO 相关内容,还是建议使用普通 DOM。

💡 调试技巧:善用 DevTools

Chrome DevTools 对 Web Components 支持很好:

  • Elements 面板可以看到 shadow root 结构
  • Styles 面板可以看到 scoped 的样式信息
  • Network 查看组件 JS 加载情况

此外还可以使用 window.customElements.get('your-element-name') 来检查是否成功注册组件。


未来的方向

随着现代浏览器对 Web Standards 的进一步普及,以及像 Lit、StencilJS 等生态的发展,我相信 Web Components 会在未来几年成为越来越多企业的选择。

目前我们还在探索以下几个方向:

  1. 通过 CDN 动态加载组件脚本,做到真正的按需加载
  2. 结合微前端架构实现组件级别的远程加载和通信
  3. 继续完善 Design System,让 UI 设计与代码真正对齐

结语:做工程师而不是搬砖工

其实最开始我也怀疑 Web Components 是不是噱头,但在实际做完几个核心组件之后,我真香了。

与其不断学习新框架、迁徙代码、重构项目,不如回到浏览器原生能力本身,写出稳定、可复用、易维护的组件。毕竟,浏览器才是我们真正的操作系统。

如果你现在正在为组件跨项目复用头疼,或者想搭建一个长期维护的设计系统,强烈建议你尝试一下 Web Components。它或许不是银弹,但在合适场景下,它确实是一种优雅而持久的解法。

最后想说一句话送给每一位前端开发者:框架是会变的,但标准不会变。

祝你写码愉快,bug 少少。


参考资料

评论 0

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