怎么写一个即插即用的Vue Loading插件

发布时间:2022-05-06 13:49:23 作者:iii
来源:亿速云 阅读:139
# 怎么写一个即插即用的Vue Loading插件

## 前言

在现代Web开发中,良好的用户体验至关重要。页面或组件的加载状态处理是提升用户体验的关键环节之一。一个优雅的Loading效果不仅能缓解用户等待的焦虑,还能体现产品的专业性和细节把控。本文将详细介绍如何开发一个即插即用的Vue Loading插件,让你能够在项目中轻松实现统一的加载效果。

## 一、需求分析与设计

### 1.1 核心功能需求

一个完善的Vue Loading插件应该具备以下核心功能:

1. **多种触发方式**:支持指令调用、组件调用和服务调用
2. **自定义样式**:允许用户自定义加载动画和样式
3. **全屏/局部加载**:支持全屏遮罩和局部区域加载
4. **多实例管理**:能够同时管理多个加载状态
5. **配置可扩展**:提供默认配置同时支持全局和单次调用的配置覆盖

### 1.2 技术选型

- **Vue 2.x/3.x兼容**:使用Vue插件系统开发
- **TypeScript支持**:提供更好的类型提示
- **CSS/SCSS样式**:支持样式自定义
- **动画方案**:CSS动画或轻量级JS动画库

## 二、基础插件结构搭建

### 2.1 初始化项目

