Vue2 里实现 loading 有哪些常见思路?
在 Vue2 项目开发里,loading 效果是提升用户体验的关键细节——页面请求数据时转个圈、按钮点击后短暂 loading 防止重复操作…但刚接触 Vue2 的同学常会纠结「怎么选方案?代码咋写?不同场景咋适配?」今天就把 Vue2 里 loading 实现的思路、场景方案、实战技巧拆透,从基础到进阶一步步讲明白~
想给页面、按钮、表格加 loading,思路其实分「粒度」和「触发时机」来选:- 元素级 loading:给单个按钮、表格加loading,适合局部交互(比如按钮点击后防止重复提交),常用「自定义指令」实现,因为指令能直接绑定DOM,控制元素的loading状态。
- 页面级 loading:整个页面加载数据时显示,适合路由切换、初始化请求,用「自定义组件 + 全局状态」更方便,比如搞个
组件,通过Vuex或全局变量控制显隐。 - 接口级全局 loading:所有Ajax请求时自动显示,请求结束自动隐藏,这时候结合「axios拦截器」最顺手,拦截请求和响应,统一管理loading状态。
举个简单对比:做登录按钮loading,用指令绑在按钮上,点击后触发loading;做首页列表加载,用页面级组件覆盖整个页面;做全局接口loading,用axios拦截器统一处理。
用自定义指令做元素级 loading 咋写?
很多同学第一次写指令容易懵,其实步骤很清晰:
先写指令逻辑(注册全局/局部指令)
全局指令可以在main.js
里注册:
Vue.directive('loading', { bind(el, binding) { // 初始化:给元素加loading容器 const loadingDiv = document.createElement('div'); loadingDiv.className = 'custom-loading'; loadingDiv.innerHTML = ` <div class="spinner"></div> <p>加载中...</p> `; el.loadingDiv = loadingDiv; // 存到el上,方便后续操作 el.style.position = 'relative'; // 让loading容器绝对定位 el.appendChild(loadingDiv); }, update(el, binding) { // binding.value 是指令绑定的值,比如v-loading="isLoading" el.loadingDiv.style.display = binding.value ? 'block' : 'none'; } });
写CSS样式(让loading动起来)
.custom-loading { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.8); display: none; /* 初始隐藏 */ justify-content: center; align-items: center; flex-direction: column; } .spinner { width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
在组件里用指令
比如按钮组件:
<template> <button v-loading="isLoading" @click="handleClick">提交</button> </template> <script> export default { data() { return { isLoading: false } }, methods: { handleClick() { this.isLoading = true; // 模拟接口请求 setTimeout(() => { this.isLoading = false; }, 2000); } } } </script>
这样按钮点击后,loading层就会覆盖按钮,直到请求完成~
页面级 loading 组件咋设计?
页面级loading要考虑「全局可控」和「灵活定制」,推荐用「单文件组件 + 全局注册 + 状态管理」:
写LoadingPage
组件(支持插槽和props)
<template> <div class="page-loading" v-show="visible"> <div class="loading-spinner"></div> <slot> {{ tip }} </slot> <!-- 插槽自定义内容,没传就显示tip --> </div> </template> <script> export default { name: 'LoadingPage', props: { visible: Boolean, // 是否显示 tip: { type: String, default: '页面加载中...' } } } </script> <style scoped> .page-loading { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #fff; display: flex; justify-content: center; align-items: center; z-index: 9999; } .loading-spinner { width: 60px; height: 60px; border: 6px solid #eee; border-top: 6px solid #27ae60; border-radius: 50%; animation: spin 1s linear infinite; } </style>
全局注册 + 用Vuex控制显隐
在main.js
全局注册:
import LoadingPage from './components/LoadingPage.vue'; Vue.component('LoadingPage', LoadingPage);
然后用Vuex管理状态(也可以用全局事件总线,看项目复杂度):
// store/index.js const store = new Vuex.Store({ state: { pageLoading: false }, mutations: { SET_PAGE_LOADING(state, val) { state.pageLoading = val; } } });
在页面中使用
比如首页加载列表数据:
<template> <div> <LoadingPage :visible="pageLoading" tip="列表加载中..." /> <ul v-if="!pageLoading"> <li v-for="item in list" :key="item.id">{{ item.name }}</li> </ul> </div> </template> <script> import { mapState, mapMutations } from 'vuex'; export default { computed: { ...mapState(['pageLoading']) }, methods: { ...mapMutations(['SET_PAGE_LOADING']) }, created() { this.SET_PAGE_LOADING(true); // 开始加载 // 模拟接口请求 setTimeout(() => { this.list = [/* 数据 */]; this.SET_PAGE_LOADING(false); // 加载完成 }, 1500); }, data() { return { list: [] } } } </script>
这样整个页面加载时,会全屏显示loading,数据回来后自动隐藏~
结合 axios 做全局接口 loading 有啥技巧?
很多项目需要「所有接口请求时显示loading,全部完成后隐藏」,这时候axios拦截器+「请求计数器」是关键:
封装axios实例,加拦截器
import axios from 'axios'; import Vue from 'vue'; let pendingRequests = 0; // 请求计数器 const service = axios.create({ baseURL: '/api', timeout: 5000 }); // 请求拦截器:发起请求时,计数器+1,显示loading service.interceptors.request.use( config => { pendingRequests++; // 这里可以用Vuex或全局方法显示loading,比如this.$store.commit('SET_GLOBAL_LOADING', true) Vue.prototype.$showLoading(); // 假设全局方法 return config; }, error => { pendingRequests = 0; // 请求错误时重置计数器 Vue.prototype.$hideLoading(); return Promise.reject(error); } ); // 响应拦截器:响应后,计数器-1,为0时隐藏loading service.interceptors.response.use( response => { pendingRequests--; if (pendingRequests === 0) { Vue.prototype.$hideLoading(); } return response; }, error => { pendingRequests = 0; Vue.prototype.$hideLoading(); return Promise.reject(error); } ); export default service;
全局方法实现loading(比如用自定义组件)
在main.js
里挂载全局方法:
import LoadingComponent from './components/GlobalLoading.vue'; const LoadingConstructor = Vue.extend(LoadingComponent); let loadingInstance = null; Vue.prototype.$showLoading = () => { if (!loadingInstance) { loadingInstance = new LoadingConstructor({ el: document.createElement('div') }); document.body.appendChild(loadingInstance.$el); } loadingInstance.visible = true; // 控制组件显隐的props }; Vue.prototype.$hideLoading = () => { if (loadingInstance) { loadingInstance.visible = false; // 可以加延迟销毁,避免闪屏 setTimeout(() => { document.body.removeChild(loadingInstance.$el); loadingInstance = null; }, 300); } };
为啥要用计数器?
比如页面同时发3个请求(列表、用户信息、广告),如果不用计数器,第一个请求完成就隐藏loading,后面两个还在请求,用户就会看到「加载中突然消失,内容还没渲染完」的情况,用计数器后,只有所有请求都完成(pendingRequests
为0),才隐藏loading,体验更流畅~
复杂场景下咋优化 loading 体验?
只做基础loading还不够,这些细节能让体验飞升:
延迟显示loading(避免闪屏)
场景:接口请求特别快(比如50ms内完成),loading一闪而过,反而让用户困惑。
解决:给loading加「延迟显示」,比如请求发起后,延迟300ms再显示loading,若请求已完成则不显示。
代码示例(在axios请求拦截器改):
let timer = null; service.interceptors.request.use( config => { pendingRequests++; // 延迟300ms显示loading timer = setTimeout(() => { if (pendingRequests > 0) { // 请求还没完成 Vue.prototype.$showLoading(); } }, 300); return config; }, error => { clearTimeout(timer); // 请求错误,清除定时器 pendingRequests = 0; Vue.prototype.$hideLoading(); return Promise.reject(error); } ); service.interceptors.response.use( response => { clearTimeout(timer); // 响应回来,清除定时器(不管是否显示过loading) pendingRequests--; if (pendingRequests === 0) { Vue.prototype.$hideLoading(); } return response; }, error => { clearTimeout(timer); pendingRequests = 0; Vue.prototype.$hideLoading(); return Promise.reject(error); } );
区分「全局loading」和「局部loading」
有些接口不需要全局loading(比如用户头像上传时,页面其他功能还能操作),可以给axios请求配置加标记:
// 请求时加meta.noLoading service.get('/user/avatar', { meta: { noLoading: true } }); // 拦截器里判断 service.interceptors.request.use( config => { if (!config.meta?.noLoading) { // 没有noLoading标记才计数 pendingRequests++; // ... 延迟显示逻辑 } return config; } );
错误状态下的loading处理
请求失败时,loading要隐藏,还要给用户反馈,可以在响应拦截器里加错误提示:
service.interceptors.response.use( response => { ... }, error => { clearTimeout(timer); pendingRequests = 0; Vue.prototype.$hideLoading(); // 全局提示错误(比如Element UI的this.$message) Vue.prototype.$message.error(error.message || '请求失败,请重试'); return Promise.reject(error); } );
骨架屏 + loading 过渡
列表加载时,先用骨架屏占位,再显示真实数据,比纯loading更友好,比如用Vue2的<skeleton>
组件(自己写或用UI库):
<template> <div> <LoadingPage :visible="pageLoading" tip="加载中..." v-if="pageLoading" /> <ul v-else> <skeleton v-if="!list.length" :rows="5" /> <!-- 骨架屏占位 --> <li v-for="item in list" :key="item.id">{{ item.name }}</li> </ul> </div> </template>
Vue2 + UI 框架(Element UI)的 loading 咋灵活用?
很多项目用Element UI,它自带loading组件,学会「服务式」和「指令式」能省很多事:
服务式 loading(全局/局部)
// 全局loading(全屏) const loadingInstance = this.$loading({ lock: true, // 是否锁屏 text: '拼命加载中...', spinner: 'el-icon-loading', background: 'rgba(0, 0, 0, 0.7)' }); // 请求完成后关闭 setTimeout(() => { loadingInstance.close(); }, 2000); // 局部loading(绑定元素) const loadingInstance = this.$loading({ target: document.querySelector('.table-box'), // 目标元素 text: '表格加载中...' });
指令式 v-loading
直接绑在元素上,通过布尔值控制:
<template> <el-table v-loading="tableLoading" :data="tableData" element-loading-text="表格数据加载中" element-loading-spinner="el-icon-loading" element-loading-background="rgba(255,255,255,0.8)" > <!-- 列定义 --> </el-table> </template> <script> export default { data() { return { tableLoading: true, tableData: [] } }, created() { setTimeout(() => { this.tableData = [/* 数据 */]; this.tableLoading = false; }, 1500); } } </script>
自己封装 vs UI 框架
UI框架的loading胜在「快捷」,但样式和逻辑定制性弱(比如想改spinner动画,得覆盖CSS);自己封装的loading「灵活」,但要写更多代码,项目里可以结合用:全局接口loading自己封装,页面内的表格、按钮用UI框架的指令式,效率拉满~
SEO 友好的 loading 咋处理?
Vue2是SPA,首屏loading可能让爬虫看不到内容,得针对性优化:
服务端渲染(SSR)下的 loading
用Nuxt.js的话,页面数据请求用asyncData
,加载时的loading可以通过布局组件控制:
<!-- layouts/default.vue --> <template> <div> <LoadingPage v-if="isLoading" /> <nuxt /> <!-- 页面内容 --> </div> </template> <script> export default { data() { return { isLoading: false } }, async asyncData({ app }) { this.isLoading = true; // 注意:asyncData里this不是组件实例,实际要改写法,用Vuex或全局状态 await app.$axios.get('/init-data'); this.isLoading = false; } } </script>
实际要结合Nuxt的loading配置,或者用中间件管理状态,确保服务端渲染时loading状态正确传递。
客户端渲染(CSR)的 SEO 优化 加「预渲染」或「v-cloak」,避免loading导致内容闪烁:
[v-cloak] { display: none; }
<template> <div v-cloak> <LoadingPage :visible="isLoading" /> <div v-else> <!-- 首屏内容 --> </div> </div> </template>
v-cloak
能让Vue实例编译完成后再显示内容,防止loading闪烁时爬虫抓到空白。
选对方案,让 loading 既好用又好看
Vue2里实现loading,核心是「分场景选方案」:
- 局部交互(按钮、表格)→ 自定义指令,精准控制单个元素;
- 页面级加载(路由、初始化)→ 自定义组件 + 状态管理,全局把控;
- 接口级全局加载 → axios拦截器 + 请求计数器,自动化处理;
- 复杂体验 → 延迟显示、错误处理、骨架屏,提升用户感知;
- 结合UI框架 → 快速实现基础需求
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。