导读:本期聚焦于小伙伴创作的《如何使用 Web Components 构建一套与框架无关的跨项目 UI 组件库?》,敬请观看详情。把按钮、表单、弹窗做成能在 React、Vue、Angular 项目里直接用的组件,难点往往不在写法而在隔离与通信。Web Components 依靠 Shadow DOM 实现样式和 DOM 隔离,用 Custom Elements 定义标签,再通过 Attributes 与 Events 跟任何框架交换数据。不少团队误以为封装一次就能彻底避开框架差异,结果在表单值双向同步、插槽内容渲染和样式穿透上踩坑。一套可用的跨项目组件库要先定好属性命名规范、事件派发格式和主题变量注入方式,再配合构建工具输出符合标准的自定义元素,才能在多技术栈中保持行为一致、维护成本低。

在企业级前端开发中,不同业务线往往采用不同的框架,React、Vue、Angular 并存是常态。如果为每个框架单独维护一套 UI 组件,成本极高且容易风格割裂。Web Components 是一组浏览器原生标准,包含 Custom Elements、Shadow DOM 和 HTML Templates,它允许开发者定义自己的 HTML 标签,并且这些标签的行为不依赖任何上层框架,从而成为构建与框架无关的 UI 组件库的理想选择。

如何使用 Web Components 构建一套与框架无关的跨项目 UI 组件库?

为什么选择 Web Components 来做跨项目组件库

传统 UI 库如 Ant Design 面向 React,Element 面向 Vue,它们的组件逻辑和生命周期深度绑定框架运行时。一旦项目技术栈切换,旧组件无法直接复用。Web Components 则运行在浏览器底层,框架只是它的使用者而非载体。在 React 里你可以写 <my-button>,在 Vue 模板里同样可以写 <my-button>,两者调用的都是同一个原生自定义元素。

另一个关键优势是隔离性。通过 Shadow DOM,组件的内部结构和样式对外不可见,外部全局 CSS 不会污染组件,组件样式也不会泄漏到页面。这对跨项目交付尤其重要,因为不同项目的 CSS 重置方案和命名习惯千差万别,原生隔离能大幅降低样式冲突概率。

核心标准与基本结构

Web Components 主要依赖三项技术。Custom Elements 用来注册自定义标签并定义其生命周期;Shadow DOM 提供封装;HTML Templates 和 Slots 用于内容分发。下面我们用一个最简按钮组件说明基本写法。

class MyButton extends HTMLElement {
  constructor() {
    super();
    // 创建影子根,开启样式与 DOM 隔离
    const shadow = this.attachShadow({ mode: 'open' });
    const style = document.createElement('style');
    style.textContent = `
      button {
        padding: 8px 16px;
        border-radius: 4px;
        border: none;
        background: #3b82f6;
        color: #fff;
        cursor: pointer;
      }
    `;
    const btn = document.createElement('button');
    btn.textContent = this.getAttribute('label') || '按钮';
    shadow.appendChild(style);
    shadow.appendChild(btn);
  }
}
// 注册自定义元素
customElements.define('my-button', MyButton);

上面代码中,我们继承 HTMLElement 并实现构造函数,在内部挂载 Shadow DOM。注意 customElements.define 的标签名必须包含连字符,这是规范硬性要求,避免与未来原生标签冲突。

该组件在任意框架中都能以 <my-button label="提交"></my-button> 形式使用。如果需要在 React 中使用,由于 React 对未知属性处理有差异,建议通过 ref 设置属性或使用事件代理,但标签本身无需任何包装层。

属性与事件的跨框架通信设计

组件库要真正跨项目,必须规范数据进出方式。推荐做法是:单向数据流用 Attributes 或 Properties,用户交互用 CustomEvent 派发事件。框架层负责监听事件并更新自身状态,而不是由组件内部直接修改外部状态。

以下示例展示一个带计数的组件,父级可以通过属性传入初始值,点击后派发事件通知外部:

