怎么完全掌握Vue自定义指令

免费教程   2024年05月09日 20:05  

这篇文章主要介绍“怎么完全掌握Vue自定义指令”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“怎么完全掌握Vue自定义指令”文章能帮助大家解决问题。

准备:自定义指令介绍

除了核心功能默认内置的指令 (v-model 和 v-show等), 也允许注册自定义指令。注意,在 2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

作为使用Vue的开发者,我们对Vue指令一定不陌生,诸如v-model、v-on、v-for、v-if等,同时Vue也为开发者提供了自定义指令的api,熟练的使用自定义指令可以极大的提高了我们编写代码的效率,让我们可以节省时间开心的摸鱼~

试炼:实现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是指令绑定组件对应的dom,binding是我们的指令本身,包含name、value、expression、arg等,vnode就是当前绑定组件对应的vnode结点,oldVnode就是vnode更新前的状态。

接下来我们要做两件事:

绑定input事件,同步input的value值到外部

value值绑定,监听value的变化,更新到input的value

这对于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同步input的value值到外部(context),与使用@input添加事件监听效果是一样的;然后我们需要做第二件事,做value值的绑定,监听value的变化,同步值的变更到input的value上,我们想到我们可以使用Vue实例上的额$watch方法监听值的变化,而context就是那个Vue实例,binding.expression就是我们想要监听的属性,如果我们这样写

<inputv-mymodel='message'/>

那么binding.expression就是字符串'message'。所以我们想下面的代码这样监听绑定的响应式数据。

//监听绑定的变量vnode.context.$watch(binding.expression,(v)=>{el.value=v;})

