那些你应该掌握的HTML DOM APIs

370 阅读5分钟

引言

Web 前端在过去几年发展迅速,从最早的 jQuery 到 Angular、React、Vue 框架,我们对 DOM 的操作方式的也在变化。

jQuery 提供了一系列快速操作 DOM 的方式,同时保证了兼容性。React 等这些框架则封装了DOM,隐藏了直接的 DOM 操作,提供例如 ref 等的方式。似乎我们对 DOM 的操作变的越来越少,但是 DOM 操作对于 Web 开发来说肯定是重要的一环,掌握相关知识也是必须的。

本人之所以打算重新全面的学习下 DOM 操作,是因为工作中发现自己对 DOM 的操作不够优雅,通过和第三方优秀的库的写法对比,发现自己有待提升。

实例

让我们来看一个简单又很常见的功能:对于一个出现弹出框,实现点击其他空白地方隐藏它。

点击其他空白地方隐藏,事件就需要绑定到 document 上,接下来就是判断“其他地方”。这个逻辑也很简单,判断点击的对象是否是弹出框,如果不是,属于“其他地方”,故实现了前面的判断也就实现了这个功能。

事件是绑定在 document 上的,所以我们通过 event.target 拿到点击的对象,再做判断。由于 event.target 可能是弹出框内容各个层级的 DOM 元素,所以需要判断 event.target 本身是或者父元素链上有弹出框最外层的 DOM 元素。

// document 事件绑定最好在弹出框出现后,同时在弹出框隐藏后也解绑事件
document.addEventListener('click', (event) => {
    const popWrapper = ... // 弹出框最外层 DOM
    const target = event.target
    const isClickAway = target !== popWrapper && !isDescendant(popWrapper, target)
    if (isClickAway) {
        ... // close pop
    }
})

const isDescendant = function(parent, child) {
    let node = child.parentNode;
    while (node) {
        if (node === parent) {
            return true;
        }
        node = node.parentNode;
    }
    return false;
}

上面的方法是可以实现需求,代码看上去也很清晰。本人之前也都是这个实现方案,但是实际上有更优雅的实现方式,如下:

// document 事件绑定最好在弹出框出现后,同时在弹出框隐藏后也解绑事件
document.addEventListener('click', (event) => {
    const popWrapper = ... // 弹出框最外层 DOM
    const isClickAway = popWrapper.contains(event.target)
    if (isClickAway) {
        ... // close pop
    }
})

DOM APIs 中提供了 contains 方法可以直接判断,兼容性方面也没大问题(不兼容可以通过 polyfill 解决)。可以看到第一种实现方案中的判断其实 DOM APIs 是有提供直接方法的,我们完全可以不必重复写代码。

所以说,掌握 DOM APIs 可以提升你编码效率和代码的整洁度。

DOM APIs

接下来介绍一些你应该掌握的 HTML DOM APIs 或 相关知识。本文例子均来自 HTML DOM。这是个优秀的网站,建议收藏。

classList

新增或移除类名时可以使用,ie 部分方法有兼容性问题。

ele.classList.add('class-name')

// 添加多个类名 (Not supported in IE 11)
ele.classList.add('another', 'class', 'name')

ele.classList.remove('class-name')

// 移除多个类名 (Not supported in IE 11)
ele.classList.remove('another', 'class', 'name')

ele.classList.toggle('class-name')

ele.classList.contains('class-name')

实际项目中通常使用 classnames 库来处理管理类名操作,但是还是需要了解 classList。

querySelector, querySelectorAll

通过 CSS 选择器查询 DOM 元素

// 返回匹配到的第一个元素
document.querySelector('.demo')

// 返回所有匹配的元素
document.querySelectorAll('.demo')

这个方法目前应该是很常见又实用的方法。

matches

判断 DOM 元素是否和提供的 CSS 选择器匹配

const matches = function(ele, selector) {
    return (
        ele.matches || 
        ele.matchesSelector || 
        ele.msMatchesSelector || 
        ele.mozMatchesSelector || 
        ele.webkitMatchesSelector || 
        ele.oMatchesSelector
    ).call(ele, selector);
}

这个方法比 classList 的 contains 方法适用范围更广。

判断 DOM 元素是否另外一个元素的子元素

直接使用 DOM APIs,这个方法 parent 和 child 相同也会返回 true

const isDescendant = parent.contains(child)

遍历查看父元素

// Check if `child` is a descendant of `parent`
const isDescendant = function(parent, child) {
    let node = child.parentNode;
    while (node) {
        if (node === parent) {
            return true;
        }

        // Traverse up to the parent
        node = node.parentNode;
    }

    // Go up until the root but couldn't find the `parent`
    return false;
};

