Home >Web Front-end >Vue.js >Learn more about custom directives in Vue

Learn more about custom directives in Vue

青灯夜游
青灯夜游forward
2022-11-21 20:20:141474browse

Learn more about custom directives in Vue

Preparation: Introduction to custom instructions

In addition to the default built-in instructions for core functions (v-model and v-show, etc.), Vue also allows registration of custom directives. Note that in Vue2.0, the main form of code reuse and abstraction is components. However, in some cases, you still need to perform low-level operations on ordinary DOM elements, in which case custom directives are used. [Learning video sharing: vue video tutorial, web front-end video]

As developers who use Vue, we are Vue instructions must be familiar, such as v-model, v-on, v-for, v-if, etc. At the same time, Vue also provides developers with an API for custom instructions. Proficient use of custom instructions can greatly improve the efficiency of our code writing, allowing us to save time and happily fish~

I believe many students already know about the custom instructions of Vue. The specific writing method of custom instructions will not be explained in detail here. The official documents are very detailed. But I don’t know if you students feel this way. This technology feels very convenient and not difficult. I also feel that I have learned it, but I just don’t know how to apply it. This document was written to solve these problems of some students.

PS: The custom instructions we are going to talk about this time mainly use the vue2.x writing method, but vue3.x is just a few hook functions. There are changes, as long as you understand the meaning of each hook function, there is not much difference in usage between the two.

Trial: Implement v-mymodel

My last article mentioned that I need to implement a v-model instruction myself, Here we use v-myodel to simulate a simple version. By the way, we will familiarize unfamiliar students with the steps and precautions of custom instructions.

Definition instructions

First sort out the ideas: the implementation of native input controls and components needs to be distinguished, and the implementation of input It is relatively simple. Let’s implement the processing of input first. First, we first define an instruction that does not perform any operation

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) {
        },
})

The above comments explain in detail the calling timing of each hook function, because we are adding input events and # to the component. ##valueBinding, so we can define it in the bind hook function. So we remove the others first, and the code becomes like this.

Vue.directive('mymodel', {
        //只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
        bind(el, binding, vnode, oldVnode) { 
        }
})

Let’s briefly talk about several callback parameters of the

bind function. el is the dom corresponding to the instruction binding component, binding is our instruction itself, including name, value, expression, arg, etc., vnode It is the vnode node corresponding to the currently bound component, and oldVnode is the state before vnode is updated.

Next we have to do two things:

    Bind the
  • input event and synchronize the value## of input #Bind the value to the external
  • value
  • value, listen for changes in value, and update to the value# of input ##This is easier to implement for
  • input
native components:

//第一步,添加inout事件监听
el.addEventListener('input', (e) => {
   //context是input所在的父组件,这一步是同步数据
   vnode.context[binding.expression] = e.target.value;
})
//监听绑定的变量
vnode.context.$watch(binding.expression, (v) => {
     el.value = v;
})
Here is an explanation of the above code, what is vnode.context

, It is the context of the component where our instruction is located. It can be understood that it is the component instance where the value bound to the instruction is located. Students who are not familiar with the

vnode structure are advised to read the official documentation first. However, the description in the documentation is relatively simple and not very comprehensive, so it is best to log on the console vnodeThe object has a look at its specific structure. This is very helpful for us to encapsulate custom instructions and is also helpful for understanding the Vue principle. We can get the binding value on v-model through context[binding.expression]

, and we can also modify it. In the above code, we first synchronize the

value of input by operating vnode.context[binding.expression] = e.target.value in the added input event. The effect of adding the value to the outside (context) is the same as using @input to add event listening; then we need to do the second thing, do the value value Binding, monitoring changes in value, synchronizing value changes to value of input, we thought we could use the amount$ on the Vue instance The watch method monitors value changes, and context is the Vue instance, and binding.expression is the property we want to monitor. If we write like this

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

<input v-mymodel=&#39;message&#39;/>

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

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

至此,inputv-mymodel的处理就完成了(当然input组件还有typecheckbox,radio,select等类型都需要去特别处理,这里就不再一一处理了,感兴趣的同学可以自己尝试去完善一下),但是对于非原生控件的组件,我们要特殊处理。 因此我们完善代码如下:

