闽公网安备 35020302035485号
// 堆代码 duidaima.com
// 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>
三要素、生命周期和示例的解析// 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)
})()
这种方式虽然可行但却不是很优雅。HTML 中会有很长的数据。
// 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 原生的能力归还,而是不是采用配置的方式,就解决了这个问题,但是这样同时也决定了我们的组件并不支持太过复杂的能力。<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 前者是监听单个,后者可以监听多个状态改变并做出处理。.那我们应该怎么使用呢? 以 vue 为例子,vue 的双向绑定 v-model 其实是一个语法糖, 我们的组件则没有办法使用这个语法糖,与 v-model 不简化写法类似 <cai-input :value="data" @change="(e) => { data = e.detail }">
.
└── cai-ui
├── components // 自定义组件
| ├── Button
| | ├── index.js
| └── ...
└── 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 文件中。// index.js import './components/Button/index.js' import './components/xxx/xxx.js'
2.按需导入我们只需要导入组件的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>
`
// 后面省略...
})()
这样我们就能在全局中修改主题色了。 <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 中的差异:// vite.config.js
import vue from '@vitejs/plugin-vue'
export default {
plugins: [
vue({
template: {
compilerOptions: {
// 将所有包含短横线的标签作为自定义元素处理
isCustomElement: tag => tag.includes('-')
}
}
})
]
}
组件的具体使用方法和 Vue 2x 类似。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