优雅地在 Vue、React 中使用 Web Components 封装的 UI 组件库

更新日期: 2022-07-09阅读: 1.7k标签: 组件
以下文章来源于政采云前端团队 ,作者海绵

作为一名前端攻城狮,相信大家也都在关注着前端的一些新技术,近些年来前端组件化开发已为常态,我们经常把重用性高的模块抽离成一个个的组件,来达到复用的目的,这样减少了我们的维护成本,提高了开发的效率。但是都有一个缺点离不开框架本身,因为我们浏览器本身解析不了那些组件。那么有没有一种技术也可以达到这种效果呢?答案就是今天的主角 Web Components。

Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。

目前 W3C 也在积极推动,并且浏览器的支持情况还不错。FireFox、Chrome、Opera 已全部支持,Safari 也大部分支持,Edge 也换成 webkit 内核了,离全面支持应该也不远了。当然社区也有兼容的解决方案 webcomponents/polyfills。

WebComponents 三要素和生命周期

Button 组件示例

首先我们就从一个最简单的 Button 组件开始,我们可以通过在组件中传入 type 来改变按钮的样式,并且动态监听了数据的变化。

// html
<cai-button type="primary">
<span slot="btnText">
按钮
</span>
</cai-button>
<template id="caiBtn">
<style>
.cai-button {
display: inline-block;
padding: 4px 20px;
font-size: 14px;
line-height: 1.5715;
font-weight: 400;
border: 1px solid #1890ff;
border-radius: 2px;
background-color: #1890ff;
color: #fff;
box-shadow: 0 2px #00000004;
}
.cai-button-warning {
border: 1px solid #faad14;
background-color: #faad14;
}
.cai-button-danger {
border: 1px solid #ff4d4f;
background-color: #ff4d4f;
}
</style>
<div class="cai-button"> <slot name="btnText"></slot> </div>
</template>
<script>
const template = document.getElementById("caiBtn");
class CaiButton extends HTMLElement {
constructor() {
super()
this._type = {
primary: 'cai-button',
warning: 'cai-button-warning',
danger: 'cai-button-danger',
}
// 开启 shadow dom
const shadow = this.attachShadow({
mode: 'open'
})
const type = this
const content = template.content.cloneNode(true) // 克隆一份 防止重复使用 污染
// 把响应式数据挂到 this
this._btn = content.querySelector('.cai-button')
this._btn.className += ` ${this._type[type]}`
shadow.appendChild(content)
}
static get observedAttributes() {
return ['type']
}
attributeChangedCallback(name, oldValue, newValue) {
this[name] = newValue;
this.render();
}
render() {
this._btn.className = `cai-button ${this._type[this.type]}`
}
}
// 挂载到 window
window.customElements.define('cai-button', CaiButton)
</script>

三要素、生命周期和示例的解析

  • Custom elements(自定义元素):一组 JavaScript api,允许您定义 custom elements 及其行为,然后可以在您的用户界面中按照需要使用它们。在上面例子中就指的是我们的自定义组件,我们通过 class CaiButton extends HTMLElement {} 定义我们的组件,通过 window.customElements.define('cai-button', CaiButton) 挂载我们的已定义组件。

  • Shadow DOM(影子 DOM ):一组 JavaScript API,用于将封装的“影子” DOM 树附加到元素(与主文档 DOM 分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。使用 const shadow = this.attachShadow({mode : 'open'}) 在 WebComponents 中开启。

  • HTML templates(HTML 模板)slot :template 可以简化生成 dom 元素的操作,我们不再需要 createElement 每一个节点。slot 则和 vue 里面的 slot 类似,只是使用名称不太一样。

    内部生命周期函数

  • connectedCallback : 当 WebComponents 第一次被挂在到 dom 上是触发的钩子,并且只会触发一次。类似 Vue 中的 mounted react 中的 useEffect(() => {}, []),componentDidMount。

  • disconnectedCallback : 当自定义元素与文档 DOM 断开连接时被调用。

  • adoptedCallback : 当自定义元素被移动到新文档时被调用。

  • attributeChangedCallback : 当自定义元素的被监听属性变化时被调用。上述例子中我们监听了 type 的变化,使 Button 组件呈现不同状态。

    虽然 WebComponents 有三个要素,但却不是缺一不可的,WebComponents 借助 shadow dom  来实现样式隔离,借助 templates 来简化标签的操作。

在这个例子用我们使用了 slot 传入了俩个标签之间的内容,如果我们想要不使用 slot 传入标签之间的内容怎么办?

我们可以通过 innerHTML 拿到自定义组件之间的内容,然后把这段内容插入到对应节点即可。

组件通信

了解上面这些基本的概念后,我们就可以开发一些简单的组件了,但是如果我们想传入一些复杂的数据类型(对象,数组等)怎么办?我们只传入字符串还可以么?答案是肯定的!

传入复杂数据类型

使用我们上面的 Button,我们不仅要改变状态,而且要想要传入一些配置,我们可以通过传入一个 JSON 字符串

// html
<cai-button id="btn">
</cai-button>
<script>
btn.setAttribute('config', JSON.stringify({icon: '', posi: ''}))
</script>

// button.js
class CaiButton extends HTMLElement {
constructor() {
xxx
}
static get observedAttributes() {
return ['type', 'config'] // 监听 config
}
attributeChangedCallback(name, oldValue, newValue) {
if(name === 'config') {
newValue = JSON.parse(newValue)
}
this[name] = newValue;
this.render();
}
render() {
}
}
window.customElements.define('cai-button', CaiButton)
})()

