深入瞭解Vue中的自定義指令

2022-11-21 22:00:28

前端(vue)入門到精通課程:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:

準備:自定義指令介紹

除了核心功能預設內建的指令 (v-modelv-show等),Vue 也允許註冊自定義指令。注意,在 Vue2.0 中,程式碼複用和抽象的主要形式是元件。然而,有的情況下,你仍然需要對普通 DOM 元素進行底層操作,這時候就會用到自定義指令。【學習視訊分享:、】

作為使用Vue的開發者,我們對Vue指令一定不陌生,諸如v-modelv-onv-forv-if等,同時Vue也為開發者提供了自定義指令的api,熟練的使用自定義指令可以極大的提高了我們編寫程式碼的效率,讓我們可以節省時間開心的摸魚~

對於的自定義指令相信很多同學已經有所瞭解,自定義指令的具體寫法這裡就不細講了,官方檔案很詳細。 但是不知道各位同學有沒有這種感覺,就是這個技術感覺很方便,也不難,我也感覺學會了,就是不知道如何去應用。這篇檔案就是為了解決一些同學的這些問題才寫出來的。

PS:這次要講的自定義指令我們主要使用的是vue2.x的寫法,不過vue3.x不過是幾個勾點函數有所改變,只要理解每個勾點函數的含義,兩者的用法差別並不大。

試煉:實現v-mymodel

我的上篇文章說到要自己實現一個v-model指令,這裡使用v-myodel模擬一個簡易版的,順便再領不熟悉的同學熟悉一下自定義指令的步驟和注意事項。

定義指令

首先梳理思路:原生input控制元件與元件的實現方式需要區分,input的實現較為簡單,我們先實現一下input的處理。 首先我們先定義一個不做任何操作的指令

Vue.directive('mymodel', {
        //只呼叫一次,指令第一次繫結到元素時呼叫。在這裡可以進行一次性的初始化設定。
        bind(el, binding, vnode, oldVnode) {
        },
        //被繫結元素插入父節點時呼叫 (僅保證父節點存在,但不一定已被插入檔案中),需要父節點dom時使用這個勾點
        inserted(el, binding, vnode, oldVnode) {
        },
        //所在元件的 VNode 更新時呼叫,**但是可能發生在其子 VNode 更新之前**。指令的值可能發生了改變,也可能沒有。但是你可以通過比較更新前後的值來忽略不必要的模板更新 (詳細的勾點函數引數見下)。
        update(el, binding, vnode, oldVnode) {
        },
        //指令所在元件的 VNode **及其子 VNode** 全部更新後呼叫。
        componentUpdated(el, binding, vnode, oldVnode) {
        },
        只呼叫一次,指令與元素解綁時呼叫。
        unbind(el, binding, vnode, oldVnode) {
        },
})
登入後複製

上面的註釋中詳細的說明了各個勾點函數的呼叫時機,因為我們是給元件上新增input事件和value繫結,因此我們在bind這個勾點函數中定義即可。所以我們把其他的先去掉,程式碼變成這樣。

Vue.directive('mymodel', {
        //只呼叫一次,指令第一次繫結到元素時呼叫。在這裡可以進行一次性的初始化設定。
        bind(el, binding, vnode, oldVnode) { 
        }
})
登入後複製

簡單說一下bind函數的幾個回撥引數,el是指令繫結元件對應的dombinding是我們的指令本身,包含namevalueexpressionarg等,vnode就是當前繫結元件對應的vnode結點,oldVnode就是vnode更新前的狀態。

接下來我們要做兩件事:

  • 繫結input事件,同步inputvalue值到外部
  • value值繫結,監聽value的變化,更新到inputvalue

這對於input原生元件比較容易實現:

//第一步,新增inout事件監聽
el.addEventListener('input', (e) => {
   //context是input所在的父元件,這一步是同步資料
   vnode.context[binding.expression] = e.target.value;
})
//監聽繫結的變數
vnode.context.$watch(binding.expression, (v) => {
     el.value = v;
})
登入後複製

