├── LICENSE ├── components ├── button.js └── modal.js ├── index.html └── readme.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 MrXujiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /components/button.js: -------------------------------------------------------------------------------- 1 | class Button extends HTMLElement { 2 | constructor() { 3 | super(); 4 | // 获取模板内容 5 | let template = document.getElementById('btn_tpl'); 6 | let templateContent = template.content; 7 | 8 | const shadowRoot = this.attachShadow({ mode: 'open' }); 9 | const btn = document.createElement('button'); 10 | btn.appendChild(templateContent.cloneNode(true)); 11 | btn.setAttribute('class', 'xu-button'); 12 | // 定义并获取按钮类型 primary | warning | default 13 | const type = { 14 | 'primary': '#06c', 15 | 'warning': 'red', 16 | 'default': '#f0f0f0' 17 | } 18 | const btnType = this.getAttribute('type') || 'default'; 19 | const btnColor = btnType === 'default' ? '#888' : '#fff'; 20 | 21 | // 创建样式 22 | const style = document.createElement('style'); 23 | // 为shadow Dom添加样式 24 | style.textContent = ` 25 | .xu-button { 26 | position: relative; 27 | margin-right: 3px; 28 | display: inline-block; 29 | padding: 6px 20px; 30 | border-radius: 30px; 31 | background-color: ${type[btnType]}; 32 | color: ${btnColor}; 33 | outline: none; 34 | border: none; 35 | box-shadow: inset 0 5px 10px rgba(0,0,0, .3); 36 | cursor: pointer; 37 | } 38 | ` 39 | shadowRoot.appendChild(style); 40 | shadowRoot.appendChild(btn); 41 | // shadowRoot.appendChild(pElem); 42 | } 43 | attributeChangedCallback(name, oldValue, newValue) { 44 | console.log('changed.', name, oldValue, newValue); 45 | } 46 | } 47 | customElements.define('xu-button', Button); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Web Component 7 | 8 | 9 |

Web Component 实战 - 趣谈前端-徐小夕

10 |

1. Button

11 | 趣谈button 12 | 趣谈button 13 | 趣谈button 14 |

2. Modal

15 | 显示弹窗 16 | 关闭弹窗 17 |
web component 实现一个有点意思的modal组件?
18 | 19 | 20 | 23 | 36 | 37 | 38 | 47 | 48 | -------------------------------------------------------------------------------- /components/modal.js: -------------------------------------------------------------------------------- 1 | class Modal extends HTMLElement { 2 | constructor() { 3 | super(); 4 | // 获取模板内容 5 | let template = document.getElementById('modal_tpl'); 6 | let templateContent = template.content; 7 | 8 | const shadowRoot = this.attachShadow({ mode: 'open' }); 9 | const wrap = document.createElement('div'); 10 | const modal = document.createElement('div'); 11 | const header = document.createElement('header'); 12 | const btnClose = document.createElement('span'); 13 | const mask = document.createElement('div'); 14 | const footer = document.createElement('footer'); 15 | const btnCancel = document.createElement('xu-button'); 16 | const btnOk = document.createElement('xu-button'); 17 | 18 | // wrap 19 | wrap.setAttribute('class', 'wrap'); 20 | 21 | // modal 22 | modal.setAttribute('class', 'xu-modal'); 23 | 24 | // header 25 | let title = this.getAttribute('title'); 26 | header.textContent = title; 27 | btnClose.setAttribute('class', 'xu-close'); 28 | btnClose.textContent = 'x'; 29 | header.appendChild(btnClose); 30 | modal.appendChild(header); 31 | 32 | btnClose.addEventListener('click', () => { 33 | wrap.style.display = 'none'; 34 | }) 35 | 36 | // content 37 | modal.appendChild(templateContent.cloneNode(true)); 38 | 39 | // footer 40 | btnOk.setAttribute('type', 'primary'); 41 | const slot1 = document.createElement('span'); 42 | slot1.setAttribute('slot', 'btn-content'); 43 | slot1.textContent = '确认'; 44 | btnOk.appendChild(slot1); 45 | 46 | const slot2 = document.createElement('span'); 47 | slot2.setAttribute('slot', 'btn-content'); 48 | slot2.textContent = '取消'; 49 | btnCancel.appendChild(slot2); 50 | 51 | footer.appendChild(btnCancel); 52 | footer.appendChild(btnOk); 53 | modal.appendChild(footer); 54 | 55 | // mask 56 | mask.setAttribute('class', 'mask'); 57 | wrap.appendChild(mask); 58 | wrap.appendChild(modal); 59 | 60 | // 创建样式 61 | const style = document.createElement('style'); 62 | const width = this.getAttribute('width'); 63 | const isVisible = this.getAttribute('visible'); 64 | // 为shadow Dom添加样式 65 | style.textContent = ` 66 | .wrap { 67 | position: fixed; 68 | left: 0; 69 | right: 0; 70 | bottom: 0; 71 | top: 0; 72 | display: ${isVisible === 'true' ? 'block' : 'none'} 73 | } 74 | .xu-modal { 75 | position: absolute; 76 | z-index: 500; 77 | left: 50%; 78 | top: 50%; 79 | transform: translate(-50%, -50%); 80 | width: ${width}; 81 | box-shadow: 0 0 20px rgba(0,0,0, .2); 82 | background: #fff; 83 | border-radius: 3px; 84 | } 85 | header { 86 | padding: 10px 20px; 87 | border-bottom: 1px solid #f0f0f0; 88 | font-weight: bold; 89 | background: #f0f0f0; 90 | } 91 | .xu-close { 92 | float: right; 93 | color: #ccc; 94 | cursor: pointer; 95 | } 96 | footer { 97 | margin-top: 20px; 98 | border-top: 1px solid #f0f0f0; 99 | padding: 10px 20px; 100 | text-align: right; 101 | } 102 | .mask { 103 | position: fixed; 104 | left: 0; 105 | right: 0; 106 | bottom: 0; 107 | top: 0; 108 | background: rgba(0,0,0, .3); 109 | z-index: 100; 110 | } 111 | ` 112 | shadowRoot.appendChild(style); 113 | shadowRoot.appendChild(wrap); 114 | } 115 | connectedCallback(el) { 116 | console.log('insert dom', el) 117 | } 118 | disconnectedCallback() { 119 | console.log('Custom square element removed from page.'); 120 | } 121 | adoptedCallback() { 122 | console.log('Custom square element moved to new page.'); 123 | } 124 | attributeChangedCallback(name, oldValue, newValue) { 125 | if(oldValue) { 126 | const childrenNodes = this.shadowRoot.childNodes; 127 | for(let i = 0; i < childrenNodes.length; i++) { 128 | if(childrenNodes[i].nodeName === 'DIV' && childrenNodes[i].className === 'wrap') { 129 | if(newValue === 'true') { 130 | childrenNodes[i].style.display = 'block'; 131 | }else { 132 | childrenNodes[i].style.display = 'none'; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | // 如果需要在元素属性变化后,触发 attributeChangedCallback()回调函数, 139 | // 你必须监听这个属性。这可以通过定义observedAttributes() get函数来实现 140 | static get observedAttributes() { 141 | return ['visible']; 142 | } 143 | } 144 | customElements.define('xu-modal', Modal); -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![](https://user-gold-cdn.xitu.io/2020/6/23/172dce6378d1f00d?w=1768&h=764&f=png&s=87274) 2 | ## 前言 3 | 作为一名前端工程师,我们每天都在和组件打交道,我们也许基于**react/vue**使用过很多第三方组件库,比如**ant design**,**elementUI**,**iView**等,或者基于它们进行过组件的二次开发,比如**业务组件**,**UI组件**等,亦或者觉得团队能力很强,可以不依赖第三方而独立开发属于自己的组件库。无论何种形式,组件开发已然成为我们工作中的必备技能,为了更好的复用性和可维护性,组件化开发是必然选择,也正是因为组件化开发越来越重要,几年前web标准推出了**Web Component**这一概念,意在解决html原生标记语言复用性的问题。 4 | 5 | 目前**vue**或者**react**框架中也支持使用**Web Component**,而且在**Web Component**中也可以动态的调用**react**或者**vue**的api来实现组件或页面的渲染,这给我们开发者提供了更大的自由度,从而在日益复杂的项目中能使用更加灵活且优雅的方式来进行组件化开发。 6 | 7 | 我们使用**Web Component**可以通过原生的方式来实现组件化而不依赖与vue或者react这些第三方框架,并且现代浏览器对其支持还算不错,相信未来**Web Component**将会成为组件开发的趋势。所以接下来笔者将会带大家一步步来学习**Web Component**,并且使用**Web Component**实现两个常用组件: 8 | * **Button** 9 | * **Modal** 10 | 11 | 大家在掌握了**Web Component**之后可以开发更多自定义组件,那么写下来就来学习一下吧。 12 | 13 | ## 正文 14 | 在开始正文之前笔者还想多啰嗦一下,也是之前有很多朋友问我的问题:**如何在公司平衡好工作和成长?** 15 | 16 | 其实笔者也经历过这种迷茫期,之前因为公司业务繁忙而不得不忙于编写业务代码,几乎没有时间去学习和成长。有些时候项目做完之后又有新的需求要处理,感觉瞬间被掏空。(B端产品为了满足客户需求往往在产品把控上很难做取舍,因为客户就是上帝, 所以工程师和产品的关系很微妙~) 17 | ![](https://user-gold-cdn.xitu.io/2020/6/23/172dcdf5fa5369f9?w=1374&h=1104&f=png&s=153200) 18 | 一般情况下遇到以上的情景,作为一个合格的企业员工的,当然是业务和任务优先,在完成工作之后再去考虑成长和学习。当然公司也不会一直这么忙,所以当空闲的时候,我们可以好好利用(当然偶尔刷刷手机也是允许的,取决于个人)。 19 | 20 | 另一方面,我们可以通过提高工作效率来压缩工作时间,因为业务代码做多了总会有点规律和总结,如果整体架构设计的好,一般第一次做过了,第二次再遇到类似的业务几乎“秒关”,这一块对于前端来说,组件系统和模块化尤其重要;对于后端来说,微服务是很好的例子。 21 | 22 | 所以说如何学习和成长,以上两点是笔者3年工作的总结,希望能给大家以启发。 23 | 24 | 另一个问题就是如何快速掌握新技术?这个答案在这篇文章结束后,大家也许会明白些许。 25 | 26 | 好了,废话到此为止,接下来进入我们的**Web Component**实战。笔者对其知识点梳理成如下的思维导图: 27 | ![](https://user-gold-cdn.xitu.io/2020/6/22/172db26dad07b33b?w=1996&h=1184&f=png&s=278370) 28 | 29 | ### 1. Web Component基础知识 30 | **Web Components**主要由三项技术组成,分别为 31 | * **Custom elements(自定义元素)** 32 | * **Shadow DOM(影子DOM)** 33 | * **HTML templates(HTML模板)** 34 | 35 | 它们可以一起使用来创建功能强大的定制元素,并且可以在我们喜欢的任何地方重用,不必担心代码冲突。接下来笔者就分别介绍这三项技术。 36 | 37 | #### 1.1 Custom elements(自定义元素) 38 | **custom elements**也就是我们常说的自定义标签,它主要通过**CustomElementRegistry**接口来定义,**CustomElementRegistry.define(name, class, extends)** 方法用来注册一个**custom element**,该方法接受以下参数: 39 | 40 | * **name** 所创建的元素名称,且需符合 DOMString 标准的字符串。注意,custom element 的名称不能是单个单词,且其中必须要有短横线 41 | * **class** 用于定义元素行为的类 42 | * **extends** 可选参数,一个包含 extends 属性的配置对象,指定了所创建的元素继承自哪个内置元素,可以继承任何内置元素。 43 | 44 | 具体案例如下: 45 | ``` js 46 | customElements.define( 47 | 'word-count', 48 | class WordCount extends HTMLParagraphElement { 49 | constructor() { 50 | super(); 51 | 52 | // 元素的功能代码 53 | ... 54 | } 55 | }, { extends: 'p' }); 56 | ``` 57 | 接下来另一个比较重要的知识点就是**custom element**的**生命周期回调函数**,具体介绍如下: 58 | * **connectedCallback**:当 custom element首次被插入文档DOM时,被调用 59 | * **disconnectedCallback**:当 custom element从文档DOM中删除时,被调用 60 | * **adoptedCallback**:当 custom element被移动到新的文档时,被调用 61 | * **attributeChangedCallback**: 当 custom element增加、删除、修改自身属性时,被调用 62 | 63 | 大家可以先理解一下生命周期函数的用法,在下面的组件实战中会有详细的应用。 64 | 65 | #### 1.2 Shadow DOM(影子DOM) 66 | **Shadow DOM** 接口可以将一个隐藏的、独立的 DOM附加到一个元素上,并且允许将隐藏的 DOM 树附加到常规的 DOM 树中:**以 shadow root 节点为起始根节点,在这个根节点的下方,可以是任意元素,和普通的 DOM 元素一样**。MDN对其有一张详细的草图方便大家理解: 67 | ![](https://user-gold-cdn.xitu.io/2020/6/23/172dd3506c339abb?w=1138&h=543&f=png&s=66327) 68 | 上图中4个术语意思如下: 69 | * Shadow host:一个常规 DOM节点,Shadow DOM 会被附加到这个节点上。 70 | * Shadow tree:Shadow DOM内部的DOM树。 71 | * Shadow boundary:Shadow DOM结束的地方,也是常规 DOM开始的地方。 72 | * Shadow root: Shadow tree的根节点 73 | 74 | 如果我们想将一个 **Shadow DOM** 附加到 custom element 上,可以在 custom element 的构造函数中添加如下实现: 75 | ``` js 76 | class Button extends HTMLElement { 77 | constructor() { 78 | super(); 79 | let shadow = this.attachShadow({mode: 'open'}); 80 | } 81 | } 82 | ``` 83 | 我们将 Shadow DOM 附加到一个元素之后,可以使用 DOM APIs对它进行操作,如下: 84 | ``` js 85 | class Button extends HTMLElement { 86 | constructor() { 87 | super(); 88 | let shadow = this.attachShadow({mode: 'open'}); 89 | let para = document.createElement('p'); 90 | shadow.appendChild(para); 91 | } 92 | } 93 | ``` 94 | 我们甚至可以将样式插入到Shadow DOM中, 如下: 95 | ``` js 96 | let style = document.createElement('style'); 97 | 98 | style.textContent = ` 99 | .btn-wrapper { 100 | position: relative; 101 | } 102 | .btn { 103 | // ... 104 | } 105 | ` 106 | 107 | shadow.appendChild(style); 108 | ``` 109 | 以上是定义组件的最基本方式,一个完整的demo如下: 110 | ``` js 111 | class Button extends HTMLElement { 112 | constructor() { 113 | super(); 114 | let shadow = this.attachShadow({mode: 'open'}); 115 | let para = document.createElement('p'); 116 | shadow.appendChild(para); 117 | let style = document.createElement('style'); 118 | 119 | style.textContent = ` 120 | .btn-wrapper { 121 | position: relative; 122 | } 123 | .btn { 124 | // ... 125 | } 126 | ` 127 | 128 | shadow.appendChild(style); 129 | } 130 | } 131 | customElements.define('xu-button', Button); 132 | ``` 133 | #### 1.3 HTML templates(HTML模板) 134 | \