至此,input的v-mymodel的处理就完成了(当然input组件还有type为checkbox,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'}}letmodelValue=componentOptions.Ctor.extendOptions.model.value;letmodelEvent=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需要同学去多了解一下Vnode和Component的API,就像之前说的,最简单的方法就是直接在控制台中直接打印出vnode对象,组件的vnode上有Component的实例componentInstance。

接下来简单说一下上面的代码,首先我们可以在componentOptions.Ctor.extendOptions上找到model的定义,如果没有的话需要设置默认值value和input,然后分别对想原生input的处理一样,分别监听binding.expression的变化和modelEvent事件即可。

需要注意的是,我们上面的代码直接给_prop做了赋值操作,这实际上是不符合规范的,但是我目前没有找到更好的方法去实现。

应用实践:4个实用的自定义指令权限控制

下面我们定义一个v-permission指令用于全平台的权限控制

role:角色控制;

currentUser:当前登录人判断;当前用户是否是业务数据中的创建人或者负责人

bussinessStatus:业务状态判断;

every:与操作;

some:或操作;

示例代码

//定义权限类型constpermissionType={ROLE:'role',CURRENTUSER:'currentUser',BUSSINESSSTATUS:'bussinessStatus',MIX_EVERY:'every',MIX_SOME:'some'}exportdefault{//只调用一次,指令第一次绑定到元素时调用bind:function(){},//当前vdom插入到真实dom时,因为是对dom的样式操作,在这里操作inserted:function(el,binding){letshow=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;letshow=false;show=processingType(binding.arg,binding.value);el.style.display=`${show?'inline-block':'none'}`},//指令所在组件的VNode及其子VNode全部更新后componentUpdated:function(el,binding){},unbind:function(){},}//处理不同类型的权限控制functionprocessingType(type,value){letvalues=[];switch(type){casepermissionType.ROLE:returnpermissionByRole(value);casepermissionType.CURRENTUSER:returnpermissionCreater(value);casepermissionType.BUSSINESSSTATUS:returnpermissionBusinessStatus(value);casepermissionType.MIX_EVERY:for(lettypeinvalue){values.push(processingType(type,value[type]))}returnvalues.every(v=>{returnv;})casepermissionType.MIX_SOME:for(lettypeinvalue){values.push(processingType(type,value[type]))}returnvalues.some(v=>{returnv;})default:returnfalse;}}//业务状态判断functionpermissionBusinessStatus(bindingValue){returnbindingValue.status==bindingValue.value;}//当前用户?functionpermissionCreater(bindingValue){constuserInfo=JSON.parse(sessionStorage.CDTPcookie);//console.log(userInfo.userInfo.id,bindingValue)if(bindingValueinstanceofArray){returnbindingValue.some(v=>{returnuserInfo.userInfo.id==v;})}returnuserInfo.userInfo.id==bindingValue;}//角色控制exportfunctionpermissionByRole(bindingValue){//这里也可以是store里的用户信息constuserInfo=JSON.parse(sessionStorage.userInfo);letroles=[]if(userInfo){roles=userInfo.roleList}letshow=false;if(bindingValueinstanceofArray){returnroles.some(role=>{//多角色处理returnbindingValue.some(item=>{returnrole.roleCode===item})})}elseif(typeofbindingValue=='string'){show=roles.some(role=>{returnrole.roleCode===bindingValue;})}returnshow;}

简单说一下上面????指令的定义思路和使用方法。整体思路就是通过processingType处理权限逻辑,使用el.style.display控制组件显示或隐藏。我在这里从日常应用中提取了一些通用的processingType中的权限处理方式,方便大家理解也供大家参考。

下面逐一说一下权限指令各个类型的使用方法:

//角色权限<componentv-permission:role='leader'></component>//判断当前登录人<componentv-permission:currentUser='orderInfo.createUser'></component>//判断业务状态<componentv-permission:bussinessStatus='{status:orderStatus.RUNNING,value:orderInfo.status}'></component>//角色是leader或者是当前订单的创建者,有权限<componentv-permission:some="{role:'leader',currentUser:'orderInfo.createUser'}"></component>//角色是leader并且是当前订单的创建者,有权限<componentv-permission:every="{role:'leader',currentUser:'orderInfo.createUser'}"></component>输入限制

v-input 输入框限制,限制数字、保留n位小数点等。

exportdefault{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)}})},}functiontoFiexd(target,vnode,v){console.log(v);letln=2;if(v){ln=v;}varregStrs=[['^0(\\d+)$','$1'],//禁止录入整数部分两位以上,但首位为0['[^\\d\\.]+$',''],//禁止录入任何非数字和点['\\.(\\d?)\\.+','.$1'],//禁止录入两个以上的点['^(\\d+\\.\\d{'+ln+'}).+','$1']//禁止录入小数点后两位以上];for(vari=0;i<regStrs.length;i++){varreg=newRegExp(regStrs[i][0]);target.value=target.value.replace(reg,regStrs[i][1]);}//对于封装的像el-input组件,因为其需要通过input事件同步状态if(vnode.componentInstance){vnode.componentInstance.$listeners.input(target.value)}}functionInteger(target,vnode){letvalueStr=target.valueif(valueStr.length==1){//第一个数字不为0valueStr=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的语法糖原理即可理解,

使用方法:

<!--限制输入两位小数数字--><inputv-input:toFixed="2"/><!--限制输入正整数--><el-inputv-input:integer/>内容处理

我们也可以通过自定义指令做对内容到处理,比如

空值处理

数字千分数逗号分割

exportdefault{bind:function(){},inserted:function(el,binding){dealContent(el,binding)},update:function(el,binding){dealContent(el,binding)},componentUpdated:function(){},unbind:function(){},}functiondealContent(el,binding){const{arg}=binding;if(arg=='empty'){if(!el.textContent){//空值显示el.textContent=binding.value||'暂无数据';}}elseif(arg=='money'){//金额千分位逗号分割,如10000000显示为100,000,00if(binding.value){el.textContent=dealMoney(binding.value);}else{el.textContent=dealMoney(el.textContent);}}}

千分位分割代码:

//金额处理exportfunctiondealMoney(money,places=2){constzero=`0.00`;if(isNaN(money)||money==='')returnzero;if(money&&money!=null){money=`${money}`;letleft=money.split('.')[0];//小数点左边部分letright=money.split('.')[1];//小数点右边//保留places位小数点,当长度没有到places时,用0补足。right=right?(right.length>=places?'.'+right.substr(0,places):'.'+right+'0'.repeat(places-right.length)):('.'+'0'.repeat(places));vartemp=left.split('').reverse().join('').match(/(\d{1,3})/g);//分割反向转为字符串然后最多3个,最少1个,将匹配的值放进数组返回return(Number(money)<0?'-':'')+temp.join(',').split('').reverse().join('')+right;//补齐正负号和货币符号,数组转为字符串,通过逗号分隔,再分割(包含逗号也分割)反向转为字符串变回原来的顺序}elseif(money===0){returnzero;}else{returnzero;}}

使用方法:

<spanv-content:empty="'无'">{{message}}</span><!--金额千分位逗号分割--><spanv-content:money>100000</span>文件预览

v-preview方便的实现文件预览功能

预览图片;

预览文件;

其他预览类业务功能

import{isOffic,isPdf,isImage}from'@/utils/base'import{previewWithOffice}from'@/utils/fileUtils.js'exportdefault{inserted:function(el,binding){el.onclick=function(e){letparams=binding.valueif(isOffic(params.name)){e.preventDefault()e.stopPropagation()previewWithOffice(params.url)//使用office在线预览打开}elseif(isPdf(params.name)||isImage(params.name)){e.preventDefault()e.stopPropagation()if(params.url){//直接打开urlpreviewFile(params)}}}},//指令所在组件的VNode及其子VNode全部更新后componentUpdated:function(el,binding){el.onclick=function(e){letparams=binding.valueif(isOffic(params.name)){//使用插件预览Office文件e.preventDefault()e.stopPropagation()previewWithOffice(params.url)}elseif(isPdf(params.name)||isImage(params.name)){//预览图片和pdf等能直接打开的文件e.preventDefault()e.stopPropagation()previewFile(params)}}},unbind(el){el.onclick=null;}}//预览图片和pdf等能直接打开的文件functionpreviewFile(params){leta=document.createElement("a");a.download=params.namea.href=params.url;a.target="_blank";a.click();a=null;}

使用方法:

<!--预览图片--><image:src='url'v-preview="{name:file.name,url:file.url}"></image><!--预览文件--><spanv-preview="{name:file.name,url:file.url}">{{file.name}}</span>

关于“怎么完全掌握Vue自定义指令”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注行业资讯频道,小编每天都会为大家更新不同的知识点。

域名注册
购买VPS主机

您或许对下面这些文章有兴趣:                    本月吐槽辛苦排行榜

看贴要回贴有N种理由!看帖不回贴的后果你懂得的!


评论内容 (*必填):
(Ctrl + Enter提交)   

部落快速搜索栏

各类专题梳理

网站导航栏

X
返回顶部