這裡解釋一下上面的程式碼,vnode.context是什麼呢,他就是我們指令所在元件的上下文環境,可以理解就是指令繫結的值所在的元件範例。不熟悉vnode結構的同學建議先看一下官方的檔案,不過檔案描述的比較簡單,不是很全面,所以最好在控制檯log一下vnode的物件看一下它具體的結構,這很有助於我們封裝自定義指令,對理解Vue原理也很有幫助。

我們可以通過context[binding.expression]獲取v-model上到繫結的值,同樣可以修改它。上面的程式碼中我們首先通過在新增的input事件中操作vnode.context[binding.expression] = e.target.value同步inputvalue值到外部(context),與使用@input新增事件監聽效果是一樣的;然後我們需要做第二件事,做value值的繫結,監聽value的變化,同步值的變更到inputvalue上,我們想到我們可以使用Vue範例上的額$watch方法監聽值的變化,而context就是那個Vue範例,binding.expression就是我們想要監聽的屬性,如果我們這樣寫

參考vue實戰視訊講解:

<input v-mymodel='message'/>
登入後複製

那麼binding.expression就是字串'message'。所以我們想下面的程式碼這樣監聽繫結的響應式資料。

//監聽繫結的變數
vnode.context.$watch(binding.expression, (v) => {
     el.value = v;
})
登入後複製

至此,inputv-mymodel的處理就完成了(當然input元件還有typecheckbox,radio,select等型別都需要去特別處理,這裡就不再一一處理了,感興趣的同學可以自己嘗試去完善一下),但是對於非原生控制元件的元件,我們要特殊處理。 因此我們完善程式碼如下:

Vue.directive('mymodel', {
        //只呼叫一次,指令第一次繫結到元素時呼叫。在這裡可以進行一次性的初始化設定。
        bind(el, binding, vnode, oldVnode) {
           //原生input元件的處理
           if(vnode.tag==='input'){
                //第一步,新增inout事件監聽
                el.addEventListener('input', (e) => {
                   //context是input所在的父元件,這一步是同步資料
                   vnode.context[binding.expression] = e.target.value;
                })
                //監聽繫結的變數
                vnode.context.$watch(binding.expression, (v) => {
                     el.value = v;
                })
           }else{//元件

           }
        }
})
登入後複製

接下來我們要處理的是自定義元件的邏輯,

//vnode的結構可以參見檔案。不過我覺得最直觀的方法就是直接在控制檯列印處理
let {
    componentInstance,
    componentOptions,
    context
} = vnode;
const {
   _props
} = componentInstance;
//處理model選項
if (!componentOptions.Ctor.extendOptions.model) {
  componentOptions.Ctor.extendOptions.model = {
        value: 'value',
        event: 'input'
  }
}
let modelValue = componentOptions.Ctor.extendOptions.model.value;
let modelEvent = componentOptions.Ctor.extendOptions.model.event;
//屬性繫結,這裡直接修改了屬性,沒有想到更好的辦法,友好的意見希望可以提出
_props[modelValue] = binding.value;
context.$watch(binding.expression, (v) => {
     _props[modelValue] = v;
})
//新增事件處理常式,做資料同步
componentInstance.$on(modelEvent, (v) => {
     context[binding.expression] = v;
})
登入後複製

宣告一下,上面的實現不是vue原始碼的實現方式,vue原始碼中實現v-model更加複雜一點,是結合自定義指令、模板編譯等去實現的,因為我們是應用級別的封裝,所以採用了上述的方式實現。

實現此v-mymodel需要同學去多瞭解一下VnodeComponentAPI,就像之前說的,最簡單的方法就是直接在控制檯中直接列印出vnode物件,元件的vnode上有Component的範例componentInstance

接下來簡單說一下上面的程式碼,首先我們可以在componentOptions.Ctor.extendOptions上找到model的定義,如果沒有的話需要設定預設值valueinput,然後分別對想原生input的處理一樣,分別監聽binding.expression的變化和modelEvent事件即可。

