网站LOGO
白雾茫茫丶
页面加载中
1月18日
网站LOGO 白雾茫茫丶
记录学习、生活和有趣的事
菜单
  • 白雾茫茫丶
    记录学习、生活和有趣的事
    用户的头像
    首次访问
    上次留言
    累计留言
    我的等级
    我的角色
    打赏二维码
    打赏博主
    Next.js 实战 (五):添加路由 Transition 过渡效果和 Loading 动画
    点击复制本页信息
    微信扫一扫
    文章二维码
    文章图片 文章标题
    创建时间
  • 一 言
    确认删除此评论么? 确认
  • 本弹窗介绍内容来自,本网站不对其中内容负责。
    • 复制图片
    • 复制图片地址
    • 百度识图
    按住ctrl可打开默认菜单

    Next.js 实战 (五):添加路由 Transition 过渡效果和 Loading 动画

    谢明伟 · 原创 ·
    前端开发Next 实战 · ReactNext
    共 6289 字 · 约 1 分钟 · 145

    什么是 Framer Motion

    Framer Motion 是一个专门为 React 设计的、功能强大且易于使用的动画库。它允许开发者轻松地为他们的应用添加流畅的交互和动画效果,而不需要深入理解复杂的动画原理。Framer Motion 提供了声明式的 API 来处理动画、手势以及页面转换,非常适合用来创建响应式用户界面。

    首屏加载动画

    如果你使用 next.js 构建单页面应用程序,页面一开始资源加载会导致页面空白,一般我们的做法都是在首屏添加加载动画,等资源加载完成再把动画取消。

    1. 新建 components/FullLoading/index.tsx 文件:

      tsx 代码:
      'use client';
      
      import { useEffect, useState } from 'react';
      
      const FullLoading = () => {
        const [mounted, setMounted] = useState(false);
      
        useEffect(() => {
       setMounted(true);
        }, []);
      
        // 判断组件是否挂载
        if (!mounted) {
       return (
         <div className="fixed flex w-screen h-screen justify-center items-center flex-col z-[99] overflow-hidden bg-white dark:bg-slate-900">
           <div className="relative w-12 h-12 rotate-[165deg] before:content-[''] after:content-[''] before:absolute after:absolute before:top-2/4 after:top-2/4 before:left-2/4 after:left-2/4 before:block after:block before:w-[.5em] after:w-[.5em] before:h-[.5em] after:h-[.5em] before:rounded after:rounded before:-translate-x-1/2 after:-translate-x-1/2 before:-translate-y-2/4 after:-translate-y-2/4 before:animate-[loaderBefore_2s_infinite] after:animate-[loaderAfter_2s_infinite]"></div>
         </div>
       );
        }
        return null;
      };
      export default FullLoading;
    2. app/globals.scss 中加入代码:

      css 代码:
      @keyframes loaderBefore {
        0% {
      width: 0.5em;
      box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75), -1em 0.5em rgba(111, 202, 220, 0.75);
        }
       
        35% {
      width: 2.5em;
      box-shadow: 0 -0.5em rgba(225, 20, 98, 0.75), 0 0.5em rgba(111, 202, 220, 0.75);
        }
       
        70% {
      width: 0.5em;
      box-shadow: -1em -0.5em rgba(225, 20, 98, 0.75), 1em 0.5em rgba(111, 202, 220, 0.75);
        }
       
        100% {
      box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75), -1em 0.5em rgba(111, 202, 220, 0.75);
        }
       }
       
       @keyframes loaderAfter {
        0% {
      height: 0.5em;
      box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75), -0.5em -1em rgba(233, 169, 32, 0.75);
        }
       
        35% {
      height: 2.5em;
      box-shadow: 0.5em 0 rgba(61, 184, 143, 0.75), -0.5em 0 rgba(233, 169, 32, 0.75);
        }
       
        70% {
      height: 0.5em;
      box-shadow: 0.5em -1em rgba(61, 184, 143, 0.75), -0.5em 1em rgba(233, 169, 32, 0.75);
        }
       
        100% {
      box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75), -0.5em -1em rgba(233, 169, 32, 0.75);
        }
       }
    3. layout.tsx 中引用组件:

      tsx 代码:
      export default async function RootLayout({
        children,
      }: Readonly<{
        children: React.ReactNode;
      }>) {
        return (
       <html suppressHydrationWarning>
         <body>
           <NextUIProvider>
               <ThemeProvider attribute="class" defaultTheme="light">
                 {/* 全局 Loading */}
                 <FullLoading />
                 {children}
               </ThemeProvider>
           </NextUIProvider>
         </body>
       </html>
        );
      }

    实际效果可参考网站:今日热榜

    路由加载 Loading

    next.js 提供了现成的方案,官方文档参考:loading.js

    新建 app/loading.tsx 文件:

    tsx 代码:
    import { Spinner } from '@nextui-org/react';
    
    export default function Loading() {
      return (
        <div className="flex justify-center items-center min-h-60">
          <Spinner label="页面正在加载中..." />
        </div>
      );
    }

    路由进场动画

    1. 新建 app/template.tsx 文件:

      tsx 代码:
      "use client";
      
      import { motion } from "framer-motion";
      
      export default function Template({ children }: { children: React.ReactNode }) {
        const variants = {
       hidden: { opacity: 0, x: 100 },
       enter: { opacity: 1, x: 0 },
        };
      
        return (
       <motion.main
         data-scroll
         className="mb-auto p-4"
         initial="hidden"
         animate="enter"
         variants={variants}
         transition={{ duration: 0.5, ease: 'easeOut' }}
       >
         {children}
       </motion.main>
        );
      }

    路由退场动画

    1. 新建 components/PageAnimatePresence/index.tsx 文件

      tsx 代码:
      "use client";
      
      import { AnimatePresence, motion } from "framer-motion";
      import { LayoutRouterContext } from "next/dist/shared/lib/app-router-context.shared-runtime";
      import { usePathname } from "next/navigation";
      import { useContext, useRef } from "react";
      
      // 阻止页面立即打开,先让退场动画走完,再显示新的页面内容
      function FrozenRouter(props: { children: React.ReactNode }) {
        const context = useContext(LayoutRouterContext ?? {});
        const frozen = useRef(context).current;
      
        return (
       <LayoutRouterContext.Provider value={frozen}>
         {props.children}
       </LayoutRouterContext.Provider>
        );
      }
      
      const PageAnimatePresence = ({ children }: { children: React.ReactNode }) => {
        const pathname = usePathname();
      
        return (
       <AnimatePresence mode="wait">
         <motion.div
           key={pathname}
           initial="initialState"
           animate="animateState"
           exit="exitState"
           transition={{
             duration: 0.5,
             ease: 'easeOut'
           }}
           variants={{
             exitState: { opacity: 0, x: 100 }
           }}
           className="w-full min-h-screen"
         >
           <FrozenRouter>{children}</FrozenRouter>
         </motion.div>
       </AnimatePresence>
        );
      };
      
      export default PageAnimatePresence;
    2. layout.tsx 文件中引入组件包裹 children:

      tsx 代码:
      import PageAnimatePresence from '@/components/PageAnimatePresence'
      
      export default async function RootLayout({
        children,
      }: Readonly<{
        children: React.ReactNode;
      }>) {
        return (
       <html suppressHydrationWarning>
         <body>
           <NextUIProvider>
               <ThemeProvider attribute="class" defaultTheme="light">
                 {/* 全局 Loading */}
                 <FullLoading />
                 <PageAnimatePresence>
                    {children}
                 </PageAnimatePresence>
               </ThemeProvider>
           </NextUIProvider>
         </body>
       </html>
        );
      }

    效果演示

    总结

    大家如果有更好的实现方案,可以一起探讨一下。

    本文部分效果参考了文章:Next.js 如何实现导航时的过渡动画?(使用 Framer Motion)

    线上预览地址Next Admin

    声明:本文由 谢明伟(博主)原创,依据 CC-BY-NC-SA 4.0 许可协议 授权,转载请注明出处。

    还没有人喜爱这篇文章呢

    我要发表评论 我要发表评论
    博客logo 白雾茫茫丶 记录学习、生活和有趣的事 51统计 百度统计
    MOEICP 萌ICP备20236860号 ICP 粤ICP备2023007649号 ICP 粤公网安备44030402006402号

    💻️ 谢明伟 昨天 17:26 在线

    🕛

    本站已运行 3 年 17 天 16 小时 20 分

    🌳

    自豪地使用 Typecho 建站,并搭配 MyLife 主题
    白雾茫茫丶. © 2022 ~ 2025.
    网站logo

    白雾茫茫丶 记录学习、生活和有趣的事