// 堆代码 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