Code前端首页关于Code前端联系我们

Vue2 里实现 loading 有哪些常见思路?

terry 1天前 阅读数 20 #Vue
文章标签 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前端网发表,如需转载,请注明页面地址。

发表评论:

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

热门