这种方式虽然可行但却不是很优雅。

  • 对于使用者说:我用你个组件你还要让我把所有的复杂类型都转换成字符串?

  • 对于开发组件者来说:我为什么要每次都 JSON.parse() 一下?

  • HTML 中会有很长的数据。


因此我们需要换一个思路,我们上面使用的方式都是 attribute 传值,数据类型只能是字符串,那我们可以不用它传值吗?答案当然也是可以的。和 attribute 形影不离还有我们 js 中的 property,它指的是 dom 属性,是 js 对象并且支持传入复杂数据类型。

// table 组件 demo,以下为伪代码 仅展示思路
<cai-table id="table">
</cai-table>

table.dataSource = [{ name: 'xxx', age: 19 }]
table.columns = [{ title: '', key: '' }]

这种方式虽然解决上述问题,但是又引出了新的问题 -- 自定义组件中没有办法监听到这个属性的变化,那现在我们应该怎么办?或许从一开始是我们的思路就是错的,显然对于数据的响应式变化是我们原生 js 本来就不太具备的能力,我们不应该把使用过的框架的思想过于带入,因此从组件使用的方式上我们需要做出改变,我们不应该过于依赖属性的配置来达到某种效果,因此改造方法如下。

<cai-table thead="Name|Age">
<cai-tr>
<cai-td>zs</cai-td>
<cai-td>18</cai-td>
</cai-tr>
<cai-tr>
<cai-td>ls</cai-td>
<cai-td>18</cai-td>
</cai-tr>
</cai-table>

我们把属于 HTML 原生的能力归还,而是不是采用配置的方式,就解决了这个问题,但是这样同时也决定了我们的组件并不支持太过复杂的能力。

状态的双向绑定

上面讲了数据的单向绑定,组件状态页面也会随之更新,那么我们怎么实现双向绑定呢?

接下来我们封装一个 input 来实现双向绑定。

<cai-input id="ipt" :value="data" @change="(e) => { data = e.detail }"></cai-input>

// js
(function () {
const template = document.createElement('template')
template.innerHTML = `
<style>
.cai-input {

}
</style>
<input type="text" id="caiInput">
`
class CaiInput extends HTMLElement {
constructor() {
super()
const shadow = this.attachShadow({
mode: 'closed'
})
const content = template.content.cloneNode(true)
this._input = content.querySelector('#caiInput')
this._input.value = this.getAttribute('value')
shadow.appendChild(content)
this._input.addEventListener("input", ev => {
const target = ev.target;
const value = target.value;
this.value = value;
this.dispatchEvent(new CustomEvent("change", { detail: value }));
});
}
get value() {
return this.getAttribute("value");
}
set value(value) {
this.setAttribute("value", value);
}
}
window.customElements.define('cai-input', CaiInput)
})()
  • 这样就封装了一个简单双向绑定的 input 组件,代码中 get / set 和 observedAttributes / attributeChangedCallback 前者是监听单个,后者可以监听多个状态改变并做出处理。

  • 这里面核心的一步是我们监听了这个表单的 input 事件,并且在每次触发 input 事件的时候触发 自定义的 change 事件 ,并且把输入的参数回传。

  • 那我们应该怎么使用呢?以 Vue 为例子, Vue 的双向绑定 v-model 其实是一个语法糖, 我们的组件则没有办法使用这个语法糖,与 v-model 不简化写法类似 <cai-input :value="data" @change="(e) => { data = e.detail }">

封装我们自己的组件库

设计目录结构

第一步:要有一个优雅的组价库我们首先要设计一个优雅的目录结构,设计目录结构如下

.
└── cai-ui
├── components // 自定义组件
| ├── Button
| | ├── index.js
| └── ...
└── index.js. // 主入口

