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)
      }
      if (binding.value) {
        if (binding.modifiers.fullscreen) {  
          addClass(el.lastChild, fullscreenCls)
        }
      }
    }
  }

  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的服务方式调用
先写测试demo;

<script setup>
  import { sanorinLoading } from '@/sanorin/packages/loading/service' // 因为没npm打包,后续改成import { sanorinLoading } from 'sanorinUI'

  sanorinLoading.show();
  setTimeout(() => {
    sanorinLoading.hide()
  }, 5000)
  </script>
    
  <template>
    <sanorin-exhibit-frame :header="header" :subHeader="subHeader" :metaTitle="metaTitle" :metaCode="metaCode">
      <div class="zz">
        2
      </div>
    </sanorin-exhibit-frame>
  </template>
  
  <style scoped>
    .zz{
      width: 200px;
      height: 200px;
      border: 1px solid red;
    }
  </style>

指令同级下新建一个service.js

import { createApp, ref } from 'vue'
import myLoad from './loading.vue'

const titleService = ref('')
const $loading = createApp(myLoad, { titleService }).mount(document.createElement('div'))

const sanorinLoading = {
  show(e) {
    titleService.value = e
    document.body.appendChild($loading.$el)
  },

  hide() {
    titleService.value = ''
    document.body.removeChild($loading.$el)
  }
}
export  { sanorinLoading }

最后loading组件也要相应改一下

<script setup>
import { computed, ref } from "vue"
// service
const props = defineProps({
    titleService: {
      type: String
    },
})

// directive
let titleDirective = ref('')
let setTitle = (e) => titleDirective.value = e
defineExpose({
  setTitle
})

let title = computed(() => props.titleService?.value || titleDirective.value)
</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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值