vue3+web component 实现跨UI框架的前端组件库

1,954 阅读11分钟

背景

前端组件库跨框架是什么?

前端组件库跨框架是指在不同的前端框架(如 React、Vue、Solid 等)之间共享和复用组件的能力。这种能力可以让开发者在不同的项目中使用同一套组件库,从而提高开发效率和代码复用性。

为什么需要做前端组件库跨框架?

首先,不同的前端框架有不同的语法和 API,如果每个框架都要写一套组件库,那么开发成本和维护成本都会很高。其次,跨框架的组件库可以让开发者更加灵活地选择框架,而不必担心组件库的兼容性问题。而 TinyVue 组件库是市面上实现的一种跨框架组件库实现方案,接下来我们用另一个角度,通过最近比较火的web component去探究一下可行性

web component

Web Components是由W3C Web Applications工作组提出的,最初于2011年在Web Components的规范草案中进行了描述。Web Components的概念被认为是一种在Web应用程序中开发可重用的自定义元素和组件的技术,旨在解决现有Web开发中存在的命名空间冲突、作用域问题和CSS样式污染等问题。最终它被分为三个部分:Custom Elements、Shadow DOM和HTML Templates,并在W3C标准化的过程中不断演变和改进。

虽然Web Components的概念最早于2011年提出,但是直到近些年(大约在2019年以后),随着浏览器支持的增加和相关框架(如React、Vue、Angular等)对其支持的改进,Web Components才开始得到广泛应用。

概念和基本用法

Custom elements(自定义元素):JavaScript API,允许定义custom elements及其行为,然后可以在我们的用户界面中按照需要使用它们。

Shadow DOM(影子DOM):JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,开发者可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。

HTML templates(HTML模板):和元素使开发者可以编写与HTML结构类似的组件和样式。然后它们可以作为自定义元素结构的基础被多次重用。

eg:

<body>
    <test-item-shadow></test-item-shadow>
    <div class="container">Test item</div>
    <style>
        .container {
            color: red !important;
        }
    </style>
    <template id="tpl">
        <style>
            .container {
                color: blue;
            }
        </style>
        <div class="container">Test Item</div>
    </template>
    <script>
        class TestItemShadow extends HTMLElement {
            constructor() {
                super();
            }
            connectedCallback() {
                const content = document.getElementById("tpl").content.cloneNode(true);
                const shadow = this.attachShadow({ mode: "open" });
                shadow.append(content);
            }
        }
        window.customElements.define("test-item-shadow", TestItemShadow); </script>
</body>

ps:通过浏览器开发者工具,可以查看html的结构,生成了一个#shadow-root(open)标签,内部包含了我们自定义的组件,不会受到外部影响。

  • HTML template(HTML 模板): <template><slot> 元素使你可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。

与vue封装组件的区别

区别Web ComponentsVue封装组件
技术标准一组标准化的浏览器API,包括自定义元素、Shadow DOM和HTML模板等一个流行的JavaScript框架,提供了数据驱动的视图和组件系统
模板语法相对较繁琐,并且需要使用JavaScript代码来动态操作DOM直观简洁的模板语法,易于描述组件的结构和行为
状态管理没有预置的状态管理机制,需要开发者自己实现提供了data、computed和watch等方式来管理组件的状态
兼容性不同浏览器对Web Components的支持程度不同,可能会存在兼容性问题可以在不同的浏览器和平台上正常运行,并提供了一致的开发体验
组件间通信需要开发者自己实现组件间通信机制可以通过props和自定义事件等方式来实现组件之间的通信

Web Components和Vue封装组件都可以用于创建可重用的自定义组件,但它们之间的区别在于技术标准、模板语法、状态管理、兼容性和组件间通信等方面。具体使用哪个方案,需要根据实际情况进行选择。如果需要更高的开发效率和更好的开发体验,可以选择使用Vue开发组件;如果需要更大的自由度和更强的跨平台兼容性,可以选择使用Web Components。

vue官方认为两者是互补的技术,vue3原生将Web Components结合了进来

Vue3+Web Components

Vue3在原生支持Web Components标准的自定义元素上进行了重大改进,并提供了更好的桥接能力,使得Vue3中的组件可以无缝地转换为Web Components标准的自定义元素,而无需使用第三方库。这使得Vue3更加适合于使用Web Components构建跨平台应用。

使用Vue3+Web Components封装组件库方案是一个更具有潜力和前瞻性的方案。Vue3提供了数据驱动的组件系统,并支持将Vue组件转换为符合 Web Components 标准的自定义元素,这使得 Vue 组件可以被其他框架或者原生的HTML应用所使用。Web Components则提供了标准化的浏览器API,可让开发者构建具有高度复用性和可扩展性的自定义组件。

Vue3+Web Components封装组件库方案具有以下优点:

  1. 跨平台兼容:由于Web Components标准是由W3C组织制定的,因此可以在所有现代浏览器中运行,并且不依赖于任何特定的JavaScript框架或库。
  2. 可重用性:由于Web Components具有标准化的API,因此组件具有高度的可重用性,并且可以轻松地在不同项目之间共享。
  3. 独立性:Vue3+Web Components封装组件库方案中的组件是独立的,不依赖于任何其他框架或库。这使得它们适用于各种不同的项目和应用程序,并且可以在不同的技术栈之间共享。

综上所述,Vue3+Web Components封装组件库方案是一个更具前瞻性和潜力的方案

具体实现

image.png

image.png

其中style展开是

image.png

参考 www.6hu.cc/archives/40…

web component实现微前端

见之前文章 juejin.cn/post/734609…

对webcomponent的一些思考

基础组件库 市面上已经有很多基于 Web Components 实现的跨框架 UI 组件库。 它可以同时在任意框架或无框架中使用。

  • Svelte

svelte是目前比较火的前端框架,有一个特点就是可以自定义组件转成通用的web组件(web component)。在多团队协同完成的大项目中,各个团队可能使用不同的框架版本,甚至不同的框架,这让不同项目之间的组件复用变得困难。这种情况下Svelte就变成了沟通跨越框架鸿沟的桥梁,使用Svelte开发的无框架依赖的Web Components,可以在各个框架间复用。

  • 微前端

微前端有几个基本概念: 技术栈无关、应用间隔离、独立开发。目前 Web Components 都符合。在一些微前端方案里就采用了Web Component的模式。

Web Component的缺点

再日常开发中,难免会遇到需要调整组件内部样式的时候,但由于shadowDOM的隔离机制,会导致我们很难去修改内部的样式。 在一些复杂的组件中,数据通信和事件传递存在一定使用成本。

问题描述原因
兼容性问题Vue 3 对 Web Component 的支持相对较弱,特别是在使用自定义事件和插槽时需要特殊处理,在不同浏览器上的兼容性也需要考虑。Vue 3 还不太稳定,Web Component 的规范也还在不断更新,这导致兼容性方面的问题比较多。
组件样式冲突在使用 Web Component 开发组件库时,组件的样式通常是封装在 Web Component 内部的,但是在 Vue 3 中,scoped 样式仅限于 Vue 组件内部,如果 Web Component 和 Vue 组件样式共存,容易出现冲突。scoped 样式的限制也导致 Web Component 开发中的一些样式问题。
数据驱动问题在 Vue 3 中,数据驱动是通过 props 和 emit 来实现的,但是在 Web Component 中,数据驱动需要自己实现,这增加了开发难度。在使用 Web Component 开发组件库时,需要考虑数据驱动方面的问题,这也增加了开发的难度。
模板处理问题在 Vue 3 中,可以使用模板来将复杂的 HTML 结构封装起来,但是在 Web Component 中,模板的处理需要自己实现。Web Component 不像 Vue 组件一样拥有内置的模板处理机制,这也增加了开发的难度。
文件大小问题使用 Web Component 开发组件库时,每个组件都需要封装成单独的 Web Component,这会导致组件库的文件大小增加。这是由 Web Component 的封装机制所决定的,同时也会影响到组件库的性能。

