僅100行的JavaScript DOM操作類庫(kù)

2018-06-16 19:20 更新

如果你構(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元素。

API

身為開(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;
}

查詢DOM元素

按照我們上面所說(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)該支持:

  • DOM元素——類庫(kù)的val方法會(huì)非常方便,所以我們可能需要使用已經(jīng)引用的元素;
  • JavaScript對(duì)象——為了創(chuàng)建包含多個(gè)DOM元素的JavaScript對(duì)象。

下面的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

獲取或設(shè)置元素的值

表單元素的值如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ù)。

最后結(jié)果

最后完成的類庫(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的例子,你可以看看類作品。

總結(jié)

我上面討論的類庫(kù)是AbsurdJS客戶端組件的一部分。該模塊的完成文檔可以在這里找到。這代碼的目的并非要取代jQuery或其他可以訪問(wèn)DOM的流行類庫(kù)。函數(shù)的思想是自成一體,一個(gè)函數(shù)只做一件事并把它做好。這是AbsurdJS背后的主要思想,它也是基于模塊化建設(shè)的,如routerAjax模塊。

原文 http://flippinawesome.org/2014/03/10/a-dom-manipulation-class-in-100-lines-of-javascript/

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)