在移动端web页面切换的时候增加页面进入退出的动画效果,在vue中可以使用keepalive标签和transition轻松实现,在react中,可以使用react-activation和动画库实现类似效果,这里使用react-transition-group。参考:微信公众号 - 前端学习总结
使用react-router-dom配置好路由,这里使用v6版本的数据路由书写方式。需要添加的动画的页面使用同一个outlet。
使用React-transition-group
配置路由
/// router.tsx
const router = createBrowserRouter([
{
path: "/",
element: <BaseLayout />,
errorElement: <ErrorBoundary />,
children: [
{
index: true,
element: <Home />,
},
// 登录注册页面
{
path: "/login",
element: <Login />,
},
],
},
]);
export default router;
/// BaseLayout.tsx
function BaseLayout() {
// ...
return <Outlet />;
}
export default BaseLayout;
/// App.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import router from "@/router";
import "@/assets/styles/global.less";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
{/* router */}
<RouterProvider router={router} />
</React.StrictMode>
);
添加过渡动画
react-transition-group 结合react-router v6使用时 需要给每个router配置一个nodeRef。官方示例:react-transition-group/with-react-router
// router.tsx
// ...
// 所有的路由都在这里配置
const defaultRouters: RouteObject[] = [
{
index: true,
path: "/",
element: <Home />,
},
// 登录注册页面
{
path: "/login",
element: <Login />,
}
];
/**
* 将路由展平,并添加 nodeRef 字段
* @param routerParams RouteObject[]
* @returns RouteObject[]
*/
function flatRouters(routerParams: RouteObject[]) {
let newRouters: Array<RouteObject & { nodeRef: RefObject<any> }> = [];
routerParams.forEach((router) => {
newRouters.push({
...router,
nodeRef: createRef(),
});
if (router.children?.length) {
newRouters = newRouters.concat(flatRouters(router.children));
}
});
return newRouters;
}
const newRouters = flatRouters(defaultRouters);
// react-router-dom 创建的路由
const router = createBrowserRouter([
{
path: "/",
element: <BaseLayout />,
errorElement: <ErrorBoundary />,
children: defaultRouters,
},
]);
export default router;
export { newRouters };
在 BaseLayout
中添加过渡。根据 useNavigationType
获取当前页面是 push
还是 pop
更改 CSSTransition
的 className
。
// BaseLayout.tsx
import React, { useEffect } from "react";
import { useOutlet, useNavigationType } from "react-router-dom";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import { newRouters } from "@/router";
import "./style.less";
const ANIMATION_MAP = {
PUSH: "forward",
POP: "back",
REPLACE: "fade-route",
};
// 授权组件
function BaseLayout() {
const currentOutlet = useOutlet();
const navigateType = useNavigationType();
const { nodeRef } =
newRouters.find((route) => route.path === location.pathname) ?? {};
const fullPath = `${location.pathname}${location.search}`;
return (
<TransitionGroup
childFactory={(child) =>
React.cloneElement(child, { classNames: ANIMATION_MAP[navigateType] })
}
>
<CSSTransition
key={location.pathname}
nodeRef={nodeRef}
timeout={500}
unmountOnExit
>
{() => (
<div ref={nodeRef}>
{currentOutlet}
</div>
)}
</CSSTransition>
</TransitionGroup>
);
}
export default BaseLayout;
因为 react-transition-group
是结合着 css-transition
一起使用的,使用 CSSTransition
组件,它会自动地在页面过渡时,给节点加上:
-
*-enter
-
*-enter-active
-
*-enter-done
-
*-exit
-
*-exit-active
-
*-exit-done
-
...
等 className
,所以再添加一下对应的 CSS
动画效果,过渡的效果就实现了。
// style.less
/* 路由前进时的入场/离场动画 */
.forward-enter {
.base-layout;
transform: translate3d(100vw, 0, 0);
z-index: 2;
}
.forward-enter-active {
.base-layout;
transform: translate3d(0, 0, 0);
transition: all 500ms;
z-index: 2;
}
.forward-exit {
.base-layout;
transform: translate3d(0, 0, 0);
z-index: 1;
}
.forward-exit-active {
.base-layout;
transform: translate3d(-100vw, 0, 0);
transition: all 500ms;
z-index: 1;
}
/* 路由后退时的入场/离场动画 */
.back-enter {
transform: translate3d(-100vw, 0, 0);
z-index: 1;
}
.back-enter-active {
.base-layout;
transform: translate3d(0, 0, 0);
transition: all 500ms ease-out;
z-index: 1;
}
.back-exit {
.base-layout;
transform: translate3d(0, 0, 0);
z-index: 2;
}
.back-exit-active {
.base-layout;
transform: translate3d(100vw, 0, 0);
transition: all 500ms ease-out;
z-index: 2;
}
使用虚拟任务栈缓存页面
在进入下一页时重新请求,返回上一页时展示缓存页面。
使用React-activation 包实现缓存页面
// app.tsx
ReactDOM.createRoot(document.getElementById("root")!).render(
<Provider store={store}>
<AliveScope>
{/* router */}
<RouterProvider router={router} />
</AliveScope>
</Provider>
);
// BaseLayout.tsx
// BaseLayout.tsx
// 授权组件
function BaseLayout() {
// ...
return (
<TransitionGroup
childFactory={(child) =>
React.cloneElement(child, { classNames: ANIMATION_MAP[navigateType] })
}
>
<CSSTransition
key={location.pathname}
nodeRef={nodeRef}
timeout={500}
unmountOnExit
>
{() => (
<div ref={nodeRef}>
<KeepAlive
id={fullPath}
saveScrollPosition="screen"
name={fullPath}
>
{currentOutlet}
</KeepAlive>
</div>
)}
</CSSTransition>
</TransitionGroup>
);
}
export default BaseLayout;
这样,所有访问过的页面都会被缓存起来。
返回上一页时,我们需要清理掉当前页面的缓存,使页面再次进入时,可以重新请求并渲染页面。
封装一个自定义 Hooks useGoBack()
。
使用 自定义 Hooks - useGoBack()
返回上一页的页面,当前页面就会从缓存中被清理掉,再将进入页面时,会重新走 useEffect
等生命周期。
// useGoBack.tsx
import { useNavigate } from "react-router-dom";
import { useAliveController } from "react-activation";
// 页面返回 hooks
const useGoBack = () => {
const navigate = useNavigate();
const { dropScope, getCachingNodes } = useAliveController();
return (pageNum = -1) => {
const allCachingNodes = getCachingNodes() || [];
navigate(pageNum);
// 清除 keepAlive 节点缓存
const pageNumAbs = Math.abs(pageNum);
const dropNodes = allCachingNodes.slice(
allCachingNodes.length - pageNumAbs
);
dropNodes.forEach((node) => {
dropScope(node.name!);
});
};
};
export default useGoBack;
注意
-
需要根据
useNavigationType
获取当前页面是push
还是pop
更改CSSTransition
的className
。 -
CSSTransition
下面要紧挨着需要过渡的div
,KeepAlive
要放在这个div
下面。 -
react-activation
需要配置babel
。 -
返回上一页时,一定要清理掉不需要的缓存页面,以防止缓存页面过多,页面使用卡顿。
-
要实现两个页面同时在页面上展示并过渡,需要使用
TransitionGroup