本文不会详细的说明 vue 中指令这些知识点,如果存在疑问,请自行查阅文档或者其他资料
为什么使用指令实现 loading
- 在日常的开发中,加载效果是非常常见的,但是怎么才能方便的使用,那就还是值得思考一番的,
- 比如在 vue 中,最简单的方式就是封装为一个组件了,但是如果封装为组件的话,在不想注册为全局组件的时候,每次都需要引入、注册、使用;如果注册为全局组件,你也往往需要分析结构在合适的位置插入组件,貌似使用起来都会麻烦一点,loading 这种使用频率高的效果,使用一次麻烦一点,使用100次就会觉得更加麻烦
- 而使用指令只需要在需要的位置像使用属性一样即可;封装可以麻烦,但是使用越简单越好
具体实现
封装准备
-
首先需要一个 js 文件,因为指令实际上就是一个对象,通过在不同的钩子函数中执行对应的逻辑,在本文中,需要使用的钩子函数是 inserted 和 update,因此可以写一个基础的结构,如下:
export default { inserted(el, binding){ }, update(el, binding){ } }
-
然后将这个指令在入口文件 main.js 内进行全局注册,如下:
import vLoading from '你封装指令js文件的路径' // 注册指令 Vue.directive('jc-loading', vLoading)
-
创建一个 vue 文件来使用这个指令,如下:
<template> <div class="container"> <button style="margin-bottom: 20px" @click="handleClick"> 开关 </button> <div class="box" v-jcLoading="isLoading"> Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero, temporibus veniam! Totam temporibus ipsam, atque amet aliquid corporis molestiae, perspiciatis asperiores doloremque enim explicabo aperiam. Vel doloremque voluptatibus incidunt quae suscipit cupiditate. Obcaecati sunt, consectetur voluptas sequi aliquam omnis, rem non molestiae assumenda illum quasi excepturi error voluptatibus pariatur nulla. </div> </div> </template> <script> export default { data() { return { isLoading: false } }, methods: { handleClick() { this.isLoading = !this.isLoading } } } </script> <style lang="less" scoped> .container { width: 100vw; height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; .box { width: 500px; height: 300px; padding: 20px; border: 1px solid #999; color: #f40; } } </style>
-
查看一下指令内的输出语句是否正常执行,如图:
- 正常进行了打印,现在我们进行正式的编写
实现 loading 效果
-
实现这一步其实也非常的简单,找一个你觉得好看或者合适的加载效果,按照正常的 html+css+js 进行实现就好,当你实现好之后,需要做的就是使用 js 进行动态的创建这些元素,所以我们需要有一个函数帮助我们完成这一步,如下:
// 导入模块化的 less 文件 import styles from './loading.module.less' // 创建 loading 元素 function createLoading() { // 创建 load 遮罩 const loadingMask = document.createElement('div') loadingMask.dataset.role = 'jc-loading' loadingMask.classList.add(styles['jc-loading-mask']) // 创建 loading 旋转容器元素 const loadingSpinner = document.createElement('div') loadingSpinner.classList.add(styles['jc-loading-spinner']) loadingMask.appendChild(loadingSpinner) // 创建文本片段 const fragment = document.createDocumentFragment() // 创建子元素进行旋转缩放 for (let i = 0; i < 12; i++) { const div = document.createElement('div') div.style = `--i:${i}` div.classList.add(styles['jc-loading-spinner__circle']) fragment.appendChild(div) } loadingSpinner.appendChild(fragment) return loadingMask }
-
代码还是非常简单的,具体取决于你本身实现的 loading 效果,我这个是一个比较简单的动效,上面这个地方如果有疑问那应该就是证据导入语句,在 vue 中,如果希望一个 less 文件作为一个模块导入和使用,需要将文件命名改为
文件名.module.less
这种格式,即文件后缀为 .module.less,我们在 inserted 钩子函数中打印一下这个导入的 styles,如下:export default { inserted(el, binding){ console.log(styles) }, update(el, binding){ } }
-
结果如图:
-
k 为 less 文件中开发时书写的类名,而后面的 v 表示实际的类名,本案例中 less 文件代码如下:
.jc-loading-mask { position: absolute; inset: 0; background-color: rgba(0, 0, 0, 0.7); } .jc-loading-spinner { position: absolute; left: calc(50% - 25px); top: calc(50% - 25px); width: 50px; height: 50px; animation: sp 4s linear infinite; } .jc-loading-spinner__circle { position: absolute; top: 0; left: calc(50% - 3px); width: 6px; height: 6px; transform: rotate(calc(var(--i) * (360deg / 12))); transform-origin: center 25px; } .jc-loading-spinner__circle::before { content: ''; inset: 0; border-radius: 50%; position: absolute; background-color: #ff6348; animation: zoom 2.5s linear infinite; animation-delay: calc(var(--i) * 0.2s); } @keyframes sp { to { transform: rotate(360deg); } } @keyframes zoom { 0% { transform: scale(1.2); } 50% { transform: scale(0.5); } 100% { transform: scale(1.2); } }
-
这些 css 样式我就不再赘述了,先不进行其他逻辑判断,只展示到页面上,看看效果,代码如下:
export default { inserted(el, binding){ el.appendChild(createLoading()) }, update(el, binding){ } }
-
效果如图:
-
其实也不难对吧,这个效果你可以根据自己的需求来进行更换,但是实现方法都是可以套用的
loading 显示与隐藏
-
把这个需求梳理清楚之后,后面的就呼之欲出了,什么时候显示,必然是指令上的值为 true 的时候,隐藏则相反,这是一个先决条件
-
在这个条件之后呢?还需要考虑什么呢?是不是需要创建这个 loading 效果的元素啊,当指令的值为 true 且不存在当前的 loading 元素的时候,才需要创建,而指令的值为 false ,则是当前的 loading 元素存在的话,就需要移除啊
-
基于上面的条件,我们需要一个辅助函数,来帮助我们查找当前 loading 效果的元素是否存在,如下:
function getLoading(container) { return container.querySelector(`[data-role="jc-loading"]`) }
-
所以我们在 inserted 钩子函数中,应该进行判断,当指令的值为 true 且元素不存在时,就创建元素并添加,如下:
inserted(el, binding){ // 如果为 true 且不存在加载元素就创建元素添加加载效果 if (!getLoading(el)) { const loading = createLoading() el.appendChild(loading) } }
-
而 update 函数中的代码是不是也可以写出来了,进行条件判断来执行逻辑,而且不难发现其实这个条件与 inserted 中的条件是重合的,所以我们可以封装为一个函数,如下:
// 开启加载效果 function openLoading(el, binding) { // 如果为 false 且存在加载元素就移除加载元素 if (!binding.value) { const dom = getLoading(el) dom && dom.remove() } else { // 如果为 true 且不存在加载元素就创建元素添加加载效果 if (!getLoading(el)) { const loading = createLoading() el.appendChild(loading) } } }
-
当然,还需要考虑当前显示加载元素的 dom 是不是存在相对定位,如果不存在则改为相对定位,最后指令对象的实际代码如下:
export default { inserted(el, binding){ // 检测绑定的元素的 position 属性是否为 static if (window.getComputedStyle(el).position === 'static') { // 如果是则改为相对定位 el.style.position = 'relative' } openLoading(el, binding) }, update(el, binding){ openLoading(el, binding) } }
-
我们看一下实际的效果,如图:
使用修饰符扩展
-
通过 modifiers(修饰符) 进行一个扩展,当指令了添加了修饰符 body 的时候,loading 就会插入到 body 里面,填充 body,所以我们还需要进行一些额外的判断,如下:
function getContainer(el, binding) { return binding.modifiers.body ? document.body : el } export default { inserted(el, binding) { if (window.getComputedStyle(el).position === 'static') { el.style.position = 'relative' } openLoading(getContainer(el, binding), binding) }, update(el, binding) { openLoading(getContainer(el, binding), binding) } }
-
此时在组件中使用添加修饰符 body 即可,如下:
<!-- 添加修饰符.body --> <div class="box" v-jcLoading.body="isLoading"> ... </div>
-
查看效果,如图:
-
元素实际插入的位置,如图:
完整代码与结语
-
现在已经具备了一个 loading 指令基本的效果,如果还需要进行其他扩展,比如传递给 loading 指令的值不是一个单纯的布尔值,而是一个对象,如下:
{ loading:true, color: 'blue', text: '拼命加载中...' ... }
-
通过这些配置来增强指令的效果,有兴趣的可以自己试试
-
完整指令代码:
import styles from './loading.module.less' function getLoading(container) { return container.querySelector(`[data-role="jc-loading"]`) } function createLoading() { const loadingMask = document.createElement('div') loadingMask.dataset.role = 'jc-loading' loadingMask.classList.add(styles['jc-loading-mask']) const loadingSpinner = document.createElement('div') loadingSpinner.classList.add(styles['jc-loading-spinner']) loadingMask.appendChild(loadingSpinner) const fragment = document.createDocumentFragment() for (let i = 0; i < 12; i++) { const div = document.createElement('div') div.style = `--i:${i}` div.classList.add(styles['jc-loading-spinner__circle']) fragment.appendChild(div) } loadingSpinner.appendChild(fragment) return loadingMask } function openLoading(el, binding) { if (!binding.value) { const dom = getLoading(el) dom && dom.remove() } else { if (!getLoading(el)) { const loading = createLoading() el.appendChild(loading) } } } function getContainer(el, binding) { return binding.modifiers.body ? document.body : el } export default { inserted(el, binding) { if (window.getComputedStyle(el).position === 'static') { el.style.position = 'relative' } openLoading(getContainer(el, binding), binding) }, update(el, binding) { openLoading(getContainer(el, binding), binding) } }
-
less 样式代码:
.jc-loading-mask { position: absolute; inset: 0; background-color: rgba(0, 0, 0, 0.7); } .jc-loading-spinner { position: absolute; left: calc(50% - 25px); top: calc(50% - 25px); width: 50px; height: 50px; animation: sp 4s linear infinite; } .jc-loading-spinner__circle { position: absolute; top: 0; left: calc(50% - 3px); width: 6px; height: 6px; transform: rotate(calc(var(--i) * (360deg / 12))); transform-origin: center 25px; } .jc-loading-spinner__circle::before { content: ''; inset: 0; border-radius: 50%; position: absolute; background-color: #ff6348; animation: zoom 2.5s linear infinite; animation-delay: calc(var(--i) * 0.2s); } @keyframes sp { to { transform: rotate(360deg); } } @keyframes zoom { 0% { transform: scale(1.2); } 50% { transform: scale(0.5); } 100% { transform: scale(1.2); } }