| 
                            
                                  我们知道的第一:我们熟知的HTML标签有 a, p, div, section, ul, li, h2, article, head, body, strong, video, audio 等等 第二:我们知道,a标签是链接,p标签是段落,div是块级,h2是字体,strong 是粗体,video 可以播放视频,标签可以添加click等事件等等 所以,那么显然我们知道这些标签的名称,而且知道他们的默认的css样式,而且知道他们默认的js事件 由此,我们是否可以自定义标签呢,由我们自己规定名称,规定默认css样式,规定标签默认显示dom结构,规定默认事件呢? 答案当然是肯定的!!下面就介绍主角Web组件 Web组件使用名称规范
	在定义它们时必须使用至少两个单词和一个连字符必须小写自定义元素不能自闭合 
	
		
			| 1 2 3 4 5 6 | class UserCard extends HTMLElement {   constructor() {     super();   } } window.customElements.define('user-card', UserCard); |  组件传参数并可以写模板包括js和css组件的样式应该与代码封装在一起,只对自定义元素生效,不影响外部的全局样式,:host伪类,指代自定义元素本身 
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <html>     <head></head>     <body>         <user-card name="Marty Mcfly"></user-card>         <template id="userCardTemplate">           <div class="profile-picture">             <img src="marty.png" alt="Marty Mcfly" />           </div>           <div class="name"></div>           <style>            :host {              display: flex;              align-items: center;              width: 450px;              height: 180px;              background-color: #d4d4d4;              border: 1px solid #d5d5d5;              box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);              border-radius: 3px;              overflow: hidden;              padding: 10px;              box-sizing: border-box;              font-family: 'Poppins', sans-serif;            }            .image {              flex: 0 0 auto;              width: 160px;              height: 160px;              vertical-align: middle;              border-radius: 5px;            }         </style>         </template>         <script>             class UserCard extends HTMLElement {               constructor() {                 super();                 var templateElem = document.getElementById('userCardTemplate');                 var content = templateElem.content.cloneNode(true);                 // 使用name参数                 content.querySelector('.container>.name').innerText = this.getAttribute('name');                 this.appendChild(content);               }             }             window.customElements.define('user-card', UserCard);            </script>     </body> </html> |  Shadow Dom 影子节点
	没有开启Shadow Dom 前template的内容会直接append到user-card节点上开启Shadow Dom后,template和user-card中间多了一个节点叫shadowRootattachShadow 的mode参数有 open 和 closed 两个不同参数,区别是open时,外部能访问内部节点,closed时完全隔离attachShadow 是大多数标签都支持的,比如 div,p,selection,但是a,ul,li等不支持 
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <script>     class UserCard extends HTMLElement {       constructor() {         super();         var shadow = this.attachShadow( { mode: 'closed' } );   /******这行开启shadow*****/         var templateElem = document.getElementById('userCardTemplate');         var content = templateElem.content.cloneNode(true);         // 使用name参数         content.querySelector('.container>.name').innerText = this.getAttribute('name');         shadow.appendChild(content);         /******这行template添加到shadow*****/       }     }     window.customElements.define('user-card', UserCard);    </script> |  类中的构造函数和钩子函数
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class UserCard extends HTMLElement {   // attributeChangedCallback 能监听的属性   static get observedAttributes() {return ['name', 'url']; }   constructor() {     super();     // 可以创Shadom,通过this.shadowRoot获取     // this.attachShadow({ mode: 'open' })   }   connectedCallback (){       console.log('钩子,元素append到ducument触发')   }   disconnectedCallback (){       console.log('钩子,元素从document删除触发')   }   // 只有observedAttributes 中监听的属性name,url变化会触发下面回调   attributeChangedCallback (attr, oldVal, newVal){       console.log('钩子,元素属性改变时触发')   } } window.customElements.define('user-card', UserCard); |  
	
		
			| 姓名 | 何时调用 |  
			| constructor | 元素的一个实例被创建。对于初始化状态、设置事件监听器或创建shadow Dom。 |  
			| connectedCallback | 每次将元素插入 DOM 时调用。一般主要工作代码写在这里。 |  
			| disconnectedCallback | 每次从 DOM 中删除元素时调用。一般写清理的一些代码。 |  
			| attributeChangedCallback(attrName, oldVal, newVal) | 观察属性在添加、删除、更新、替换时被调用。只有属性中列出的observedAttributes属性才会收到此回调。 |  
			| adoptedCallback | 自定义元素已被移动到一个新的document(例如有人称为document.adoptNode(el))。 |  getter/setter属性和属性反射html中attribute 和 类中property 是各自独立,想要建立映射需要手动设置 getter和setter生效 
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <html>   <head></head>   <body>     <user-card id="usercard" name="Marty"></user-card>     <script>       class UserCard extends HTMLElement {         _name = null;         constructor() {           super();         }         set name(value) {           this._name = name;         }         get name() {           return this._name;         }       }       window.customElements.define("user-card", UserCard);     </script>   </body> </html> |  测试 
	
		
			| 1 2 3 4 5 | document.getElementById('usercard').name = 'jack'  // 会进入setter document.getElementById('usercard').name  // 会进入getter,返回jack document.getElementById('usercard').getAttribute('name')  // 不会进入getter,返回Marty document.getElementById('usercard').setAttribute('name','bob')  // 不会进入setter document.getElementById('usercard').getAttribute('name')  // 不会进入getter,返回bob |  此时,我们看到 setAttribute 和 gettAttribute 都不会触发类中的 getter 和 setter 方法,但是可以看到如果是 .name 这样的方式可以触发 ,那么改造方式如下: 方式一 重写 setAttribute : 
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <html>   <head></head>   <body>     <user-card id="usercard" name="Marty"></user-card>     <user-card id="uc" name="Marty Mcfly"></user-card>     <script>       const rawSetAttribute = Element.prototype.setAttribute;       class UserCard extends HTMLElement {         _name = null;         constructor() {           super();           Element.prototype.setAttribute = function setAttribute(key, value) {             // 特定的指定name             if (key == "name") {               this.name = value; // 这样就能触发 setter             }             rawSetAttribute.call(this, key, value);           };         }         set name(value) {           debugger;           this._name = name;         }         get name() {           debugger;           return this._name;         }       }       window.customElements.define("user-card", UserCard);     </script>   </body> </html> |  方式二 重写 observedAttributes和attributeChangedCallback 监听 name : 
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <html>   <head></head>   <body>     <user-card id="uc" name="Marty Mcfly"></user-card>     <script>       class UserCard extends HTMLElement {         static get observedAttributes() {           return ["name"];         }         _name = null;         constructor() {           super();         }         attributeChangedCallback(attr, _oldVal, newVal) {           if (attr == "name") {             this.name = newVal;           }         }         set name(value) {           debugger;           this._name = name;         }         get name() {           debugger;           return this._name;         }       }       window.customElements.define("user-card", UserCard);     </script>   </body> </html> |  由此,可以看到组件在页面渲染时会进入setter方法,而且 setAttribute,getAttribute 均进入到setter方法 
	
		
			| 1 2 | document.getElementById('usercard').setAttribute('name','bob')  // 会进入setter document.getElementById('usercard').getAttribute('name')  // 会进入getter,返回bob |  扩展原生 HTML假设您想创建一个更高级的<button>. 与其复制<button>的行为和功能,更好的选择是使用自定义元素逐步增强现有元素。 
	扩展现有元素的主要好处是获得其所有特性(DOM 属性、方法、可访问性)。Safari浏览器兼容性待提高要扩展一个元素,您需要创建一个继承自正确 DOM 接口的类定义。前面例子都是继承HTMLElement,现在更多继承例如,按钮HTMLButtonElement,图片HTMLImageElement 
	
		
			| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class FancyButton extends HTMLButtonElement {     constructor() {         super();         this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));     }     drawRipple(x, y) {         let div = document.createElement('div');         div.classList.add('ripple');         this.appendChild(div);         div.style.top = `${y - div.clientHeight/2}px`;         div.style.left = `${x - div.clientWidth/2}px`;         div.style.backgroundColor = 'currentColor';         div.classList.add('run');         div.addEventListener('transitionend', e => div.remove());     } } customElements.define('fancy-button', FancyButton, {extends: 'button'}); |  注意:扩展基础元素时,对的调用 define() 略有变化。必需的第三个参数告诉浏览器您正在扩展哪个标签 
	
		
			| 1 | <button is="fancy-button" disabled>Fancy button!</button> |  
	
		
			| 1 2 3 4 | let button = document.createElement('button', {is: 'fancy-button'}); button.textContent = 'Fancy button!'; button.disabled = true; document.body.appendChild(button); |  
	
		
			| 1 2 3 | let button = new FancyButton(); button.textContent = 'Fancy button!'; button.disabled = true; |  
 |