独立封装

独立封装我们的组件,由于我们组件库中组件的引入,我们肯定是需要把每个组件封装到单独文件中的。

在我们的 Button/index.js 中写入如下:

(function () {
const template = document.createElement('template')
template.innerHTML = `
<style>
/* css 和上面一样 */
</style>
<div class="cai-button"> <slot name="text"></slot> </div>
`
class CaiButton extends HTMLElement {
constructor() {
super()
// 其余和上述一样
}
static get observedAttributes() {
return ['type']
}
attributeChangedCallback(name, oldValue, newValue) {
this[name] = newValue;
this.render();
}
render() {
this._btn.className = `cai-button ${this._type[this.type]}`
}
}
window.customElements.define('cai-button', CaiButton)
})()

封装到组件到单独的 js 文件中

全部导入和按需导入

  1. 支持全部导入,我们通过一个 js 文件全部引入组件

// index.js
import './components/Button/index.js'
import './components/xxx/xxx.js'
  1. 按需导入我们只需要导入组件的 js 文件即可如 import 'cai-ui/components/Button/index.js'

自定义配置主题

支持主题色可配置 我们只需把颜色写成变量即可,改造如下:

(function () {
const template = document.createElement('template')
template.innerHTML = `
<style>
/* 多余省略 */
.cai-button {
border: 1px solid var(--primary-color, #1890ff);
background-color: var(--primary-color, #1890ff);
}
.cai-button-warning {
border: 1px solid var(--warning-color, #faad14);
background-color: var(--warning-color, #faad14);
}
.cai-button-danger {
border: 1px solid var(--danger-color, #ff4d4f);
background-color: var(--danger-color, #ff4d4f);
}
</style>
<div class="cai-button"> <slot name="text"></slot> </div>
`
// 后面省略...
})()

