【问题标题】:Custom page / route transitions in Next.jsNext.js 中的自定义页面/路由转换
【发布时间】:2023-04-01 01:12:01
【问题描述】:

我正在尝试使用 Next.js 的框架和 Greensock 动画库(如果适用)来实现基于回调的路由转换。例如,当我从主页开始然后导航到 /about 时,我希望能够执行以下操作:

HomepageComponent.transitionOut(() => router.push('/about'))

理想情况下,在推送状态之前像中间件或其他东西一样监听路由器

Router.events.on('push', (newUrl) => { currentPage.transitionOut().then(() => router.push(newUrl)) });

主要问题

主要问题是我还有一个在后台运行的 WebGL 应用程序,与 React 生态系统解耦(因为它使用 requestAnimationFrame)。所以我想要基于回调的转换的原因是因为我需要在 WebGL 转换完成后运行它们。

当前实施

我已经研究过使用React Transition Group,也看到了docs for the Router object,但似乎都不是基于回调的。换句话说,当我转换到新页面时,WebGL 和页面转换同时运行。而且我不想做一个 hacky 解决方案,比如为页面转换添加延迟,以便它们发生在 WebGL 之后。

这就是我现在拥有的:

app.js

<TransitionGroup>
  <Transition
    timeout={{ enter: 2000, exit: 2000 }}
    // unmountOnExit={true}
    onEnter={(node) => {
      gsap.fromTo(node, { opacity: 0 }, { opacity: 1, duration: 1 });
    }}
    onExit={(node) => {
      gsap.to(node, { opacity: 0, duration: 1 });
    }}
    key={router.route}
  >
    <Component {...pageProps}></Component>
  </Transition>
</TransitionGroup>

webgl 部分

Router.events.on('routeChangeStart', (url) => {
  // transition webGL elements

  // ideally would transition webGL elements and then allow callback to transition out html elements
});

我也尝试过使用 eventemitter3 库来做类似的事情:

// a tag element click handler
onClick(e, href) {
  e.preventDefault();
  this.transitionOut().then(() => { Emitter.emit('push', href); });
  // then we listen to Emitter 'push' event and that's when we Router.push(href)
}

但是,当使用后退/前进按钮进行导航时,这种方法遇到了很大的问题

