如果你構(gòu)建過(guò)Web引用程序,你可能處理過(guò)很多DOM操作。訪問(wèn)和操作DOM元素幾乎是每一個(gè)Web應(yīng)用程序的通用需求。我們我們經(jīng)常從不同的控件收集信息,我們需要設(shè)置value值,修改div或span標(biāo)簽的內(nèi)容。當(dāng)然有許多庫(kù)能幫助處理這些行為,其中最流行的當(dāng)屬jQuery,已經(jīng)成為事實(shí)上的標(biāo)準(zhǔn)。有事你并不需要jQuery提供每一樣?xùn)|西,所以在這篇文章中,我們將看看如何創(chuàng)建自己的類庫(kù)來(lái)操作DOM元素。
身為開(kāi)發(fā)者的我們每天都要做決定。我相信在測(cè)試驅(qū)動(dòng)開(kāi)發(fā)中,我真的非常喜歡的一個(gè)事實(shí)是它迫使你在開(kāi)始實(shí)際編碼之前必須做出設(shè)計(jì)決定。沿著這些思路,我想我想要的DOM操作類庫(kù)的API最終看起來(lái)可能像這樣:
//返回 DOM 元素
dom('.selector').el
//返回元素的值/內(nèi)容
dom('.selector').val()
//設(shè)置元素的值/內(nèi)容
dom('.selector').val('value')
這應(yīng)該包括了大多數(shù)可能用到的操作。然而如何我們可以一次操作多個(gè)對(duì)象會(huì)顯得個(gè)更好。如果能生成一個(gè)JavaScript對(duì)象,那將是偉大之舉。
//生成包裝DOM元素的對(duì)象
dom({
structure: {
propA: '.selector',
propB: '.selector'
},
propC: '.selector'
})
一旦我們將元素存下來(lái),我們能很容易對(duì)它們執(zhí)行val方法。
//檢索DOM元素的值
dom({
structure: {
propA: '.selector',
propB: '.selector'
},
propC: '.selector'
}).val()
這將是將數(shù)據(jù)直接從DOM轉(zhuǎn)換為JavaScript對(duì)象的有效方法。
現(xiàn)在我們心理已經(jīng)清楚我們的API看起來(lái)的樣子,我們類庫(kù)代碼看起來(lái)像下面這樣:
var dom = function(el) {
var api = { el: null }
api.val = function(value) {
// ...
}
return api;
}
很明顯,我們打算使用類似getElementById,querySelector或querySelectorAll這樣的方法。通常情況下,你可以像下面這樣訪問(wèn)DOM:
var header = document.querySelector('.header');
querySeletor是非常有趣的,例如,它不僅僅是document對(duì)象的方法,同時(shí)也是其他DOM元素的方法。這意味著,我們可以在特定上下文中運(yùn)行查詢。比如:
<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樹(shù)上操作,并且我們的類庫(kù)應(yīng)該支持傳遞作用域。所以,如果它接受一個(gè)父元素選擇符是非常棒的。
var dom = function(el, parent) {
var api = { el: null }
api.val = function(value) {
// ...
}
return api;
}
按照我們上面所說(shuō)的,我們將使用querySelector和querySelectorAll查詢DOM元素。讓我們?yōu)檫@些函數(shù)創(chuàng)建兩個(gè)快捷方式。
var qs = function(selector, parent) {
parent = parent || document;
return parent.querySelector(selector);
};
var qsa = function(selector, parent) {
parent = parent || document;
return parent.querySelectorAll(selector);
};
在那之后我們應(yīng)該傳遞el參數(shù)。通常情況下將是一個(gè)(選擇符)字符串,但我們也應(yīng)該支持:
下面的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;
}
如果開(kāi)發(fā)者傳遞字符串將執(zhí)行第一個(gè)case。我們轉(zhuǎn)換parent并且調(diào)用querySelector的快捷方式。第二個(gè)case將會(huì)被執(zhí)行如果我們傳遞一個(gè)DOM元素或JavaScript對(duì)象。我們檢查對(duì)象是否有nodeName屬性,如果有這個(gè)屬性,我們直接將它的值作為api.el的值。如果沒(méi)有,那么我們遍歷對(duì)象的所有屬性并且為每個(gè)屬性初始化為類庫(kù)實(shí)例。這里有一些測(cè)試用例:
<p>text</p>
<header>
<p>Big</p>
</header>
<footer>
<p>Small</p>
</footer>
訪問(wèn)第一個(gè)段落:
dom('p').el
訪問(wèn)header節(jié)點(diǎn)里的段落:
dom('p', 'header').el
傳遞一個(gè)DOM元素:
dom(document.querySelector('header')).el
傳遞一個(gè)JavaScript對(duì)象:
var els = dom({
footer: 'footer',
paragraphs: {
header: 'header p',
footer: 'footer p'
}
}))
// 最后我們?cè)诖说玫絁avaScript對(duì)象。
// 它的屬性是實(shí)際的結(jié)果
// 執(zhí)行dom函數(shù)。例如,獲取值
// footer是paragraphs的屬性
els.paragraphs.footer.el
表單元素的值如input或select可以被很容易的檢索到——我們可以使用元素的value屬性。我們我們已經(jīng)有一個(gè)能訪問(wèn)的DOM元素了——存儲(chǔ)在api.el。然而,當(dāng)我們碰到單選框或復(fù)選框是有些棘手。對(duì)于其他HTML節(jié)點(diǎn)像div,section或span我們獲取元素的值實(shí)際上是獲取textContent屬性。如果textContent是undefined那么可以用innerHTML代替(相似)。讓我們寫出另一個(gè)switch語(yǔ)句:
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是布爾類型變量告訴我們是獲取還是設(shè)置元素的value屬性。有.value屬性的元素包括一個(gè)輔助方法。switch語(yǔ)句將包含方法的實(shí)際邏輯。最后我們返回api本身,為了保持鏈?zhǔn)讲僮?。?dāng)然我們這樣做僅當(dāng)我們使用設(shè)置器函數(shù)時(shí)。
讓我們看看如何處理不能同類型的元素。例如input節(jié)點(diǎn):
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;
這可能是最有趣的例子了。有兩種類型的元素需要不同的處理——單選框和復(fù)選框。這些元素實(shí)際上是一組,我們要牢記這點(diǎn)。這就是為什么我們使用querySelectorAll獲取整組并找出哪個(gè)是被選擇/選中的。更復(fù)雜的是,復(fù)選框可能不止被選中一個(gè)。上面的方法完美處理所有這些情況。 處理textarea元素非常簡(jiǎn)單,這要得益于我們上面寫的輔助函數(shù)。
case 'textarea':
return useValueProperty.apply(this, [value]);
break;
下面看我們?nèi)绾翁幚硐吕斜恚╯elect):
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;
最后是默認(rèn)操作:
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方法。這里有一個(gè)簡(jiǎn)單的HTML表單和相應(yīng)的測(cè)試:
<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();
我們會(huì)得到:
{
data: {
count: "30",
options: "B"
},
name: "sample text",
version: "version: 0.3"
}
這方法對(duì)于把數(shù)據(jù)沖HTML導(dǎo)成JavaScript對(duì)象非常有幫助。這正是我們很多人每天都很常見(jiàn)的任務(wù)。
最后完成的類庫(kù)代碼僅有100行代碼,但它仍然滿足我們所需的訪問(wèn) DOM元素并且獲取和設(shè)置value值/內(nèi)容。
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;
}
我創(chuàng)建了一個(gè)jsbin的例子,你可以看看類作品。
我上面討論的類庫(kù)是AbsurdJS客戶端組件的一部分。該模塊的完成文檔可以在這里找到。這代碼的目的并非要取代jQuery或其他可以訪問(wèn)DOM的流行類庫(kù)。函數(shù)的思想是自成一體,一個(gè)函數(shù)只做一件事并把它做好。這是AbsurdJS背后的主要思想,它也是基于模塊化建設(shè)的,如router或Ajax模塊。
原文 http://flippinawesome.org/2014/03/10/a-dom-manipulation-class-in-100-lines-of-javascript/
更多建議: