跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScriptSaaS大前端

基于 Web Components 的跨框架组件库实践

多技术栈并存导致组件重复开发和维护成本高,分享了基于 Web Components 构建跨框架组件库的实践。通过 Stencil.js 实现一次开发多端复用,结合 Design Tokens 统一设计规范。重点解决了宿主类名覆盖导致组件消失、事件捕获跨框架差异及低版本浏览器兼容等核心问题,提供了具体的配置与适配方案,验证了该方案在多技术栈环境下的可行性与稳定性。

游戏玩家发布于 2026/4/5更新于 2026/6/1421 浏览

前言

在业务早期阶段,产品需求迭代迅速,为了支持快速试错与灵活交付,团队内部形成了多种技术栈并存的局面:历史项目基于 Vue2,新业务则转向 React。同时,由于早期各项目独立推进,尚未形成统一的设计规范和组件标准,不同项目在组件实现方式、样式规范与交互体验上存在较大差异。

这种多样化在短期内带来了灵活性,使团队能够快速响应业务需求,但随着项目规模和业务复杂度的增加,也逐渐演变成了技术挑战:

  • 组件复用困难:相同功能组件需要在不同框架中重复实现。
  • 维护成本增加:功能或样式的调整须在多套组件库中分别修改。
  • 用户体验不一致:不同框架实现可能导致交互和视觉风格不统一。

为解决这些问题,我们移动端前端团队开始探索一种能够'一次开发,多处复用'的组件库方案。

目标与场景

核心目标

为了解决团队多框架并存、组件重复开发和体验不一致的痛点,我们确定了三大核心目标:

  • 统一设计规范:建立标准化设计体系和组件规范,确保视觉风格与交互行为在各业务线、各技术栈中保持一致。
  • 跨框架复用:构建框架无关的组件实现层,使同一组件可在 Vue、React、小程序等技术栈中复用。
  • 提升交付一致性:减少风格偏差,降低多端维护成本和迭代风险,快速响应业务需求。
应用场景

组件库主要面向以下场景:

  • 多技术栈并存:组件库提供统一的组件标准和实现,降低重复开发。
  • 多终端 / 多容器运行:支持 App WebView、小程序、移动浏览器等环境,保持一致体验。
  • 多租户与模块化业务:可快速适配多租户 SaaS、主题定制及模块化功能,实现灵活复用。
现状分析与开发优先级

在启动统一组件库建设前,我们对现有项目使用的组件库进行了调研,发现共有 10 个独立组件库分布在不同业务线和技术栈中,但高频组件主要集中在少数几类,如 Button、Exception、Loading 等。

基于这些数据,我们制定了'以高频、通用组件优先建设'的策略,为快速落地和跨业务复用提供依据。

高频组件统计(示例)

组件使用频次技术栈分布开发优先级
Exception138Vue、React高
Toast123Vue、React高
Button82Vue、React高
Loading62Vue、React高
Popup57Vue、React高

注:实际组件库包含 20+ 组件,这里仅展示高频组件作为示例。

业界方案调研与对比

为实现跨框架组件复用与统一设计规范,我们调研了多种业界主流方案,包括多套组件库维护、单一框架统一、F/A 分层设计及 Web Components 标准方案。

方案实现方式优点缺点代表方案
多套组件库
针对不同框架分别维护
简单快速上手
成本高、复用性差
-
单一框架统一全量迁移至单一框架(如 React)规范统一
复用性好
迁移成本、风险高Vant/Ant Design
F/A 分层设计将组件拆分为基础逻辑层(Foundation)和框架适配层(Adapter)可复用核心逻辑、支持多技术栈扩展抽象复杂,学习门槛高Semi Design
Web Components基于浏览器原生标准实现一次开发、多框架复用工具链与兼容性需处理Shoelace/FAST/Taro/QuarkD

综合对比可见,多套组件库方案维护成本高;单一框架方案迁移成本与风险较大;F/A 分层设计抽象度高、开发复杂。而 Web Components 方案在复用性、标准化及跨框架兼容性上最具长期价值,已被多个成熟组件库验证可行。因此,我们选择以 Web Components 为底层技术路径,结合工具链(如 StencilJS)实现跨框架适配与一致的设计规范落地。

组件库架构与设计规范

在明确目标与应用场景后,我们基于 Web Components 构建了跨框架组件库。整体过程可以沿着组件库架构图的层级顺序理解,从底层的设计规范到组件实现,再到框架适配,最后落地到各业务线支持的技术平台。

组件库架构概览

文章配图

设计规范统一

