封装 DOM 操作


DOM 原本的 API 在编写上冗长且操作便,在这边将 DOM API 做简单封装,并创建一个XD模块,首先,在 XD-1.0.0.js 中创建一些常数与函数:

// 标准化属性名称
const PROPS = new Map([
    ['for', 'htmlFor'],
    ['class', 'className'],
    ['readonly', 'readOnly'],
    ['maxlength', 'maxLength'],
    ['cellspacing', 'cellSpacing'],
    ['rowspan', 'rowSpan'],
    ['colspan', 'colSpan'],
    ['tabindex', 'tabIndex'],
    ['usemap', 'useMap'],
    ['frameborder', 'frameBorder']
]);

// 判断元素的类型  

function isElementNode(elem) {
    return elem.nodeType === Node.ELEMENT_NODE;
}

function isTextNode(elem) {
    return elem.nodeType === Node.TEXT_NODE;
}

function isCommentNode(elem) {
    return elem.nodeType === Node.COMMENT_NODE;
}

function isInputNode(elem) {
    return elem.nodeName === 'INPUT';
}

虽然要修补 JavaScript 的对象非常容易,除非是为了兼容于新的标准,否则不建议在任何原生对象或 DOM 对象上添加特性,以免开发者无法辨别,这些特性是原生的或者来自于程序库,因此,通常会采取包裹器的形式,将原生对象或 DOM 对象等包裹,开发者创建并操作包裹器,由包裹器来操作原生 API。

因此,接下来在 XD-1.0.0.js 中定义ElemCollection类:

class ElemCollection {
    // 构造时传入原生 DOM 对象的 Array 清单
    constructor(elems) {
        this.elems = elems;
    }

    // 指定索引获取元素
    get(index = 0) {
        return this.elems[index];
    }

    // 包裹器管理的 DOM 对象个数
    size() {
        return this.elems.length;
    }

    // 包裹器中的 DOM 元素清单是否为空
    isEmpty() {
        return this.elems.length === 0;
    }

    // 逐一操作管理的 DOM 元素
    each(consume) {
        this.elems.forEach(consume);
        return this;
    }

    // 如果 value 为 undefined,返回第一个 DOM 元素的 innerHTML 
    // 否则用 value 设定全部 DOM 元素之 innerHTML
    html(value) {
        let elems = this.elems;
        if(value === undefined) {
            return elems[0] && isElementNode(elems[0]) ? elems[0].innerHTML : null;
        }
        else {
            elems.filter(isElementNode)
                 .forEach(elem => elem.innerHTML = value);
            return this;
        }
    }

    // 如果 value 为 undefined,返回第一个 DOM 元素的属性对应之特性
    // 否则用 value 设定全部 DOM 元素之指定特性       
    attr(name, value) {
        let elems = this.elems;
        let propName = PROPS.has(name) ? PROPS.get(name) : name;
        if(value === undefined) {
            return elems[0] && !isTextNode(elems[0]) && !isCommentNode(elems[0]) ?
                    elems[0][propName] : undefined;
        }
        else {
            elems.filter(elem => !isTextNode(elem) && !isCommentNode(elem))
                 .forEach(elem => elem[propName] = value);
            return this;
        }       
    }

    // 如果 value 为 undefined,返回第一个 input 元素的 value
    // 否则用 value 设定全部 input 元素的 value           
    val(value) {
        let elems = this.elems;
        // 先只处理 <input> 元素
        if(value === undefined) {
            return elems[0] && isInputNode(elems[0]) ? elems[0].value : null;
        }
        else {
            elems.filter(isInputNode)
                 .forEach(elem => elem.value = value);
            return this;
        }       
    }

    // 如果只有一个父节点,将指定的 elemsCollection 管理之元素附加至该节点
    // 否则用复制 elemsCollection 管理之元素,再附加至各个父节点            
    append(elemsCollection) {
        let parents = this.elems;
        if(parents.length === 1) { // 只有一个父节点
            let parent = parents[0];
            elemsCollection.each(elem => parent.appendChild(elem));
        }
        else if(parents.length > 1){ // 有多个父节点
            parents.forEach(parent => {
                elemsCollection.each(elem => {
                    // 复制子节点
                    var container = document.createElement('div');
                    container.appendChild(elem);
                    container.innerHTML = container.innerHTML;
                    parent.appendChild(container.firstChild);
                });
            });
        }

        return this;
    }

    // 将管理之元素从 DOM 树上移除     
    remove() {
        this.elems.forEach(elem => {
            elem.parentNode.removeChild(elem);
        });
        return this;
    }
}

接着在选取元素上,基于原生的getElementByIdgetElementsByTagNamegetElementsByNamequerySelectorAll等 API 来创建包裹器:

