如何在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
相关产品推荐
相关产品推荐

