基于 Vue 3 构建企业级 Web Components 组件库

前言

在前端技术栈百花齐放的今天,我们经常面临一个痛点:组件复用难。React 组件无法直接在 Vue 项目中使用,Vue 2 的组件难以平滑迁移到 Vue 3。

Web Components 的出现正是为了解决这个问题。它是一套 W3C 标准,允许开发者创建可重用、封装良好且独立于框架的 UI 组件。无论你的主应用是 Vue、React 还是纯原生 JS,Web Components 都能完美运行。

一、 技术全景:什么是 Web Components?

Web Components 并非单一技术,而是由四项核心技术组成的规范集合,旨在实现组件的高内聚与低耦合。

1.1 核心组成体系

我们可以通过下图理解其运作机制:

复制代码
graph TD
    WC[Web Components] --> CE[Custom Elements]
    WC --> SD[Shadow DOM]
    WC --> HT[HTML Templates]
    WC --> ES[ES Modules]

    subgraph "逻辑层: Custom Elements"
    CE --> CER[CustomElementRegistry]
    CE --> LC[生命周期回调]
    LC --> C1[connectedCallback <br/>(挂载)]
    LC --> C2[disconnectedCallback <br/>(卸载)]
    LC --> C3[attributeChangedCallback <br/>(属性变更)]
    end

    subgraph "视图层: Shadow DOM"
    SD --> SR[ShadowRoot]
    SD --> DOMI[DOM 隔离]
    SD --> CSSI[样式 隔离]
    end
  • Custom Elements:通过 CustomElementRegistry 定义浏览器直接识别的新标签(如 <tera-chat-root>)。

  • Shadow DOM :这是组件化的灵魂。它将组件内的 HTML 和 CSS 隐藏在 #shadow-root 中,完全隔离于外部文档。外部的 CSS 无法影响组件,组件的样式也不会污染外部。

  • HTML Templates:使用 <template> 标签定义结构。

  • ES Modules:标准的模块化加载方案。


二、 方案选型:为什么选择 Vue 3?

虽然原生 API 可以编写 Web Components,但通过 HTMLElement 手写繁琐的 DOM 操作和状态管理效率极低。

Vue 3 提供了 defineCustomElement API,让我们能用熟悉的 SFC (单文件组件) 语法开发,最后编译成标准的 Custom Element。

2.1 转换原理

Vue 编译器将组件转换为 Web Component 的流程如下:

复制代码
graph TD
    VueSFC[Vue 单文件组件 (.vue)] -->|编译| VueCE[defineCustomElement]
    
    VueCE -->|封装| CE[HTMLElement 类]
    
    subgraph "运行时行为"
    CE -->|Props 映射| Atts[HTML Attributes]
    CE -->|Emits 映射| Events[Custom Events]
    CE -->|挂载| SR[Shadow Root]
    end
    
    SR -->|注入| Styles[CSS (Inline)]
    SR -->|渲染| Template[DOM 结构]

三、 工程化架构

为了满足企业级开发需求(TypeScript、Pinia 状态管理、多环境构建),我们需要设计合理的目录结构。

3.1 项目结构 (vite-shadow-dom)

复制代码
vite-shadow-dom/
├── demo/                   # 调试/演示应用(模拟真实使用场景)
│   ├── main.ts             
│   └── index.html          
├── src/                    # 组件库源码
│   ├── components/
│   │   └── ChatRoot.vue    # 核心业务组件
│   ├── styles/             # 全局样式
│   ├── entry.ts            # 【核心】自定义元素注册入口
│   └── vite-env.d.ts       
├── scripts/                # 构建脚本 (npm publish, build)
├── vite.config.ts          # 标准构建配置 (Vue 3)
├── vite.compat.config.ts   # 兼容构建配置 (Vue 2/无框架)
└── package.json

3.2 产出物设计

为了兼顾不同使用场景,我们设计了两套构建产物:

  1. Standard (标准版):依赖外部 Vue 运行时,体积小。适用于宿主环境已经是 Vue 3 的项目。

  2. Compat (兼容版)内联 Vue 运行时。适用于 Vue 2、React 或 jQuery 等非 Vue 3 环境,避免版本冲突。


四、 核心代码实现

4.1 解决痛点:Shadow DOM 中的样式与状态管理

在 Shadow DOM 中使用 Vue 生态库(如 Pinia)和全局样式会遇到两个挑战:

  1. Pinia 挂载问题:Web Component 内部没有常规的 Vue App 实例。

  2. 样式隔离问题:全局 CSS 无法穿透 Shadow DOM。

我们需要在 entry.ts 入口文件中进行特殊处理:

复制代码
// src/entry.ts
import { defineCustomElement, provide, h } from 'vue';
import { createPinia, setActivePinia } from 'pinia';
import ChatRoot from './components/ChatRoot.vue';
// 利用 ?inline 导入样式字符串,而非通过 style 标签插入 head
import commonStyles from '@/styles/index.scss?inline'; 