function elemsById(...ids) {
    let container = this || document; 
    let elems = ids.map(id => container.getElementById(id));
    return new ElemCollection(elems);
}

function elemsByTag(...tags) {
    let container = this || document; 
    let elems = tags.map(tag => Array.from(container.getElementsByTagName(tag)))
                    .reduce((acc, arr) => acc.concat(arr), []);

    return new ElemCollection(elems);
}

function elemsByName(...names) {
    let container = this || document; 
    let elems = names.map(name => Array.from(container.getElementsByName(name)))
                     .reduce((acc, arr) => acc.concat(arr), []);

    return new ElemCollection(elems);
}

function elemsBySelector(...selectors) {
    let container = this || document; 
    let elems = selectors.map(selector => Array.from(container.querySelectorAll(selector)))
                         .reduce((acc, arr) => acc.concat(arr), []);

    return new ElemCollection(elems);   
} 

// 指定一或多个标签名称,创建 DOM 元素
function create(...tags) {
    return new ElemCollection(tags.map(tag => document.createElement(tag)));
}

这几个函数的名称会是模块导出的名称:

export {elemsById, elemsByTag, elemsByName, elemsBySelector, create};

除了创建包裹器来管理一组 DOM 元素外,也可以有个包裹器来包裹单一 DOM 元素:

// 包裹单一 DOM 元素
class XD {
    constructor(elem) {
        this.elem = elem;
    }

    elemsById(...ids) {
        return elemsById.apply(this.elem, ids);
    }

    elemsByTag(...tags) {
        return elemsByTag.apply(this.elem, tags);
    }

    elemsByName(...names) {
        return elemsByName.apply(this.elem, names);
    }

    elemsBySelector(...selectors) {
        return elemsBySelector.apply(this.elem, selectors);
    }

    toElemCollection() {
        return new ElemCollection([this.elem]);
    }
}

// 默认导出的工厂函数,用来创建  X 实例
// 如果传入字符串,会创建新元素
// 否则直接包裹 DOM 元素
export default function(elem) {
    if(typeof elem === 'string') {
        return new XD(document.createElement(elem));
    }
    return new XD(elem);
}

现在,若 XD-1.0.0.js 放在 js 文件夹中,若想使用这个XD模块,可以在 HTML 页面中如下编写程序(你的浏览器必须支持 ES6 模块):

<script type="module">
    import {elemsById, elemsByTag, elemsByName, elemsBySelector} from './js/XD-1.0.0.js';

    elemsById('console', 'cmd').html('<b>Hello, World</b>');

    console.log(elemsBySelector('#console').html());

    elemsByTag('span', 'div').attr('class', 'red')
                             .html('<i>Red Color</i>');

    elemsByName('name').val('100')
                       .each(elem => console.log(elem.value));
</script>

或者可以使用默认导入:

<script type="module">
    import x from './js/XD-1.0.0.js';

    let doc = x(document);

    doc.elemsById('console', 'cmd').html('<b>Hello, World</b>');

    console.log(doc.elemsBySelector('#console').html());

    doc.elemsByTag('span', 'div').attr('class', 'red')
                                 .html('<i>Red Color</i>');

    doc.elemsByName('name').val('100')
                           .each(elem => console.log(elem.value));
</script>

来用在先前的范例上,看看改写之后会长什么样,首先改写一下〈修改文件〉中的第一个范例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
</head>
<body>

    <input id="src" type="text"><button id="add">新增图片</button>
    <div id="images"></div>

<script type="module">
    import {elemsById, create} from './js/XD-1.0.0.js';

    elemsById('add').get().onclick = function() {
        let img = create('img');

        img.attr('src', elemsById('src').val())
           .get()
           .onclick = function() {
               img.remove();
           };       

        elemsById('images').append(img);              
    };
</script>

</body>
</html>

按此观看结果

来稍微简化一下〈修改文件〉中第二个范例:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
</head>
<body>  
    容器一:
    <div id="container1">
        <img id="image" src="https://openhome.cc/Gossip/images/caterpillar_small.jpg"/>
    </div><br>
    容器二:
    <div id="container2"></div>  

<script type="module">
    import {elemsById} from './js/XD-1.0.0.js';

    let image = elemsById('image');

    image.get().onclick = function() {
        let c1 = elemsById('container1');
        let c2 = elemsById('container2');
        if(this.parentNode === c1.get()) {
            c2.append(image);
        } else {
            c1.append(image);
        }
    };

</script>  
</body>
</html>

按此观看结果

这两个范例的事件处理,还没有进一步做适当地封装,因而看来风格不一致,这会是后续讨论事件处理时的主题。

完整的 XD-1.0.0.js可以按此下载


展开阅读全文