验证码: 看不清楚,换一张 查询 注册会员,免验证
  • {{ basic.site_slogan }}
  • 打开微信扫一扫,
    您还可以在这里找到我们哟

    关注我们

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

阅读:935 来源:乙速云 作者:代码code

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

      准备:自定义指令介绍

      除了核心功能默认内置的指令 (v-model 和 v-show等),Vue 也允许注册自定义指令。注意,在 Vue2.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就是我们想要监听的属性,如果我们这样写

      那么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-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中的权限处理方式,方便大家理解也供大家参考。

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

      //角色权限
      
      //判断当前登录人
      
      //判断业务状态
      
      //角色是leader或者是当前订单的创建者,有权限
      
      //角色是leader并且是当前订单的创建者,有权限
      

      输入限制

      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的语法糖原理即可理解,

      使用方法:

      
      
      
      

      内容处理

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

      • 空值处理

      • 数字千分数逗号分割

      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;
          }
      }

      使用方法:

      {{message}}
      
      100000

      文件预览

      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;
      }

      使用方法:

      
      
      
      {{file.name}}
    分享到:
    *特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: hlamps#outlook.com (#换成@)。
    相关文章
    {{ v.title }}
    {{ v.description||(cleanHtml(v.content)).substr(0,100)+'···' }}
    你可能感兴趣
    推荐阅读 更多>