自動獲取HTML5的data-*屬性

sp42發表於2012-12-26

專案中需要設計一系列的自定義標籤,於是涉及到標籤的屬性如何訪問。使用 HTML5 的 data-* 屬性的話,獲取元素後直接訪問 dataset.屬性名稱 即可,dataset 的型別為 DOMStringMap {},一個 MAP 物件,仍是 key / value 式的物件,使用上比較方便。但卻遇到一個相容性的問題, 在 Android 2.3 等的一些舊有瀏覽器上尚不支援。

對此,我們可以寫一個相容性的補丁。主要原理是在“劫持” document.querySelector/querySelectorAll 這類獲取元素的方法之後,通過提供自定義的欄位 dataset = {….} 即可實現類似標準寫法。當然,簡單地,你可以通過一個 API 方法來提供類似 getDataAttrib() 獲取屬性,效果一樣。我們之所以不採取這種方式,而採用另外一種方式,是為了更好向標準靠攏,使用大家都一致的訪問方式。

我的實現如下:

// 如瀏覽器不支援 HTML5 data-* 屬性,設定一個。
;(function(){
	// 測試元素
	var el;
	el = document.createElement(`div`);
	el.setAttribute(`data-id`, `111`);
	if(!el.dataset){
		Element.prototype.dataset = {};
		
		var querySelectorAll = document.querySelectorAll; // 儲存一個
		document.querySelectorAll = function(){
			var resultEls = querySelectorAll.apply(this, arguments);
			for(var resultEl, i = 0, j = resultEls.length; i < j; i++){
				resultEl = resultEls[i];
				resultEl.dataset = getAttrib(resultEl.attributes)
			}
			
			return resultEls;
		}
		
		// 也就是單個的 document.querySelectorAll()。不儲存,直接覆蓋
		document.querySelector = function(){
			var resultEls = document.querySelectorAll.apply(this, arguments);
			return resultEls ? resultEls[0] : null;
		};	
		
	}
	el = null; // 要完全移除 dummy 元素,是否這樣就 ok?
	
	/**
	 * 把元素儲存為 JSON 物件
	 * @param {Element.attributes} 元素屬性集合
	 * @return {Object}
	 */
    function getAttrib(attributes) {
        if (!attributes) return;
        var hash = {};
        
        for (var attribute, i = 0, j = attributes.length; i < j; i++) {
            attribute = attributes[i];
            if(attribute.nodeName.indexOf(`data-`) != -1){
	            hash[attribute.nodeName.slice(5)] = attribute.nodeValue;
            }
        }
        
        return hash;
    }
})();

考慮到 querySelector 獲取元素的方式已經足夠,故所以當前沒有提供 documeny.getElementByID(“#id”) 方法。

請注意:對於非 document 物件身上執行的 querySelector /querySelectorAll 將不支援,仍不會返回 dataset。 針對該問題,已於2013.1.16 通過重寫 Element.prototype 方法解決。詳細過程如下:

if(!canSupportDataSet()){
    Element.prototype.dataset = {};


    modifyQuerySelectorAll_By(document);  // document 的好像不一樣……
    modifyQuerySelectorAll_By(Element.prototype);
}


/**
 * 覆蓋系統的 querySelector/querySelectorAll 方法。
 * @param host {Element.prototype/Document}
 */
function modifyQuerySelectorAll_By(host){
    var querySelectorAll = host.querySelectorAll; // 儲存一個
    host.querySelectorAll = function(){
        var resultEls = querySelectorAll.apply(this, arguments);
        for(var resultEl, i = 0, j = resultEls.length; i < j; i++){
            resultEl = resultEls[i];
            resultEl.dataset = getAttrib(resultEl.attributes)
        }


        return resultEls;
    }


    // 也就是單個的 document.querySelectorAll()。不儲存,直接覆蓋
    host.querySelector = function(){
        var resultEls = host.querySelectorAll.apply(this, arguments);
        return resultEls ? resultEls[0] : null;
    };
}

測試例子:

<listview id="foo" data-id="1">
    Hello World
    <div data-id="2"></div>
</listview>

<script>
var el = document.querySelector(`#foo`);
alert(el.querySelector(`div`).dataset.id);
</script>

問題小結:

  1. 瀏覽器必須能夠支援 querySelector/querySelectorAll 方法,否則該方法也沒有意義;
  2. 只能從獲取元素的方法提供 dataset。比如事件處理器中引數 e 的 e.tartget. dataset 則是空物件。
  3. 如上程式碼所示,單個的 querySelector() 是經過 querySelectorAll(),當中有遍歷陣列的操作,能否適當優化一下,使用原生的來做?
  4. 對於 CSS Selector Engine 速度敏感的同學,本方法不適用。因為修改系統方法,可見必然效能會下降。但可以保證,這種下降是屬於小幅度的;
  5. 尚不支援 documeny.getElementByID,待增加之。


相關文章