Web Components跨框架组件库探索
1. 前言
在网约车业务早期阶段,产品需求迭代迅速,为了支持快速试错与灵活交付, 内部形成了多种技术栈并存的局面:历史项目基于 Vue2,新业务则转向 React。同时,由于早期各项目独立推进,尚未形成统一的设计规范和组件标准,不同项目在组件实现方式、样式规范与交互体验上存在较大差异。
这种多样化在短期内带来了灵活性,使团队能够快速响应业务需求,但随着项目规模和业务复杂度的增加,也逐渐演变成了技术挑战:
- 组件复用困难:相同功能组件需要在不同框架中重复实现。
- 维护成本增加:功能或样式的调整须在多套组件库中分别修改。
- 用户体验不一致:不同框架实现可能导致交互和视觉风格不统一。
为解决这些问题,我们移动端前端团队今年开始探索一种能够“一次开发,多处复用”的组件库方案。
2. 目标与场景
2.1. 核心目标
为了解决团队多框架并存、组件重复开发和体验不一致的痛点,我们确定了三大核心目标:
- 统一设计规范:建立标准化设计体系和组件规范,确保视觉风格与交互行为在各业务线、各技术栈中保持一致。
- 跨框架复用:构建框架无关的组件实现层,使同一组件可在 Vue、React、小程序等技术栈中复用。
- 提升交付一致性:减少风格偏差,降低多端维护成本和迭代风险,快速响应业务需求。
2.2. 应用场景
组件库主要面向以下场景:
- 多技术栈并存:组件库提供统一的组件标准和实现,降低重复开发。
- 多终端 / 多容器运行:支持 App WebView、小程序、移动浏览器等环境,保持一致体验。
- 多租户与模块化业务:可快速适配多租户 SaaS、主题定制及模块化功能,实现灵活复用。
2.3. 现状分析与开发优先级
在启动统一组件库建设前,我们对现有项目使用的组件库进行了调研,发现共有 10 个独立组件库分布在不同业务线和技术栈中,但高频组件主要集中在少数几类,如 Button、Exception、Loading 等。
基于这些数据,我们制定了“以高频、通用组件优先建设”的策略,为快速落地和跨业务复用提供依据。
高频组件统计(示例)
组件 | 使用频次 | 技术栈分布 | 开发优先级 |
Exception | 138 | Vue、React | 高 |
Toast | 123 | Vue、React | 高 |
Button | 82 | Vue、React | 高 |
Loading | 62 | Vue、React | 高 |
Popup | 57 | Vue、React | 高 |
注:实际组件库包含 20+ 组件,这里仅展示高频组件作为示例。
3. 业界方案调研与对比
为实现跨框架组件复用与统一设计规范,我们调研了多种业界主流方案,包括多套组件库维护、单一框架统一、F/A 分层设计及 Web Components 标准方案。
方案 | 实现方式 | 优点 | 缺点 | 代表方案 |
多套组件库 | 针对不同框架分别维护 | 简单快速上手 | 成本高、复用性差 | - |
单一框架统一 | 全量迁移至单一框架(如 React) | 规范统一 复用性好 | 迁移成本、风险高 | Vant/Ant Design |
F/A分层设计 | 将组件拆分为基础逻辑层(Foundation)和框架适配层(Adapter) | 可复用核心逻辑、支持多技术栈扩展 | 抽象复杂,学习门槛高 | Semi Design |
Web Components | 基于浏览器原生标准实现 | 一次开发、多框架复用 | 工具链与兼容性需处理 | Shoelace/FAST/Taro/QuarkD |
综合对比可见,多套组件库方案维护成本高;单一框架方案迁移成本与风险较大;F/A 分层设计抽象度高、开发复杂。而 Web Components 方案在复用性、标准化及跨框架兼容性上最具长期价值,已被多个成熟组件库(Shoelace、FAST、Quarkd、Taro)验证可行。因此,我们选择以 Web Components 为底层技术路径,结合工具链(如 StencilJS)实现跨框架适配与一致的设计规范落地。
4. 组件库架构与设计规范
在明确目标与应用场景后,我们基于 Web Components 构建了跨框架组件库。整体过程可以沿着组件库架构图的层级顺序理解,从底层的设计规范到组件实现,再到框架适配,最后落地到各业务线支持的技术平台。
4.1. 组件库架构概览

4.2. 设计规范统一
在组件开发前,我们首先与 UI 同学协作,共同梳理了统一的设计规范,并基于 Design Tokens 统一了颜色、字体、间距、圆角、阴影等基础变量;同时也定义了组件的样式与交互规范,以保证不同业务线组件的视觉与行为一致。
- 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 的样式隔离特性,使组件在多框架、多终端下均能继承统一的主题。
- 组件设计示例

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

