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

npm

terry 47分钟前 阅读数 14 #Vue

怎么快速给Vue3项目加个丝滑的NProgress进度条,还能适配路由懒加载和页面请求?

给你说个真实的小经历:上周我帮朋友调试一个刚上线的Vue3个人作品展示站,他说不管点哪个菜单项、加载哪个插件包,页面都像“卡成白屏PPT转圈圈”——哦不对,连转圈圈都没有,就是纯空白晃一下,等半天内容才出来,后来我俩聊了半小时,他不知道有专门的轻量级进度条工具,更不知道怎么和Vue3的核心功能结合,今天就用最接地气的方式,一步步拆解NProgress的用法,连踩坑和优化的细节都给你摆明白。

先搞懂:为什么选NProgress给Vue3用?

朋友一开始问过我:“能不能自己写个进度条?用CSS动画加个div不就行?”我给他算了一笔账——自己写要解决多少麻烦:

  1. 进度状态难控制:是用定时器从0到99%,请求结束跳100%?还是要结合路由切换、组件加载、API响应的多个阶段状态?
  2. 丝滑过渡难调:自己写CSS的话,容易出现进度条卡停、闪烁、消失太生硬的问题,用户看着难受;
  3. 适配场景太费时间:路由懒加载的空白期、多个API并行/串行请求的持续期、局部刷新的小提示,都得单独写逻辑,重复代码一堆;
  4. 样式兼容性得自己试:适配移动端、不同浏览器的滚动条冲突问题,处理不好容易翻车。

而NProgress刚好完美避开了这些坑,它是GitHub上一个18k+星标(现在应该更高了)的轻量级库,压缩后才几KB,官方的描述就是“一个像YouTube、Medium那样的顶部加载进度条”——你刷这两个平台肯定见过,一条细细的蓝紫色(默认)进度条从左往右滑,加载完悄悄淡出,体验特别自然。

它的核心逻辑也很简单:自己不会去监听任何业务逻辑,只暴露几个API(start、done、inc、set这些),你只要把这些API插到Vue3的路由守卫、请求拦截器/响应拦截器、组件生命周期这些地方就行,完全可以根据自己的项目需求自定义什么时候动、什么时候停、动多少。

第一步:先搭个基础Vue3项目练手?或者直接拿现有项目改

不管是练手还是改现有项目,步骤都差不多,先从安装开始说。

安装依赖

你可以用npm、yarn或者pnpm,推荐用pnpm,现在Vue官方脚手架都默认推荐这个了,下载速度快还省空间:

# yarn
yarn add nprogress
# pnpm
pnpm add nprogress

安装完别着急用,记得看一眼package.json里有没有nprogress和对应的版本号,别漏装了。

第二步:在Vue3项目里怎么引入和配置?

NProgress本身不带Vue3的封装,所以得自己动手把它和项目的核心文件绑在一起,主要分两个地方:全局样式引入进度条API的初始化与触发

引入全局样式,解决默认样式不生效的问题

很多新手第一次用NProgress都会踩这个坑:明明写了start和done,页面上就是看不到进度条——别慌,大概率是没引入它的CSS样式文件。

你可以直接在main.js(或者main.ts,TypeScript项目的话)里引入:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import NProgress from 'nprogress' // 引入NProgress核心库
import 'nprogress/nprogress.css' // 一定要引入这个默认CSS,不然看不到

如果你想改样式,别直接去node_modules里改nprogress.css,换个思路:用全局CSS覆盖默认的就行,后面我会讲怎么改得更好看。

配置NProgress的基础参数,让它更符合你的需求

在main.js引入之后,可以先给它做个全局的基础配置,常用的参数有这些:

  • showSpinner:控制进度条右边那个小转圈圈(Spinner)显不显示,Medium和YouTube现在好像都隐藏了,看起来更简洁;
  • minimum:进度条的最小值,默认是0.08(也就是8%),设置这个值是为了避免进度条一开始不动,显得卡;
  • easing:进度条的动画缓动函数,用CSS的transition-easing就行,默认是'ease',可以改成'ease-out'或者'linear';
  • speed:进度条从start到inc、inc到set、set到done的过渡时间,单位是毫秒,默认是200;
  • trickle:自动生成的“小流量”动画,默认是true,就是说即使你不手动调用inc,进度条也会自己慢慢往前滑一点(但不会到100%),显得加载更真实;
  • trickleSpeed:自动小流量的滑动间隔时间,单位是毫秒,默认是800。

