未来组件化开发趋势WebComponent
优点:原生组件,不需要框架,性能好代码少。
缺点:兼容性问题
组件化好处: 高内聚、可重用、可组合
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components
核心三项技术
- Custom elements:一组JavaScript API,允许您定义custom elements及其行为,然后可以在您的用户界面中按照需要使用它们
- Shadow DOM:一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
- HTML templates:
<template>
和<slot>
元素使您可以编写不在呈现页面中显示的标记模板。然后它们可以作为自定义元素结构的基础被多次重用。
WebComponent生命周期
connectedCallback
:当custom element首次被插入文档DOM时,被调用disconnectedCallback
:当 custom element从文档DOM中删除时,被调用adoptedCallback
:当 custom element被移动到新的文档时,被调用 (移动到iframe中)attributeChangedCallback
:当 custom element增加、删除、修改自身属性时,被调用
案例组件实现
shadowDOM完全隔离
组件间通信,通过dispatchEvent派发自定义监听事件
customEvent -> webcomponent 兼容性差,没有自动更新机制
实现自定义Button组件
index.html
-
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <style> :root{ --background-color: black; --text-color:yellow } </style> <hs-button type="primary">按钮</hs-button> <hs-button>按钮</hs-button> <!-- 内容是不会被渲染到视图上,不会影响页面展示,可以使用模板 --> <template id="btn"> <button class="hs-button"> <slot></slot> </button> </template> <script> class HSButton extends HTMLElement { constructor() { super(); let shadow = this.attachShadow({ mode: 'open' }); let btnTmpl = document.getElementById('btn'); let cloneTemplate = btnTmpl.content.cloneNode(true) const style = document.createElement('style'); let type = this.getAttribute('type') || 'default'; const btnList = { 'primary': { background: '#409eff', color: '#fff' }, 'default': { background: '#909399', color: '#fff' } } style.textContent = ` .hs-button{ outline:none; border:none; border-radius:4px; padding:5px 20px; display:inline-flex; background:var(--background-color,${btnList[type].background}); color:var(--text-color,${btnList[type].color}); cursor:pointer } ` // dom操作具备移动型 shadow.appendChild(style) shadow.appendChild(cloneTemplate) } } // 定义了一个自定义标签 组件 window.customElements.define('hs-button', HSButton) </script> </body> </html>
template
中的内容是我们定义的button
组件的样子。slot
可以获取自定义组件中的内容,插入到模板对应的位置
shadowDOM
shadow DOM 可以实现真正的隔离机制
实现自定义Collapse 折叠面板组件
collapse.html
-
collapse.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <hs-collapse > <hs-collapse-item title="Node" name="1"> <div>nodejs welcome</div> </hs-collapse-item> <hs-collapse-item title="react" name="2"> <div>react welcome</div> </hs-collapse-item> <hs-collapse-item title="vue" name="3"> <div>vue welcome</div> </hs-collapse-item> </hs-collapse> <!-- 没有实际意义, 不会渲染到页面上 --> <template id="collapse_tmpl"> <div class="hs-collapse"> <slot></slot> </div> </template> <template id="collapse_item_tmpl"> <div class="hs-collapse-item"> <div class="title"></div> <div class="content"> <slot></slot> </div> </div> </template> <!-- vite 实现原理 就依赖于 type="module" --> <script src="./index.js" type="module"></script> </body> </html>
collapse-item.js
-
collapse-item.js
class CollapseItem extends HTMLElement { constructor() { super(); let shadow = this.attachShadow({ mode: 'open' }); let tmpl = document.getElementById('collapse_item_tmpl'); let cloneTemplate = tmpl.content.cloneNode(true); let style = document.createElement('style'); this.isShow = true; // 标识自己是否需要显示 style.textContent = ` :host{ width:100%; } .title{ background:#f1f1f1; line-height:35px; height:35px; } .content{ font-size:14px; } ` shadow.appendChild(style) shadow.appendChild(cloneTemplate); this.titleEle = shadow.querySelector('.title'); this.titleEle.addEventListener('click',()=>{ // 如果将结果传递给父亲 组件通信? 派发一个事件 document.querySelector('hs-collapse').dispatchEvent(new CustomEvent('changeName',{ detail:{ name:this.getAttribute('name'), isShow:this.isShow } })) }) } static get observedAttributes() { // 监控属性的变化 return ['active', 'title', 'name'] } // update attributeChangedCallback(key, oldVal, newVal) { switch (key) { case 'active': this.activeList = JSON.parse(newVal); // 子组件接受父组件的数据 break; case 'title': this.titleEle.innerHTML = newVal; // 接受到title属性 作为dom的title break; case 'name': this.name = newVal break; } let name = this.name; if (this.activeList && name) { this.isShow = this.activeList.includes(name); this.shadowRoot.querySelector('.content').style.display = this.isShow ? 'block' : 'none' } } } export default CollapseItem
collapse.js
-
collapse.js
class Collapse extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: 'open' }); const tmpl = document.getElementById('collapse_tmpl'); let cloneTemplate = tmpl.content.cloneNode(true); let style = document.createElement('style'); // :host 代表的是影子的根元素 style.textContent = ` :host{ display:flex; border:3px solid #ebebeb; border-radius:5px; width:100%; } .hs-collapse{ width:100%; } ` shadow.appendChild(style); shadow.appendChild(cloneTemplate); // 从影子中拿到插槽 let slot = shadow.querySelector('slot'); // 监控slot变化 // 监听插槽的变化 slot.addEventListener('slotchange', (e) => { this.slotList = e.target.assignedElements(); this.render(); }) } static get observedAttributes() { // 监控属性的变化 return ['active'] } // update 属性变化时执行 attributeChangedCallback(key, oldVal, newVal) { if (key == 'active') { this.activeList = JSON.parse(newVal); this.render(); } } render() { // 获取插槽里的元素 if (this.slotList && this.activeList) { [...this.slotList].forEach(child => { child.setAttribute('active', JSON.stringify(this.activeList)) }); } } // connectedCallback(){ // console.log('插入到dom时执行的回调') // } // disconnectedCallback(){ // console.log('移除到dom时执行的回调') // } // adoptedCallback(){ // console.log('将组件移动到iframe 会执行') // } } export default Collapse
index.js
-
index.js
import Collapse from './collapse.js'; import CollapseItem from './collapse-item.js'; window.customElements.define('hs-collapse',Collapse); window.customElements.define('hs-collapse-item',CollapseItem); // 设置组件默认显示的状态 let defaultActive = ['1','2']; // name:1 name:2 默认展开 3 应该隐藏 // 拿不到影子里面的东西(也就是template里的) document.querySelector('hs-collapse').setAttribute('active',JSON.stringify(defaultActive)); // 每个item需要获取到defaultActive 和自己的name属性比较,如果在里面就显示,不在里面就隐藏 document.querySelector('hs-collapse').addEventListener('changeName',(e)=>{ let {isShow,name} = e.detail; if(isShow){ let index = defaultActive.indexOf(name); defaultActive.splice(index,1); }else{ defaultActive.push(name); } document.querySelector('hs-collapse').setAttribute('active',JSON.stringify(defaultActive)); }); // shadowDOM 完全隔离 // 组件间的通信 属性,事件 // customEvent -> webcomponent 兼容性差,没有自动更新机制