4.3. 组件实现
在组件实现的技术选型上,我们选择 Stencil.js 作为核心开发框架。它能够在保留 Web Components 原生特性的同时,提供类型推导、属性声明、状态管理等工程化能力,极大简化了组件开发与跨框架适配的复杂度。关于Stencil 本身的原理与生态网上资料已经较为丰富,这里就不再展开,更多可以参考官方文档或社区教程。在我们的实践中,Stencil 的关键价值主要体现在:组件的跨框架输出、类型一致性和样式隔离 三个方面。
接下来,我们以 Button 组件为例,展示具体实现思路与核心设计特性
import { Component, Host, h, Prop, Listen } from '@stencil/core'; import { pxToVw } from '../../utils/utils'; @Component({ tag: 'blm-button', styleUrl: './blm-button.css', shadow: true, }) export class BlmButton { /** 按钮类型 */ @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 <blm-icon name={this.icon} />; } if (this.loading) { return ( <blm-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 管理颜色、尺寸等变量,实现跨业务、跨框架一致的视觉体验。
4.4. 框架适配
在核心组件开发完成后,我们进入跨框架适配阶段。此阶段主要依赖 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: '@leopard-h5/components', proxiesFile: '../react/src/components.ts', ... }), // Vue3 输出 vueOutputTarget({ ... }), // Vue2 输出 vue2OutputTarget({ ... }), ], }; 5. 框架适配与问题解决
到目前为止,一切都很顺利:我们使用 Stencil 编写出标准的 Web Components,并成功在 Vue 和 React中集成使用。理论上,这样的组件可以在框架中直接使用,但实际使用过程中,我们发现一些细节问题如:组件类名无法动态更新、事件绑定存在跨框架差异,以及低版本浏览器兼容等问题。
5.1. 宿主类名覆盖导致组件“消失”问题
在 Stencil 中,我们通常使用Host 组件为宿主元素(host element)添加默认类名,例如:
import { Component, Host, h } from '@stencil/core' @Component({ tag: 'blm-button', }) export class BlmButton { render() { return ( <Host> <slot></slot> </Host> ) } } 渲染后,宿主元素<blm-button>会包含两个类名:
<blm-button>...</blm-button>其中hydrated为 Stencil 初始化完成后自动添加,用于控制可见性(加载前为visibility: hidden)
在业务层中,当我们给组件动态绑定类名,会发现组件在运行时突然“变白”或“消失”。例如:
// React <BlmButton className={dynamicCls}>Click</BlmButton> // Vue <blm-button :class="dynamicCls">Click</blm-button> 造成这种现象的原因是因为Stencil 的 hydrated类名在组件可见性中起关键作用,而 React 与 Vue 在更新类名时会整体替换宿主元素的class属性,从而移除了hydrated 和原始的 blm-button,导致组件保持隐藏状态。视觉上表现为组件“消失”或“变白”,但实际上 DOM 仍然存在,只是不可见。
解决此问题的核心思路是:动态类名更新时保留内置类名,可从两种方向解决:
- 框架层适配:在 React 中通过高阶组件 + ref 合并类名;在 Vue 中通过自定义包装组件或渲染函数,确保每次更新时保留
hydrated与原始类。 - 组件层适配:提供专用属性(如
cssClass),由组件内部合并到宿主元素上,从源头避免覆盖。
本质上,这是框架与 Web Components 在类名、样式、事件等宿主绑定策略上的差异问题。提前在封装层处理这些差异,可显著提升跨框架稳定性。
5.2. 事件捕获与跨框架一致性问题
在 Stencil 中,我们通常通过 @Listen('click') 来监听点击事件,例如:
@Listen('click') onClick(e: Event) { if (this.disabled || this.loading) { e.stopPropagation(); } } // Vue <blm-button disabled @click="handleClick">默认按钮</blm-button> // React <BlmButton disabled onClick={handleClick}>默认按钮</BlmButton>在实际使用中,我们发现行为在不同框架下存在差异:
在 Vue 中,即便按钮设置了 disabled,点击事件仍然会触发绑定的 handleClick;而在 React 中,则不会触发。
这种差异的原因在于 React 使用了合成事件系统,默认在冒泡阶段会禁用disabled 元素的事件,而 Vue 监听的是原生事件,冒泡阶段仍会触发 click。
为了解决这个问题,可以将事件监听放在捕获阶段:
@Listen('click', { capture: true })这样事件会先被拦截,从而保证在 Vue 和 React 中行为一致。这也反映出了不同框架在事件捕获和阻止机制上的差异,需要在组件封装层进行跨框架的适配。
5.3. 低版本浏览器兼容与 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, // 支持 CSS 变量 dynamicImportShim: true, // 支持动态 import shadowDomShim: true, // 支持 Shadow DOM safari10: true, // 修复 Safari 10 特性问题 scriptDataOpts: true, // 兼容低版本浏览器 script.dataset appendChildSlotFix: false, // 可选修复 slot appendChild 问题 cloneNodeFix: false, // 可选修复 cloneNode 问题 slotChildNodesFix: true, // 修复 slot.childNodes 在低版本浏览器获取异常 }, } 5.4. 其他常见集成问题与注意事项
在做框架集成过程中还存在以下典型问题需要关注:
- 事件监听问题:React 无法直接监听到 Web Components 发出的自定义事件,需通过
addEventListener手动绑定。 - Ref 引用问题:不同框架对 Web Components 的 ref 获取方式不同(如 React 需用
forwardRef,Vue 需用ref+$el)。 - 复杂 Props 处理:传递对象或数组时,框架的响应式系统可能导致数据格式不一致,需手动序列化/反序列化。
实践证明,跨框架集成并不存在“一步到位”的方案,它更像是一场持续的打磨——需要依靠封装与适配把分散的坑一点点补平,才能实现一致、可维护的使用体验。
6. 未来展望
通过本次探索,我们验证了 Web Components 在多技术栈、多终端环境下的可行性和稳定性。借助封装与适配层,我们解决了框架差异、事件一致性以及样式隔离等关键问题,实现了可维护、可复用的组件体系。
未来,随着业务场景和组件数量的扩展,这套体系仍有优化空间:持续完善封装与适配策略,可以进一步降低跨框架使用门槛,使组件在不同技术栈间的行为更加一致。同时,团队可以积累更多最佳探索和使用经验,为多端开发和跨团队协作提供更稳固的技术支撑。