筹备:自定义指令介绍
除了外围性能默认内置的指令 (
v-model
和v-show
等),Vue 也容许注册自定义指令。留神,在 Vue2.0 中,代码复用和形象的次要模式是组件。然而,有的状况下,你依然须要对一般 DOM 元素进行底层操作,这时候就会用到自定义指令。
作为应用 Vue
的开发者,咱们对 Vue
指令肯定不生疏,诸如 v-model
、v-on
、v-for
、v-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
是指令绑定组件对应的 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
就是咱们想要监听的属性,如果咱们这样写
<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
做了赋值操作,这实际上是不符合规范的,然而我目前没有找到更好的办法去实现,有好思路的同学能够在评论区留言指教。
上面👇是残缺的源码:
利用实际: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
- 通用的自定义指令应用技巧