You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Gatsby路由切换时仅按需触发菜单关闭动画?

解决方案:路由切换时仅在菜单打开时触发关闭动画

要实现菜单打开状态下切换路由时,先完成关闭动画再跳转页面,核心是拦截路由跳转流程,结合菜单状态控制动画触发时机,具体实现如下:

1. 维护菜单状态与动画类的动态控制

在菜单组件中用状态跟踪打开状态,仅在需要关闭时添加closeMenu类,动画结束后立即移除该类,避免页面渲染时重复触发动画:

import { useState, useRef, useEffect } from 'react';
import { navigate, useLocation } from 'gatsby';

const Menu = () => {
  const [isMenuOpen, setIsMenuOpen] = useState(false);
  const menuRef = useRef(null);
  const location = useLocation();
  const prevLocationRef = useRef(location.pathname);

  // 切换菜单:打开时加openMenu,关闭时加closeMenu触发动画
  const toggleMenu = () => {
    setIsMenuOpen(prev => {
      if (prev) {
        menuRef.current.classList.add('closeMenu');
        menuRef.current.classList.remove('openMenu');
      } else {
        menuRef.current.classList.add('openMenu');
        menuRef.current.classList.remove('closeMenu');
      }
      return !prev;
    });
  };

  // 动画结束后移除closeMenu类,防止后续渲染误触发
  const handleAnimationEnd = () => {
    if (!isMenuOpen) {
      menuRef.current.classList.remove('closeMenu');
    }
  };

  // 自定义导航:先关闭菜单再跳转
  const navigateAfterMenuClose = (to) => {
    if (isMenuOpen) {
      // 触发关闭动画
      setIsMenuOpen(false);
      menuRef.current.classList.add('closeMenu');
      menuRef.current.classList.remove('openMenu');
      // 动画结束后执行跳转
      const endHandler = () => {
        navigate(to);
        menuRef.current.removeEventListener('animationend', endHandler);
      };
      menuRef.current.addEventListener('animationend', endHandler);
    } else {
      navigate(to);
    }
  };

  // 标题点击返回根路由
  const handleTitleClick = () => {
    navigateAfterMenuClose('/');
  };

  // 监听浏览器前进/后退导致的路由变化
  useEffect(() => {
    if (prevLocationRef.current !== location.pathname && isMenuOpen) {
      // 触发关闭动画,动画结束后页面已完成路由切换
      setIsMenuOpen(false);
      menuRef.current.classList.add('closeMenu');
      menuRef.current.classList.remove('openMenu');
    }
    prevLocationRef.current = location.pathname;
  }, [location.pathname]);

  // 绑定动画结束事件
  useEffect(() => {
    menuRef.current.addEventListener('animationend', handleAnimationEnd);
    return () => menuRef.current.removeEventListener('animationend', handleAnimationEnd);
  }, [isMenuOpen]);

  return (
    <nav className="navbar">
      <h1 onClick={handleTitleClick}>站点标题</h1>
      <button onClick={toggleMenu} className="menu-btn">菜单</button>
      <div
        ref={menuRef}
        id="menu"
        className={isMenuOpen ? 'openMenu' : ''}
      >
        <button onClick={() => navigateAfterMenuClose('/page1')}>页面1</button>
        <button onClick={() => navigateAfterMenuClose('/page2')}>页面2</button>
      </div>
    </nav>
  );
};

export default Menu;

2. 拦截全局路由跳转(处理浏览器前进后退)

如果需要覆盖浏览器前进/后退的场景,在gatsby-browser.js中使用onPreRouteChange钩子拦截跳转,确保动画完成后再执行路由切换:

import { navigate } from 'gatsby';

let pendingRoute = null;

export const onPreRouteChange = ({ location }) => {
  const menuElement = document.getElementById('menu');
  const isMenuOpen = menuElement.classList.contains('openMenu');

  if (isMenuOpen) {
    // 阻止默认路由跳转
    pendingRoute = location.pathname;
    // 触发关闭动画
    menuElement.classList.add('closeMenu');
    menuElement.classList.remove('openMenu');
    // 动画结束后执行跳转
    const handleEnd = () => {
      if (pendingRoute) {
        navigate(pendingRoute);
        pendingRoute = null;
      }
      menuElement.removeEventListener('animationend', handleEnd);
    };
    menuElement.addEventListener('animationend', handleEnd);
    return false;
  }
};

关键逻辑说明

  • 仅在菜单从打开→关闭的状态变化时添加closeMenu类,动画结束后立即移除,避免页面初始化或路由切换后重复触发动画。
  • 自定义导航函数navigateAfterMenuClose统一处理所有主动跳转(菜单链接、标题点击),确保动画优先执行。
  • 监听路由变化和使用onPreRouteChange覆盖被动路由切换场景,保证所有路由切换都遵循动画逻辑。

为什么之前的方案无效?

  • useMemo阻止重渲染:路由切换时页面组件必然更新,且核心问题是拦截跳转流程而非阻止重渲染,因此无效。
  • 动态修改className被重置:未结合状态管理className,组件重渲染时会覆盖手动修改的类,必须用状态驱动className变化。
  • onRouteChange钩子:该钩子在路由切换完成后触发,无法在跳转前处理动画,应使用onPreRouteChange拦截跳转。

内容的提问来源于stack exchange,提问作者Hamilton S

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 07:32:20