跳到主要内容
基于 Web Components 的跨框架组件库实践 | 极客日志
JavaScript SaaS 大前端
基于 Web Components 的跨框架组件库实践 综述由AI生成 多技术栈并存导致组件重复开发和维护成本高,分享了基于 Web Components 构建跨框架组件库的实践。通过 Stencil.js 实现一次开发多端复用,结合 Design Tokens 统一设计规范。重点解决了宿主类名覆盖导致组件消失、事件捕获跨框架差异及低版本浏览器兼容等核心问题,提供了具体的配置与适配方案,验证了该方案在多技术栈环境下的可行性与稳定性。
游戏玩家 发布于 2026/4/5 更新于 2026/4/28 4 浏览前言
在业务早期阶段,产品需求迭代迅速,为了支持快速试错与灵活交付,团队内部形成了多种技术栈并存的局面:历史项目基于 Vue2,新业务则转向 React。同时,由于早期各项目独立推进,尚未形成统一的设计规范和组件标准,不同项目在组件实现方式、样式规范与交互体验上存在较大差异。
这种多样化在短期内带来了灵活性,使团队能够快速响应业务需求,但随着项目规模和业务复杂度的增加,也逐渐演变成了技术挑战:
组件复用困难 :相同功能组件需要在不同框架中重复实现。
维护成本增加 :功能或样式的调整须在多套组件库中分别修改。
用户体验不一致 :不同框架实现可能导致交互和视觉风格不统一。
为解决这些问题,我们移动端前端团队开始探索一种能够'一次开发,多处复用'的组件库方案。
目标与场景
核心目标
为了解决团队多框架并存、组件重复开发和体验不一致的痛点,我们确定了三大核心目标:
统一设计规范 :建立标准化设计体系和组件规范,确保视觉风格与交互行为在各业务线、各技术栈中保持一致。
跨框架复用 :构建框架无关的组件实现层,使同一组件可在 Vue、React、小程序等技术栈中复用。
提升交付一致性 :减少风格偏差,降低多端维护成本和迭代风险,快速响应业务需求。
应用场景
组件库主要面向以下场景:
多技术栈并存 :组件库提供统一的组件标准和实现,降低重复开发。
多终端 / 多容器运行 :支持 App WebView、小程序、移动浏览器等环境,保持一致体验。
多租户与模块化业务 :可快速适配多租户 SaaS、主题定制及模块化功能,实现灵活复用。
现状分析与开发优先级
在启动统一组件库建设前,我们对现有项目使用的组件库进行了调研,发现共有 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+ 组件,这里仅展示高频组件作为示例。
业界方案调研与对比
为实现跨框架组件复用与统一设计规范,我们调研了多种业界主流方案,包括多套组件库维护、单一框架统一、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 统一了颜色、字体、间距、圆角、阴影等基础变量;同时也定义了组件的样式与交互规范,以保证不同业务线组件的视觉与行为一致。
在工程实现上,我们采用双层 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 的样式隔离特性,使组件在多框架、多终端下均能继承统一的主题。
为提升图标的可维护性和跨终端一致性,我们将图标统一为 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 版本的组件库。
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" , ... },
reactOutputTarget ({
componentCorePackage : "@core-ui/components" ,
proxiesFile : '../react/src/components.ts' ,
... }),
vueOutputTarget ({ ... }),
vue2OutputTarget ({ ... }),
],
};
框架适配与问题解决 到目前为止,一切都很顺利:我们使用 Stencil 编写出标准的 Web Components,并成功在 Vue 和 React 中集成使用。理论上,这样的组件可以在框架中直接使用,但实际使用过程中,我们发现一些细节问题,如:组件类名无法动态更新、事件绑定存在跨框架差异,以及低版本浏览器兼容等问题。
宿主类名覆盖导致组件'消失'问题 在 Stencil 中,我们通常使用 Host 组件为宿主元素(host element)添加默认类名。渲染后,宿主元素 <ui-button> 会包含两个类名:
<ui-button class ="hydrated" > ...</ui-button >
其中 hydrated 为 Stencil 初始化完成后自动添加,用于控制可见性(加载前为 visibility: hidden)。
在业务层中,当我们给组件动态绑定类名,会发现组件在运行时突然'变白'或'消失'。例如:
<UiButton className={dynamicCls}>Click </UiButton >
<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
}
}
其他常见集成问题与注意事项
事件监听问题 :React 无法直接监听到 Web Components 发出的自定义事件,需通过 addEventListener 手动绑定。
Ref 引用问题 :不同框架对 Web Components 的 ref 获取方式不同(如 React 需用 forwardRef,Vue 需用 ref + $el)。
复杂 Props 处理 :传递对象或数组时,框架的响应式系统可能导致数据格式不一致,需手动序列化/反序列化。
实践证明,跨框架集成并不存在'一步到位'的方案,它更像是一场持续的打磨——需要依靠封装与适配把分散的坑一点点补平,才能实现一致、可维护的使用体验。
未来展望 通过本次探索,我们验证了 Web Components 在多技术栈、多终端环境下的可行性和稳定性。借助封装与适配层,我们解决了框架差异、事件一致性以及样式隔离等关键问题,实现了可维护、可复用的组件体系。
未来,随着业务场景和组件数量的扩展,这套体系仍有优化空间:持续完善封装与适配策略,可以进一步降低跨框架使用门槛,使组件在不同技术栈间的行为更加一致。同时,团队可以积累更多最佳探索和使用经验,为多端开发和跨团队协作提供更稳固的技术支撑。
相关免费在线工具 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