在当下,前端三巨头vue react ng都是提倡组件化开发的,在原生领域,web-components也逐渐成为标准。近段时间大热的omi 就是基于web-components实现的
web-components主要由3部分组成
custom-elements
shadow-dom
slot template
custom-elements 从字面意思可以知道这是自定义元素的意思。区别于原生html元素,我们可以自己定义它的行为。按照是否从原生html元素继承,可分下面两类
两类custom元素
Autonomous custom elements。完全自定义元素
Customized built-in elements .从常规html元素继承的
生命周期 custom-elements 比较赞的一点是具有以下的生命周期
connectedCallback 连接到dom后触发 类似于react的componentDidMount,当自定义元素首次加载到dom会触发,如果我们想获取传入的attributes来选择展示内容的话,需要将逻辑放在这个周期内而不是constructor中,constructor是取不到attributes的值,还需要注意的是,受html限制,通过html传入的attributes值都是字符串
disconnectedCallback 当自定义元素从DOM树中脱离触发 对于绑定元素的事件监听,可以在这里进行解绑,防止内存泄漏
adoptedCallback 当自定义元素移动到新的document触发
attributeChangedCallback 自定义元素属性值改变时触发。这个需要配合static get observedAttributes(){return ['需要监听的属性']}
使用,表示哪些属性变化才会触发这个生命周期。对于动态attributes进行渲染,这个非常好用
一个Autonomous custom elements web-components通常使用方法如下
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 class App extends HTMLElement { static get observedAttributes () { return ['text' ]; } constructor ( ) { super (); const shadow = this .attachShadow ({mode : 'open' }); const div = document .createElement ('div' ); const style = document .createElement ('style' ); shadow.appendChild (style); shadow.appendChild (div); } connectedCallback ( ) {} disconnectedCallback ( ) {} adoptedCallback ( ) {} attributeChangedCallback (name, oldValue, newValue ) {} } customElements.define ('my-app' , App );
如果是扩展原生元素的web-components则是类似
1 2 3 4 class CustomP extends HTMLParagraphElement { ... } customElements.define ('custom-p' , CustomP ,{extend :'p' });
shadom-dom shadom-dom操作和平常的dong操作差不多,对this.attachShadow({mode: 'open'});
。shadow-dom最大的好处就是实现了dom隔离。例如css只会对内部的shadow-dom有效,并不影响外部的元素。这应该是css最完美的解决方案了,目前很多组件化css解决方案css modules、各种css in js都不太优雅
1 2 3 4 5 6 7 8 9 const shadow = this .attachShadow ({mode : 'open' }); const div = document .createElement ('div' ); const style = document .createElement ('style' ); shadow.appendChild (style); shadow.appendChild (div);
template 和 slot 类似于vue的概念,用来实现html复用和插槽效果
template结合custom-elements用法 1 2 3 4 5 6 7 8 9 10 <template id ="my-paragraph" > <style > p { color : white; background-color : #666 ; padding : 5px ; } </style > <p > My paragraph</p > </template >
1 2 3 4 5 6 7 8 9 10 11 12 customElements.define ('my-paragraph' , class extends HTMLElement { constructor ( ) { super (); let template = document .getElementById ('my-paragraph' ); let templateContent = template.content ; const shadowRoot = this .attachShadow ({mode : 'open' }) .appendChild (templateContent.cloneNode (true )); } })
slot用法则和vue的基本一致 使用 web-components的使用非常方便,有几种方法 1、直接html中使用自定义标签
1 <custom-element > </custom-element >
2、通过js引入
1 2 3 4 5 6 const CustomElement = customElements.get ('custom-element' );const customElement = new CustomElement ();document .createElement ('custom-elemen' )
实际开发结合polymer 体验更佳
最后写了个web-compoennts todolist
demo
代码如下
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 49 50 51 52 53 54 55 56 57 58 59 class TodoList extends HTMLElement { constructor ( ) { super (); this .shadowdom = this .attachShadow ({ mode : "open" }); this .handleRemove = this .handleRemove .bind (this ); } get data () { const dataAttribute = this .getAttribute ("data" ); if (dataAttribute) { return Array .isArray (dataAttribute) ? dataAttribute : JSON .parse (dataAttribute); } else { return []; } } set data (val ) { this .setAttribute ("data" , JSON .stringify (val)); this .render (); } handleRemove (e ) { this .remove (e.detail .index ); } connectedCallback ( ) { this .render (); this .shadowdom .addEventListener ("sub" , this .handleRemove ); } disconnectedCallback ( ) { this .shadowdom .removeEventListener ("sub" , this .handleRemove ); } render ( ) { let last = null ; while ((last = this .shadowdom .lastChild )) { this .shadowdom .removeChild (last); } this .data .forEach ((item, index ) => { const todoiterm = new (customElements.get ("todo-iterm" ))(); todoiterm.innerHTML = `<span slot='text'>${item} </span>` ; todoiterm.setAttribute ("data-index" , index); this .shadowdom .appendChild (todoiterm); }); } addIterm (text ) { this .data = [...this .data , text]; } remove (deleteIndex ) { this .data = this .data .filter ((item, index ) => index != deleteIndex); } } customElements.define ("todo-list" , TodoList );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class TodoIterm extends HTMLElement { constructor ( ) { super (); const template = document .getElementById ("list-item" ); const templateContent = template.content ; const shadowdom = this .attachShadow ({ mode : "open" }); shadowdom.appendChild (templateContent.cloneNode (true )); shadowdom.getElementById ("sub" ).onclick = e => { const event = new CustomEvent ("sub" , { bubbles : true , detail : { index : this .dataset .index }, }); this .dispatchEvent (event) }; } } customElements.define ("todo-iterm" , TodoIterm );
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 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <meta http-equiv ="X-UA-Compatible" content ="ie=edge" /> <title > web-components</title > <script src ="./TodoList.js" > </script > <script src ="./TodoIterm.js" > </script > </head > <body > <template id ="list-item" > <style > * { color : red; } </style > <li > <slot name ="text" > nothing write</slot > <button id ="sub" > -</button > </li > </template > <div > <input id ='input' /> <button id ='add' > +</button > </div > <script > const List = customElements.get ('todo-list' ); const todoList = new List () document .body .appendChild (todoList) document .getElementById ('add' ).onclick = function ( ){ const value = document .getElementById ('input' ).value todoList.addIterm (value) } </script > </body > </html >
一些需要注意的地方: 1、通过html传递属性值,由于是通过attributes传入,所以都是字符串 2、组件之间的通信传递需要通过自定义事件