需要注意的是,我們上面的程式碼直接給_prop做了賦值操作,這實際上是不符合規範的,但是我目前沒有找到更好的方法去實現,有好思路的同學可以在評論區留言指教。

下面?是完整的原始碼:

應用實踐:4個實用的自定義指令

上文我們通過封裝v-mymodel為各位同學展示瞭如何封裝和使用自定義指令,接下來我把自己在生產實踐中使用自定義指令的一些經驗分享給大家,通過範例,我相信各位同學能夠更深刻的理解如何在在應用中封裝自己的指令,提高效率。

許可權控制

下面我們定義一個v-permission指令用於全平臺的許可權控制

  • role:角色控制;
  • currentUser:當前登入人判斷;當前使用者是否是業務資料中的建立人或者負責人
  • bussinessStatus:業務狀態判斷;
  • every:與操作;
  • some:或操作;

範例程式碼

//定義許可權型別
const permissionType = {
    ROLE: 'role',
    CURRENTUSER:'currentUser',
    BUSSINESSSTATUS: 'bussinessStatus',
    MIX_EVERY: 'every',
    MIX_SOME: 'some'
}
export default {
    //只呼叫一次,指令第一次繫結到元素時呼叫
    bind: function () {
    },
    //當前vdom插入到真實dom時,因為是對dom的樣式操作,在這裡操作
    inserted: function (el, binding) {
        let show = false;
        show=processingType(binding.arg,binding.value); 
        el.style.display = `${show ? 'inline-block' : 'none'}`
    },
    //所在元件的VNode更新時呼叫,狀態更新後需要更新顯示狀態
    update: function (el, binding) {
        //避免無效的模板更新
        if(binding.value===binding.oldValue) return;
        let show = false;
        show=processingType(binding.arg,binding.value); 
        el.style.display = `${show ? 'inline-block' : 'none'}`
    },
    //指令所在元件的 VNode 及其子 VNode 全部更新後
    componentUpdated: function (el, binding) {
    },
    unbind: function () {
    },
}
//處理不同型別的許可權控制
function processingType(type,value){
    let values=[];
    switch (type) {
        case permissionType.ROLE:
            return permissionByRole(value);
        case permissionType.CURRENTUSER:
            return permissionCreater(value);
        case permissionType.BUSSINESSSTATUS:
            return permissionBusinessStatus(value);
        case permissionType.MIX_EVERY:
            for(let type in value){
                values.push(processingType(type,value[type]))
            }
            return values.every(v=>{
                return v;
            })
        case permissionType.MIX_SOME:
            for(let type in value){
                values.push(processingType(type,value[type]))
            }
            return values.some(v=>{
                return v;
            })
        default:
            return false;
    }
}
//業務狀態判斷
function permissionBusinessStatus(bindingValue){
   return bindingValue.status==bindingValue.value;
}
//當前使用者?
function permissionCreater(bindingValue){
    const userInfo = JSON.parse(sessionStorage.CDTPcookie);
    // console.log(userInfo.userInfo.id,bindingValue)
    if(bindingValue instanceof Array){
        return bindingValue.some(v=>{
            return userInfo.userInfo.id==v;
        })
    }
    return userInfo.userInfo.id==bindingValue;
}
//角色控制
export function permissionByRole(bindingValue) {
    //這裡也可以是store裡的使用者資訊
    const userInfo = JSON.parse(sessionStorage.userInfo);  
    let roles = []
    if (userInfo) {
        roles = userInfo.roleList
    }
    let show = false;
    if (bindingValue instanceof Array) {
        return roles.some(role => {//多角色處理
            return bindingValue.some(item => {
                return role.roleCode === item
            })
        })
    } else if (typeof bindingValue == 'string') {
        show = roles.some(role => {
            return role.roleCode === bindingValue;
        })
    }
    return show;
}
登入後複製