我一般喜欢把Spinner关掉,最小值调到0.1,缓动函数改成'ease-out',过渡时间改成300,你可以在main.js里这么写:

// main.js 引入之后加配置
NProgress.configure({
  showSpinner: false, // 隐藏右边的小转圈圈
  minimum: 0.1, // 最小值设为10%
  easing: 'ease-out', // 缓动函数改成先快后慢
  speed: 300, // 过渡时间设为300ms
  trickle: true, // 开启自动小流量
  trickleSpeed: 600 // 自动小流量间隔改成600ms,比默认快一点,更明显
})

第三步:最核心的!怎么适配路由懒加载?

现在的Vue3项目,不管是用Vite还是Vue CLI,基本都会用路由懒加载(也就是动态import),不然首屏加载会特别慢——但懒加载也有个问题:点击菜单之后,浏览器要去下载对应的JS/CSS文件,这段时间如果是纯空白,用户体验特别差,所以第一个要绑定NProgress的地方就是Vue Router的路由守卫

路由守卫有三种:全局前置守卫(beforeEach)、全局后置钩子(afterEach)、组件内的守卫,我们用全局前置和全局后置就够了,因为不管进哪个路由都能触发。

先确认你的路由配置文件是不是用了懒加载

给你看个标准的Vue3+Vite+Vue Router 4的路由配置(router/index.js或者.ts),确保你的路由是这样的:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
// 这里的Home不用懒加载没关系,因为是首屏
import Home from '../views/Home.vue'
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // 这里用了动态import,就是路由懒加载,点击/about的时候才会下载对应的文件
    component: () => import('../views/About.vue')
  },
  {
    path: '/works',
    name: 'Works',
    component: () => import('../views/Works.vue')
  },
  {
    path: '/contact',
    name: 'Contact',
    component: () => import('../views/Contact.vue')
  }
]
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL), // Vite项目用这个,Vue CLI用process.env.BASE_URL
  routes
})
export default router

在全局前置和后置守卫里绑定NProgress

在router/index.js的最后,export default router之前,加上这两段代码就行:

// router/index.js 路由配置之后加守卫
router.beforeEach((to, from, next) => {
  // 每次路由跳转之前,先启动NProgress
  NProgress.start()
  // 别忘了next(),不然路由不会跳转
  next()
})
router.afterEach(() => {
  // 每次路由跳转完成之后,关闭NProgress
  NProgress.done()
})

现在你可以试一下:点击/about或者/works菜单,应该能看到顶部有个细细的进度条从左往右滑,加载完之后就淡出了——懒加载的空白期是不是瞬间有了缓冲?

不过这里有个小坑:如果你的路由配置里有重定向(redirect)或者嵌套路由(children),会不会触发多次NProgress?比如从/home重定向到/home/recommend,会不会start两次?答案是不会的,因为NProgress的start方法是幂等的——就是说多次调用start,只会有一个进度条在动,不会叠加;done方法也是一样的,必须进度条已经start了,调用done才会生效。

第四步:进阶优化!怎么适配页面内的API请求?

路由懒加载的空白期解决了,但还有个场景:比如你进入Works页面之后,要调用后端API去获取作品列表,这段时间页面上的内容可能是“加载中...”或者占位符,但如果再加个NProgress的“加速阶段”,体验会不会更好?还有,如果有多个API并行请求,比如进入Works页面之后,既要获取作品列表,又要获取分类筛选列表,怎么避免NProgress提前done?

这个时候就要用到请求拦截器响应拦截器了,如果你用的是Axios(大部分Vue3项目都是用这个),那就直接在Axios的封装文件里加;如果是用原生fetch或者其他库,逻辑也是一样的。

先封装一下Axios,顺便加拦截器

假设你的Axios封装文件是utils/request.js(或者.ts),里面的代码可以这么写:

