说来惭愧,用 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"
orv-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 就获取到了焦点
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 上,然后被插入到了父节点中。
当我们点击 “更换title” 按钮时,其实指定绑定的元素肯定是会更新的,但是指令的 value 值是还没有更新的,仍然是 red。
当我们点击 “更换 color” 按钮时,指令的值就发生变化而,由 red 变成了 blue。
当我们点击 “测试解绑 v-color”,我们其实就是销毁了指令所绑定的组件,指令就解绑了。
demo3:钩子函数参数
在上述 demo 中我们看到钩子函数的参数有 el、binding 等,可能不是很理解,这一个 demo 就带大家了解一下钩子函数的参数。
照例我们先看看官网是怎么说明钩子函数参数的。
el
:指令所绑定的元素,可以用来直接操作 DOM-
binding
:一个对象,包含一下属性:name
:指令名,不包括v-
前缀value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为 2。oldValue
:指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否改变都可用。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
:上一个虚拟节点,仅在update
和componentUpdated
钩子中可用。
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>
我们来看下结果:是不是很简单参数就那些
文档中关于动态指令参数、字面量我就不接说了,大家自己看下文档就可以了。
带大家实现一个仿 element-UI 的 v-loading
上面的基本知识介绍了那么多,我们终于可以综合运用来写一个实用的例子了。
首先我们看下 element 的 v-loading 有哪些属性
但是这里我们不会实现上面那么多属性,因为我懒,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>
这样写下来,发现也没什么可讲的,一切都很自然。
铛铛~,我们来看下效果:
还蛮好看的。是不是很简单。