簡單說一下上面?指令的定義思路和使用方法。整體思路就是通過processingType處理許可權邏輯,使用el.style.display控制元件顯示或隱藏。我在這裡從日常應用中提取了一些通用的processingType中的許可權處理方式,方便大家理解也供大家參考。

下面逐一說一下許可權指令各個型別的使用方法:

//角色許可權
<component v-permission:role='leader'></component>
//判斷當前登入人
<component v-permission:currentUser='orderInfo.createUser'></component>
//判斷業務狀態
<component v-permission:bussinessStatus='{status:orderStatus.RUNNING,value:orderInfo.status}'></component>
//角色是leader或者是當前訂單的建立者,有許可權
<component v-permission:some="{role:'leader',currentUser:'orderInfo.createUser'}"></component>
//角色是leader並且是當前訂單的建立者,有許可權
<component v-permission:every="{role:'leader',currentUser:'orderInfo.createUser'}"></component>
登入後複製

輸入限制

v-input 輸入框限制,限制數位、保留n位小數點等。

export default {
    inserted: function (el, binding, vnode) {
        el.addEventListener('input', function (e) {
            if (binding.arg == 'toFixed') {
                //限制輸入n位小數點
                toFiexd(e.target, vnode, binding.value)
            } else {
                //限制數位輸入
                Integer(e.target, vnode)
            }
        })
    },
}
function toFiexd(target, vnode, v) {
    console.log(v);
    let ln = 2;
    if (v) {
        ln = v;
    }
    var regStrs = [
        ['^0(\\d+)$', '$1'], //禁止錄入整數部分兩位以上,但首位為0
        ['[^\\d\\.]+$', ''], //禁止錄入任何非數位和點
        ['\\.(\\d?)\\.+', '.$1'], //禁止錄入兩個以上的點
        ['^(\\d+\\.\\d{' + ln + '}).+', '$1'] //禁止錄入小數點後兩位以上
    ];
    for (var i = 0; i < regStrs.length; i++) {
        var reg = new RegExp(regStrs[i][0]);
        target.value = target.value.replace(reg, regStrs[i][1]);
    }
    //對於封裝的像el-input元件,因為其需要通過input事件同步狀態
    if(vnode.componentInstance){
      vnode.componentInstance.$listeners.input(target.value)
    }
}
function Integer(target, vnode) {
    let valueStr = target.value
    if (valueStr.length == 1) {
        //第一個數位不為0
        valueStr = valueStr.replace(/[^0-9]/g, "");
    } else {
        //只能輸入正整數
        valueStr = valueStr.replace(/\D/g, "");
    }
    target.value = valueStr;
    if(vnode.componentInstance){
      vnode.componentInstance.$listeners.input(target.value)
    }
}
登入後複製

這裡需要特別注意的是下面這行程式碼

vnode.componentInstance.$listeners.input(target.value)
登入後複製

