当前位置: 首页 > news >正文

vue3新拟态组件库开发流程——loading组件源码

众所周知,loading有指令和服务两种实现,写指令实现的时候遇到了很多困难。首先是指令要根据生命周期钩子,vue3的钩子和vue2不一样了。再者是指令传值的解决方案,vue官方文档上写的例子有一定的省略,有一定的误导倾向。这些先不谈,先一步步从最简单开始。
首先作出一个组件,把这个组件调用,能显示出loading的画面。代码如下,其中css中的var是定义的全局css变量,相关代码未展示,可以换成一个固定的颜色比如#000,即可

<template>
  <div class="loading-mask">
    <div class="loading-spinner ">
      <div class="loading"></div>
    </div>
  </div>
</template>

<style scoped>
.loading-mask {
  position: absolute;
  z-index: 2000;
  background-color: hsla(0,0%,100%,.9);
  margin: 0;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  transition: opacity .3s;
}

.loading-spinner {
  position: absolute;
  height: 50px;
    top: 50%;
    margin-top: -21px;
    width: 100%;
    text-align: center;
}

.loading {
  display: inline-block;
  width: 50px;
  perspective: 200px;
}

.loading:before,
.loading:after {
  position: absolute;
  width: 20px;
  height: 20px;
  content: "";
  animation: jumping 0.5s infinite alternate;
  background: rgba(0, 0, 0, 0);
}

.loading:before {
  left: 0;
}

.loading:after {
  right: 0;
  animation-delay: 0.15s;
}

@keyframes jumping {
  0% {
    transform: scale(1) translateY(0px) rotateX(0deg);
    box-shadow: 0 0 0 rgba(0, 0, 0, 0);
  }

  100% {
    transform: scale(1.2) translateY(-25px) rotateX(45deg);
    background: var(--main-color);
    box-shadow: 0 25px 40px #000;
  }
}
</style>

效果如下
在这里插入图片描述
成功显示在了页面的正中间居中,是css中的位置absolute和 text-align: center;的功劳。
为什么要这么写居中呢,是为了后面一个需求的考虑,我们要实现loading页面的某一部分,比如页面中有三个div,我们只想loading其中一个,就需要定位,只需要给那个需要的元素的css position改为relative即可。
这里我们就先实现指令模式,在html中添加一个指令,名为v-laoding。被加的那个div就是我们想要让它loading的那个。但是loading完了总要结束吧,所以要给loading传值,传入的是true,就显示,false就是代表loading结束了。
所以我们的测试代码就写好了

<template>
  <div>
    <div class="zz">
      1
    </div>
    <div class="zz" v-loading="'loading'">
      2
    </div>
    <div class="zz">
      3
    </div>
  </div>
</template>

<style scoped>
.zz{
  width: 500px;
  height: 500px;
}
</style>

这样一个demo放进去会报错,因为loading指令,也就是v-loading没部署,接下来部署一下。我们部署要做两件事,第一件:放入新的dom元素(loading),第二件:给加指令的那个div添加新的样式(css position改为relative)
然后考虑一下这两件事要不要提出公共方法,第一件不用以为加dom就一句话的事情,更多的是判断啥时候要加,这个判断运用不到未来可能会添加的代码中,是逻辑不同的。所以第一件不用。第二件则要提取出来,添加class名,和去除class名,可以提取成2个方法,入参是class名字。于是先再公共utils里加入这个公共方法。

// 往dom元素中添加class
export function addClass(el, className) {
  if (!el.classList.contains(className)) {
    el.classList.add(className)
  }
}

export function removeClass(el, className) {
  el.classList.remove(className)
}

接下来考虑vue3的函数式编程思路,考虑要不要将添加指令的js文件进行函数化拆分。要的。拆成两个,一个是构造参数部分,一个是添加部分。

// directive.js
import Loading from './loading.vue'
import createLoadingLikeDirective from './create-loading-like-directive'

const loadingDirective = createLoadingLikeDirective(Loading)

export default loadingDirective
// create-loading-like-directive.js
import { createApp } from 'vue'
import { addClass, removeClass } from '@/sanorin/utils/dom'

const relativeCls = 'sanorin-loading-parent--relative'
// 这个样式的代码写在全局样式里,所谓全局样式,对标的是elementui使用的时候在main.js中要引入的三段话中的css那段。
//如下:
//.sanorin-loading-parent--relative{
//  position: relative !important;
//}

export default function createLoadingLikeDirective(Comp) {
  return {
    mounted(el, binding) {
      console.log(el, binding)
      const app = createApp(Comp)
      console.log(app)
      const instance = app.mount(document.createElement('div'))
      console.log(instance)
      const name = Comp.name
      if (!el[name]) {
        el[name] = {}
      }
      el[name].instance = instance
      const title = binding.arg
      if (typeof title !== 'undefined') {
        instance.setTitle(title)
      }

        append(el)
    },
    updated(el, binding) {
      const title = binding.arg
      const name = Comp.name
      if (typeof title !== 'undefined') {
        el[name].instance.setTitle(title)
      }
      if (binding.value !== binding.oldValue) {
        console.log(binding.value)
        binding.value ? append(el) : remove(el)
      }
    }
  }

  function append(el) {
    const name = Comp.name
    const style = getComputedStyle(el)
    console.log(style.position)
    if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) {
      addClass(el, relativeCls)
    }
    el.appendChild(el[name].instance.$el)
  }

  function remove(el) {
    const name = Comp.name
    removeClass(el, relativeCls)
    el.removeChild(el[name].instance.$el)
  }
}

之后把directive.js在main.js中使用即可,有注释部分为新添加部分

import { createApp } from 'vue'
import App from './App.vue'

import loadingDirective from './sanorin/packages/loading/directive' // 引入指令函数
import './sanorin/style/global.css' // 引入组件库全局样式

const app = createApp(App)
app.directive('loading', loadingDirective) // 使用指令函数,注意要在#app之前
app.mount('#app')

我们上面的测试demo效果如下
在这里插入图片描述
因为我们以前部署了了组件库的use,于是把关于这个组件库的指令,也放进去,相关代码看以前的文章,应该是这个系列的第一篇。代码如下,有注释的为新加的loading指令相关代码

import menu from './packages/menu/menu.vue';
import exhibitFrame from './packages/exhibit-frame/exhibit-frame.vue';
import button from './packages/button/button.vue';
import input from './packages/input/input.vue';
import radio from './packages/radio/radio.vue';
import radioGroup from './packages/radio/radio-group.vue';
import loadingDirective from './packages/loading/directive' // 引入指令

const components = [menu,exhibitFrame,button,input,radio,radioGroup]

const sanorin = {
  install: (app:any) => {
    components.forEach(component => {
      app.component(component.name, component)
    })
    app.directive('loading', loadingDirective) // 部署指令
  }
}

export default sanorin;

接下来完善一下,加入可自定义的提示语
想要用指令中的arg来进行提示语的传递,详见vue官方文档
还是先写测试demo

<script setup>
import { ref } from 'vue'
let loadingFlag = ref(true)
setInterval(() => {
  loadingFlag.value = !loadingFlag.value
}, 2000);
</script>
  
<template>
    <div class="zz" v-loading:[`拼命加载中……`]="loadingFlag">
      2
    </div>
</template>

<style scoped>
  .zz{
    width: 200px;
    height: 200px;
    border: 1px solid red;
  }
</style>

前面部署loading的时候已经写了进去setTitle方法,现在要在组件里真正把这个方法写上

<script setup>
import { ref } from "vue"
let title = ref('')
let setTitle = (e) => title.value = e
defineExpose({ // 使用 <script setup> 的组件是默认关闭的——即通过模板引用或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。可以通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的属性
  setTitle
})
</script>
  

<template>
  <div class="loading-mask">
    <div class="loading-spinner ">
      <div class="dec">{{title}}</div>
      <div class="loading"></div>
    </div>
  </div>
</template>

<style scoped>
.loading-mask {
  position: absolute;
  z-index: 2000;
  background-color: hsla(0,0%,100%,.9);
  margin: 0;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  transition: opacity .3s;
}

.loading-spinner {
  position: absolute;
  height: 50px;
  top: 50%;
  margin-top: -21px;
  width: 100%;
  text-align: center;
}

.dec {
  transform: translateY(-25px)
}

