善用vue指令

前言

directive是一個容易被人遺忘的vue屬性,由於vue的開發思想不推崇直接對dom進行操做,可是適當的dom操做有利於提高工程的健壯性。html

使用方式

關於指令的具體講解請看官方文檔vue

https://cn.vuejs.org/v2/guide/custom-directive.html算法

其中bind函數使用較爲頻繁,如下使用幾個示例進行講解數組

輸入框聚焦後自動select文本

QQ20190311-103104

代碼:緩存

Vue.directive('selectTextOnFocus', {
    bind: function(el, binding) {
        function bindDom(el) {
            if (el.tagName !== 'INPUT') {
                [...el.children].forEach(dom => {
                    return bindDom(dom)
                })
            } else {
                el.onfocus = function() {
                    setTimeout(() => {
                        el.select()
                    }, 30)
                }
                return true
            }
        }

        bindDom(el)
    }
})
複製代碼

大體的思路就是從父元素遞歸的查找input子元素(對於組件也可使用),若是找到input子元素,那麼就綁定focus事件,而且在input focus時將元素select。閉包

移除input[type="number"]的默認事件

對於數字輸入框聚焦後按下鍵盤方向鍵或者滾動鼠標滾輪,數字會自動遞增或者遞減,這一功能可能會在用戶不經意的狀況下改變輸入的值,致使提交錯誤的數據,可使用以下代碼解決這個問題。app

Vue.directive('removeMouseWheelEvent', {
    bind: function(el, binding) {
        el.onmousewheel = function(e) {
            el.blur()
        }

        el.onkeydown = function(e) {
            if ([38, 40, 37, 39].includes(e.keyCode)) {
                e.preventDefault()
            }
        }
    }
})
複製代碼

對於以上兩個指令,使用方式以下dom

<input type="number" v-selectTextOnFocus v-removeMouseWheelEvent>
複製代碼

很方便的就給輸入框添加了這兩個功能異步

table滾動加載

QQ20190311-105635

element ui的表格組件不提供一個滾動加載的功能,可是既想使用element ui的table組件又想得到滾動加載的功能,那麼就須要指令來完成這一功能,先看看指令的寫法。ide

Vue.directive('scrollLoad', {
    bind: function(el, binding) {
        let lastPotion = 0
        const scrollWrap = el.querySelector('.el-table__body-wrapper')

        scrollWrap.onscroll = function() {
            const distanceRelativeToBottom = this.scrollHeight - this.scrollTop - this.clientHeight
            const direction = getDirection(lastPotion, this.scrollTop)
            lastPotion = this.scrollTop
            binding.value({
                direction,
                scrollTop: this.scrollTop,
                distanceRelativeToBottom
            })
        }

        function getDirection(last, now) {
            return now - last > 0 ? 'down' : 'up'
        }
    }
})
複製代碼

首先找到 .el-table__body-wrapper 這一元素,這是element ui 表格的容器(除去表頭),其次給它添加onscroll事件,在滾動時進行位置的計算,而且將計算獲得的方向以及位置信息傳遞給傳入的回調函數,由回調函數來判斷是否應該進行數據請求。

<el-table v-scrollLoad="scrollLoad">
複製代碼
binding.value({
    direction,
    scrollTop: this.scrollTop,
    distanceRelativeToBottom
})
複製代碼

方向鍵切換input輸入框

QQ20190311-113641

一個表單的優化體驗功能,在一兩個月前對於這個功能,大體是這樣實現的

<tr v-for="(goods, index) in tableData">
    <td class="t3">
        <input :ref="getRef(index, 1)" :data-ref="getRef(index, 1)" @focus="inputFocus($event)">
    </td>
    <td class="t4">
        <input :ref="getRef(index, 2)" :data-ref="getRef(index, 2)" @focus="inputFocus($event)">
    </td>
    <td class="t5">
        <input :ref="getRef(index, 3)" :data-ref="getRef(index, 3)" @focus="inputFocus($event)">
    </td>
</tr>
複製代碼
inputFocus(e) {
    this.nowInputAt = e.target.getAttribute('data-ref')
},

getRef(row, column) {
    return row + ':' + column
},

keydown(event) {
    let [row, column] = this.nowInputAt.split(':').map(value => parseInt(value))
    let pos = {
        row,
        column
    }
    if ([38, 40, 37, 39, 13].includes(event.keyCode)) {
        event.preventDefault()
    }

    function up() {
        if (pos.row === 0) return
        pos.row -= 1
    }

    function down() {
        if (pos.row === this.tableData.length - 1) return
        let a = pos.row + 1
        pos.row = a
    }

    function left() {
        if (pos.row === 0 && pos.column === 1) return
        if (pos.column === 1) {
            pos.row -= 1
            pos.column = 3
        } else {
            pos.column -= 1
        }
    }

    function right() {
        if (pos.row === this.tableData.length - 1 && pos.column === 3) return
        if (pos.column === 3) {
            pos.row += 1
            pos.column = 1
        } else {
            pos.column += 1
        }
    }

    switch (event.keyCode) {
        case 38: up.call(this); break
        case 40: down.call(this); break
        case 37: left.call(this); break
        case 39: right.call(this); break
        case 13: right.call(this); break
    }
    this.$nextTick(() => {
        this.nowInputAt = pos.row + ':' + pos.column
        this.$refs[pos.row + ':' + pos.column][0].focus()
    })
},
複製代碼

