经过《WebComponent魔法堂:深究Custom Element 之 面向痛点编程》,咱们明白到其实Custom Element并非什么新东西,咱们甚至能够在IE5.5上定义本身的alert
元素。但这种简单粗暴的自定义元素并非咱们须要的,咱们须要的是具备如下特色的自定义元素:html
自定义元素可经过原有的方式实例化(<custom-element></custom-element>
,new CustomElement()
和document.createElement('CUSTOM-ELEMENT')
)html5
可经过原有的方法操做自定义元素实例(如document.body.appendChild
,可被CSS样式所修饰等)python
能监听元素的生命周期
而Google为首提出的H5 Custom Element让咱们能够在原有标准元素的基础上向浏览器注入各类抽象层次更高的自定义元素,而且在元素CRUD操做上与原生API无缝对接,编程体验更平滑。下面咱们一块儿来经过H5 Custom Element来从新定义alert
元素吧!web
在正式撸代码前我想让各位最头痛的事应该就是如何命名元素了,下面3个因素将影响咱们的命名:chrome
命名冲突。自定义组件如同各类第三方类库同样存在命名冲突的问题,那么很天然地会想到引入命名空间来解决,但因为组件的名称并不涉及组件资源加载的问题,所以咱们这里简化一下——为元素命名添加前缀便可,譬如采用很JAVA的com-cnblogs-fsjohnhuang-alert
。django
语义化。语义化咱们理解就是元素名称达到望文生义的境界,譬如x-alert
一看上去就是知道x
是前缀而已跟元素的功能无关,alert
才是元素的功能。编程
足够的吊:)高大上的名称总让人赏心悦目,就像咱们项目组以前开玩笑说要把预警系统更名为"超级无敌全球定位来料品质不间断跟踪预警综合平台",呵呵!
除了上述3点外,H5规范中还有这条规定:自定义元素必须至少包含一个连字符,即最简形式也要这样a-b
。而不带连字符的名称均留做浏览器原生元素使用。换个说法就是名称带连字符的元素被识别为有效的自定义元素,而不带连字符的元素要么被识别为原生元素,要么被识别为无效元素。数组
const compose = (...fns) => { const lastFn = fns.pop() fns = fns.reverse() return a => fns.reduce((p, fn) => fn(p), lastFn(a)) } const info = msg => console.log(msg) const type = o => Object.prototype.toString.call(o) const printType = compose(info, type) const newElem = tag => document.createElement(tag) // 建立有效的自定义元素 const xAlert = newElem('x-alert') infoType(xAlert) // [object HTMLElement] // 建立无效的自定义元素 const alert = newElem('alert') infoType(alert) // [object HTMLUnknownElement] // 建立有效的原生元素 const div = newElem('div') infoType(div) // [object HTMLDivElement]
那若是我偏要用alert
来自定义元素呢?浏览器自当会说一句“悟空,你又调皮了”promise
如今咱们已经经过命名规范来有效区分自定义元素和原生元素,而且经过前缀解决了命名冲突问题。嘿稍等,添加前缀真的是解决命名冲突的好方法吗?这其实跟经过添加前缀解决id冲突同样,假若有两个元素发生命名冲突时,咱们就再把前缀加长直至再也不冲突为止,那就有可能出现很JAVA的com-cnblogs-fsjohnhuang-alert
的命名,噪音明显有点多,直接下降语义化的程度,重点还有每次引用该元素时都要敲这么多字符,打字的累看的也累。这一切的根源就是有且仅有一个Scope——Global Scope,所以像解决命名冲突的附加信息则没法经过上下文来隐式的提供,直接致使须要经过前缀的方式来硬加上去。
前缀的方式我算是认了,但能不能少打写字呢?像命名空间那样
木有命名冲突时浏览器
#!usr/bin/env python # -*- coding: utf-8 -*- from django.http import HttpResponse def index(request): return HttpResponse('Hello World!')
存在命名冲突时
#!usr/bin/env python # -*- coding: utf-8 -*- import django.db.models import peewee type(django.db.models.CharField) type(peewee.CharField)
前缀也能有选择的省略就行了!
对元素命名吐嘈一地后,是时候把玩API了。
/** x-alert元素定义 **/ const xAlertProto = Object.create(HTMLElement.prototype, { /* 元素生命周期的事件 */ // 实例化时触发 createdCallback: { value: function(){ console.log('invoked createCallback!') const raw = this.innerHTML this.innerHTML = `<div class="alert alert-warning alert-dismissible fade in"> <button type="button" class="close" aria-label="Close"> <span aria-hidden="true">×</span> </button> <div class="content">${raw}</div> </div>` this.querySelector('button.close').addEventListener('click', _ => this.close()) } }, // 元素添加到DOM树时触发 attachedCallback: { value: function(){ console.log('invoked attachedCallback!') } }, // 元素DOM树上移除时触发 detachedCallback: { value: function(){ console.log('invoked detachedCallback!') } }, // 元素的attribute发生变化时触发 attributeChangedCallback: { value: function(attrName, oldVal, newVal){ console.log(`attributeChangedCallback-change ${attrName} from ${oldVal} to ${newVal}`) } }, /* 定义元素的公有方法和属性 */ // 重写textContent属性 textContent: { get: function(){ return this.querySelector('.content').textContent }, set: function(val){ this.querySelector('.content').textContent = val } }, close: { value: function(){ this.style.display = 'none' } }, show: { value: function(){ this.style.display = 'block' } } }) // 向浏览器注册自定义元素 const XAlert = document.registerElement('x-alert', { prototype: xAlertProto }) /** 操做 **/ // 实例化 const xAlert1 = new XAlert() // invoked createCallback! const xAlert2 = document.createElement('x-alert') // invoked createCallback! // 添加到DOM树 document.body.appendChild(xAlert1) // invoked attachedCallback! // 从DOM树中移除 xAlert1.remove() // invoked detachedCallback! // 仅做为DIV的子元素,而不是DOM树成员不会触发attachedCallback和detachedCallback函数 const d = document.createElement('div') d.appendChild(xAlert1) xAlert1.remove() // 访问元素实例方法和属性 xAlert1.textContent = 1 console.log(xAlert1.textContent) // 1 xAlert1.close() // 修改元素实例特性 xAlert1.setAttribute('d', 1) // attributeChangedCallback-change d from null to 1 xAlert1.removeAttribute('d') // attributeChangedCallback-change d from 1 to null // setAttributeNode和removeAttributeNode方法也会触发attributeChangedCallback
上面经过定义x-alert
元素展示了Custom Element的全部API,其实就是继承HTMLElement
接口,而后选择性地实现4个生命周期回调方法,而在createdCallback
中书写自定义元素内容展开的逻辑。另外能够定义元素公开属性和方法。最后经过document.registerElement
方法告知浏览器咱们定义了全新的元素,你要好好对它哦!
那如今的问题在于假如<x-alert></x-alert>
这个HTML Markup出如今document.registerElement
调用以前,那会出现什么状况呢?这时的x-alert
元素处于unresolved状态,而且能够经过CSS Selector :unresolved
来捕获,当执行document.registerElement
后,x-alert
元素则处于resolved状态。因而可针对两种状态做样式调整,告知用户处于unresolved状态的元素暂不可用,敬请期待。
<style> x-alert{ display: block; } x-alert:unresolved{ content: 'LOADING...'; } </style>
有时候咱们只是想在现有元素的基础上做些功能加强,假若又要从头作起那也太折腾了,幸亏Custom Element规范早已为咱们想好了。下面咱们来对input元素做加强
const xInputProto = Object.create(HTMLInputElement.prototype, { createdCallback: { value: function(){ this.value = 'x-input' } }, isEmail: { value: function(){ const val = this.value return /[0-9a-zA-Z]+@\S+\.\S+/.test(val) } } }) document.registerElement('x-input', { prototype: xInputProto, extends: 'input' }) // 操做 const xInput1 = document.createElement('input', 'x-input') // <input is="x-input"> console.log(xInput1.value) // x-input console.log(xInput1.isEmail()) // false
Custom Element API如今已经升级到v1版本了,其实就是提供一个专门的window.customElements
做为入口来统一管理和操做自定义元素,而且以对ES6 class更友善的方式定义元素,其中的步骤和概念并无什么变化。下面咱们采用Custom Element v1的API重写上面两个示例
从头定义
class XAlert extends HTMLElement{ // 至关于v0中的createdCallback,但要注意的是v0中的createdCallback仅元素处于resolved状态时才触发,而v1中的constructor就是即便元素处于undefined状态也会触发,所以尽可能将操做延迟到connectedCallback里执行 constructor(){ super() // 必须调用父类的构造函数 const raw = this.innerHTML this.innerHTML = `<div class="alert alert-warning alert-dismissible fade in"> <button type="button" class="close" aria-label="Close"> <span aria-hidden="true">×</span> </button> <div class="content">${raw}</div> </div>` this.querySelector('button.close').addEventListener('click', _ => this.close()) } // 至关于v0中的attachedCallback connectedCallback(){ console.log('invoked connectedCallback!') } // 至关于v0中的detachedCallback disconnectedCallback(){ console.log('invoked disconnectedCallback!') } // 至关于v0中的attributeChangedCallback,但新增一个可选的observedAttributes属性来约束所监听的属性数目 attributeChangedCallback(attrName, oldVal, newVal){ console.log(`attributeChangedCallback-change ${attrName} from ${oldVal} to ${newVal}`) } // 缺省时表示attributeChangedCallback将监听全部属性变化,若返回数组则仅监听数组中的属性变化 static get observedAttributes(){ return ['disabled'] } // 新增事件回调,就是经过document.adoptNode方法修改元素ownerDocument属性时触发 adoptedCallback(){ console.log('invoked adoptedCallback!') } get textContent(){ return this.querySelector('.content').textContent } set textContent(val){ this.querySelector('.content').textContent = val } close(){ this.style.display = 'none' } show(){ this.style.display = 'block' } } customElements.define('x-alert', XAlert)
渐进加强
class XInput extends HTMLInputElement{ constructor(){ super() this.value = 'x-input' } isEmail(){ const val = this.value return /[0-9a-zA-Z]+@\S+\.\S+/.test(val) } } customElements.define('x-input', XInput, {extends: 'input'}) // 实例化方式 document.createElement('input', {is: 'x-input'}) new XInput() <input is="x-input">
除此以外以前的unresolved状态改为defined和undefined状态,CSS对应的选择器为:defined
和:not(:defined)
。
还有就是新增一个customeElements.whenDefined({String} tagName):Promise
方法,让咱们能监听自定义元素从undefined转换为defined的事件。
<share-buttons> <social-button type="twitter"><a href="...">Twitter</a></social-button> <social-button type="fb"><a href="...">Facebook</a></social-button> <social-button type="plus"><a href="...">G+</a></social-button> </share-buttons> // Fetch all the children of <share-buttons> that are not defined yet. let undefinedButtons = buttons.querySelectorAll(':not(:defined)'); let promises = [...undefinedButtons].map(socialButton => { return customElements.whenDefined(socialButton.localName); )); // Wait for all the social-buttons to be upgraded. Promise.all(promises).then(() => { // All social-button children are ready. });
到这里我想你们已经对Custom Element API有所认识了,下面咱们尝试自定义一个完整的元素吧。不过再实操前,咱们先看看一个恰好可用的元素应该注意哪些细节。
1.constructor
用于初始化元素的状态和设置事件监听,或者建立Shadow Dom。
2.connectedCallback
资源获取和元素渲染等操做适合在这里执行,但该方法可被调用屡次,所以对于只执行一次的操做要自带检测方案。
3.disconnectedCallback
适合做资源清理等工做(如移除事件监听)
1.constructor中的细节
1.1. 第一句必须调用super()
保证父类实例建立
1.2. return
语句要么没有,要么就只能是return
或return this
1.3. 不能调用document.write
和document.open
方法
1.4. 不要访问元素的特性(attribute)和子元素,由于元素可能处于undefined状态并无特性和子元素可访问
1.5. 不要设置元素的特性和子元素,由于即便元素处于defined状态,经过document.createElement
和new
方式建立元素实例时,本应该是没有特性和子元素的
2.打造focusable元素 by tabindex特性
默认状况下自定义元素是没法获取焦点的,所以须要显式添加tabindex
特性来让其focusable。另外还要注意的是若元素disabled
为true
时,必须移除tabindex
让元素unfocusable。
3.ARIA特性
经过ARIA特性让其余阅读器等其余访问工具能够识别咱们的自定义元素。
4.事件类型转换
经过addEventListener
捕获事件,而后经过dispathEvent
发起事件来对事件类型进行转换,从而触发更符合元素特征的事件类型。
下面咱们来撸个x-btn
吧
class XBtn extends HTMLElement{ static get observedAttributes(){ return ['disabled'] } constructor(){ super() this.addEventListener('keydown', e => { if (!~[13, 32].indexOf(e.keyCode)) return this.dispatchEvent(new MouseEvent('click', { cancelable: true, bubbles: true })) }) this.addEventListener('click', e => { if (this.disabled){ e.stopPropagation() e.preventDefault() } }) } connectedCallback(){ this.setAttribute('tabindex', 0) this.setAttribute('role', 'button') } get disabled(){ return this.hasAttribute('disabled') } set disabled(val){ if (val){ this.setAttribute('disabled','') } else{ this.removeAttribute('disabled') } } attributeChangedCallback(attrName, oldVal, newVal){ this.setAttribute('aria-disabled', !!this.disabled) if (this.disabled){ this.removeAttribute('tabindex') } else{ this.setAttribute('tabindex', '0') } } } customElements.define('x-btn', XBtn)
Chrome54默认支持Custom Element v1,Chrome53则需要修改启动参数chrome --enable-blink-features=CustomElementsV1
。其余浏览器可以使用webcomponets.js这个polyfill。
关于Custom Element咱们就说到这里吧,不过我在此提一个有点怪但又确实应该被注意到的细节问题,那就是自定义元素是否是必定要采用<x-alert></x-alert>
来声明呢?可否采用<x-alert/>
或<x-alert>
的方式呢?
答案是不行的,因为自定义元素属于Normal Element,所以必须采用<x-alert></x-alert>
这种开始标签和闭合标签来声明。那么什么是Normal Element呢?
其实元素分为如下5类:
Void elements
格式为<tag-name>
,包含如下元素area
,base
,br
,col
,embed
,hr
,img
,keygen
,link
,meta
,param
,source
,track
,wbr
Raw text elements
格式为<tag-name></tag-name>
,包含如下元素script
,style
escapable raw text elements
格式为<tag-name></tag-name>
,包含如下元素textarea
,title
Foreign elements
格式为<tag-name/>
,MathML和SVG命名空间下的元素
Normal elements
格式为<tag-name></tag-name>
,除上述4种元素外的其余元素。某些条件下能够省略结束标签,由于浏览器会自动为咱们补全,但结果每每会很吊轨,因此仍是本身写完整比较安全。
当头一回听到Custom Element时我是那么的兴奋不已,犹如找到根救命稻草似的。但如同其余新技术的出现同样,利弊同行,如何判断和择优利用是让人头痛的事情,也许前人的经验能给我指明方向吧!下篇《WebComponent魔法堂:深究Custom Element 之 从过去看如今》,咱们将穿越回18年前看看先驱HTML Component的黑历史,而后再次审视WebComponent吧!
尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohn... ^_^肥仔John
How to Create Custom HTML Elements
A vocabulary and associated APIs for HTML and XHTML
Custom Elements v1
custom-elements-customized-builtin-example