Vue.directive(&#39;mymodel&#39;, {
        //只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
        bind(el, binding, vnode, oldVnode) {
           //原生input组件的处理
           if(vnode.tag===&#39;input&#39;){
                //第一步,添加inout事件监听
                el.addEventListener(&#39;input&#39;, (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: &#39;value&#39;,
        event: &#39;input&#39;
  }
}
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: &#39;role&#39;,
    CURRENTUSER:&#39;currentUser&#39;,
    BUSSINESSSTATUS: &#39;bussinessStatus&#39;,
    MIX_EVERY: &#39;every&#39;,
    MIX_SOME: &#39;some&#39;
}
export default {
    //只调用一次,指令第一次绑定到元素时调用
    bind: function () {
    },
    //当前vdom插入到真实dom时,因为是对dom的样式操作,在这里操作
    inserted: function (el, binding) {
        let show = false;
        show=processingType(binding.arg,binding.value); 
        el.style.display = `${show ? &#39;inline-block&#39; : &#39;none&#39;}`
    },
    //所在组件的VNode更新时调用,状态更新后需要更新显示状态
    update: function (el, binding) {
        //避免无效的模板更新
        if(binding.value===binding.oldValue) return;
        let show = false;
        show=processingType(binding.arg,binding.value); 
        el.style.display = `${show ? &#39;inline-block&#39; : &#39;none&#39;}`
    },
    //指令所在组件的 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 == &#39;string&#39;) {
        show = roles.some(role => {
            return role.roleCode === bindingValue;
        })
    }
    return show;
}

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

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

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

输入限制

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

export default {
    inserted: function (el, binding, vnode) {
        el.addEventListener(&#39;input&#39;, function (e) {
            if (binding.arg == &#39;toFixed&#39;) {
                //限制输入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 = [
        [&#39;^0(\\d+)$&#39;, &#39;$1&#39;], //禁止录入整数部分两位以上,但首位为0
        [&#39;[^\\d\\.]+$&#39;, &#39;&#39;], //禁止录入任何非数字和点
        [&#39;\\.(\\d?)\\.+&#39;, &#39;.$1&#39;], //禁止录入两个以上的点
        [&#39;^(\\d+\\.\\d{&#39; + ln + &#39;}).+&#39;, &#39;$1&#39;] //禁止录入小数点后两位以上
    ];
    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==&#39;empty&#39;){
       if(!el.textContent){//空值显示
            el.textContent=binding.value||&#39;暂无数据&#39;;
        }
   }else if(arg==&#39;money&#39;){//金额千分位逗号分割,如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 === &#39;&#39;) return zero;
    if (money && money != null) {
        money = `${money}`;
        let left = money.split(&#39;.&#39;)[0]; // 小数点左边部分
        let right = money.split(&#39;.&#39;)[1]; // 小数点右边
        // 保留places位小数点,当长度没有到places时,用0补足。
        right = right ? (right.length >= places ? &#39;.&#39; + right.substr(0, places) : &#39;.&#39; + right + &#39;0&#39;.repeat(places - right.length)) : (&#39;.&#39; + &#39;0&#39;.repeat(places));
        var temp = left.split(&#39;&#39;).reverse().join(&#39;&#39;).match(/(\d{1,3})/g); // 分割反向转为字符串然后最多3个,最少1个,将匹配的值放进数组返回
        return (Number(money) < 0 ? &#39;-&#39; : &#39;&#39;) + temp.join(&#39;,&#39;).split(&#39;&#39;).reverse().join(&#39;&#39;) + right; // 补齐正负号和货币符号,数组转为字符串,通过逗号分隔,再分割(包含逗号也分割)反向转为字符串变回原来的顺序
    } else if (money === 0) {
        return zero;
    } else {
        return zero;
    }
}

使用方法:

<span v-content:empty="&#39;无&#39;">{{message}}</span>
<!-- 金额千分位逗号分割 -->
<span v-content:money>100000</span>

文件预览

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

  • 预览图片;

  • 预览文件;

  • 其他预览类业务功能

import {isOffic,isPdf,isImage} from &#39;@/utils/base&#39;
import {previewWithOffice} from &#39;@/utils/fileUtils.js&#39;
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=&#39;url&#39; 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
  • 通用的自定义指令使用技巧

(学习视频分享:web前端开发编程基础视频

The above is the detailed content of Learn more about custom directives in Vue. For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:juejin.cn. If there is any infringement, please contact admin@php.cn delete