寫在前面
如今,在項目中使用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 = valuereturn this
}export function html (value) {if (!isDef(value)) {return this.node.innerHTML}this.node.innerHTML = valuereturn this
}export function value (val) {if (!isDef(val)) {return this.node.value}this.node.value = valreturn 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.classListlet 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.classListlet 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才返回trueif (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 = nullif (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 = nulllet realReference = getRealNode(reference) || this.node.firstChildif (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 = nullif (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.lengthfunction 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] + upperNameif (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 nodeif (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 || emptyNodethis.__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_TYPEthis.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] = valuereturn 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] = thisFnreturn 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系列之無限循環輪播圖。
附:本文源碼