class CounterEle extends HTMLElement {
  static get observedAttributes() {
    return ['count'];
  }
  constructor() {
    super();
    this._count = 0;
    const shadow = this.attachShadow({ mode: 'open' });
    this.btn = document.createElement('button');
    this.btn.textContent = 'count: 0';
    this.btn.addEventListener('click', () => {
      this._count++;
      this.btn.textContent = 'count: ' + this._count;
      // 向外部派发自定义事件
      this.dispatchEvent(new CustomEvent('count-change', {
        detail: { value: this._count },
        bubbles: true,
        composed: true
      }));
    });
    shadow.appendChild(this.btn);
  }
  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'count') {
      this._count = parseInt(newVal, 10) || 0;
      this.btn.textContent = 'count: ' + this._count;
    }
  }
}
customElements.define('counter-ele', CounterEle);

这里 observedAttributes 声明需要监听的属性,attributeChangedCallback 在属性变化时触发。事件设置 composed: true 才能让事件穿透 Shadow DOM 边界,被外部框架捕获。Vue 中可用 v-on:count-change 监听,React 中可在 JSX 写 onCountChange 或通过 addEventListener 绑定。

很多团队忽略事件命名规范,导致 Angular 和 React 监听写法不一致。建议统一使用 kebab-case 事件名,并在文档中明确 detail 结构,避免对接时反复试错。

样式主题与插槽机制

跨项目组件库常需适配不同主题。利用 CSS 自定义属性(变量)是最轻量的方案。组件内部样式引用 var(--my-primary, #3b82f6),使用方在全局或宿主元素上定义变量即可换肤,不需要穿透 Shadow DOM。

内容分发则用 Slot。下面的卡片组件允许外部插入标题和正文:

<template id="card-tpl">
  <style>
    .card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; }
    ::slotted(h3) { margin: 0 0 8px; }
  </style>
  <div class="card">
    <slot name="title"></slot>
    <slot name="body"></slot>
  </div>
</template>
<script>
  class MyCard extends HTMLElement {
    constructor() {
      super();
      const shadow = this.attachShadow({ mode: 'open' });
      const tpl = document.getElementById('card-tpl');
      shadow.appendChild(tpl.content.cloneNode(true));
    }
  }
  customElements.define('my-card', MyCard);
</script>

使用方式为 <my-card><h3 slot="title">标题</h3><p slot="body">内容</p></my-card>。Slot 让组件保持结构灵活,又不影响内部封装。需注意 ::slotted 只能选择直接插槽节点,不能深层选择,复杂样式应交给使用方通过变量控制。

构建与跨框架集成建议

手写原生类适合演示,实际组件库应使用构建工具。推荐用 Vite 配合 @web/dev-server 或 Rollup 将每个组件打包为独立 ES Module,输出 my-button.js 这类文件。各项目通过 import 'ipipp.com/ui/my-button.js' 引入即可,无需 npm 依赖具体框架包。

在 React 中集成时,由于 React 17 之前不会把部分属性如 class 映射到自定义元素,建议使用 reactify-wc 类小工具或直接在 useEffect 里操作 ref。Vue 3 对 Custom Elements 支持良好,只需在编译选项配置 isCustomElement 忽略标签校验。Angular 则提供 CUSTOM_ELEMENTS_SCHEMA 直接在模块中声明。

框架集成要点常见坑
React用 ref 设属性,监听原生事件属性名驼峰映射、事件需手动绑定
Vue 3配置 isCustomElement模板校验误报、v-model 不支持
Angular添加 CUSTOM_ELEMENTS_SCHEMA变更检测需手动触发

最后,组件库应配套编写运行时文档站点,用原生示例展示用法,并提供 TypeScript 类型声明文件,描述 Attributes 和 Events 的接口。这样不论哪个框架的开发者,都能依据类型提示正确接入,减少沟通成本。

总结与实践路线

构建与框架无关的 UI 组件库,核心是用 Web Components 标准把视图和交互封进自定义元素,对外只暴露属性、事件和插槽。第一步盘点通用组件清单,第二步用原生或轻量封装库如 Lit 编写组件,第三步规范通信协议与主题变量,第四步配置构建并验证在 React、Vue、Angular 项目的表现。沿着这条路线,团队可以用一套代码支撑多个技术栈,显著降低长期维护负担。

Web_ComponentsUI组件库跨框架修改时间:2026-08-09 23:03:40

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。