#除了核心功能預設內建的指令(
v-model
和v-show
等),Vue 也允許註冊自訂指令。注意,在 Vue2.0 中,程式碼重複使用和抽象的主要形式是元件。然而,有的情況下,你仍然需要對普通 DOM 元素進行底層操作,這時候就會用到自訂指令。 【學習影片分享:vue影片教學、web前端影片】
作為使用Vue
的開發者,我們對Vue
指令一定不陌生,諸如v-model
、v-on
、v-for
、v-if
等,同時Vue也為開發者提供了自訂指令的api,熟練的使用自訂指令可以極大的提高了我們編寫程式碼的效率,讓我們可以節省時間開心的摸魚~
對於Vue的自訂指令相信很多同學已經有所了解,自訂指令的具體寫法這裡就不細講了,官方文件很詳細。但不知道各位同學有沒有這種感覺,就是這個技術感覺很方便,也不難,我也感覺學會了,就是不知道如何去應用。這篇文檔就是為了解決一些同學的這些問題才寫出來的。
PS:這次要講的自訂指令我們主要使用的是vue2.x
的寫法,不過vue3.x
不過是幾個鉤子函數有所改變,只要理解每個鉤子函數的意義,兩者的用法差異並不大。
我的上篇文章說到要自己實作一個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
就是我們想要監聽的屬性,如果我們這樣寫
参考vue实战视频讲解:进入学习
<input v-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' } } 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
需要同学去多了解一下Vnode
和Component
的API
,就像之前说的,最简单的方法就是直接在控制台中直接打印出vnode
对象,组件的vnode
上有Component
的实例componentInstance
。
接下来简单说一下上面的代码,首先我们可以在componentOptions.Ctor.extendOptions
上找到model
的定义,如果没有的话需要设置默认值value
和input
,然后分别对想原生input
的处理一样,分别监听binding.expression
的变化和modelEvent
事件即可。
需要注意的是,我们上面的代码直接给_prop
做了赋值操作,这实际上是不符合规范的,但是我目前没有找到更好的方法去实现,有好思路的同学可以在评论区留言指教。
下面?是完整的源码:
上文我们通过封装v-mymodel
为各位同学展示了如何封装和使用自定义指令,接下来我把自己在生产实践中使用自定义指令的一些经验分享给大家,通过实例,我相信各位同学能够更深刻的理解如何在在应用中封装自己的指令,提高效率。
下面我们定义一个v-permission
指令用于全平台的权限控制
示例代码
//定义权限类型 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中的自訂指令的詳細內容。更多資訊請關注PHP中文網其他相關文章!