.loading {
  display: inline-block;
  width: 50px;
  perspective: 200px;
}

.loading:before,
.loading:after {
  position: absolute;
  width: 20px;
  height: 20px;
  content: "";
  animation: jumping 0.5s infinite alternate;
  background: rgba(0, 0, 0, 0);
}

.loading:before {
  left: 0;
}

.loading:after {
  right: 0;
  animation-delay: 0.15s;
}

@keyframes jumping {
  0% {
    transform: scale(1) translateY(0px) rotateX(0deg);
    box-shadow: 0 0 0 rgba(0, 0, 0, 0);
  }

  100% {
    transform: scale(1.2) translateY(-25px) rotateX(45deg);
    background: var(--main-color);
    box-shadow: 0 25px 40px #000;
  }
}
</style>

接下来写全屏,注释中是完整写法,这里有三种传值(即flag,fullscreen,提示语arg)只有一种的话简略的写法都不一样,测试案例:

<script setup>
  import { ref } from 'vue'
  let loadingFlag = ref(true)
  </script>
    
  <template>
    <sanorin-exhibit-frame :header="header" :subHeader="subHeader" :metaTitle="metaTitle" :metaCode="metaCode">
      <!-- <div class="zz" v-loading:[`年后`].fullscreen="loadingFlag"> -->
      <div class="zz" v-loading.fullscreen="loadingFlag">
        2
      </div>
    </sanorin-exhibit-frame>
  </template>
  
  <style scoped>
    .zz{
      width: 200px;
      height: 200px;
      border: 1px solid red;
    }
  </style>

部署指令的mounted中的if(binding.value)判断中加一个

if (binding.modifiers.fullscreen) {  
    addClass(el.lastChild, fullscreenCls)
}

至此指令部分就完事了,最后整理一下,除了测试demo文件,有以下文件
1、loading.vue 组件文件

<script setup>
import { ref } from "vue"
let title = ref('')
let setTitle = (e) => title.value = e
defineExpose({
  setTitle
})
</script>
  

<template>
  <div class="loading-mask">
    <div class="loading-spinner ">
      <div class="dec">{{title}}</div>
      <div class="loading"></div>
    </div>
  </div>
</template>

<style scoped>
.loading-mask {
  position: absolute;
  z-index: 2000;
  background-color: hsla(0,0%,100%,.9);
  margin: 0;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  transition: opacity .3s;
}

.loading-spinner {
  position: absolute;
  height: 50px;
  top: 50%;
  margin-top: -21px;
  width: 100%;
  text-align: center;
}

.dec {
  transform: translateY(-25px)
}

.loading {
  display: inline-block;
  width: 50px;
  perspective: 200px;
}

.loading:before,
.loading:after {
  position: absolute;
  width: 20px;
  height: 20px;
  content: "";
  animation: jumping 0.5s infinite alternate;
  background: rgba(0, 0, 0, 0);
}

.loading:before {
  left: 0;
}

.loading:after {
  right: 0;
  animation-delay: 0.15s;
}

@keyframes jumping {
  0% {
    transform: scale(1) translateY(0px) rotateX(0deg);
    box-shadow: 0 0 0 rgba(0, 0, 0, 0);
  }

  100% {
    transform: scale(1.2) translateY(-25px) rotateX(45deg);
    background: var(--main-color);
    box-shadow: 0 25px 40px #000;
  }
}
</style>

2、create-loading-like-directive.js 指令构造文件

import { createApp } from 'vue'
import { addClass, removeClass } from '@/sanorin/utils/dom'

const relativeCls = 'sanorin-loading-parent--relative'
const fullscreenCls = 'sanorin-loading-parent--fullscreen'