【问题讨论】:

    标签: reactjs next.js gsap react-transition-group


    【解决方案1】:

    首先我推荐阅读Greensock’s React documentation

    Next.JS 中的动画介绍

    对于介绍动画,如果您将useLayoutEffect 与 SSR 一起使用,您的控制台将充满警告。为避免这种情况,请改用useIsomorphicLayoutEffectGo to useIsomorphicLayoutEffect.

    为了防止使用 SSR 的无样式内容 (FOUC) 闪烁,您需要设置组件的初始样式状态。例如,如果我们正在淡入,该组件的初始样式应该是不透明度为零。

    Next.JS 中的 Outro 动画

    对于outro动画,截取页面过渡,做退出动画,然后onComplete路由到下一页。

    为了实现这一点,我们可以使用 TransitionLayout 高阶组件作为包装器来延迟路由更改,直到任何动画完成之后,以及一个 TransitionProvider 组件将利用 React 的 useContext 挂钩来共享跨多个组件的结尾时间线,无论它们嵌套在哪里。

    过渡上下文

    为了制作页面过渡效果,我们需要在outro动画完成之前阻止渲染新页面。

    我们的页面中可能嵌套了许多具有不同动画效果的组件。为了跟踪所有不同的结尾转换,我们将结合使用 React 的 Context API 和顶级 GSAP 时间线。

    TransitionContext 中,我们将创建我们的TransitionProvider,这将使我们的GSAP 时间线为outro 动画提供给任何想要在页面更改期间过渡的组件。

    import React, { useState, createContext, useCallback } from "react"
    import gsap from "gsap"
    
    const TransitionContext = createContext({})
    
    const TransitionProvider = ({ children }) => {
      const [timeline, setTimeline] = useState(() =>
        gsap.timeline({ paused: true })
      )
    
      return (
        <TransitionContext.Provider
          value={{
            timeline,
            setTimeline,
          }}
        >
          {children}
        </TransitionContext.Provider>
      )
    }
    
    export { TransitionContext, TransitionProvider }
    

    接下来,我们有 TransitionLayout,它将是我们的控制器,它将启动 outro 动画并在它们全部完成后更新页面。

    import { gsap } from "gsap"
    import { TransitionContext } from "../context/TransitionContext"
    import { useState, useContext, useRef } from "react"
    import useIsomorphicLayoutEffect from "../animation/useIsomorphicLayoutEffect"
    
    export default function TransitionLayout({ children }) {
      const [displayChildren, setDisplayChildren] = useState(children)
      const { timeline, background } = useContext(TransitionContext)
      const el = useRef()
    
      useIsomorphicLayoutEffect(() => {
        if (children !== displayChildren) {
          if (timeline.duration() === 0) {
            // there are no outro animations, so immediately transition
            setDisplayChildren(children)
          } else {
            timeline.play().then(() => {
              // outro complete so reset to an empty paused timeline
              timeline.seek(0).pause().clear()
              setDisplayChildren(children)
            })
          }
        }
      }, [children])
    
      return <div ref={el}>{displayChildren}</div>
    }
    

    在自定义 App 组件中,我们可以让 TransitionProviderTransitionLayout 包装其他元素,以便它们可以访问 TransitionContext 属性。 HeaderFooter 存在于 Component 之外,因此它们在初始页面加载后将是静态的。

    import { TransitionProvider } from "../src/context/TransitionContext"
    import TransitionLayout from "../src/animation/TransitionLayout"
    import { Box } from "theme-ui"
    import Header from "../src/ui/Header"
    import Footer from "../src/ui/Footer"
    
    export default function MyApp({ Component, pageProps }) {
      return (
        <TransitionProvider>
          <TransitionLayout>
            <Box
              sx={{
                display: "flex",
                minHeight: "100vh",
                flexDirection: "column",
              }}
            >
              <Header />
              <Component {...pageProps} />
              <Footer />
            </Box>
          </TransitionLayout>
        </TransitionProvider>
      )
    }
    

    组件级动画

    这是我们可以在组件级别执行的基本动画示例。我们可以将任意数量的这些添加到页面中,它们都会做同样的事情,将所有子元素包裹在一个透明的 div 中,并在页面加载时淡入,然后在导航到不同页面时淡出。

    import { useRef, useContext } from "react"
    import { gsap } from "gsap"
    import { Box } from "theme-ui"
    import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect"
    import { TransitionContext } from "../context/TransitionContext"
    
    const FadeInOut = ({ children }) => (
      const { timeline } = useContext(TransitionContext)
      const el = useRef()
    
      // useIsomorphicLayoutEffect to avoid console warnings
      useIsomorphicLayoutEffect(() => {
        // intro animation will play immediately
        gsap.to(el.current, {
          opacity: 1,
          duration: 1,
        })
    
        // add outro animation to top-level outro animation timeline
        timeline.add(
          gsap.to(el.current, {
            opacity: 1,
            duration: .5,
          }),
          0
        )
      }, [])
    
      // set initial opacity to 0 to avoid FOUC for SSR
      <Box ref={el} sx={{opacity: 0}}>
        {children}
      </Box>
    )
    
    export default FadeInOut
    

    我们可以采用这种模式并将其提取到可扩展的 AnimateInOut 帮助器组件中,以便在我们的应用程序中实现可重用的介绍/结尾动画模式。

    import React, { useRef, useContext } from "react"
    import { gsap } from "gsap"
    import { Box } from "theme-ui"
    import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect"
    import { TransitionContext } from "../context/TransitionContext"
    
    const AnimateInOut = ({
      children,
      as,
      from,
      to,
      durationIn,
      durationOut,
      delay,
      delayOut,
      set,
      skipOutro,
    }) => {
      const { timeline } = useContext(TransitionContext)
      const el = useRef()
    
      useIsomorphicLayoutEffect(() => {
        // intro animation
        if (set) {
          gsap.set(el.current, { ...set })
        }
        gsap.to(el.current, {
          ...to,
          delay: delay || 0,
          duration: durationIn,
        })
    
        // outro animation
        if (!skipOutro) {
          timeline.add(
            gsap.to(el.current, {
              ...from,
              delay: delayOut || 0,
              duration: durationOut,
            }),
            0
          )
        }
      }, [])
    
      return (
        <Box as={as} sx={from} ref={el}>
          {children}
        </Box>
      )
    }
    
    export default AnimateInOut
    

    AnimateInOut 组件具有针对不同场景的内置灵活性:

    • 为片头和片尾设置不同的动画、持续时间和延迟
    • 跳过结尾
    • 为包装器设置元素标签,例如使用 &lt;span&gt; 而不是 &lt;div&gt;
    • 使用 GSAP 的 set 选项为 intro 定义初始值

    使用它我们可以创建各种可重复使用的开场/开场动画,例如&lt;FlyInOut&gt;&lt;ScaleInOut&gt;&lt;RotateInOut3D&gt; 等等。

    我有一个演示项目,您可以在其中看到上述内容:TweenPages

    【讨论】:

      【解决方案2】:

      这有点晚了,但我今天自己正在研究这个。使用 Framer Motion 真的很容易,但我也想使用 GSAP / React Transition Group。

      对于 Framer Motion,我只是将 Next 包裹在一个运动组件中:

        <motion.div
          key={router.asPath}
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        >
          <Component {...pageProps} />
        </motion.div>
      

      对于 GSAP / React Transition Group,不确定这是否是正确的方法,但它按我的预期工作(请参阅 cmets):

        const [state, setstate] = useState(router.asPath) // I set the current asPath as the state
      
        useEffect(() => {
        const handleStart = () => {
          setstate(router.asPath) // then on a router change, I'm setting the state again
          // other handleStart logic goes here 
        }
        const handleStop = () => {
          ... // handleStop logic goes here
        }
      
        router.events.on("routeChangeStart", handleStart)
        router.events.on("routeChangeComplete", handleStop)
        router.events.on("routeChangeError", handleStop)
      
        return () => {
          router.events.off("routeChangeStart", handleStart)
          router.events.off("routeChangeComplete", handleStop)
          router.events.off("routeChangeError", handleStop)
        }
      }, [router])
      
        <Transition
          in={router.asPath !== state} // here I'm just checking if the state has changed, then triggering the animations
          onEnter={enter => gsap.set(enter, { opacity: 0 })}
          onEntered={entered => gsap.to(entered, { opacity: 1, duration: 0.3 })}
          onExit={exit => gsap.to(exit, { opacity: 0, duration: 0.3 })}
          timeout={300}
          appear
        >
          <Component {...pageProps} />
        </Transition>
      

      【讨论】:

        猜你喜欢
        • 2023-03-24
        • 2018-10-18
        • 1970-01-01
        • 2020-10-27
        • 2019-09-03
        • 2016-01-13
        • 2017-05-17
        • 1970-01-01
        • 2015-09-10
        相关资源
        最近更新 更多