Vue3+Pinia最佳实践:如何优雅管理全局Loading状态?Axios拦截器自动接管请求

AI 概述
产品经理提出全局 Loading 需求,但原有代码混乱,存在多个 Loading 状态管理问题。Vue 官方推荐的全局 Loading 控制思路是:组件告知加载状态,全局 store 统计加载情况。据此,可创建 Pinia 版 loading store,用 Map 记录加载情况,用 computed 折算为布尔值。在根组件或 Layout 中挂上全局 Loading 组件,监听 store 的布尔值。业务组件按需接入,还可通过 axios 拦截器让所有 HTTP 请求自动关联全局 loading。此方案集中管理 Loading 状态,便于维护和修改。
目录
文章目录隐藏
  1. 写一个最小可用的 loading store(Pinia 版)
  2. 全局 Loading 组件怎么挂上去
  3. 组件里用法长啥样
  4. 把 axios 也接上去会更爽一点

Vue3+Pinia 最佳实践:如何优雅管理全局 Loading 状态?Axios 拦截器自动接管请求

产品经理提了一个需求:页面上所有接口一请求,就要全局蒙一层 Loading,等所有请求都结束再关。听着很简单对吧,结果一翻代码,A 组件自己搞了一套 loading,B 页面又塞了个 v-loading 全屏遮罩,切个路由还会忘记关,用户以为卡死了。

后来我去看了下 Vue 这边的思路,其实官方推荐那套全局 loading 控制,很朴素一句话:组件只管告诉“我在加载”,全局 store 只管统计“还有没有人在忙”。

你可以这么想像一下场景: 每个组件自己维护一个小旗子,发请求之前插个旗子,结束之后把旗子拔掉。至于到底有多少旗子插在地上,是不是所有人都干完活了,这事交给全局的仓库(Pinia / Vuex)去算。Vue 现在官方推荐 Pinia,当主力状态管理库用。

所以角色大概是这样:

  • 组件:只关心“我这趟请求的 loading 状态”,开始、结束的时候 ping 一下 store。
  • store:维护一张表,谁在 loading、计数是多少;只要表里还有人没干完,就把全局 loading 打开。
  • 顶层布局组件:盯着 store 的 isGlobalLoading,为 true 就盖一层蒙板。

这样几个好处你一下就能体会到:

  1. 多个请求会自动合并成一次全局 Loading;
  2. 不用在每个页面写一堆“if(请求 1 && 请求 2 && …)”那种判断;
  3. 以后你想把 Loading 换成 skeleton、进度条,也只改一处。

写一个最小可用的 loading store(Pinia 版)

下面这个是我自己平时用得比较顺手的一版,纯 js 写的,你可以直接放到 stores/loading.js 里:

// stores/loading.js
import { defineStore } from'pinia'
import { reactive, computed } from'vue'

exportconst useLoadingStore = defineStore('loading', () => {
// key -> 计数,比如 'user/profile' -> 2 表示这个地方有两次并发请求
const loadingMap = reactive(newMap())

function start(key) {
    const old = loadingMap.get(key) || 0
    loadingMap.set(key, old + 1)
  }

function end(key) {
    const old = loadingMap.get(key) || 0
    const next = old - 1
    if (next <= 0) {
      loadingMap.delete(key)
    } else {
      loadingMap.set(key, next)
    }
  }

// 还可以额外留个兜底,比如组件异常时一键清理自己那一类 key
function clearByPrefix(prefix) {
    for (const k of loadingMap.keys()) {
      if (k.startsWith(prefix)) {
        loadingMap.delete(k)
      }
    }
  }

const isGlobalLoading = computed(() => {
    // 只要 map 里还有东西,就代表有人在加载
    return loadingMap.size > 0
  })

return {
    start,
    end,
    clearByPrefix,
    isGlobalLoading
  }
})

这个不算复杂对吧,核心其实就俩点:

  • 用 Map 记住“谁在 loading,数量是多少”;
  • 用一个 computed 把它折算成一个布尔值,给全局遮罩用。

全局 Loading 组件怎么挂上去

在根组件或者你的 Layout 里,直接把 store 拉进来,监听就完事了,比如 App.vue 里:

<script setup>
import { useLoadingStore } from '@/stores/loading'
import GlobalLoading from '@/components/GlobalLoading.vue'

const loadingStore = useLoadingStore()
</script>

<template>
  <GlobalLoading v-if="loadingStore.isGlobalLoading" />
  <router-view />
</template>

GlobalLoading.vue 就随你发挥了,一个最简单的写法大概这样:

<template>
  <div class="global-loading">
    <div class="spinner">Loading...</div>
  </div>
</template>

<style scoped>
.global-loading {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0,0,0,.35);
  z-index: 9999;
}
</style>

到这一步,其实全局那一层“遮罩开关”就已经打通了,剩下的就是每个业务组件怎么把自己的 loading 接上来。

组件里用法长啥样

举个接口请求的例子,假设这是一个用户资料页组件:

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useLoadingStore } from '@/stores/loading'
import { getUserProfile } from '@/api/user'

const loadingStore = useLoadingStore()
const loadingKey = 'user/profile'  // 随便起个有语义的 key
const localLoading = ref(false)
const user = ref(null)

async function fetchData() {
  localLoading.value = true
  loadingStore.start(loadingKey)
  try {
    const res = await getUserProfile()
    user.value = res.data
  } finally {
    // 不管成功失败,都要把本地和全局的 loading 关掉
    localLoading.value = false
    loadingStore.end(loadingKey)
  }
}

// 一挂载就拉一次数据
onMounted(() => {
  fetchData()
})

// 如果你这个页面路由切来切去,心里不踏实,还可以兜底清一下
onUnmounted(() => {
  loadingStore.clearByPrefix('user/')
})
</script>

<template>
  <div>
    <div v-if="localLoading">局部 loading...</div>
    <pre v-else>{{ user }}</pre>
  </div>
</template>

这里有个小细节: 为什么还要留 localLoading?因为有时候你只想在组件内部转个小圈圈,而不是每个请求都去触发全屏遮罩;两者不冲突,局部的体验更细腻,全局的更多是防止“界面乱点”。

把 axios 也接上去会更爽一点

如果你项目里绝大多数请求都是通过 axios 走的,可以顺手在拦截器里加一层,让“所有 HTTP 请求自动挂在全局 loading 上”,组件特别简单的场景甚至可以不写 start / end。

大致的伪代码是这样:

// request.js
import axios from'axios'
import { useLoadingStore } from'@/stores/loading'

const instance = axios.create({
baseURL: '/api'
})

let loadingKey = 'axios/global'

instance.interceptors.request.use((config) => {
const store = useLoadingStore()
  store.start(loadingKey)
return config
})

instance.interceptors.response.use(
(response) => {
    const store = useLoadingStore()
    store.end(loadingKey)
    return response
  },
  (error) => {
    const store = useLoadingStore()
    store.end(loadingKey)
    returnPromise.reject(error)
  }
)

exportdefault instance

这样做其实就把“全局 loading”当成一个很普通的“全局状态”,跟你在 store 里面记登录信息、记主题色没啥区别,只是它的生命周期特别短而已。

我个人是挺喜欢这种写法的,比起在一大堆页面里手写 isLoading = true/false,集中放在一个 loading store 里好管多了,有点像我之前吐槽 SpringBoot 默认配置那一堆坑,统一收束一下,线上心里才安稳。

先写到这,你可以按自己项目的风格改一改 key 的命名、是不是要按模块分前缀之类的,改起来都不疼。

以上关于Vue3+Pinia最佳实践:如何优雅管理全局Loading状态?Axios拦截器自动接管请求的文章就介绍到这了,更多相关内容请搜索码云笔记以前的文章或继续浏览下面的相关文章,希望大家以后多多支持码云笔记。

「点点赞赏,手留余香」

6

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 admin@mybj123.com 进行投诉反馈,一经查实,立即处理!
重要:如软件存在付费、会员、充值等,均属软件开发者或所属公司行为,与本站无关,网友需自行判断
码云笔记 » Vue3+Pinia最佳实践:如何优雅管理全局Loading状态?Axios拦截器自动接管请求

发表回复