大體的作法就是給每個input設置一個座標信息,當輸入框聚焦時存儲當前的座標,當方向鍵按下時利用存儲的座標信息進行計算獲得下一個輸入框同時進行聚焦。計算座標的算法有點相似於推箱子游戲。

現在一樣的需求出如今了另外一個表單,若是複製一份代碼,就很不優雅,因而決定使用指令來完成這一需求,先看看實現,接下來拆分代碼進行講解。

import _ from 'lodash'

export default function() {
    let gridSquare = []
    let pos = {
        column: 0,
        row: 0
    }
    let parentEl = null
    let keyUpDebounceFn = null

    function findRow(element) {
        if (!element) return
        if (element.dataset && 'sokobanrow' in element.dataset) {
            const row = []
            const findCol = function(htmlEl) {
                if (!htmlEl) return
                if (htmlEl.dataset && 'sokobancol' in htmlEl.dataset) {
                    row.push(htmlEl)
                } else {
                    [...htmlEl.childNodes].forEach(dom => {
                        findCol(dom)
                    })
                }
            }
            findCol(element)
            gridSquare.push(row)
        } else {
            [...element.childNodes].forEach(dom => {
                findRow(dom)
            })
        }
    }

    function registerGrid() {
        findRow(parentEl)
    }

    function bindEvent() {
        bindFocusEvent()
        bindKeyDownEvent()
    }

    function bindFocusEvent() {
        gridSquare.forEach((row, rowIndex) => {
            row.forEach((cell, cellIndex) => {
                cell.addEventListener('focus', function() {
                    pos = {
                        column: cellIndex,
                        row: rowIndex
                    }
                })
            })
        })
    }

    function bindKeyDownEvent() {
        const keyEvent = function(event) {
            // 上 38
            // 下 40
            // 左 37
            // 右 39
            // 回車 13
            if ([38, 40, 37, 39, 13].includes(event.keyCode)) {
                event.preventDefault()
            }

            function up() {
                if (pos.row === 0) return
                pos.row -= 1
            }

            function down() {
                if (pos.row === gridSquare.length - 1) return
                pos.row += 1
            }

            function left() {
                if (pos.row === 0 && pos.column === 0) return
                if (pos.column === 0) {
                    pos.row -= 1
                    pos.column = gridSquare[pos.row].length - 1
                } else {
                    pos.column -= 1
                }
            }

            function right() {
                if (pos.row === gridSquare.length - 1 && pos.column === gridSquare[gridSquare.length - 1].length - 1) return
                if (pos.column === gridSquare[pos.row].length - 1) {
                    pos.row += 1
                    pos.column = 1
                } else {
                    pos.column += 1
                }
            }

            switch (event.keyCode) {
                case 38: up(); break
                case 40: down(); break
                case 37: left(); break
                case 39: right(); break
                case 13: right(); break
            }
            try {
                gridSquare[pos.row][pos.column].focus()
            } catch (e) {}
        }

        keyUpDebounceFn = _.debounce(keyEvent, 100)
        window.addEventListener('keyup', keyUpDebounceFn)
    }

    return {
        bind(el, binding) {
            parentEl = el
        },

        unbind() {
            gridSquare = null
            pos = null
            parentEl = null
            window.removeEventListener('keyup', keyUpDebounceFn)
            keyUpDebounceFn = null
        }

        init() {
            gridSquare = []
            pos = {
                column: 0,
                row: 0
            }
            registerGrid()
            bindEvent()
        }
    }
}

複製代碼

首先定義一個閉包函數用於緩存dom節點,以及當前聚焦的位置信息等相關信息。其次閉包函數返回vue指令須要的對象,同時在此對象中,包含了自定義的init函數。這個函數的做用在於,由於對於動態渲染的dom節點,bind函數是沒法獲取到最新的dom節點,那麼就須要暴露出init函數,用於延時綁定。其實指令也提供了 update componentUpdated 函數用於檢測dom的改變,可是若是dom節點有一些例如v-if v-show 或者style的改變,都會觸發這兩個事件,因此這裏暫不使用這兩個事件進行初始化,會下降性能,同時提供 unbind 鉤子以供元素銷燬時釋放閉包內的變量,代碼以下:

import _ from 'lodash'

