如何为Next.js13(App Router)实现非装饰性、可计算资源加载时长的启动屏
解决方案:Next.js 13 App Router 全局预加载器实现
核心疑问解答:能否为整个应用配置loading.js?
App Router 的loading.js是路由级别的,无法直接全局配置,但可以通过客户端组件+全局状态管理模拟全局加载逻辑,同时结合路由级loading.js实现路由切换时的加载状态同步。
方法一:客户端组件监听全局资源加载
利用'use client'组件操作DOM,监听所有资源(包括图片)的加载状态,控制预加载器的显示/隐藏。
1. 创建全局预加载器组件
// app/components/Preloader.tsx 'use client'; import { useEffect, useState } from 'react'; import { usePathname } from 'next/navigation'; export default function Preloader() { const [isLoading, setIsLoading] = useState(true); const pathname = usePathname(); // 监听路由切换,重置加载状态 useEffect(() => { // 检查所有图片是否加载完成 const checkAllImagesLoaded = () => { const allImages = Array.from(document.querySelectorAll('img')); return allImages.every(img => img.complete || img.naturalWidth > 0); }; // 隐藏预加载器的逻辑 const hidePreloader = () => { if (checkAllImagesLoaded()) { setIsLoading(false); } }; // 初始触发一次检查 const timer = setTimeout(hidePreloader, 100); // 避免DOM未就绪 // 监听全局资源加载完成事件 window.addEventListener('load', hidePreloader); // 监听动态添加的图片(比如懒加载、路由切换后新增的图片) const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { const newImages = Array.from(mutation.addedNodes).filter( node => node instanceof HTMLImageElement ); newImages.forEach(img => { img.addEventListener('load', hidePreloader); img.addEventListener('error', hidePreloader); // 加载失败也算完成 }); }); }); observer.observe(document.body, { childList: true, subtree: true }); // 路由切换时重置加载状态 setIsLoading(true); return () => { clearTimeout(timer); window.removeEventListener('load', hidePreloader); observer.disconnect(); }; }, [pathname]); if (!isLoading) return null; return ( <div className="fixed inset-0 bg-white/90 z-[9999] flex items-center justify-center"> {/* 自定义加载动画 */} <div className="w-16 h-16 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div> </div> ); }
2. 在RootLayout中引入组件
// app/layout.tsx import Preloader from './components/Preloader'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="zh-CN"> <body className="min-h-screen"> <Preloader /> {children} </body> </html> ); }
方法二:结合路由loading.js与全局状态
如果需要同步路由切换的加载状态(比如Server Components数据加载),可以用全局状态管理配合路由级loading.js。
1. 创建全局加载状态上下文
// app/context/LoadingContext.tsx 'use client'; import { createContext, useContext, useState } from 'react'; const LoadingContext = createContext({ isGlobalLoading: false, setIsGlobalLoading: (state: boolean) => {}, }); export function LoadingProvider({ children }: { children: React.ReactNode }) { const [isGlobalLoading, setIsGlobalLoading] = useState(true); return ( <LoadingContext.Provider value={{ isGlobalLoading, setIsGlobalLoading }}> {children} </LoadingContext.Provider> ); } export function useLoading() { return useContext(LoadingContext); }
2. 修改RootLayout包裹Provider
// app/layout.tsx import { LoadingProvider } from './context/LoadingContext'; import Preloader from './components/Preloader'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="zh-CN"> <body className="min-h-screen"> <LoadingProvider> <Preloader /> {children} </LoadingProvider> </body> </html> ); }
3. 修改Preloader使用全局状态
// app/components/Preloader.tsx 'use client'; import { useEffect } from 'react'; import { usePathname } from 'next/navigation'; import { useLoading } from '../context/LoadingContext'; export default function Preloader() { const { isGlobalLoading, setIsGlobalLoading } = useLoading(); const pathname = usePathname(); useEffect(() => { const checkAllImagesLoaded = () => { const allImages = Array.from(document.querySelectorAll('img')); return allImages.every(img => img.complete || img.naturalWidth > 0); }; const hidePreloader = () => { if (checkAllImagesLoaded()) { setIsGlobalLoading(false); } }; window.addEventListener('load', hidePreloader); const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { const newImages = Array.from(mutation.addedNodes).filter( node => node instanceof HTMLImageElement ); newImages.forEach(img => { img.addEventListener('load', hidePreloader); img.addEventListener('error', hidePreloader); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); // 路由切换时重置状态 setIsGlobalLoading(true); setTimeout(hidePreloader, 100); return () => { window.removeEventListener('load', hidePreloader); observer.disconnect(); }; }, [pathname, setIsGlobalLoading]); if (!isGlobalLoading) return null; return ( <div className="fixed inset-0 bg-white/90 z-[9999] flex items-center justify-center"> <div className="w-16 h-16 border-4 border-gray-200 border-t-blue-600 rounded-full animate-spin"></div> </div> ); }
4. 在路由中配置loading.js
// app/products/loading.js 'use client'; import { useLoading } from '../context/LoadingContext'; import { useEffect } from 'react'; export default function Loading() { const { setIsGlobalLoading } = useLoading(); useEffect(() => { setIsGlobalLoading(true); return () => setIsGlobalLoading(false); }, [setIsGlobalLoading]); return null; // 全局预加载器已处理显示逻辑 }
关键注意事项
- 所有DOM操作逻辑必须放在
'use client'组件中,Server Components无法直接访问浏览器API。 - 必须处理图片加载失败的情况,避免预加载器无限显示。
- 路由切换时要重置加载状态,因为
window.load事件只会在页面首次加载时触发。 - 不要依赖固定超时(比如4秒),需根据实际资源加载状态动态控制。
内容的提问来源于stack exchange,提问作者Ritik Jangir
相关产品推荐
相关产品推荐

