写在前面
如今,在项目中使用React、Vue等框架作为技术栈已成为一种常态,在享受带来便利性的同时,也许我们渐渐地遗忘原生js的写法。
现在,是时候回归本源,响应原始的召唤了。本文将一步一步带领大家封装一套属于自己的DOM操作库,我将其命名为qnode
。
功能特性
qnode
吸收了jquery优雅的链式写法,并且融入了我个人的一些经验和思考:
- 自定义DOM工厂模式(缓存节点、跨文件操作)
- 快速优雅地创建新的DOM节点
- CSS3样式前缀自动识别和添加
大家可能会比较疑惑,什么是DOM工厂模式?
实际上是这样,有一些需要共享的节点、数据和方法,它在某个文件中定义,在另一个文件中调用。我最初的方式是在文件中暴露(export)出这些东西,但是后来发现文件多了,这种方式很难维护,而且需要引入很多的文件,非常麻烦。
于是在不断的摸索和思考中,想出了DOM工厂模式这个概念,咱们可以这么理解:DOM工厂就是取快递的地方,不管是从哪里发来的货品,统一送到这里,然后再由特定的人群来取。
当然,未来还有很长的路要走,我会不断地探索和改进,融入更多的想法和改进。
目录结构
qnode
├── QNode.js
├── README.md
├── api.js
├── core
│ ├── attr.js
│ ├── find.js
│ ├── index.js
│ ├── klass.js
│ ├── listener.js
│ ├── node.js
│ └── style.js
├── index.js
├── q.js
└── tools.js
-
q.js
是DOM操作的集合,融合了所有core的方法 -
QNode.js
是工厂模式,提供节点、数据以及方法的缓存和获取 -
core
目录下的文件是DOM操作的具体方法
编写代码
core/attr.js
:内容属性操作
import { isDef } from '../tools'
export function text (value) {
if (!isDef(value)) {
return this.node.textContent
}
this.node.textContent = value
return this
}
export function html (value) {
if (!isDef(value)) {
return this.node.innerHTML
}
this.node.innerHTML = value
return this
}
export function value (val) {
if (!isDef(val)) {
return this.node.value
}
this.node.value = val
return this
}
export function attr (name, value) {
if (!isDef(value)) {
return this.node.getAttribute(name)
}
if (value === true) {
this.node.setAttribute(name, '')
} else if (value === false || value === null) {
this.node.removeAttribute(name)
} else {
this.node.setAttribute(name, value)
}
return this
}
core/find.js
:节点信息获取
export function tagName () {
return this.node.tagName
}
export function current () {
return this.node
}
export function parent () {
return this.node.parentNode
}
export function next () {
return this.node.nextSibling
}
export function prev () {
return this.node.previousSibling
}
export function first () {
return this.node.firstChild
}
export function last () {
return this.node.lastChild
}
export function find (selector) {
return this.node.querySelector(selector)
}
core/klass.js
:class样式操作
import { isArray } from '../tools'
export function addClass (cls) {
let classList = this.node.classList
let classes = isArray(cls) ? cls : [].slice.call(arguments, 0)
classList.add.apply(classList, classes.filter(c => c).join(' ').split(' '))
return this
}
export function removeClass (cls) {
let classList = this.node.classList
let classes = isArray(cls) ? cls : [].slice.call(arguments, 0)
classList.remove.apply(classList, classes.filter(c => c).join(' ').split(' '))
return this
}
export function hasClass (cls) {
let classList = this.node.classList
// 若有空格,则必须满足所有class才返回true
if (cls.indexOf(' ') !== -1) {
return cls.split(' ').every(c => classList.contains(c))
}
return classList.contains(cls)
}
core/listener.js
:事件监听处理
export function on (type, fn, useCapture = false) {
this.node.addEventListener(type, fn, useCapture)
return this
}
export function off (type, fn) {
this.node.removeEventListener(type, fn)
return this
}
core/node.js
:DOM操作
import { createFragment } from '../api'
import { getRealNode, isArray } from '../tools'
export function append (child) {
let realNode = null
if (isArray(child)) {
let fragment = createFragment()
child.forEach(c => {
realNode = getRealNode(c)
if (realNode) {
fragment.appendChild(realNode)
}
})
this.node.appendChild(fragment)
} else {
realNode = getRealNode(child)
if (realNode) {
this.node.appendChild(realNode)
}
}
return this
}
export function appendTo (parent) {
parent = getRealNode(parent)
if (parent) {
parent.appendChild(this.node)
}
return this
}
export function prepend (child, reference) {
let realNode = null
let realReference = getRealNode(reference) || this.node.firstChild
if (isArray(child)) {
let fragment = createFragment()
child.forEach(c => {
realNode = getRealNode(c)
if (realNode) {
fragment.appendChild(realNode)
}
})
this.node.insertBefore(fragment, realReference)
} else {
realNode = getRealNode(child)
if (realNode) {
this.node.insertBefore(realNode, realReference)
}
}
return this
}
export function prependTo (parent, reference) {
parent = getRealNode(parent)
if (parent) {
parent.insertBefore(this.node, getRealNode(reference) || parent.firstChild)
}
return this
}
export function remove (child) {
// 没有要移除的子节点则移除本身
if (!child) {
if (this.node.parentNode) {
this.node.parentNode.removeChild(this.node)
}
} else {
let realNode = null
if (isArray(child)) {
child.forEach(c => {
realNode = getRealNode(c)
if (realNode) {
this.node.removeChild(realNode)
}
})
} else {
realNode = getRealNode(child)
if (realNode) {
this.node.removeChild(realNode)
}
}
}
return this
}
core/style.js
:内联样式操作
import { isDef, isObject } from '../tools'
const htmlStyle = document.documentElement.style
const prefixes = ['webkit', 'moz', 'ms', 'o']
const prefixLen = prefixes.length
function getRealStyleName (name) {
if (name in htmlStyle) {
return name
}
// 首字母大写
let upperName = name[0].toUpperCase() + name.substr(1)
// 前缀判断
for (let i = 0; i < prefixLen; i++) {
let realName = prefixes[i] + upperName
if (realName in htmlStyle) {
return realName
}
}
// 都不支持则返回原值
return name
}
export function css (name) {
if (!this.computedStyle) {
this.computedStyle = window.getComputedStyle(this.node)
}
if (!isDef(name)) {
return this.computedStyle
}
return this.computedStyle[name]
}
export function style (a, b) {
if (isObject(a)) {
Object.keys(a).forEach(name => {
name = getRealStyleName(name)
this.node.style[name] = a[name]
})
} else {
a = getRealStyleName(a)
if (!isDef(b)) {
return this.node.style[a]
}
this.node.style[a] = b
}
return this
}
export function show () {
if (this.node.style.display === 'none') {
this.node.style.display = ''
}
return this
}
export function hide () {
this.node.style.display = 'none'
return this
}
export function width () {
return this.node.clientWidth
}
export function height () {
return this.node.clientHeight
}
core/index.js
:所有操作集合
import * as attr from './attr'
import * as find from './find'
import * as klass from './klass'
import * as listener from './listener'
import * as node from './node'
import * as style from './style'
export default Object.assign({},
attr,
find,
klass,
listener,
node,
style
)
api.js
:DOM API
// 创建dom节点
export function createElement (tagName) {
return document.createElement(tagName)
}
// 创建dom节点片段
export function createFragment () {
return document.createDocumentFragment()
}
tools.js
:工具方法
import { createElement } from './api'
export const Q_TYPE = (typeof Symbol === 'function' && Symbol('q')) || 0x89bc
export const QNODE_TYPE = (typeof Symbol === 'function' && Symbol('QNode')) || 0x7b96
// 占位node节点
export const emptyNode = createElement('div')
export function isQ (ele) {
return ele && ele.node && ele.__type__ === Q_TYPE
}
/**
* 判断值是否定义
* @param {any} t
* @returns {boolean}
*/
export function isDef (t) {
return typeof t !== 'undefined'
}
/**
* 判断是否为字符串
* @param {any} t
* @returns {boolean}
*/
export function isString (t) {
return typeof t === 'string'
}
/**
* 是否为对象
* @param {any} t
* @param {boolean} [includeArray=false] 是否包含数组
* @returns {boolean}
*/
export function isObject (t) {
return t && typeof t === 'object'
}
/**
* 判断是否为数组
* @param {any} t
* @returns {boolean}
*/
export function isArray (t) {
if (Array.isArray) {
return Array.isArray(t)
}
return Object.prototype.toString.call(t) === '[object Array]'
}
// 判断是否为dom元素
export function isElement (node) {
if (isObject(HTMLElement)) {
return node instanceof HTMLElement
}
return node && node.nodeType === 1 && isString(node.nodeName)
}
export function getRealNode (ele) {
if (isElement(ele)) {
return ele
} else if (isQ(ele)) {
return ele.node
}
return null
}
q.js
import { createElement } from './api'
import { Q_TYPE, isElement, isString, emptyNode } from './tools'
import core from './core'
class Q {
constructor (selector) {
let node
if (isElement(selector)) {
node = selector
} else if (isString(selector)) {
if (selector[0] === '$') {
node = createElement(selector.substring(1))
} else {
node = document.querySelector(selector)
}
}
// node不存在,则创建一个占位node,避免操作dom报错
this.node = node || emptyNode
this.__type__ = Q_TYPE
}
}
// 集合
Object.assign(Q.prototype, core)
export default function q (selector) {
return new Q(selector)
}
QNode.js
import { QNODE_TYPE, isQ, isArray } from './tools'
import q from './q'
export default class QNode {
constructor () {
this.__type__ = QNODE_TYPE
this.qNodes = {}
this.store = {}
this.methods = {}
}
q (selector) {
return q(selector)
}
getNode (name) {
return this.qNodes[name]
}
setNode (name, node) {
if (isArray(node)) {
this.qNodes[name] = node.map(n => isQ(n) ? n : q(n))
} else {
this.qNodes[name] = isQ(node) ? node : q(node)
}
return this.qNodes[name]
}
getStore (name) {
return this.store[name]
}
setStore (name, value) {
this.store[name] = value
return value
}
getMethod (name) {
return this.methods[name]
}
execMethod (name) {
let fn = this.methods[name]
return fn && fn.apply(this, [].slice.call(arguments, 1))
}
setMethod (name, fn) {
let thisFn = fn.bind(this)
this.methods[name] = thisFn
return thisFn
}
}
index.js
import q from './q'
import QNode from './QNode'
export {
q,
QNode
}
export default {
q,
QNode
}
到这里为止,所有代码已经编写完成了。
API Reference
q(获取|创建节点)
参数:
- #id 根据id获取节点
- .class 根据class获取节点
- tagName 根据标签获取节点
- $tagName 创建新的节点
备注:如果有多个节点,则只获取第一个
方法:
attr
- text: str 【设置文本内容,若无参数则获取文本内容】
- html: str 【设置html,若无参数则获取html】
- value: val 【设置表单值,若无参数则获取表单值】
- attr: name, value 【设置name属性的值,若value无参数则获取name的值】
find
- tagName 【获取节点名称】
- current 【获取节点本身】
- parent 【获取父节点】
- next 【获取后一个节点】
- prev 【获取上一个节点】
- first 【获取第一个子节点】
- last 【获取最后一个子节点】
- find: #id | .class | tagName 【找子节点】
class
- addClass: str | arr | a, b, … 【添加样式class】
- removeClass: str | arr | a, b, … 【移除样式class】
- hasClass: str 【是否含有样式class】
listener
- on: type, fn, useCapture=false 【添加事件监听】
- off: type, fn 【移除事件监听】
node
- append: node | nodeList 【填充子节点到最后】
- appendTo: parent 【填充到父节点中最后】
- prepend: node | nodeList, reference 【填充子节点到最前或指定节点前】
- prependTo: parent, reference【填充到父节点中最前或指定节点前】
- remove: child 【移除子节点,若无参数则移除自身】
style
- css: name 【获取css文件中定义的样式】
- style: (name, value) | object 【1.设置或获取内联样式;2.设置一组样式】
- show 【显示节点】
- hide 【隐藏节点】
- width 【获取节点宽度】
- height 【获取节点高度】
QNode(节点仓库,包括数据和方法)
方法:
- q: 同上述q
- getNode: name 【获取节点】
- setNode: name, node 【设置节点,返回节点】
- getStore: name 【获取数据】
- setStore: name, value 【设置数据,返回数据】
- getMethod: name 【获取方法】
- setMethod: name, fn 【设置方法,返回方法,this绑定到qnode】
- execMethod: name 【执行方法,name后面可以传入方法需要的参数,this为qnode】
结语
本文到这里就要结束了,读者对文中的代码感兴趣的话,建议自己动手试试,在编程这块儿,实践才能出真知。
写完之后,是不是跃跃欲试呢?下一篇文章我将基于本文封装的DOM库来开发无限循环轮播图,详细请看下文:原生js系列之无限循环轮播图。
附:本文源码