在组件开发前,我们首先与 UI 同学协作,共同梳理了统一的设计规范,并基于 Design Tokens 统一了颜色、字体、间距、圆角、阴影等基础变量;同时也定义了组件的样式与交互规范,以保证不同业务线组件的视觉与行为一致。

  1. Token 设计和实现

文章配图

在工程实现上,我们采用双层 Token 架构,同时支持 Sass 编译期 Token 与 CSS Variables 运行时 Token。编译期 Token 负责系统通用变量的静态管理和派生计算,运行时 Token 则支持多品牌定制与主题动态切换,以兼顾体系化管理与动态扩展的灵活性。

例如,以下示例定义了一组中性色阶,用于统一全局灰度体系:

// 透明黑衍生的中性灰(从浅到深)
$grays: (
  100: rgba(0, 0, 0, 0.03),
  500: rgba(0, 0, 0, 0.2),
  ... 
  900: rgba(0, 0, 0, 0.9)
);

// 便捷函数
@function gray($level) {
  @return map-get($grays, $level);
}

这些系统通用 Token(如间距、圆角、阴影等)通常不会被业务直接修改,但品牌色或主题相关的变量是支持覆盖的。例如 $brand-color 或 CSS Variable --brand-color 可以在不同品牌主题文件中自定义,并通过函数式工具,快速生成不同状态的颜色,实现差异化:

$brand-color: #409EFF;

// 透明度
@function brand-alpha($opacity) {
  @return rgba($brand-color, $opacity);
}

// 亮度调整
@function brand-lightness($delta) {
  @return adjust-color($brand-color, $lightness: $delta);
}

// 饱和度调整
@function brand-saturation($delta) {
  @return adjust-color($brand-color, $saturation: $delta);
}

// 色相调整
@function brand-hue($delta) {
  @return adjust-color($brand-color, $hue: $delta);
}

在构建阶段,我们会将 Sass Token 输出为 CSS Variables,确保运行时可覆盖与动态切换:

:root {
  --color-brand: #{$brand-color};
  --color-brand-text: #{brand-lightness(-30)};
  --gray-100: #{gray(100)};
  --gray-500: #{gray(500)};
  --gray-900: #{gray(900)};
}

通过这种方式,我们实现了 编译期 Token 的结构化管理 + 运行时变量覆盖 的双重能力,既保证了系统通用 Token 的一致性,又允许针对品牌和业务场景进行灵活扩展,同时兼顾了 Web Components Shadow DOM 的样式隔离特性,使组件在多框架、多终端下均能继承统一的主题。

  1. 组件设计示例

文章配图

  1. Icon 设计示例

为提升图标的可维护性和跨终端一致性,我们将图标统一为 SVG 字体进行管理。

文章配图

组件实现

在组件实现的技术选型上,我们选择 Stencil.js 作为核心开发框架。它能够在保留 Web Components 原生特性的同时,提供类型推导、属性声明、状态管理等工程化能力,极大简化了组件开发与跨框架适配的复杂度。关于 Stencil 本身的原理与生态网上资料已经较为丰富,这里就不再展开,更多可以参考官方文档或社区教程。在我们的实践中,Stencil 的关键价值主要体现在:组件的跨框架输出、类型一致性和样式隔离 三个方面。

接下来,我们以 ui-button 组件为例,展示具体实现思路与核心设计特性。

import { Component, Host, h, Prop, Listen } from '@stencil/core';
import { pxToVw } from '../../utils/utils';

@Component({
  tag: 'ui-button',
  styleUrl: './ui-button.css',
  shadow: true,
})
export class UiButton {
  /** 按钮类型 */
  @Prop() type: 'primary' | 'success' | 'danger' | 'warning' = 'primary';
  /** 按钮尺寸 */
  @Prop() size: 'small' | 'normal' | 'big' | 'large' = 'normal';
  /** 按钮图标 */
  @Prop() icon?: string;
  /** 按钮形状 */
  @Prop() shape: 'round' | 'square' = 'round';
  /** 禁用状态 */
  @Prop() disabled = false;
  /** 加载状态 */
  @Prop() loading = false;
  /** 加载类型 */
  @Prop() loadtype: 'circular' | 'spinner' = 'spinner';
  /** 加载颜色 */
  @Prop() loadingColor: string = 'currentColor';
  /** 加载大小 */
  @Prop() loadingSize: number = 20;

  renderIcon = () => {
    if (this.icon) {
      return <ui-icon name={this.icon} />;
    }
    if (this.loading) {
      return (
        <ui-loading
          color={this.loadingColor}
          size={pxToVw(this.loadingSize)}
          type={this.loadtype}
        />
      );
    }
    return null;
  };

  @Listen('click', { capture: true })
  onClick(e: Event) {
    if (this.disabled || this.loading) {
      e.stopPropagation();
    }
  }