// utils/request.js
import axios from 'axios'
import NProgress from 'nprogress'
// 这里可以引入你的全局状态管理,比如Pinia或者Vuex,后面可能会用到
// import { useUserStore } from '@/stores/user'
// 先定义一个请求计数器,用来解决多个API并行请求的问题
let requestCount = 0
// 创建Axios实例
const service = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_API, // Vite项目用环境变量,Vue CLI用process.env.VUE_APP_BASE_API
  timeout: 10000 // 请求超时时间10秒
})
// 封装一个启动NProgress的方法
const startNProgress = () => {
  if (requestCount === 0) {
    // 只有当请求计数器为0的时候,才启动NProgress
    NProgress.start()
  }
  // 不管之前有没有启动,计数器都加1
  requestCount++
}
// 封装一个关闭NProgress的方法
const doneNProgress = () => {
  // 不管成功还是失败,计数器都减1
  requestCount--
  if (requestCount === 0) {
    // 只有当请求计数器为0的时候,才关闭NProgress
    NProgress.done()
  }
}
// 请求拦截器
service.interceptors.request.use(
  config => {
    // 每次发起请求之前,先启动NProgress
    startNProgress()
    // 这里可以加你的请求头,比如token
    // const userStore = useUserStore()
    // if (userStore.token) {
    //   config.headers.Authorization = `Bearer ${userStore.token}`
    // }
    return config
  },
  error => {
    // 请求失败也要关闭NProgress
    doneNProgress()
    // 这里可以处理请求失败的逻辑,比如提示用户网络错误
    console.error('请求失败:', error)
    return Promise.reject(error)
  }
)
// 响应拦截器
service.interceptors.response.use(
  response => {
    // 每次响应成功之后,关闭NProgress
    doneNProgress()
    // 这里可以处理响应成功的逻辑,比如判断code
    // const res = response.data
    // if (res.code !== 200) {
    //   console.error('响应错误:', res.message)
    //   return Promise.reject(new Error(res.message || 'Error'))
    // }
    return response
  },
  error => {
    // 响应失败也要关闭NProgress
    doneNProgress()
    // 这里可以处理响应失败的逻辑,比如401未授权跳登录页
    console.error('响应失败:', error)
    return Promise.reject(error)
  }
)
export default service

这里的请求计数器是核心!一定要加——如果不加的话,假设进入Works页面之后,发起了2个API请求,第一个请求先完成,调用doneNProgress,进度条就提前关闭了,第二个请求还在加载,用户又看到了纯空白或者占位符,体验就差了,加了计数器之后,只有当所有请求都完成了,计数器才会变成0,进度条才会关闭。

那现在的NProgress触发逻辑是什么样的?

给你梳理一下完整的流程,比如你点击Works菜单:

  1. 路由全局前置守卫beforeEach触发,NProgress.start(),进度条从10%开始,慢慢自动滑;
  2. 浏览器下载Works.vue对应的懒加载文件;
  3. 懒加载文件下载完成,路由全局后置钩子afterEach触发——但这里注意!刚才我们加了请求计数器,只有当requestCount为0的时候才会done,而此时如果Works组件里的API请求已经发起了,requestCount至少是1,所以afterEach里的NProgress.done()不会生效(因为NProgress的done是幂等的,而且requestCount不为0的话,我们刚才的逻辑没在这里加计数器,所以只是单纯调用done但不会关闭,不过为了严谨,你也可以把路由守卫里的done去掉?不对,去掉的话如果有些页面没有API请求,进度条就不会关闭了——哦对,刚才的逻辑没问题,因为如果有些页面没有API请求,路由跳转完成之后,requestCount还是0,afterEach里的done就会生效,完美!
  4. Works组件挂载,发起API请求,请求拦截器触发,startNProgress,requestCount变成1;
  5. API请求完成,响应拦截器触发,doneNProgress,requestCount变成0,进度条关闭。

对,这个流程就完美覆盖了所有场景:有懒加载有API、有懒加载没API、没懒加载有API、没懒加载没API,都没问题。

第五步:锦上添花!怎么改NProgress的样式,让它和你的项目更搭?

默认的NProgress样式是蓝紫色的,可能和你的项目主题色不太搭,没关系,刚才说了,用全局CSS覆盖就行——别直接去node_modules里改,升级依赖的时候会被覆盖掉。

你可以在src/assets/css/global.css里写(或者App.vue的