摘要:明確各階段適合的操作用于初始化元素的狀態和設置事件監聽,或者創建。事件類型轉換通過捕獲事件,然后通過發起事件來對事件類型進行轉換,從而觸發更符合元素特征的事件類型。
前言
?通過《WebComponent魔法堂:深究Custom Element 之 面向痛點編程》,我們明白到其實Custom Element并不是什么新東西,我們甚至可以在IE5.5上定義自己的alert元素。但這種簡單粗暴的自定義元素并不是我們需要的,我們需要的是具有以下特點的自定義元素:
自定義元素可通過原有的方式實例化(
可通過原有的方法操作自定義元素實例(如document.body.appendChild,可被CSS樣式所修飾等)
能監聽元素的生命周期
?而Google為首提出的H5 Custom Element讓我們可以在原有標準元素的基礎上向瀏覽器注入各種抽象層次更高的自定義元素,并且在元素CRUD操作上與原生API無縫對接,編程體驗更平滑。下面我們一起來通過H5 Custom Element來重新定義alert元素吧!
?在正式擼代碼前我想讓各位最頭痛的事應該就是如何命名元素了,下面3個因素將影響我們的命名:
命名沖突。自定義組件如同各種第三方類庫一樣存在命名沖突的問題,那么很自然地會想到引入命名空間來解決,但由于組件的名稱并不涉及組件資源加載的問題,因此我們這里簡化一下——為元素命名添加前綴即可,譬如采用很JAVA的com-cnblogs-fsjohnhuang-alert。
語義化。語義化我們理解就是元素名稱達到望文生義的境界,譬如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來自定義元素呢?瀏覽器自當會說一句“悟空,你又調皮了”
?現在我們已經通過命名規范來有效區分自定義元素和原生元素,并且通過前綴解決了命名沖突問題。嘿稍等,添加前綴真的是解決命名沖突的好方法嗎?這其實跟通過添加前綴解決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)
前綴也能有選擇的省略就好了!
把玩Custome Element v0?對元素命名吐嘈一地后,是時候把玩API了。
從頭到腳定義新元素/** x-alert元素定義 **/ const xAlertProto = Object.create(HTMLElement.prototype, { /* 元素生命周期的事件 */ // 實例化時觸發 createdCallback: { value: function(){ console.log("invoked createCallback!") const raw = this.innerHTML this.innerHTML = `` 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${raw}
?上面通過定義x-alert元素展現了Custom Element的所有API,其實就是繼承HTMLElement接口,然后選擇性地實現4個生命周期回調方法,而在createdCallback中書寫自定義元素內容展開的邏輯。另外可以定義元素公開屬性和方法。最后通過document.registerElement方法告知瀏覽器我們定義了全新的元素,你要好好對它哦!
?那現在的問題在于假如
?有時候我們只是想在現有元素的基礎上作些功能增強,倘若又要從頭做起那也太折騰了,幸好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") // console.log(xInput1.value) // x-input console.log(xInput1.isEmail()) // falseCustom Element v1 —— 換個裝而已啦
?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 = `` 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)${raw}
漸進增強
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()
?除此之外之前的unresolved狀態改成defined和undefined狀態,CSS對應的選擇器為:defined和:not(:defined)。
?還有就是新增一個customeElements.whenDefined({String} tagName):Promise方法,讓我們能監聽自定義元素從undefined轉換為defined的事件。
從頭定義一個剛好可用的元素不容易啊!// Fetch all the children of G+ 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)如何開始使用Custom Element v1?
?Chrome54默認支持Custom Element v1,Chrome53則須要修改啟動參數chrome --enable-blink-features=CustomElementsV1。其他瀏覽器可使用webcomponets.js這個polyfill。
題外話一番?關于Custom Element我們就說到這里吧,不過我在此提一個有點怪但又確實應該被注意到的細節問題,那就是自定義元素是不是一定要采用
?答案是不行的,由于自定義元素屬于Normal Element,因此必須采用
其實元素分為以下5類:
Void elements
?格式為
Raw text elements
?格式為
escapable raw text elements
?格式為
Foreign elements
?格式為
Normal elements
?格式為
?當頭一回聽到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
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/87951.html
摘要:前言最近加入到新項目組負責前端技術預研和選型,一直偏向于以為代表的技術線,于是查閱各類資料想說服老大向這方面靠,最后得到的結果是資料是英語無所謂,最重要是上符合要求,技術的事你說了算。但當我們需要動態實例化元素時,命令式則是最佳的選擇。 前言 ?最近加入到新項目組負責前端技術預研和選型,一直偏向于以Polymer為代表的WebComponent技術線,于是查閱各類資料想說服老大向這方面...
摘要:五的子類對象會返回一個集合對象,集合內存儲類型的元素。七的子類初看很有可能以為集合元素就是單選表單元素,其實可以存儲任意類型的表單元素。八的子類開始,將返回子類的對象,其行為特征和一致。但在前,我們應該先了解清楚的類型的特征。 一、前言 大家先看看下面的js,猜猜結果會怎樣吧! 可選答案: ①. 獲取id屬性值為id的節點元素 ②...
摘要:魔法堂重新認識和魔法堂你一定誤解過的魔法堂就這個樣魔法堂說說那個被埋沒的志向深入細節后會發現中定位模式之間,和之間存在千絲萬縷的關系,必須以俯瞰的角度捋一下。當采用時,屬性的實際值會被重置為。由于和則需要通過來引入來提供盒子定位微調的功能。 前言 ?對于Box Model和Positioning Scheme中3種定位模式的細節,已經通過以下幾篇文章記錄了我對其的理解和思考。?《CSS...
摘要:那不是,是我不懂而已。的用途之一西文是以空格來分隔單詞的,而漢字間則無需空格分隔,但為了統一西文東亞和的排版,于是抽象出一個名為的概念用于分隔詞義單元,則作為的值域,而定義域就是語言信息。 前言 每當來個需要既要水平排版又要設置固定高寬時,我就會想起display:inline-block,還有為了支持IE5.5/6/7的hack*display:inline;*zoom:1;。然后發...
摘要:與的映射關系為。與根對應的對應的層疊上下文,是其他的祖先,的范圍覆蓋整條。注意的默認值為,自動賦值為。對于,它會將賦予給對應的,而則不會。 一、前言 ?假如只是開發簡單的彈窗效果,懂得通過z-index來調整元素間的層疊關系就夠了。但要將多個彈窗間層疊關系給處理好,那么充分理解z-index背后的原理及兼容性問題就是必要的知識...
閱讀 3095·2021-10-13 09:40
閱讀 3945·2021-09-22 15:51
閱讀 1493·2021-09-22 15:48
閱讀 1060·2021-09-06 15:00
閱讀 1790·2019-08-30 15:43
閱讀 2356·2019-08-29 18:35
閱讀 1667·2019-08-29 16:18
閱讀 3612·2019-08-29 12:49