  render() {
    const { type, size, shape, disabled } = this;
    return (
      <Host type={type} size={size} shape={shape} disabled={disabled}>
        {this.renderIcon()}
        <slot></slot>
      </Host>
    );
  }
}
:host {
  position: relative;
  display: inline-block;
  box-sizing: border-box;
  line-height: var(--button-height, 24px);
  text-align: center;
  border-radius: var(--button-border-radius, 8px);
  padding-left: var(--button-hspacing, 12px);
  padding-right: var(--button-hspacing, 12px);
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

:host([type="primary"]) {
  background-color: var(--color-brand);
  &:disabled {
    background-color: var(--gray-500);
    cursor: not-allowed;
  }
}

在这段代码中,我们通过 Prop 默认值和渲染逻辑处理按钮状态(如加载、禁用、图标),确保组件行为一致。通过 Shadow DOM 样式被隔离,同时结合 Design Token 管理颜色、尺寸等变量,实现跨业务、跨框架一致的视觉体验。

框架适配

在核心组件开发完成后,我们进入跨框架适配阶段。此阶段主要依赖 Stencil 的多框架输出能力,确保业务同学能够在熟悉的框架中直接使用组件,同时保持组件逻辑和样式的一致性。为此,我们使用了诸如 @stencil/react-output-target、@stencil/vue-output-target 和 stencil-vue2-output-target 等插件,分别生成 React、Vue3、Vue2 版本的组件库。

// stencil.config.ts
import { Config } from '@stencil/core';
import { vueOutputTarget } from "@stencil/vue-output-target";
import { vueOutputTarget as vue2OutputTarget } from "stencil-vue2-output-target";
import { reactOutputTarget } from "@stencil/react-output-target";

export const config: Config = {
  // ...
  outputTargets: [
    { type: "dist-custom-elements", ... },
    // React 输出
    reactOutputTarget({
      componentCorePackage: "@core-ui/components",
      proxiesFile: '../react/src/components.ts',
      ... }),
    // Vue3 输出
    vueOutputTarget({ ... }),
    // Vue2 输出
    vue2OutputTarget({ ... }),
  ],
};

框架适配与问题解决

到目前为止,一切都很顺利:我们使用 Stencil 编写出标准的 Web Components,并成功在 Vue 和 React 中集成使用。理论上,这样的组件可以在框架中直接使用,但实际使用过程中,我们发现一些细节问题,如:组件类名无法动态更新、事件绑定存在跨框架差异,以及低版本浏览器兼容等问题。

宿主类名覆盖导致组件'消失'问题

在 Stencil 中,我们通常使用 Host 组件为宿主元素(host element)添加默认类名。渲染后,宿主元素 <ui-button> 会包含两个类名:

<ui-button class="hydrated">...</ui-button>

其中 hydrated 为 Stencil 初始化完成后自动添加,用于控制可见性(加载前为 visibility: hidden)。

在业务层中,当我们给组件动态绑定类名,会发现组件在运行时突然'变白'或'消失'。例如:

// React
<UiButton className={dynamicCls}>Click</UiButton>
// Vue
<ui-button :class="dynamicCls">Click</ui-button>

造成这种现象的原因是因为 Stencil 的 hydrated 类名在组件可见性中起关键作用,而 React 与 Vue 在更新类名时会整体替换宿主元素的 class 属性,从而移除了 hydrated 和原始的 ui-button,导致组件保持隐藏状态。视觉上表现为组件'消失'或'变白',但实际上 DOM 仍然存在,只是不可见。

解决此问题的核心思路是:动态类名更新时保留内置类名,可从两种方向解决:

  • 框架层适配:在 React 中通过高阶组件 + ref 合并类名;在 Vue 中通过自定义包装组件或渲染函数,确保每次更新时保留 hydrated 与原始类。
  • 组件层适配:提供专用属性(如 cssClass),由组件内部合并到宿主元素上,从源头避免覆盖。

本质上,这是框架与 Web Components 在类名、样式、事件等宿主绑定策略上的差异问题。提前在封装层处理这些差异,可显著提升跨框架稳定性。

事件捕获与跨框架一致性问题

在 Stencil 中,我们通常通过 @Listen('click') 来监听点击事件。在实际使用中,我们发现行为在不同框架下存在差异:

在 Vue 中,即便按钮设置了 disabled,点击事件仍然会触发绑定的 handleClick;而在 React 中,则不会触发。

这种差异的原因在于 React 使用了合成事件系统,默认在冒泡阶段会禁用 disabled 元素的事件,而 Vue 监听的是原生事件,冒泡阶段仍会触发 click。

为了解决这个问题,可以将事件监听放在捕获阶段:

@Listen('click', { capture: true })

这样事件会先被拦截,从而保证在 Vue 和 React 中行为一致。这也反映出了不同框架在事件捕获和阻止机制上的差异,需要在组件封装层进行跨框架的适配。

低版本浏览器兼容与 Polyfill 策略

在我们的业务场景中,需要兼容到部分仍在使用 Android 5 和 iOS 10 的用户,必须支持较老版本(如 Chrome50)的 WebView。然而 Stencil 从 v3.0.0 开始,已不再支持 IE 11、Edge ≤ 18 和 Safari 10。虽然 v3.0.0 仍可通过一些配置和 polyfill 继续支持这些低版本,但 Stencil 最新版本(4.X)中这些功能已经被移除。因此,在实践中,我们最多只能使用 Stencil v3 进行开发。

我们可以使用 Stencil 2 和 3,配置 Stencil 构建支持 ES5 并开启必要的 extras,并引入 Web Components polyfill,保证自定义元素、Shadow DOM、CSS 变量、事件等特性在 Android 5 等低版本浏览器中能正常工作,从而避免组件渲染失败或报错。

{
  "buildEs5": true,
  "extras": {
    "cssVarsShim": true,
    "dynamicImportShim": true,
    "shadowDomShim": true,
    "safari10": true,
    "scriptDataOpts": true,
    "script.dataset appendChildSlotFix": false,
    "cloneNodeFix": false,
    "slotChildNodesFix": true
  }
}
其他常见集成问题与注意事项

在做框架集成过程中还存在以下典型问题需要关注:

  1. 事件监听问题:React 无法直接监听到 Web Components 发出的自定义事件,需通过 addEventListener 手动绑定。
  2. Ref 引用问题:不同框架对 Web Components 的 ref 获取方式不同(如 React 需用 forwardRef,Vue 需用 ref + $el)。
  3. 复杂 Props 处理:传递对象或数组时,框架的响应式系统可能导致数据格式不一致,需手动序列化/反序列化。

实践证明,跨框架集成并不存在'一步到位'的方案,它更像是一场持续的打磨——需要依靠封装与适配把分散的坑一点点补平,才能实现一致、可维护的使用体验。

未来展望

通过本次探索,我们验证了 Web Components 在多技术栈、多终端环境下的可行性和稳定性。借助封装与适配层,我们解决了框架差异、事件一致性以及样式隔离等关键问题,实现了可维护、可复用的组件体系。

未来,随着业务场景和组件数量的扩展,这套体系仍有优化空间:持续完善封装与适配策略,可以进一步降低跨框架使用门槛,使组件在不同技术栈间的行为更加一致。同时,团队可以积累更多最佳探索和使用经验,为多端开发和跨团队协作提供更稳固的技术支撑。

目录

  1. 前言
  2. 目标与场景
  3. 核心目标
  4. 应用场景
  5. 现状分析与开发优先级
  6. 业界方案调研与对比
  7. 组件库架构与设计规范
  8. 组件库架构概览
  9. 设计规范统一
  10. 组件实现
  11. 框架适配
  12. 框架适配与问题解决
  13. 宿主类名覆盖导致组件“消失”问题
  14. 事件捕获与跨框架一致性问题
  15. 低版本浏览器兼容与 Polyfill 策略
  16. 其他常见集成问题与注意事项
  17. 未来展望
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 数据结构:八种常见排序算法详解
  • 具身智能机器人运控通讯架构与实现指南
  • Jenkins 配置 GitLab 集成指南
  • 多机器人多 Agent 模式:构建 AI 助手团队
  • 基于 Python 与 AI Agent 的 Prompt 驱动零规则爬虫系统
  • 高校 AIGC 检测工具盘点与论文质量优化指南
  • 清华、国科大、智谱提出 LongReward:利用 AI 反馈优化长文本大模型
  • Python 爬虫快速入门实战指南
  • Transformer 核心机制解析:自注意力与多头机制
  • GitHub 学生认证及 VSCode 中 Copilot 使用教程
  • Git LFS 跨平台安装与配置实战指南
  • 通义万相 2.1 多模态生成技术解析与核心优势
  • RAG 与大模型对接的 9 大隐藏挑战及解决方案
  • Kotlin 根据日期获取星座的实现
  • OpenClaw.ai:Agentic AI 时代的 Spring Framework 时刻
  • 基于 FPGA 的北斗导航自适应抗干扰算法设计与实现
  • 大语言模型联邦微调综述:挑战、方法与应用
  • OpenClaw Web Search 工具配置与渠道详解
  • 数组模拟单双向链表:C++ 数据结构实战详解
  • 图的寻路算法详解:深度优先搜索 (DFS) 的 Java 实现

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online