您好,登录后才能下订单哦!
密码登录
登录注册
点击 登录注册 即表示同意《亿速云用户服务条款》
# 怎么写一个即插即用的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
vue-loading-plugin/
├── src/
│ ├── components/
│ │ └── Loading.vue # 核心组件
│ ├── directives/ # 指令实现
│ ├── services/ # 服务实现
│ ├── types/ # TypeScript类型定义
│ ├── utils/ # 工具函数
│ └── index.ts # 插件入口
├── styles/
│ └── loading.scss # 样式文件
├── tests/ # 测试目录
├── demo/ # 演示示例
└── package.json
<!-- 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>
/* 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;
}
// 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
// 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
}
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
}
// 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
// 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
// 在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')
<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>
<template>
<div v-loading="loadingState">
<!-- 内容区域 -->
</div>
</template>
<script>
export default {
data() {
return {
loadingState: false
}
},
methods: {
toggleLoading() {
this.loadingState = !this.loadingState
}
}
}
</script>
<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>
// 使用自定义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>
`
})
// 在项目中覆盖默认变量
$loading-text-color: #ff6700;
$loading-spinner-color: #ff6700;
$loading-background: rgba(255, 255, 255, 0.8);
// 引入插件样式前定义变量
@import '~vue-loading-plugin/styles/loading';
// 创建多个独立的Loading实例
const loading1 = this.$loading({ text: '加载用户数据...' })
const loading2 = this.$loading({ text: '加载订单数据...' })
// 分别关闭
loading1.close()
loading2.close()
// 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)
})
})
”`json { “name”: “vue-loading-plugin”, “version”: “1.0.0”, “main”: “dist/vue-loading-plugin.umd.js”,
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。