// 定义组件标签名常量
export enum SHADOW_DOM {
    CHAT_ROOT = 'tera-chat-root',
    CHAT_ROOT_UMD = 'TeraChatRoot',
};

// 封装 defineCustomElement
const ChatRootElement = defineCustomElement({
    // 继承原始组件逻辑
    ...ChatRoot,
    setup(props, ctx) {
        // 1. 手动初始化 Pinia
        const pinia = createPinia();
        setActivePinia(pinia);
        // 注入到组件树中
        provide('pinia', pinia);
        
        // 2. 调用原始组件的 setup
        return ChatRoot.setup?.(props, ctx);
    },
    // 3. 注入样式:Vue 会自动将这些 CSS 字符串注入到 ShadowRoot 的 <style> 中
    styles: [commonStyles, ...(ChatRoot.styles || [])],
});

// 注册自定义元素(防止重复注册)
if (!customElements.get(SHADOW_DOM.CHAT_ROOT)) {
    customElements.define(SHADOW_DOM.CHAT_ROOT, ChatRootElement);
}

// 导出以便 UMD 环境挂载到 window
export { ChatRootElement as TeraShadowDom };
if (typeof window !== 'undefined') {
    (window as any)[SHADOW_DOM.CHAT_ROOT_UMD] = ChatRootElement;
}

CSS 中若使用了 :root 定义变量,在 Shadow DOM 中需替换为 :host,否则无法生效。

4.2 构建配置:多版本共存

我们需要两个 Vite 配置文件来应对不同场景。

Vue 2 兼容版配置 (vite.compat.config.ts):

复制代码
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        vue({ customElement: true }), // 开启 Custom Element 模式
    ],
    build: {
        lib: {
            entry: 'src/entry.ts',
            name: 'TeraShadowDomCompat',
            fileName: (format) => `tera-shadow-dom.vue2-compat.${format}.js`,
        },
        // 关键点:将 rollupOptions.external 设为空
        // 这样 Vue 运行时会被打包进组件库中,确保在 Vue 2 环境下也能运行 Vue 3 逻辑
        rollupOptions: {
            external: [], 
        },
    },
});

五、 组件使用指南

构建完成后,我们的组件就可以在任何地方使用了。

场景 1:原生 HTML (CDN 方式)

直接引入 UMD 文件,像使用 HTML 原生标签一样使用它。

复制代码
<body>
    <!-- 引入打包后的 JS -->
    <script src="./dist/tera-shadow-dom.vue2-compat.umd.js"></script>
    
    <!-- 直接使用标签 -->
    <tera-chat-root id="my-chat" token="sk-123456"></tera-chat-root>

    <script>
        const el = document.getElementById('my-chat');
        // 监听自定义事件
        el.addEventListener('btn-click', (e) => {
            console.log('Clicked:', e.detail);
        });
        // 动态修改属性
        el.setAttribute('token', 'new-token');
    </script>
</body>

场景 2:在 Vue 2 项目中集成

由于 Vue 2 不认识 defineCustomElement,必须引入我们的 Compat (兼容) 版本。

复制代码
// main.js
// 引入包含 Vue 3 运行时的兼容包
import '@baidu/vite-shadow-dom/dist/tera-shadow-dom.vue2-compat.es.js';

<!-- 组件内使用 -->
<template>
  <div>
    <!-- Vue 2 会将其视为原生标签,跳过组件解析 -->
    <tera-chat-root 
      :token="token" 
      @btn-click="handleClick"
    ></tera-chat-root>
  </div>
</template>

场景 3:在 Vue 3 项目中集成

Vue 3 环境天然支持,可以使用轻量版(不含 Vue 运行时)。

复制代码
// main.ts
import '@baidu/vite-shadow-dom'; // 引入注册逻辑

如果使用 TS,记得在 vue 模块中补充类型声明,否则 <tera-chat-root> 可能会报类型错误。


六、 总结

基于Web Components + Vue 3能够实现

  1. 样式隔离:Shadow DOM 彻底解决了 CSS 污染问题。

  2. 框架解耦:一次编写,到处运行(Vue2/3/React/jQuery)。

  3. 开发效率:利用 Vue 3 的响应式系统简化开发,利用 Vite 实现高效构建。

这种模式非常适合开发通用的业务组件库(如 AI 助手、支付弹窗、反馈组件),让基础设施团队能够跨越业务线技术栈的差异,提供统一的服务。

相关推荐
我是伪码农8 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king8 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳9 小时前
JavaScript 的宏任务和微任务
javascript
跳动的梦想家h9 小时前
环境配置 + AI 提效双管齐下
java·vue.js·spring
夏幻灵10 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星10 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_10 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝10 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions10 小时前
2026年,微前端终于“死“了
前端·状态模式