手把手教你用指令(directives)写个仿 element-ui 的v-loading

说来惭愧,用 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

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

参考文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 先了解一下,在 vue 中,有很多内置的指令. 比如: v-for 用于遍历 v-if & v-show 用于隐藏...
    人话博客阅读 22,734评论 6 67
  • Vue 实例 属性和方法 每个 Vue 实例都会代理其 data 对象里所有的属性:var data = { a:...
    云之外阅读 2,198评论 0 6
  • 从感性的角度讲,我是不屑于用VUE,觉得react套件用起来更顺手,但是vue现在越来火,所以也不得入vue(杂烩...
    zhoulujun阅读 1,435评论 0 1
  • 一、了解Vue.js 1.1.1 Vue.js是什么? 简单小巧、渐进式、功能强大的技术栈 1.1.2 为什么学习...
    蔡华鹏阅读 3,311评论 0 3
  • 不知道这些天都干嘛去了? 钱、钱没挣着,一分没挣; 田地没有种,一分田也没有; ……………………………… 唉,自己...
    人生若只如初16阅读 498评论 0 0