这样我们就能在全局中修改主题色了。案例地址(https://github.com/lovelts/cai-ui)

在原生、Vue 和 React 中优雅的使用

在原生 HTML 中应用:

<script type="module">
import '//cai-ui';
</script>

<!--or-->
<script type="module" src="//cai-ui"></script>

<cai-button type="primary">点击</cai-button>
<cai-input id="caiIpt"></cai-button>
<script>
const caiIpt = document.getElementById('caiIpt')
/* 获取输入框的值有两种方法
* 1. getAttribute
* 2. change 事件
*/
caiIpt.getAttribute('value')
caiIpt.addEventListener('change', function(e) {
console.log(e); // e.detail 为表单的值
})
</script>

在 Vue 2x 中的应用:

// main.js
import 'cai-ui';

<template>
<div id="app">
<cai-button :type="type">
<span slot="text">哈哈哈</span>
</cai-button>
<cai-button @click="changeType">
<span slot="text">哈哈哈</span>
</cai-button>
<cai-input id="ipt" :value="data" @change="(e) => { data = e.detail }"></cai-input>
</div>
</template>
<script>
export default {
name: "App",
components: {},
data(){
return {
type: 'primary',
data: '',
}
},
methods: {
changeType() {
console.log(this.data);
this.type = 'danger'
}
},
};
</script>

在 Vue 3x 中的差异:

在最近的 Vue3 中,Vue 对 WebComponents 有了更好的支持。Vue  在 Custom Elements Everywhere 测试中获得了 100% 的完美分数(https://custom-elements-everywhere.com/libraries/vue/results/results.html)。但是还需要我们做出如下配置:

跳过 Vue 本身对组件的解析

custom Elements 的风格和 Vue 组件很像,导致 Vue 会把自定义(非原生的 HTML 标签)标签解析并注册为一个 Vue 组件,然后解析失败才会再解析为一个自定义组件,这样会消耗一定的性能并且会在控制台警告,因此我们需要在构建工具中跳过这个解析:

// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
plugins: [
vue({
template: {
compilerOptions: {
// 将所有包含短横线的标签作为自定义元素处理
isCustomElement: tag => tag.includes('-')
}
}
})
]
}

组件的具体使用方法和 Vue 2x 类似。

在 React 中的应用

import React, { useEffect, useRef, useState } from 'react';
import 'cai-ui'

function App() {
const [type, setType] = useState('primary');
const [value, setValue] = useState();
const iptRef = useRef(null)
useEffect(() => {
document.getElementById('ipt').addEventListener('change', function(e) {
console.log(e);
})
}, [])
const handleClick = () => {
console.log(value);
setType('danger')
}
return (
<div className="App">
<cai-button type={type}>
<span slot="text">哈哈哈</span>
</cai-button>
<cai-button onClick={handleClick}>
<span slot="text">点击</span>
</cai-button>
<cai-input id="ipt" ref={iptRef} value={value} ></cai-input>
</div>
);
}

export default App;

Web Components 触发的事件可能无法通过 React 渲染树正确的传递。你需要在 React 组件中手动添加事件处理器来处理这些事件。

在 React 使用有个点我们需要注意下,WebComponents 组件我们需要添加类时需要使用 claas 而不是 className

总结现阶段的劣势

看完这篇文章大家肯定会觉得为什么 WebComponents 实现了一份代码多个框架使用,却还没有霸占组件库的市场呢?我总结了以下几点:

  • 更加偏向于 UI 层面,与现在数据驱动不太符,和现在的组件库能力上相比功能会比较弱,使用场景相对单一。

  • 兼容性还有待提升:这里不仅仅指的是浏览器的兼容性,还有框架的兼容性,在框架中使用偶尔会发现意外的“惊喜”,并且写法会比较复杂。

  • 如果不借助框架开发的话,写法会返璞归真,HTML CSS JS 会糅合在一个文件,HTML CSS 都是字符串的形式 ,没有高亮,格式也需要自己调整,对于开发人员来说还是难受的。

  • 单元测试使用繁琐:单元测试是组件库核心的一项,但是在 WebComponents 中使用单元测试十分复杂。

参考文档:

  • WebComponents | MDN(https://developer.mozilla.org/en-US/docs/Web/Web_Components)

  • Vue 3.0 官方文档 (https://v3.cn.vuejs.org/guide/web-components.html#vue-%E4%B8%8E-web-components)

  • React 官方文档(https://zh-hans.reactjs.org/docs/web-components.html)


链接: https://fly63.com/article/detial/11878

Vuetify基于vue2.0,为移动而生的组件框架

Vuetify 支持SSR(服务端渲染),SPA(单页应用程序),PWA(渐进式Web应用程序)和标准HTML页面。 Vuetify是一个渐进式的框架,试图推动前端开发发展到一个新的水平。

Vue中插槽的作用_Vue组件插槽的使用以及调用组件内的方法

通过给组件传递参数, 可以让组件变得更加可扩展, 组件内使用props接收参数,slot的使用就像它的名字一样, 在组件内定义一块空间。在组件外, 我们可以往插槽里填入任何元素。slot-scope的作用就是把组件内的数据带出来

react 函数子组件(Function ad Child Component)

函数子组件(FaCC )与高阶组件做的事情很相似, 都是对原来的组件进行了加强,类似装饰者。FaCC,利用了react中children可以是任何元素,包括函数的特性,那么到底是如何进行增强呢?

Vue和React组件之间的传值方式

在现代的三大框架中,其中两个Vue和React框架,组件间传值方式有哪些?组件间的传值是灵活的,可以有多种途径,父子组件同样可以使用EventBus,Vuex或者Redux

vue.js自定义组件directives

自定义指令:以v开头,如:v-mybind。bind的作用是定义一个在绑定时执行一次的初始化动作,观察bind函数,它将指令绑定的DOM作为一个参数,在函数体中,直接操作DOM节点为input赋值。

vue中prop属性传值解析

prop的定义:在没有状态管理机制的时候,prop属性是组件之间主要的通信方式,prop属性其实是一个对象,在这个对象里可以定义一些数据,而这些数据可以通过父组件传递给子组件。 prop属性中可以定义属性的类型,也可以定义属性的初始值。

Web组件简介

Web组件由三个独立的技术组成:自定义元素。很简单,这些是完全有效的HTML元素,包含使用一组JavaScript API制作的自定义模板,行为和标记名称(例如,<one-dialog>)。

web组件调用其他web资源

web组件可以直接或间接的调用其他web资源。一个web组件通过内嵌返回客户端内容的另一个web资源的url来间接调用其他web资源。在执行时,一个web资源通过包含另一个资源的内容或者转发请求到另一个资源直接调用。

vue中如何实现的自定义按钮

在实际开发项目中,有时我们会用到自定义按钮;因为一个项目中,众多的页面,为了统一风格,我们会重复用到很多相同或相似的按钮,这时候,自定义按钮组件就派上了大用场,我们把定义好的按钮组件导出,在全局引用,就可以在其他组件随意使用啦,这样可以大幅度的提高我们的工作效率。

Vue子组件调用父组件的方法

Vue中子组件调用父组件的方法,这里有三种方法提供参考,第一种方法是直接在子组件中通过this.$parent.event来调用父组件的方法,第二种方法是在子组件里用$emit向父组件触发一个事件,父组件监听这个事件就行了。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!