export default function createLoadingLikeDirective(Comp) {
  return {
    mounted(el, binding) {
      const app = createApp(Comp)
      const instance = app.mount(document.createElement('div'))
      const name = Comp.name
      if (!el[name]) {
        el[name] = {}
      }
      el[name].instance = instance
      const title = binding.arg
      if (typeof title !== 'undefined') {
        instance.setTitle(title)
      }

      if (binding.value) {
        append(el)
        if (binding.modifiers.fullscreen) {  
          addClass(el.lastChild, fullscreenCls)
        }
      }
    },
    updated(el, binding) {
      const title = binding.arg
      const name = Comp.name
      if (typeof title !== 'undefined') {
        el[name].instance.setTitle(title)
      }
      if (binding.value !== binding.oldValue) {
        console.log(binding.value)
        binding.value ? append(el) : remove(el)
      }
    }
  }

  function append(el) {
    const name = Comp.name
    const style = getComputedStyle(el)
    console.log(style.position)
    if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) {
      addClass(el, relativeCls)
    }
    el.appendChild(el[name].instance.$el)
  }

  function remove(el) {
    const name = Comp.name
    removeClass(el, relativeCls)
    el.removeChild(el[name].instance.$el)
  }
}

3、directive.js 指令部署文件

import Loading from './loading.vue'
import createLoadingLikeDirective from './create-loading-like-directive'

const loadingDirective = createLoadingLikeDirective(Loading)

export default loadingDirective

4、global.css 组件库样式文件中添加

.sanorin-loading-parent--relative{
  position: relative !important;
}
.sanorin-loading-parent--fullscreen{
  position: fixed !important;
}

5、组件注册文件中添加

import loadingDirective from './packages/loading/directive'
....
app.directive('loading', loadingDirective)

(待续)
明天写loading的service实现(x

相关文章:

  • JS中新逻辑赋值运算符使用(?.、? ?、| |=、=)
  • 软考网络工程师需要哪些复习资料?
  • CIPT备考心得分享-下一个考过的就是你
  • 构建集团统一管控体系,低代码派上用场
  • iconfont 使用
  • 4.类的定义,变量类型,方法类型
  • pytorch中一些有关tensor的操作
  • 大数的乘法
  • DQL操作(数据库表数据查询操作)
  • linux共享内存
  • 小波神经网络的基本原理,小波神经网络什么意思
  • 被一位读者赶超,手摸手 Docker 部署 ELK Stack
  • Math类(Java)
  • Android项目中各文件的作用
  • Java比较器(Comparable接口)
  • .pyc 想到的一些问题
  • 【Redis学习笔记】2018-06-28 redis命令源码学习1
  • Android开发 - 掌握ConstraintLayout(四)创建基本约束
  • Angular 响应式表单之下拉框
  • CSS3 聊天气泡框以及 inherit、currentColor 关键字
  • iOS动画编程-View动画[ 1 ] 基础View动画
  • JAVA_NIO系列——Channel和Buffer详解
  • JavaScript对象详解
  • Mybatis初体验
  • PhantomJS 安装
  • Python进阶细节
  • Redis在Web项目中的应用与实践
  • Spring-boot 启动时碰到的错误
  • 从 Android Sample ApiDemos 中学习 android.animation API 的用法
  • 给第三方使用接口的 URL 签名实现
  • 基于Volley网络库实现加载多种网络图片(包括GIF动态图片、圆形图片、普通图片)...
  • 突破自己的技术思维
  • 我有几个粽子,和一个故事
  • 新版博客前端前瞻
  • ​ssh免密码登录设置及问题总结
  • ###C语言程序设计-----C语言学习(3)#
  • #《AI中文版》V3 第 1 章 概述
  • #Java第九次作业--输入输出流和文件操作
  • (10)工业界推荐系统-小红书推荐场景及内部实践【排序模型的特征】
  • (iPhone/iPad开发)在UIWebView中自定义菜单栏
  • (webRTC、RecordRTC):navigator.mediaDevices undefined
  • (附源码)springboot建达集团公司平台 毕业设计 141538
  • (转)关于多人操作数据的处理策略
  • .gitignore文件_Git:.gitignore
  • .mkp勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .NET CORE使用Redis分布式锁续命(续期)问题
  • .net mvc部分视图
  • .NET 中 GetProcess 相关方法的性能
  • .NET 中使用 TaskCompletionSource 作为线程同步互斥或异步操作的事件
  • .Net6支持的操作系统版本(.net8已来,你还在用.netframework4.5吗)
  • .net开发引用程序集提示没有强名称的解决办法
  • .NET学习教程二——.net基础定义+VS常用设置
  • /bin、/sbin、/usr/bin、/usr/sbin
  • @Autowired自动装配
  • @media screen 针对不同移动设备