```bash
# 创建项目目录
mkdir vue-loading-plugin
cd vue-loading-plugin

# 初始化npm项目
npm init -y

# 安装基础依赖
npm install vue @vue/compiler-sfc -D

2.2 项目目录结构

vue-loading-plugin/
├── src/
│   ├── components/
│   │   └── Loading.vue      # 核心组件
│   ├── directives/          # 指令实现
│   ├── services/            # 服务实现
│   ├── types/               # TypeScript类型定义
│   ├── utils/               # 工具函数
│   └── index.ts             # 插件入口
├── styles/
│   └── loading.scss         # 样式文件
├── tests/                   # 测试目录
├── demo/                    # 演示示例
└── package.json

三、核心组件开发

3.1 Loading组件基础实现

<!-- src/components/Loading.vue -->
<template>
  <transition name="loading-fade">
    <div 
      v-show="visible"
      class="loading-container"
      :class="[customClass, fullScreen ? 'is-fullscreen' : '']"
      :style="{ backgroundColor: background || '' }"
    >
      <div class="loading-spinner">
        <!-- 默认旋转动画 -->
        <svg v-if="!spinner" class="circular" viewBox="25 25 50 50">
          <circle class="path" cx="50" cy="50" r="20" fill="none"/>
        </svg>
        <!-- 自定义spinner -->
        <div v-else class="custom-spinner" v-html="spinner"></div>
        <!-- 加载文本 -->
        <p v-if="text" class="loading-text">{{ text }}</p>
      </div>
    </div>
  </transition>
</template>

<script lang="ts">
import { defineComponent, ref, watch } from 'vue'

export default defineComponent({
  name: 'VLoading',
  props: {
    visible: {
      type: Boolean,
      default: false
    },
    text: {
      type: String,
      default: ''
    },
    spinner: {
      type: String,
      default: ''
    },
    background: {
      type: String,
      default: ''
    },
    fullScreen: {
      type: Boolean,
      default: false
    },
    customClass: {
      type: String,
      default: ''
    }
  },
  setup(props) {
    // 可以添加组件内部逻辑
    return {}
  }
})
</script>

<style lang="scss" scoped>
@import '../styles/loading.scss';
</style>

3.2 基础样式实现

/* src/styles/loading.scss */
.loading-container {
  position: absolute;
  z-index: 9999;
  margin: 0;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: rgba(255, 255, 255, 0.9);
  transition: opacity 0.3s;

  &.is-fullscreen {
    position: fixed;
  }

  .loading-spinner {
    top: 50%;
    width: 100%;
    text-align: center;
    position: absolute;
    transform: translateY(-50%);

    .circular {
      width: 42px;
      height: 42px;
      animation: loading-rotate 2s linear infinite;
    }

    .path {
      animation: loading-dash 1.5s ease-in-out infinite;
      stroke-dasharray: 90, 150;
      stroke-dashoffset: 0;
      stroke-width: 2;
      stroke: #409eff;
      stroke-linecap: round;
    }

    .loading-text {
      color: #409eff;
      margin: 3px 0;
      font-size: 14px;
    }

    .custom-spinner {
      display: inline-block;
    }
  }
}

@keyframes loading-rotate {
  100% {
    transform: rotate(360deg);
  }
}

@keyframes loading-dash {
  0% {
    stroke-dasharray: 1, 200;
    stroke-dashoffset: 0;
  }
  50% {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -40px;
  }
  100% {
    stroke-dasharray: 90, 150;
    stroke-dashoffset: -120px;
  }
}

.loading-fade-enter-from,
.loading-fade-leave-to {
  opacity: 0;
}

四、插件系统实现

4.1 插件入口文件

// src/index.ts
import { App, createApp, h, ref, ComponentPublicInstance } from 'vue'
import LoadingComponent from './components/Loading.vue'
import type { LoadingOptions, LoadingInstance } from './types'

// 默认配置
const defaults: LoadingOptions = {
  text: '',
  spinner: '',
  background: '',
  fullScreen: false,
  customClass: '',
  visible: false
}

// 全局Loading实例
let fullscreenLoading: LoadingInstance | null = null

const Loading = {
  install(app: App, options: Partial<LoadingOptions> = {}) {
    // 合并默认配置
    const globalOptions = { ...defaults, ...options }
    
    // 创建Loading服务
    const service = (options: Partial<LoadingOptions> = {}) => {
      if (fullscreenLoading) return fullscreenLoading
      
      const mergedOptions = { ...globalOptions, ...options, fullScreen: true }
      return createLoadingComponent(mergedOptions)
    }
    
    // 注册全局方法
    app.config.globalProperties.$loading = service
    
    // 注册指令
    app.directive('loading', {
      mounted(el, binding) {
        const instance = createLoadingComponent({
          ...globalOptions,
          ...(binding.value || {}),
          fullScreen: false,
          target: el
        })
        el.instance = instance
        if (binding.value) instance.show()
      },
      updated(el, binding) {
        const instance = el.instance
        if (binding.oldValue !== binding.value) {
          binding.value ? instance.show() : instance.hide()
        }
      },
      unmounted(el) {
        el.instance.close()
      }
    })
    
    // 注册组件
    app.component('VLoading', LoadingComponent)
  }
}

function createLoadingComponent(options: LoadingOptions): LoadingInstance {
  // 实现细节将在下一节展开
}

export default Loading

4.2 类型定义

// src/types/index.ts
export interface LoadingOptions {
  text?: string
  spinner?: string
  background?: string
  fullScreen?: boolean
  customClass?: string
  visible?: boolean
  target?: HTMLElement | string
  body?: boolean
  lock?: boolean
}

export interface LoadingInstance {
  show: () => void
  hide: () => void
  close: () => void
  vm: ComponentPublicInstance
  setText: (text: string) => void
}

五、服务模式实现

5.1 创建Loading实例

function createLoadingComponent(options: LoadingOptions): LoadingInstance {
  let afterLeaveTimer: number
  const visible = ref(false)
  const text = ref(options.text || '')
  
  // 处理目标元素
  let target: HTMLElement
  if (options.target) {
    target = typeof options.target === 'string' 
      ? document.querySelector(options.target) as HTMLElement
      : options.target
  } else {
    target = document.body
  }
  
  // 如果目标元素已经有loading,则直接返回
  if (target.querySelector('.el-loading-parent--relative')) {
    target = target.querySelector('.el-loading-parent--relative') as HTMLElement
  } else if (target !== document.body) {
    target.style.position = 'relative'
    target.classList.add('el-loading-parent--relative')
  }
  
  // 创建Vue应用
  const app = createApp({
    setup() {
      return () => h(LoadingComponent, {
        visible: visible.value,
        text: text.value,
        spinner: options.spinner,
        background: options.background,
        fullScreen: options.fullScreen,
        customClass: options.customClass
      })
    }
  })
  
  // 挂载到临时DOM
  const container = document.createElement('div')
  const vm = app.mount(container) as ComponentPublicInstance
  target.appendChild(container.firstElementChild as HTMLElement)
  
  // 暴露方法
  const instance: LoadingInstance = {
    vm,
    get $el() {
      return vm.$el
    },
    show() {
      visible.value = true
    },
    hide() {
      visible.value = false
      // 添加延迟确保动画完成
      afterLeaveTimer = window.setTimeout(() => {
        if (instance.$el.parentNode) {
          instance.$el.parentNode.removeChild(instance.$el)
        }
      }, 400)
    },
    close() {
      visible.value = false
      if (afterLeaveTimer) clearTimeout(afterLeaveTimer)
      if (instance.$el.parentNode) {
        instance.$el.parentNode.removeChild(instance.$el)
      }
    },
    setText(newText: string) {
      text.value = newText
    }
  }
  
  return instance
}

5.2 全局服务封装

// src/services/index.ts
import { createLoadingComponent } from '../index'
import type { App, ComponentPublicInstance } from 'vue'
import type { LoadingOptions, LoadingInstance } from '../types'

const loadingService = {
  instances: new Map<string, LoadingInstance>(),
  
  show(options: LoadingOptions = {}): LoadingInstance {
    const instance = createLoadingComponent(options)
    this.instances.set(instance.vm.$.uid.toString(), instance)
    instance.show()
    return instance
  },
  
  hide(id?: string): void {
    if (id) {
      const instance = this.instances.get(id)
      if (instance) {
        instance.close()
        this.instances.delete(id)
      }
    } else {
      this.instances.forEach(instance => instance.close())
      this.instances.clear()
    }
  }
}

export default loadingService

六、指令模式实现

6.1 指令实现

// src/directives/index.ts
import { DirectiveBinding, ObjectDirective } from 'vue'
import { createLoadingComponent } from '../index'
import type { LoadingOptions } from '../types'

interface LoadingElement extends HTMLElement {
  instance?: ReturnType<typeof createLoadingComponent>
}

const loadingDirective: ObjectDirective<LoadingElement> = {
  mounted(el, binding) {
    const options: LoadingOptions = {
      text: typeof binding.value === 'string' ? binding.value : '',
      ...(typeof binding.value === 'object' ? binding.value : {}),
      fullScreen: false,
      target: el
    }
    
    const instance = createLoadingComponent(options)
    el.instance = instance
    
    if (binding.value) {
      instance.show()
    }
  },
  updated(el, binding) {
    const instance = el.instance
    if (!instance) return
    
    if (binding.oldValue !== binding.value) {
      if (binding.value) {
        if (typeof binding.value === 'string') {
          instance.setText(binding.value)
        } else if (typeof binding.value === 'object' && binding.value.text) {
          instance.setText(binding.value.text)
        }
        instance.show()
      } else {
        instance.hide()
      }
    }
  },
  unmounted(el) {
    el.instance?.close()
  }
}

export default loadingDirective

七、插件集成与使用

7.1 插件安装

// 在Vue项目中安装插件
import { createApp } from 'vue'
import App from './App.vue'
import Loading from 'vue-loading-plugin'

const app = createApp(App)

// 基本用法
app.use(Loading)

// 带全局配置
app.use(Loading, {
  text: '拼命加载中...',
  background: 'rgba(0, 0, 0, 0.7)'
})

app.mount('#app')

7.2 使用方式

7.2.1 组件方式

<template>
  <div>
    <v-loading :visible="isLoading" text="数据加载中..." />
    <!-- 页面内容 -->
  </div>
</template>

<script>
export default {
  data() {
    return {
      isLoading: false
    }
  },
  methods: {
    fetchData() {
      this.isLoading = true
      // 模拟异步请求
      setTimeout(() => {
        this.isLoading = false
      }, 2000)
    }
  }
}
</script>

7.2.2 指令方式

<template>
  <div v-loading="loadingState">
    <!-- 内容区域 -->
  </div>
</template>

<script>
export default {
  data() {
    return {
      loadingState: false
    }
  },
  methods: {
    toggleLoading() {
      this.loadingState = !this.loadingState
    }
  }
}
</script>

7.2.3 服务方式

<template>
  <button @click="showLoading">显示全屏Loading</button>
</template>

<script>
export default {
  methods: {
    showLoading() {
      const loading = this.$loading({
        lock: true,
        text: '加载中...',
        spinner: 'el-icon-loading',
        background: 'rgba(0, 0, 0, 0.7)'
      })
      
      // 模拟异步操作
      setTimeout(() => {
        loading.close()
      }, 2000)
    }
  }
}
</script>

八、高级功能扩展

8.1 自定义动画

// 使用自定义SVG动画
app.use(Loading, {
  spinner: `
    <svg viewBox="0 0 50 50" class="custom-spinner">
      <circle cx="25" cy="25" r="20" fill="none" stroke="#ff6700" stroke-width="4">
        <animate attributeName="stroke-dashoffset" values="0;100;0" dur="2s" repeatCount="indefinite" />
        <animate attributeName="stroke-dasharray" values="10,90;90,10;10,90" dur="2s" repeatCount="indefinite" />
      </circle>
    </svg>
  `
})

8.2 主题定制

// 在项目中覆盖默认变量
$loading-text-color: #ff6700;
$loading-spinner-color: #ff6700;
$loading-background: rgba(255, 255, 255, 0.8);

// 引入插件样式前定义变量
@import '~vue-loading-plugin/styles/loading';

8.3 多实例管理

// 创建多个独立的Loading实例
const loading1 = this.$loading({ text: '加载用户数据...' })
const loading2 = this.$loading({ text: '加载订单数据...' })

// 分别关闭
loading1.close()
loading2.close()

九、性能优化与最佳实践

9.1 性能优化策略

  1. 单例模式:对于全屏Loading,确保同一时间只有一个实例
  2. DOM复用:避免频繁创建和销毁DOM节点
  3. 动画优化:使用CSS硬件加速的动画属性
  4. 按需引入:支持Tree Shaking

9.2 最佳实践

  1. 合理使用遮罩:根据场景选择透明度和颜色
  2. 提供取消机制:长时间加载时允许用户取消
  3. 进度反馈:对于耗时操作,考虑使用进度条
  4. 错误处理:加载失败时提供友好的错误提示

十、测试与发布

10.1 单元测试

// tests/loading.spec.ts
import { mount } from '@vue/test-utils'
import VLoading from '../src/components/Loading.vue'

describe('Loading Component', () => {
  it('renders correctly', () => {
    const wrapper = mount(VLoading, {
      props: {
        visible: true,
        text: 'Loading...'
      }
    })
    expect(wrapper.find('.loading-text').text()).toBe('Loading...')
  })
  
  it('emits events correctly', async () => {
    const wrapper = mount(VLoading)
    await wrapper.setProps({ visible: true })
    expect(wrapper.isVisible()).toBe(true)
  })
})

10.2 发布到npm

  1. 配置package.json

”`json { “name”: “vue-loading-plugin”, “version”: “1.0.0”, “main”: “dist/vue-loading-plugin.umd.js”,

推荐阅读:
  1. vue实现图片懒加载的方法分析
  2. 如何写一个即插即用的Vue Loading插件

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

vue

上一篇:怎么实现vuex与组件data之间的数据更新

下一篇:怎么使用vue在路由中验证token是否存在

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》