关于前端:vue实战完全掌握Vue自定义指令

43次阅读

共计 10819 个字符,预计需要花费 28 分钟才能阅读完成。

筹备:自定义指令介绍

除了外围性能默认内置的指令 (v-modelv-show等),Vue 也容许注册自定义指令。留神,在 Vue2.0 中,代码复用和形象的次要模式是组件。然而,有的状况下,你依然须要对一般 DOM 元素进行底层操作,这时候就会用到自定义指令。

作为应用 Vue 的开发者,咱们对 Vue 指令肯定不生疏,诸如 v-modelv-onv-forv-if 等,同时 Vue 也为开发者提供了自定义指令的 api,纯熟的应用自定义指令能够极大的进步了咱们编写代码的效率,让咱们能够节省时间开心的摸鱼~

对于 Vue 的自定义指令置信很多同学曾经有所理解,自定义指令的具体写法这里就不细讲了,官网文档很具体。然而不晓得各位同学有没有这种感觉,就是这个技术感觉很不便,也不难,我也感觉学会了,就是不晓得如何去利用。这篇文档就是为了解决一些同学的这些问题才写进去的。
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就是咱们想要监听的属性,如果咱们这样写

<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/>

内容解决

咱们也能够通过自定义指令做对内容到解决,比方

  1. 空值解决
  2. 数字千分数逗号宰割
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 不便的实现文件预览性能

  1. 预览图片;
  2. 预览文件;
  3. 其余预览类业务性能
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
  • 通用的自定义指令应用技巧

正文完
 0