export default function() {
    const gridSquare = []
    let pos = {
        column: 0,
        row: 0
    }
    let parentEl = null
    let keyUpDebounceFn = null

    function registerGrid() {}

    function bindEvent() {}

    return {
        bind(el, binding) {
            parentEl = el
        },

        unbind() {
            gridSquare = null
            pos = null
            parentEl = null
            window.removeEventListener('keyup', keyUpDebounceFn)
            keyUpDebounceFn = null
        }

        init() {
            registerGrid()
            bindEvent()
        }
    }
}

複製代碼

其次初始化時,進行input輸入框的二維座標模型的創建,具體作法是,首先給每一行定義一個自定義屬性 data-sokobanrow 以及每一列定義自定義屬性 data-sokobancol,其次深度優先遞歸查找相關dom,若是是行元素,那麼就新建一個數組(X軸),若是是列元素(Y軸),那麼就將此dom push到X軸數組中,最後將X軸數組push到網格數組中,最終獲得一個內部存放input DOM的二維數組。

<table v-sokoban>
    <tr data-sokobanrow v-for="(goods, index) in tableData">
        <td>
            <input data-sokobancol>
        </td>
        <td>
            <input data-sokobancol>
        </td>
        <td>
            <input data-sokobancol>
        </td>
    </tr>
</table>
複製代碼
const gridSquare = []

function findRow(element) {
    if (!element) return
    if (element.dataset && 'sokobanrow' in element.dataset) {
        const row = []
        const findCol = function(htmlEl) {
            if (htmlEl.dataset && 'sokobancol' in htmlEl.dataset) {
                row.push(htmlEl)
            } else {
                [...htmlEl.childNodes].forEach(dom => {
                    findCol(dom)
                })
            }
        }
        findCol(element)
        gridSquare.push(row)
    } else {
        [...element.childNodes].forEach(dom => {
            findRow(dom)
        })
    }
}

function registerGrid() {
    findRow(parentEl)
}
複製代碼

而後再進行相關的事件綁定,在input focus時存儲當前座標信息,在keyup時計算相關座標獲得下一input座標而且使其focus,代碼以下:

function bindEvent() {
    bindFocusEvent()
    bindKeyDownEvent()
}

function bindFocusEvent() {
    gridSquare.forEach((row, rowIndex) => {
        row.forEach((cell, cellIndex) => {
            cell.addEventListener('focus', function() {
                pos = {
                    column: cellIndex,
                    row: rowIndex
                }
            })
        })
    })
}

function bindKeyDownEvent() {
    const keyEvent = function(event) {
        // 上 38
        // 下 40
        // 左 37
        // 右 39
        // 回車 13
        if ([38, 40, 37, 39, 13].includes(event.keyCode)) {
            event.preventDefault()
        }

        function up() {
            if (pos.row === 0) return
            pos.row -= 1
        }

        function down() {
            if (pos.row === gridSquare.length - 1) return
            pos.row += 1
        }

        function left() {
            if (pos.row === 0 && pos.column === 0) return
            if (pos.column === 0) {
                pos.row -= 1
                pos.column = gridSquare[pos.row].length - 1
            } else {
                pos.column -= 1
            }
        }

        function right() {
            if (pos.row === gridSquare.length - 1 && pos.column === gridSquare[gridSquare.length - 1].length - 1) return
            if (pos.column === gridSquare[pos.row].length - 1) {
                pos.row += 1
                pos.column = 1
            } else {
                pos.column += 1
            }
        }

        switch (event.keyCode) {
            case 38: up(); break
            case 40: down(); break
            case 37: left(); break
            case 39: right(); break
            case 13: right(); break
        }
        try {
            gridSquare[pos.row][pos.column].focus()
        } catch (e) {}
    }

    keyUpDebounceFn = _.debounce(keyEvent, 100)
    window.addEventListener('keyup', keyUpDebounceFn)
}
複製代碼

最終用法以下:

<table v-sokoban>
    <tr data-sokobanrow v-for="(goods, index) in tableData">
        <td>
            <input data-sokobancol>
        </td>
        <td>
            <input data-sokobancol>
        </td>
        <td>
            <input data-sokobancol>
        </td>
    </tr>
</table>
複製代碼
import sokobanDirectiveGenerator from '@/directives/sokoban'
const sokoban = sokobanDirectiveGenerator()

export default {
    directives: {
        sokoban
    },

    methods: {
        getServerData() {
            setTimeout(() => { // 一個異步請求
                this.$nextTick(() => {
                    sokoban.init() // 頁面渲染後進行初始化
                })
            }, 1000)
        }
    }
}
複製代碼

尾聲

其實能夠發現,這幾個指令基本上都是爲了優化體驗而編寫,而這樣的功能在一個系統中確定是大量存在的,因此使用指令,能夠極大的節省代碼,從而提高工程的健壯性。

相關文章
相關標籤/搜索