一些注意点

v-model的区别

在Vue2中,当我们在自定义组件中使用v-model指令时,它会被展开为一个名为value的prop属性和一个名为input的自定义事件。这个prop属性是用来将父组件中的数据传递给子组件的,而子组件通过触发input事件来将自己的数据传递回父组件。

也就是说,在Vue2中,v-model指令本质上是语法糖,用来简化父子组件之间双向数据绑定的操作。使用v-model指令时,只需要在父组件中使用v-model绑定子组件的值,然后在子组件中定义一个value prop和一个input事件,就可以实现父子组件之间的双向数据绑定了。

<my-component v-model="someValue"></my-component>`

它是下面这种写法的简写:

<my-component :value="someValue" @input="someValue = $event"></my-component>

而在Vue3中,v-model指令默认绑定到名为modelValue的prop属性和名为update:modelValue的自定义事件上,用于实现组件的双向绑定。与Vue2不同的是,Vue3中的v-model指令不再是简单的语法糖,而是被重构为一个真正的指令,其实现方式更加灵活。

<my-component v-model = "title"></my-component>` 

它是下面这种写法的简写:

<my-component :modelValue = "title" @update:modelValue = "title = $event"></my-component>` 

因此,Vue3中的v-model指令可以像Vue2一样实现父子组件之间的双向绑定,也可以通过其他方式实现双向绑定。例如,我们可以通过在父组件中使用v-bind绑定一个modelValue prop和一个update:modelValue事件来与子组件进行双向绑定。这种方式和Vue2中的v-model指令的实现相似,但更具有灵活性。

vue2组件迁移到vue3 v-model 的问题

image.png 解释:在你试图在一个v-model上使用一个已经被定义为prop的属性。具体来说,这可能是因为你想在一个自定义组件中使用v-model,而这个组件的props中有一个与v-model绑定的同名属性。

在Vue2中,当我们在一个子组件上使用v-model指令时,Vue会自动将该组件的value属性作为prop传递给父组件,并且在值发生变化时触发input事件来更新这个value属性的值。如果组件中定义了一个与v-model绑定同名的prop属性,Vue会自动忽略这个prop属性

如果在Vue3版本中同时使用了v-model指令和同名的属性,则会出现类似于“v-model cannot be used on a prop that is already used on the component”的报错。

这是因为vue3更好地遵循单向数据流的思想,所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

父组件实现 v-model 指令是通过

<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
//注意这里的@input里面的方法不是@input="someValue = $event" 而是通过emit子传父,让父组件去修改数据,更符合单向数据流
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

另一种实现 v-model 的方式是使用一个可写的,同时具有 getter 和 setter 的 computed 属性。get 方法需返回 modelValue prop,而 set 方法需触发相应的事件:

<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const value = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    emit('update:modelValue', value)
  }
})
</script>

<template>
  <input v-model="value" />
</template>

blog.csdn.net/qq_42543244…

封装组件的一般要素

以element-plus的Button看组件设计

image.png

如何更好的二次封装组件

属性的传递

通过$attr 属性进行

插槽的传递
获取ref

二次封装的组件需要获取到el-input

在vue3的setup语法糖的情况下,切记需要使用defineExpose这个方法!!!因为使用 <script setup> 的组件是默认关闭的——即通过模板引用或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。

是vue3新增的一个api,需要通过defineExpose编译器宏来显式指定在 <script setup> 组件中要暴露出去的属性:

提供文档和示例

Vue3官网对Web Component的解释

cn.vuejs.org/guide/extra…

使用Vue3 + Web Component开发组件库的方案

www.6hu.cc/archives/40…

Web Component的详解

zhuanlan.zhihu.com/p/150268517

实现跨框架的组件库文档的问题

juejin.cn/post/712647…

二次封装组件的技巧总结

juejin.cn/post/708344…

对比OpenTiny

www.bilibili.com/read/cv2309…