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前端网发表,如需转载,请注明页面地址。
code前端网



发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。