筹备:自定义指令介绍

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

———Vue官网

作为应用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中的权限解决形式,不便大家了解也供大家参考。

参考vue实战视频解说:进入学习

上面逐个说一下权限指令各个类型的应用办法:

//角色权限<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
  • 通用的自定义指令应用技巧