僅100行的JavaScript DOM操作類庫

2018-06-16 19:20 更新

如果你構建過Web引用程序,你可能處理過很多DOM操作。訪問和操作DOM元素幾乎是每一個Web應用程序的通用需求。我們我們經(jīng)常從不同的控件收集信息,我們需要設置value值,修改div或span標簽的內(nèi)容。當然有許多庫能幫助處理這些行為,其中最流行的當屬jQuery,已經(jīng)成為事實上的標準。有事你并不需要jQuery提供每一樣東西,所以在這篇文章中,我們將看看如何創(chuàng)建自己的類庫來操作DOM元素。

API

身為開發(fā)者的我們每天都要做決定。我相信在測試驅(qū)動開發(fā)中,我真的非常喜歡的一個事實是它迫使你在開始實際編碼之前必須做出設計決定。沿著這些思路,我想我想要的DOM操作類庫的API最終看起來可能像這樣:

//返回 DOM 元素
dom('.selector').el
//返回元素的值/內(nèi)容
dom('.selector').val() 
//設置元素的值/內(nèi)容
dom('.selector').val('value') 

這應該包括了大多數(shù)可能用到的操作。然而如何我們可以一次操作多個對象會顯得個更好。如果能生成一個JavaScript對象,那將是偉大之舉。

//生成包裝DOM元素的對象
dom({
    structure: {
        propA: '.selector',
        propB: '.selector'
    },
    propC: '.selector'
}) 

一旦我們將元素存下來,我們能很容易對它們執(zhí)行val方法。

//檢索DOM元素的值
dom({
    structure: {
        propA: '.selector',
        propB: '.selector'
    },
    propC: '.selector'
}).val()

這將是將數(shù)據(jù)直接從DOM轉(zhuǎn)換為JavaScript對象的有效方法。

現(xiàn)在我們心理已經(jīng)清楚我們的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元素。讓我們?yōu)檫@些函數(shù)創(chuàng)建兩個快捷方式。

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參數(shù)。通常情況下將是一個(選擇符)字符串,但我們也應該支持:

  • DOM元素——類庫的val方法會非常方便,所以我們可能需要使用已經(jīng)引用的元素;
  • JavaScript對象——為了創(chuàng)建包含多個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;
}

如果開發(fā)者傳遞字符串將執(zhí)行第一個case。我們轉(zhuǎn)換parent并且調(diào)用querySelector的快捷方式。第二個case將會被執(zhí)行如果我們傳遞一個DOM元素或JavaScript對象。我們檢查對象是否有nodeName屬性,如果有這個屬性,我們直接將它的值作為api.el的值。如果沒有,那么我們遍歷對象的所有屬性并且為每個屬性初始化為類庫實例。這里有一些測試用例:

<p>text</p>
<header>
    <p>Big</p>
</header>
<footer>
    <p>Small</p>
</footer>

訪問第一個段落:

dom('p').el

訪問header節(jié)點里的段落:

dom('p', 'header').el

傳遞一個DOM元素:

dom(document.querySelector('header')).el

傳遞一個JavaScript對象:

var els = dom({
    footer: 'footer',
    paragraphs: {
        header: 'header p',
        footer: 'footer p'
    }
}))
// 最后我們在此得到JavaScript對象。
// 它的屬性是實際的結(jié)果
// 執(zhí)行dom函數(shù)。例如,獲取值
// footer是paragraphs的屬性
els.paragraphs.footer.el

獲取或設置元素的值

表單元素的值如input或select可以被很容易的檢索到——我們可以使用元素的value屬性。我們我們已經(jīng)有一個能訪問的DOM元素了——存儲在api.el。然而,當我們碰到單選框或復選框是有些棘手。對于其他HTML節(jié)點像div,section或span我們獲取元素的值實際上是獲取textContent屬性。如果textContent是undefined那么可以用innerHTML代替(相似)。讓我們寫出另一個switch語句:

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本身,為了保持鏈式操作。當然我們這樣做僅當我們使用設置器函數(shù)時。

讓我們看看如何處理不能同類型的元素。例如input節(jié)點:

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;

這可能是最有趣的例子了。有兩種類型的元素需要不同的處理——單選框和復選框。這些元素實際上是一組,我們要牢記這點。這就是為什么我們使用querySelectorAll獲取整組并找出哪個是被選擇/選中的。更復雜的是,復選框可能不止被選中一個。上面的方法完美處理所有這些情況。 處理textarea元素非常簡單,這要得益于我們上面寫的輔助函數(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;

最后是默認操作:

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"
}

這方法對于把數(shù)據(jù)沖HTML導成JavaScript對象非常有幫助。這正是我們很多人每天都很常見的任務。

最后結(jié)果

最后完成的類庫代碼僅有100行代碼,但它仍然滿足我們所需的訪問 DOM元素并且獲取和設置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)建了一個jsbin的例子,你可以看看類作品。

總結(jié)

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

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號