获取 CSS 样式

const styles = window.getComputedStyle(ele, null);

const bgColor = styles.backgroundColor;

const bgColor = styles.getPropertyValue('background-color');

设置 CSS 样式

通过 style 属性

ele.style.backgroundColor = 'red';
ele.style['backgroundColor'] = 'red';
ele.style['background-color'] = 'red';

通过 cssText

el.style.cssText += 'background-color: red; color: white';

移除 CSS 样式,通过 delete 无法移除,需要使用 removeProperty

ele.style.removeProperty('background-color');

// 这个不生效
ele.style.removeProperty('backgroundColor');

DOM 元素的宽高

// 获取 styles
const styles = window.getComputedStyle(ele);

// 宽高但不包括边框和内边距
const height = ele.clientHeight - parseFloat(styles.paddingTop) 
                                - parseFloat(styles.paddingBottom);
const width = ele.clientWidth - parseFloat(styles.paddingLeft) 
                              - parseFloat(styles.paddingRight);

// 宽高包括内边距
const clientHeight = ele.clientHeight;
const clientWidth = ele.clientWidth;

// 宽高包括内边距和边框
const offsetHeight = ele.offsetHeight;
const offsetWidth = ele.offsetWidth;

// 宽高包括内外边距和边框
const heightWithMargin = ele.offsetHeight + parseFloat(styles.marginTop)
                                          + parseFloat(styles.marginBottom);
const widthWithMargin = ele.offsetWidth + parseFloat(styles.marginLeft)
                                        + parseFloat(styles.marginRight);

这边正好复习了各个 width 属性获取的是指什么宽度。

DOM 相邻插入元素操作

插入元素到后面,把 ele 插入到 refEle 后面

refEle.parentNode.insertBefore(ele, refEle.nextSibling);

// Or
refEle.insertAdjacentElement('afterend', ele);

插入元素到前面,把 ele 插入到 refEle 前面

refEle.parentNode.insertBefore(ele, refEle);

// Or
refEle.insertAdjacentElement('beforebegin', ele);

预览上传的图片

介绍两个 API: URL.createObjectURL(), FileReader's readAsDataURL()。可以在上传时预览图片这个场景使用

<input type="file" id="fileInput" />
<img id="preview" />
const fileEle = document.getElementById('fileInput');
const previewEle = document.getElementById('preview');

使用 URL.createObjectURL()

fileEle.addEventListener('change', function(e) {
    // Get the selected file
    const file = e.target.files[0];

    // Create a new URL that references to the file
    const url = URL.createObjectURL(file);

    // Set the source for preview element
    previewEle.src = url;
})

使用 FileReader's readAsDataURL()

fileEle.addEventListener('change', function(e) {
    // Get the selected file
    const file = e.target.files[0];

    const reader = new FileReader();
    reader.addEventListener('load', function() {
        // Set the source for preview element
        previewEle.src = reader.result;
    });

    reader.readAsDataURL(file);
})

选中 DOM 元素的文字内容

使用到 selection 和 range 相关的知识,这块知识可以展开深入学习

const selectText = function(ele) {
    const selection = window.getSelection();
    const range = document.createRange();
    range.selectNodeContents(node);
    selection.removeAllRanges();
    selection.addRange(range);
}

主动触发事件

触发事件,有些特殊事件在元素的方法属性中,故可以直接调用。

// For text box and textarea
inputEle.focus();
inputEle.blur();

// For form element
formEle.reset();
formEle.submit();

// For any element
ele.click();

触发原生事件

const trigger = function(ele, eventName) {
    const e = document.createEvent('HTMLEvents');
    e.initEvent(eventName, true, false);
    ele.dispatchEvent(e);
};

trigger(ele, 'mousedown')

触发自定义事件

const e = document.createEvent('CustomEvent');
e.initCustomEvent('hello', true, true, { message: 'Hello World' });

// Trigger the event
ele.dispatchEvent(e);

滚动到某个元素位置

ele.scrollIntoView();

// ie 和 safari 不支持 smooth 参数
ele.scrollIntoView({ behavior: 'smooth' });

总结

每个Web开发人员都应该要掌握 HTML DOM APIs。HTML DOM 这个网站展示了很多 API,同时提供了很多优秀的实例,本文大部分例子都取自这个网站。

除了一些简单的 API,还有一些需要深入学习才能掌握的 API,如拖拽相关、文本选中的 selection 和 range、DOMParser、window.URL 等等。