我們為什麼需要新增這一句呢,我們明明已經為target.value做了賦值。
實際上這一句程式碼相當於指令作用元件內部的$emit('input',target.value),這是因為如果我們是在antd或者elementui中的輸入框元件上新增我們定義的v-input指令,直接為target.value賦值是不能生效的,修改的只是原生input控制元件value值,並沒有修改自定義元件的value,還需要通過觸發input事件去同步元件狀態,修改value值。(這裡不瞭解為什麼需要觸發input事件區同步狀態的同學瞭解一下v-model的語法糖原理即可理解, 使用方法:

<!-- 限制輸入兩位小數數位 -->
<input v-input:toFixed="2"/>
<!-- 限制輸入正整數 -->
<el-input v-input:integer/>
登入後複製

內容處理

我們也可以通過自定義指令做對內容到處理,比如

  • 空值處理

  • 數位千分數逗號分割

export default {
    bind:function(){
    },
    inserted:function(el,binding){
        dealContent(el,binding)
    },
    update:function(el,binding){
        dealContent(el,binding)
    },
    componentUpdated:function(){
    },
    unbind:function(){
    },
}
function dealContent(el,binding){
   const {arg}=binding;
   if(arg=='empty'){
       if(!el.textContent){//空值顯示
            el.textContent=binding.value||'暫無資料';
        }
   }else if(arg=='money'){//金額千分位逗號分割,如10000000顯示為100,000,00
        if (binding.value) {
            el.textContent = dealMoney(binding.value);
        }else {
            el.textContent = dealMoney(el.textContent);
        }
   }
}
登入後複製

千分位分割程式碼:

//金額處理
export function dealMoney(money, places = 2) {
    const zero = `0.00`;
    if (isNaN(money) || money === '') return zero;
    if (money && money != null) {
        money = `${money}`;
        let left = money.split('.')[0]; // 小數點左邊部分
        let right = money.split('.')[1]; // 小數點右邊
        // 保留places位小數點,當長度沒有到places時,用0補足。
        right = right ? (right.length >= places ? '.' + right.substr(0, places) : '.' + right + '0'.repeat(places - right.length)) : ('.' + '0'.repeat(places));
        var temp = left.split('').reverse().join('').match(/(\d{1,3})/g); // 分割反向轉為字串然後最多3個,最少1個,將匹配的值放進陣列返回
        return (Number(money) < 0 ? '-' : '') + temp.join(',').split('').reverse().join('') + right; // 補齊正負號和貨幣符號,陣列轉為字串,通過逗號分隔,再分割(包含逗號也分割)反向轉為字串變回原來的順序
    } else if (money === 0) {
        return zero;
    } else {
        return zero;
    }
}
登入後複製

使用方法:

<span v-content:empty="'無'">{{message}}</span>
<!-- 金額千分位逗號分割 -->
<span v-content:money>100000</span>
登入後複製

檔案預覽

v-preview方便的實現檔案預覽功能

  • 預覽圖片;

  • 預覽檔案;

  • 其他預覽類業務功能

import {isOffic,isPdf,isImage} from '@/utils/base'
import {previewWithOffice} from '@/utils/fileUtils.js'
export default {
    inserted:function(el,binding){
        el.onclick=function(e){
            let params = binding.value
            if(isOffic(params.name)){
                e.preventDefault()
                e.stopPropagation()
                previewWithOffice(params.url)//使用office線上預覽開啟
            }else if(isPdf(params.name) || isImage(params.name)){
                e.preventDefault()
                e.stopPropagation()
                if(params.url){//直接開啟url
                    previewFile(params)
                }
            }
        }
    },
    //指令所在元件的 VNode 及其子 VNode 全部更新後
    componentUpdated: function (el, binding) {
        el.onclick=function(e){
            let params = binding.value
            if(isOffic(params.name)){
                //使用外掛預覽Office檔案
                e.preventDefault()
                e.stopPropagation()
                previewWithOffice(params.url)
            }else if(isPdf(params.name) || isImage(params.name)){
               //預覽圖片和pdf等能直接開啟的檔案
                e.preventDefault()
                e.stopPropagation()
                previewFile(params)
            }
        }
    },
    unbind(el){
       el.onclick=null;
    }
}
//預覽圖片和pdf等能直接開啟的檔案
function previewFile(params) {
    let a = document.createElement("a");
    a.download = params.name
    a.href = params.url;
    a.target = "_blank";
    a.click();
    a = null;
}
登入後複製

使用方法:

<!-- 預覽圖片 -->
<image :src='url' v-preview="{name:file.name,url:file.url}"></image>
<!-- 預覽檔案 -->
<span v-preview="{name:file.name,url:file.url}">{{file.name}}</span>
登入後複製

試著自己實現

各位同學可以試著自己實現一個v-loading的載入中的指令,通過設定一個bool值來設定容器的載入狀態。 如有疑問可以在評論區留言。

總結

本文主要講了如下幾件事:

  • vue自定義指令介紹
  • 實現一個v-model
  • 通用的自定義指令使用技巧

(學習視訊分享:、)

以上就是深入瞭解Vue中的自定義指令的詳細內容,更多請關注TW511.COM其它相關文章!