如果你構建過Web引用程序,你可能處理過很多DOM操作。訪問和操作DOM元素幾乎是每一個Web應用程序的通用需求。我們我們經常從不同的控件收集信息,我們需要設置value值,修改div或span標簽的內容。當然有許多庫能幫助處理這些行為,其中最流行的當屬jQuery,已經成為事實上的標准。有事你並不需要jQuery提供每一樣東西,所以在這篇文章中,我們將看看如何創建自己的類庫來操作DOM元素。
API
身為開發者的我們每天都要做決定。我相信在測試驅動開發中,我真的非常喜歡的一個事實是它迫使你在開始實際編碼之前必須做出設計決定。沿着這些思路,我想我想要的DOM操作類庫的API最終看起來可能像這樣:
//返回 DOM 元素 dom('.selector').el //返回元素的值/內容 dom('.selector').val() //設置元素的值/內容 dom('.selector').val('value')
這應該包括了大多數可能用到的操作。然而如何我們可以一次操作多個對象會顯得個更好。如果能生成一個JavaScript對象,那將是偉大之舉。
//生成包裝DOM元素的對象 dom({ structure: { propA: '.selector', propB: '.selector' }, propC: '.selector' })
一旦我們將元素存下來,我們能很容易對它們執行val方法。
//檢索DOM元素的值 dom({ structure: { propA: '.selector', propB: '.selector' }, propC: '.selector' }).val()
這將是將數據直接從DOM轉換為JavaScript對象的有效方法。
現在我們心理已經清楚我們的API看起來的樣子,我們類庫代碼看起來像下面這樣:
var dom = function(el) { var api = { el: null } api.val = function(value) { // ... } return api; }
作用域
很明顯,我們打算使用類似getElementById,querySelector或querySelectorAll這樣的方法。通常情況下,你可以像下面這樣訪問DOM:
var header = document.querySelector('.header');
querySeletor是非常有趣的,例如,它不僅僅是document對象的方法,同時也是其他DOM元素的方法。這意味着,我們可以在特定上下文中運行查詢。比如:
<header> <p>Big</p> </header> <footer> <p>Small</p> </footer> var header = document.querySelector('header'); var footer = document.querySelector('footer'); console.log(header.querySelector('p').textContent); // Big console.log(footer.querySelector('p').textContent); // Small
我們能在特定的DOM樹上操作,並且我們的類庫應該支持傳遞作用域。所以,如果它接受一個父元素選擇符是非常棒的。
var dom = function(el, parent) { var api = { el: null } api.val = function(value) { // ... } return api; }
查詢DOM元素
按照我們上面所說的,我們將使用querySelector和querySelectorAll查詢DOM元素。讓我們為這些函數創建兩個快捷方式。
var qs = function(selector, parent) { parent = parent || document; return parent.querySelector(selector); }; var qsa = function(selector, parent) { parent = parent || document; return parent.querySelectorAll(selector); };
在那之后我們應該傳遞el參數。通常情況下將是一個(選擇符)字符串,但我們也應該支持:
- DOM元素——類庫的val方法會非常方便,所以我們可能需要使用已經引用的元素;
- JavaScript對象——為了創建包含多個DOM元素的JavaScript對象。
下面的switch包括這兩種情況:
switch(typeof el) { case 'string': parent = parent && typeof parent === 'string' ? qs(parent) : parent; api.el = qs(el, parent); break; case 'object': if(typeof el.nodeName != 'undefined') { api.el = el; } else { var loop = function(value, obj) { obj = obj || this; for(var prop in obj) { if(typeof obj[prop].el != 'undefined') { obj[prop] = obj[prop].val(value); } else if(typeof obj[prop] == 'object') { obj[prop] = loop(value, obj[prop]); } } delete obj.val; return obj; } var res = { val: loop }; for(var key in el) { res[key] = dom.apply(this, [el[key], parent]); } return res; } break; }
<p>text</p> <header> <p>Big</p> </header> <footer> <p>Small</p> </footer>
訪問第一個段落:
dom('p').el
訪問header節點里的段落:
dom('p', 'header').el
傳遞一個DOM元素:
dom(document.querySelector('header')).el
傳遞一個JavaScript對象:
var els = dom({ footer: 'footer', paragraphs: { header: 'header p', footer: 'footer p' } })) // 最后我們在此得到JavaScript對象。 // 它的屬性是實際的結果 // 執行dom函數。例如,獲取值 // footer是paragraphs的屬性 els.paragraphs.footer.el
獲取或設置元素的值
api.val = function(value) { if(!this.el) return null; var set = !!value; var useValueProperty = function(value) { if(set) { this.el.value = value; return api; } else { return this.el.value; } } switch(this.el.nodeName.toLowerCase()) { case 'input': break; case 'textarea': break; case 'select': break; default: } return set ? api : null; }
首先我們需要確保api.el屬性存在。set是布爾類型變量告訴我們是獲取還是設置元素的value屬性。有.value屬性的元素包括一個輔助方法。switch語句將包含方法的實際邏輯。最后我們返回api本身,為了保持鏈式操作。當然我們這樣做僅當我們使用設置器函數時。
case 'input': var type = this.el.getAttribute('type'); if(type == 'radio' || type == 'checkbox') { var els = qsa('[name="' + this.el.getAttribute('name') + '"]', parent); var values = []; for(var i=0; i<els.length; i++) { if(set && els[i].checked && els[i].value !== value) { els[i].removeAttribute('checked'); } else if(set && els[i].value === value) { els[i].setAttribute('checked', 'checked'); els[i].checked = 'checked'; } else if(els[i].checked) { values.push(els[i].value); } } if(!set) { return type == 'radio' ? values[0] : values; } } else { return useValueProperty.apply(this, [value]); } break;
case 'textarea': return useValueProperty.apply(this, [value]); break;
下面看我們如何處理下拉列表(select):
case 'select': if(set) { var options = qsa('option', this.el); for(var i=0; i<options.length; i++) { if(options[i].getAttribute('value') === value) { this.el.selectedIndex = i; } else { options[i].removeAttribute('selected'); } } } else { return this.el.value; } break;
最后是默認操作:
default: if(set) { this.el.innerHTML = value; } else { if(typeof this.el.textContent != 'undefined') { return this.el.textContent; } else if(typeof this.el.innerText != 'undefined') { return typeof this.el.innerText; } else { return this.el.innerHTML; } } break;
上面這些代碼我們完成了我們的val方法。這里有一個簡單的HTML表單和相應的測試:
<form> <input type="text" value="sample text" /> <input type="radio" name="options" value="A"> <input type="radio" name="options" checked value="B"> <select> <option value="10"></option> <option value="20"></option> <option value="30" selected></option> </select> <footer>version: 0.3</footer> </form>
如果我們寫下面的:
dom({ name: '[type="text"]', data: { options: '[type="radio"]', count: 'select' }, version: 'footer' }, 'form').val();
我們會得到:
{ data: { count: "30", options: "B" }, name: "sample text", version: "version: 0.3" }
這方法對於把數據沖HTML導成JavaScript對象非常有幫助。這正是我們很多人每天都很常見的任務。
最后結果
最后完成的類庫代碼僅有100行代碼,但它仍然滿足我們所需的訪問 DOM元素並且獲取和設置value值/內容。
var dom = function(el, parent) { var api = { el: null } var qs = function(selector, parent) { parent = parent || document; return parent.querySelector(selector); }; var qsa = function(selector, parent) { parent = parent || document; return parent.querySelectorAll(selector); }; switch(typeof el) { case 'string': parent = parent && typeof parent === 'string' ? qs(parent) : parent; api.el = qs(el, parent); break; case 'object': if(typeof el.nodeName != 'undefined') { api.el = el; } else { var loop = function(value, obj) { obj = obj || this; for(var prop in obj) { if(typeof obj[prop].el != 'undefined') { obj[prop] = obj[prop].val(value); } else if(typeof obj[prop] == 'object') { obj[prop] = loop(value, obj[prop]); } } delete obj.val; return obj; } var res = { val: loop }; for(var key in el) { res[key] = dom.apply(this, [el[key], parent]); } return res; } break; } api.val = function(value) { if(!this.el) return null; var set = !!value; var useValueProperty = function(value) { if(set) { this.el.value = value; return api; } else { return this.el.value; } } switch(this.el.nodeName.toLowerCase()) { case 'input': var type = this.el.getAttribute('type'); if(type == 'radio' || type == 'checkbox') { var els = qsa('[name="' + this.el.getAttribute('name') + '"]', parent); var values = []; for(var i=0; i<els.length; i++) { if(set && els[i].checked && els[i].value !== value) { els[i].removeAttribute('checked'); } else if(set && els[i].value === value) { els[i].setAttribute('checked', 'checked'); els[i].checked = 'checked'; } else if(els[i].checked) { values.push(els[i].value); } } if(!set) { return type == 'radio' ? values[0] : values; } } else { return useValueProperty.apply(this, [value]); } break; case 'textarea': return useValueProperty.apply(this, [value]); break; case 'select': if(set) { var options = qsa('option', this.el); for(var i=0; i<options.length; i++) { if(options[i].getAttribute('value') === value) { this.el.selectedIndex = i; } else { options[i].removeAttribute('selected'); } } } else { return this.el.value; } break; default: if(set) { this.el.innerHTML = value; } else { if(typeof this.el.textContent != 'undefined') { return this.el.textContent; } else if(typeof this.el.innerText != 'undefined') { return typeof this.el.innerText; } else { return this.el.innerHTML; } } break; } return set ? api : null; } return api; }
我創建了一個jsbin的例子,你可以看看類作品。
總結
我上面討論的類庫是AbsurdJS客戶端組件的一部分。該模塊的完成文檔可以在這里找到。這代碼的目的並非要取代jQuery或其他可以訪問DOM的流行類庫。函數的思想是自成一體,一個函數只做一件事並把它做好。這是AbsurdJS背后的主要思想,它也是基於模塊化建設的,如router或Ajax模塊。
注
原文http://flippinawesome.org/2014/03/10/a-dom-manipulation-class-in-100-lines-of-javascript/
Q群推薦
GitHub家園225932282,GitHub愛好者的天堂,歡迎有興趣的同學加入