美文网首页
手把手教你用指令(directives)写个仿 element-

手把手教你用指令(directives)写个仿 element-

作者: 大侠叫谁 | 来源:发表于2020-01-09 16:54 被阅读0次

    说来惭愧,用 Vue 这么长时间了,今天第一次用指令。

    是出于什么契机呢?

    主要是今天我们需要优化一下 loading 的效果,之前项目中用的都是 element-ui 的v-loading,现在我们网站的审美提上来了,过去的效果已经跟不上我们“罗网”的气质了。修改默认 loading 效果的方法千千万,我为啥要自定义指令来写一个 loading 呢,因为我没用过,我就想用一下,我愿意,而且以后再做修改的话,这个方案的可扩展性更好,想怎么改就怎改,不用抠抠搜搜地去改样式。

    提示:本文只针对有 Vue 开发经验的同学。看完这篇文章,大家应该能够写一些东西了,如果看完还不会,很正常,每个人都有自己的天赋,对于不是自己天赋方面的东西多看几遍就好了,大家都是智商正常的人类,没有什么东西是他能学会,你不能学会的,所以不用着急,跟着节奏慢慢来就可以。分享一个我奶奶留下来的家训“活到老,学到老,还有一招学不到”,所以大家要对自己有信心。

    今天排插炸了,还好没炸的很大,只是小小的炸了一下起了点火星子冒了点小烟,没炸到脸。我可是没开空调、没坐在电热毯里,穿个睡衣坐在书桌前写的文啊。还好有我们王刚王老师推荐的“每日坚果”补充能量,加班写文必备,代码的好伴侣,大家快去买。

    本文大纲

    • 什么是指令?
    • 常见的默认指令
    • 什么是自定义指令?我会带着大家过一下 Vue 官网文档上的demo
    • 结合具体的业务场景写一个自定义的loading指定 ,暂定 v-cloading
    • 总结 指令比较适合哪些应用场景
    • 参考文档

    什么是指令?

    • 指令是带有 v-前缀的特殊属性
    • 当表达式的值改变时,将其产生的 连带影响,响应式地作用于 DOM

    第一句换很好理解,第二句我们在接下来的demo中会让你直观的感受到这句话的意思。

    常见的默认指令

    这里我们就列举三个常见的指令,想看更多指令可以看看 Vue 官方文档,或是 Vue指令基本使用大全这篇博文,这篇文章列举出了挺多的。

    v-model

    • 作用:在表单元素上创建双向的数据绑定

    • 说明:监听用户的输入事件以更新数据

    <input v-model="message" placeholder="edit me">
    <p>Message is: {{ message }}</p>
    

    v-on

    • 作用:绑定事件
    • 语法:v-on:click="say" or v-on:click="say('参数', $event)"
    • 简写:@click="say"
    • 说明:绑定的事件从methods中获取
    <!-- 完整语法 -->
    <a v-on:click="doSomething"></a>
    <!-- 缩写 -->
    <a @click="doSomething"></a>
    <!-- 方法传参 -->
    <a @click="doSomething(“123”)"></a>
    
     <script>
        // 2 创建 Vue 的实例对象
        var vm = new Vue({
          el: '#app',
          // methods属性用来给vue实例提供方法(事件)
          methods: {
            doSomething: function(str) {
              //接受参数,并输出
              console.log(str);
            }
          }
        })
      </script>
    

    v-bind

    • 作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM
    • 语法:v-bind:title="msg"
    • 简写::title="msg"
    <!-- 完整语法 -->
    <a v-bind:href="url"></a>
    <!-- 缩写 -->
    <a :href="url"></a>
    <script>
        // 2 创建 Vue 的实例对象
        var vm = new Vue({
          // el 用来指定vue挂载到页面中的元素,值是:选择器
          // 理解:用来指定vue管理的HTML区域
          el: '#app',
          // 数据对象,用来给视图中提供数据的
          data: {
            url: 'http://www.baidu.com'
          }
        })
      </script>
    

    什么是自定义指令

    这里我会把 官方文档上的 demo 和一些必要的说明搬过来。为什么要这么做?为了从来没接触过自定义指令的同学不用自己查找切换看文档。只需要看完这一篇,了解一些基本的概念,就可以循序渐进的带你写一个 v-cloading 自定义loading的指令。如果有想了解源码的同学,可以暂时移步,网上有很多介绍 v-loading 带你解读源码的文章。这里我们是第一次使用,所以只会浅显地介绍应用。

    Vue 官方文档上都是以全局注册为例子,在这里我们就都以局部注册为例子。

    demo1:当页面加载时,input 自动获取焦点

    我们先上代码

    <template>
      <div class="test-page">
        <!-- 我们的指令 v-fo我们的指令 -->
        <input type="text" v-focus>
      </div>
    </template>
    <script>
    export default {
      // directives 不用多做解释,就是放指令的地方
      directives: {
        // focus 我们的指令名称 这里我们写focus就可以了,Vue会默认给我们加上 v-的
        focus: { // 这个对象相当于我们如何去描述和定义这个 focus 指令
          // 当被绑定的元素插入到 dom 中时
          inserted: (el) => {
            // el 绑定指令的元素,聚焦
            el.focus()
          }
        }
      }
    }
    </script>
    <style lang="scss" scoped>
    .test-page {
      padding: 20px;
      input {
        width: 200px;
        height: 40px;
      }
    }
    </style>
    

    在这里我们不用过多纠结每一行代码是什么,比如 inserted 是什么,inserted 是指令的一个钩子函数,不用着急,后面我们会具体介绍一下 指令的钩子函数的。这个demo只是让我们先感受下指令是什么,能做什么,是不是很简单,感觉自己随随便也能写一个 demo 了。

    结果:可以看到确实是页面一加载就 input 就获取到了焦点

    image-20191221110017591.png
    demo2:带你感受指令的钩子函数

    虽然代码应该是很客观很理性的东西,但是学习的过程中,感受也很重要,感受会影响你想不想学,如果只是冰冷的定义我觉得会很难理解一个东西。你感受到了,也自然就学会了。

    我先列一下官网对于钩子函数的定义和描述,然后我会结合具体的例子,让大家感受一下钩子函数是怎样发挥作用的,触发的时机是什么。

    一个指令定义对象可以提供如下几个钩子函数(均为可选):

    • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    • inserted: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
    • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
    • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用
    • unbind:只调用一次,指令与元素解绑时调用。

    关于钩子函数其实我理解的也不是和深刻,就以 demo 去理解,如果有同学更加理解钩子函数,可以告诉我,大家一起交流一下。这个 demo 也是看别人写的。

    <template>
      <div class="test-page">
        <h1 v-color="color" v-if="show">{{title}}</h1>
        <button @click="show=false">测试解绑 v-color</button>
        <button @click="title='更换title'">更换 title</button>
        <button @click="color='blue'">更换 color</button>
      </div>
    </template>
    <script>
    export default {
      data () {
        return {
          color: 'red',
          title: '自定义指令',
          show: true
        }
      },
      directives: {
        color: {
          bind: () => {
            console.log('bind')
          },
          inserted: (el, binding) => {
            console.log('inserted')
            el.style.color = binding.value
          },
          update: (el, binding) => {
            console.log('update value: ', binding.value)
            console.log('update oldValue: ', binding.oldValue)
            if (binding.value !== binding.oldValue) {
              el.style.color = binding.value
            }
          },
          componentUpdated: (el, binding) => {
            console.log('componentUpdated')
          },
          unbind: () => {
            console.log('v-color 指令解绑')
          }
        }
      }
    }
    </script>
    

    当我们刷新页面时,指令显示被绑定到了 dom 上,然后被插入到了父节点中。


    image-20191221135933395.png

    当我们点击 “更换title” 按钮时,其实指定绑定的元素肯定是会更新的,但是指令的 value 值是还没有更新的,仍然是 red。

    image-20191221140302702.png

    当我们点击 “更换 color” 按钮时,指令的值就发生变化而,由 red 变成了 blue。

    image-20191221140510817.png

    当我们点击 “测试解绑 v-color”,我们其实就是销毁了指令所绑定的组件,指令就解绑了。

    image-20191221140652888.png

    demo3:钩子函数参数

    在上述 demo 中我们看到钩子函数的参数有 el、binding 等,可能不是很理解,这一个 demo 就带大家了解一下钩子函数的参数。

    照例我们先看看官网是怎么说明钩子函数参数的。

    • el:指令所绑定的元素,可以用来直接操作 DOM

    • binding:一个对象,包含一下属性:

      • name:指令名,不包括v-前缀

      • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。

      • oldValue :指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。

      • expression :字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"

      • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"

      • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }

    • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。

    • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

    Warning:

    除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行。

    <template>
      <div class="test-page">
        <div v-demo:foo.a.b="message"></div>
      </div>
    </template>
    <script>
    export default {
      data () {
        return {
          message: 'hello!'
        }
      },
      directives: {
        demo: {
          bind: (el, binding, vnode) => {
            var s = JSON.stringify
            el.innerHTML =
              'name: ' + s(binding.name) + '<br>' +
              'value: ' + s(binding.value) + '<br>' +
              'expression: ' + s(binding.expression) + '<br>' +
              'argument: ' + s(binding.arg) + '<br>' +
              'modifier: ' + s(binding.modifiers) + '<br>' +
              'vnode keys: ' + Object.keys(vnode).join(', ')
          }
        }
      }
    }
    </script>
    

    我们来看下结果:是不是很简单参数就那些


    image-20191221171512129.png

    文档中关于动态指令参数、字面量我就不接说了,大家自己看下文档就可以了。

    带大家实现一个仿 element-UI 的 v-loading

    上面的基本知识介绍了那么多,我们终于可以综合运用来写一个实用的例子了。

    首先我们看下 element 的 v-loading 有哪些属性


    image-20191221172040629.png

    但是这里我们不会实现上面那么多属性,因为我懒,demo并非本人原创,我只是做了修改。

    我们今天的 demo 实现:fullscreen text spinner background 等属性,我们的自定义指令 我们就叫它 v-cloading 吧。需要loading的时候我们就创建一个实例,把它挂到父级元素上去。

    首先我们先准备一个 loading 的模板 mask.vue:

    这个没什么好讲的,就是定义了一下 loading 的效果长什么样

    <template>
      <transition name="cv-loading-fade">
        <div
          v-show="visible"
          class="cv-loading-mask"
          :style="{ backgroundColor: background || ''}"
          :class="[customClass, { 'is-fullscreen': fullscreen }]">
          <div class="cv-loading-spinner">
            <div class="loading-animation-box">
              <span class="circle purple-circle"></span>
              <span class="circle green-circle"></span>
            </div>
            <span class="cv-loading-text">{{text}}</span>
          </div>
        </div>
      </transition>
    </template>
    <script>
    export default {
      data () {
        return {
          text: null,
          background: null,
          fullscreen: true,
          visible: false,
          customClass: ''
        }
      },
      mounted () {
        if (this.fullscreen) {
          document.body.style.overflow = 'hidden'
        }
      },
      methods: {
        setText (text) {
          this.text = text
        }
      },
      destroyed () {
        document.body.style.overflowX = 'hidden'
      }
    }
    </script>
    <style lang="scss" scoped>
    @import "assets/styles/variables.scss";
    .cv-loading-fade-enter,
    .cv-loading-fade-leave-active {
      opacity: 0;
    }
    .cv-loading-mask {
      position: absolute;
      z-index: 2000;
      background-color: rgba(255, 255, 255, 0.9);
      margin: 0;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      transition: opacity 0.3s;
      &.is-fullscreen {
        position: fixed;
        .cv-loading-spinner {
          margin-top: -25px;
        }
      }
    }
    .cv-loading-spinner {
      top: 50%;
      margin-top: -21px;
      width: 100%;
      text-align: center;
      position: absolute;
      .loading-animation-box {
        position: relative;
        width: 64px;
        height: 64px;
        margin: 0 auto;
        .circle {
          position: absolute;
          display: inline-block;
          width: 14px;
          height: 14px;
          border-radius: 50%;
          top: 25px;
          transition: all;
          &.purple-circle {
            background-color: $primary_color;
            left: 10px;
            animation: leftAnimation 1.5s ease-in-out infinite;
          }
          &.green-circle {
            background-color: $sub_color;
            right: 10px;
            animation: rightAnimation 1.5s ease-in-out infinite;
          }
        }
      }
      .cv-loading-text {
        margin: 3px 0;
        font-size: 14px;
        color: #5D37EC;
      }
    }
    @keyframes leftAnimation {
      0% {
        transform: scale(1) translateX(0px);
        z-index: 1;
      }
      25% {
        transform: scale(1.5) translateX(15px);
        z-index: 5;
      }
      50% {
        transform: scale(1) translateX(30px);
        z-index: 5;
      }
      75% {
        transform: scale(0.5) translateX(15px);
        z-index: 5;
      }
      100% {
        transform: scale(1) translateX(0px);
        z-index: 1;
      }
    }
    @keyframes rightAnimation {
      0% {
        transform: scale(1) translateX(0px);
        z-index: 1;
      }
      25% {
        transform: scale(0.5) translateX(-15px);
        z-index: 1;
      }
      50% {
        transform: scale(1) translateX(-30px);
        z-index: 1;
      }
      75% {
        transform: scale(1.5) translateX(-15px);
        z-index: 5;
      }
      100% {
        transform: scale(1) translate(0px);
        z-index: 1;
      }
    }
    </style>
    
    

    接下来我们看下最关键的部分,指令的实现部分,其实这部分的代码和 element v-loading 本身的实现比较相似。虽然这块代码还不算特别完善,但是基本的实现是可以的,这块我会重点讲一下。等我认真阅读完 element-UI v-loading 的源码后,会再进行完善的。

    import Vue from 'vue'
    import maskLoading from './mask.vue'
    
    // 我们通过模板 构造一个 Mask
    const Mask = Vue.extend(maskLoading)
    // Mask 是否需要更新,也就是 loading 展示效果是否需要更新
    const toggleLoading = (el, binding) => {
      // 如果指令传入的值为 true 或是有值,就显示这个模板,挂到父级元素上去或是body上
      if (binding.value) {
        Vue.nextTick(() => {
          if (binding.modifiers.fullscreen) {
            // 全屏的话就挂载到 body 上
            document.body.appendChild(el.mask)
          } else {
            // 非全屏就挂到当前组件上去
            let height = el.clientHeight
            let width = el.clientWidth
            let offsetTop = el.offsetTop
            el.mask.style.top = offsetTop + 'px'
            el.mask.style.height = height + 'px'
            el.mask.style.width = width + 'px'
            el.appendChild(el.mask)
          }
        })
      } else {
        // 如果传入的值是 false,或是没有值,就销毁 Mask
        el.mask && el.mask.parentNode && el.mask.parentNode.removeChild(el.mask)
        el.instance && el.instance.$destroy()
      }
    }
    
    Vue.directive('cloading', {
      bind (el, binding) {
        // 指令第一次绑定到元素上时,初始化一些属性,这些属性可以通过字面量的形式传,也可以通过 dataset或是其他方式,我还没想好。
        let background = binding.value.background
        let text = binding.value.text
        let iconSrc = binding.value.iconSrc
        let iconWidth = binding.value.iconWidth
        let iconHeight = binding.value.iconHeight
        let color = binding.value.color
        let fontSize = binding.value.fontSize
        console.log('binding.value: ', binding.value)
        // 构造了一个 Mask 实例
        const mask = new Mask({
          el: document.createElement('div'),
          data: {
            fullscreen: !!binding.modifiers.fullscreen,
            background: background || '255, 255, 255, 0.9',
            text: text || '加载中...',
            iconSrc: iconSrc || require('../../assets/images/icn_loading.png'),
            iconWidth: iconWidth || null,
            iconHeight: iconHeight || null,
            color: color || null,
            fontSize: fontSize || null
          }
        })
        el.instance = mask
        el.mask = mask.$el
        // 更新 Mask的展示
        toggleLoading(el, binding)
      },
      // 所在组件的 VNode 更新时调用
      update (el, binding) {
        if (binding.oldValue !== binding.value) {
          toggleLoading(el, binding)
        }
      },
      unbind (el) {
        el.mask && el.mask.parentNode && el.mask.parentNode.removeChild(el.mask)
        el.instance && el.instance.$destroy()
      }
    })
    
    <div
         :style="{height: '1000px', width: '100%'}"
         v-cloading.fullscreen="true"
         >
    </div>
    

    这样写下来,发现也没什么可讲的,一切都很自然。

    铛铛~,我们来看下效果:

    q8mnk-doexw.gif

    还蛮好看的。是不是很简单。

    参考文档

    相关文章

      网友评论

          本文标题:手把手教你用指令(directives)写个仿 element-

          本文链接:https://www.haomeiwen.com/subject/fnnyactx.html