npm
怎么快速给Vue3项目加个丝滑的NProgress进度条,还能适配路由懒加载和页面请求?
给你说个真实的小经历:上周我帮朋友调试一个刚上线的Vue3个人作品展示站,他说不管点哪个菜单项、加载哪个插件包,页面都像“卡成白屏PPT转圈圈”——哦不对,连转圈圈都没有,就是纯空白晃一下,等半天内容才出来,后来我俩聊了半小时,他不知道有专门的轻量级进度条工具,更不知道怎么和Vue3的核心功能结合,今天就用最接地气的方式,一步步拆解NProgress的用法,连踩坑和优化的细节都给你摆明白。
先搞懂:为什么选NProgress给Vue3用?
朋友一开始问过我:“能不能自己写个进度条?用CSS动画加个div不就行?”我给他算了一笔账——自己写要解决多少麻烦:
- 进度状态难控制:是用定时器从0到99%,请求结束跳100%?还是要结合路由切换、组件加载、API响应的多个阶段状态?
- 丝滑过渡难调:自己写CSS的话,容易出现进度条卡停、闪烁、消失太生硬的问题,用户看着难受;
- 适配场景太费时间:路由懒加载的空白期、多个API并行/串行请求的持续期、局部刷新的小提示,都得单独写逻辑,重复代码一堆;
- 样式兼容性得自己试:适配移动端、不同浏览器的滚动条冲突问题,处理不好容易翻车。
而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菜单:
- 路由全局前置守卫beforeEach触发,NProgress.start(),进度条从10%开始,慢慢自动滑;
- 浏览器下载Works.vue对应的懒加载文件;
- 懒加载文件下载完成,路由全局后置钩子afterEach触发——但这里注意!刚才我们加了请求计数器,只有当requestCount为0的时候才会done,而此时如果Works组件里的API请求已经发起了,requestCount至少是1,所以afterEach里的NProgress.done()不会生效(因为NProgress的done是幂等的,而且requestCount不为0的话,我们刚才的逻辑没在这里加计数器,所以只是单纯调用done但不会关闭,不过为了严谨,你也可以把路由守卫里的done去掉?不对,去掉的话如果有些页面没有API请求,进度条就不会关闭了——哦对,刚才的逻辑没问题,因为如果有些页面没有API请求,路由跳转完成之后,requestCount还是0,afterEach里的done就会生效,完美!
- Works组件挂载,发起API请求,请求拦截器触发,startNProgress,requestCount变成1;
- API请求完成,响应拦截器触发,doneNProgress,requestCount变成0,进度条关闭。
对,这个流程就完美覆盖了所有场景:有懒加载有API、有懒加载没API、没懒加载有API、没懒加载没API,都没问题。
第五步:锦上添花!怎么改NProgress的样式,让它和你的项目更搭?
默认的NProgress样式是蓝紫色的,可能和你的项目主题色不太搭,没关系,刚才说了,用全局CSS覆盖就行——别直接去node_modules里改,升级依赖的时候会被覆盖掉。
你可以在src/assets/css/global.css里写(或者App.vue的
code前端网