React實(shí)現(xiàn)原生APP切換效果
背景
最近需要使用 Hybrid 的方式開發(fā)一 個 APP,交互和原生 APP 相似并且需要 IM 通信。根據(jù)目前公司已有的實(shí)現(xiàn)方案,每次進(jìn)入一個新的頁面時,可以調(diào)用 Native 提供的 createWebview 方法重新創(chuàng)建一個 Webview,這樣在打開一個頁面時,就能像原生 APP 一樣實(shí)現(xiàn) push 或 pop 的效果。但新創(chuàng)建 Webview 后,之前 Webview 中的長鏈就會被掛起,IM 消息可能被中斷,這樣在功能使用上會存在著一些問題。
經(jīng)過討論,有想到兩種可行的解決方案。
- 長鏈下沉到
Native。我們調(diào)用Native提供的API來實(shí)現(xiàn)IM的唯一。但它有個缺點(diǎn),以后有調(diào)整,Native也需要發(fā)版,并且不太好保證不同Webview中IM信息的一致性和及時性。 - 用
H5實(shí)現(xiàn)一個原生APP的切換效果。讓系統(tǒng)成為一個單頁應(yīng)用。這樣長鏈就可以是唯一的并且不需要下沉到Native端。
最后決定嘗試使用第二種方案。
需求概覽
- 頁面進(jìn)入時從右向左推入、返回上一頁時頁面從左向右推出。
- 進(jìn)入下一頁時,下一頁的數(shù)據(jù)需要重新請求。
- 返回上一頁時,保持上一頁的數(shù)據(jù)展示并且不用重新請求數(shù)據(jù)。
先看一下前后效果對比。
網(wǎng)頁默認(rèn)切換效果

仿原生切換效果

技術(shù)棧
實(shí)現(xiàn)步驟
根據(jù) react-router-dom 文檔配置好路由
配置路由
// router.tsx
// ...
const router = createBrowserRouter([
{
path: "/",
element: <BaseLayout />,
errorElement: <ErrorBoundary />,
children: [
{
index: true,
element: <Home />,
},
// 登錄注冊頁面
{
path: "/login",
element: <Login />,
},
],
},
]);
export default router;
每個頁面都是 BaseLayout 的子節(jié)點(diǎn)。
// BaseLayout.tsx
function BaseLayout() {
// ...
return <Outlet />;
}
export default BaseLayout;
在項目的入口添加 RouterProvider
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-transition-group 組合著 react-router-dom v6 使用時,需要給每個 rouer 都添加一個 nodeRef(跟著官方文檔 demo 來)。
修改 router.tsx。
- 將路由展平并
export出去,方面在其他頁面調(diào)用。 - 給導(dǎo)出去的
newRouter每個路由都添加一個nodeRef。
// 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 創(chuàng)建的路由
const router = createBrowserRouter([
{
path: "/",
element: <BaseLayout />,
errorElement: <ErrorBoundary />,
children: defaultRouters,
},
]);
export default router;
export { newRouters };
在 BaseLayout 中添加過渡。根據(jù) useNavigationType 獲取當(dāng)前頁面是 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",
};
// 授權(quán)組件
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 是結(jié)合著 css-transition 一起使用的,使用 CSSTransition 組件,它會自動地在頁面過渡時,給節(jié)點(diǎn)加上:
*-enter*-enter-active*-enter-done*-exit*-exit-active*-exit-done- ...
等 className,所以再添加一下對應(yīng)的 CSS 動畫效果,過渡的效果就實(shí)現(xiàn)了。
// style.less
/* 路由前進(jìn)時的入場/離場動畫 */
.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;
}
到目前為止,和 Native 一樣的切換效果就都實(shí)現(xiàn)了。
但 Native 還有一個特點(diǎn),只有進(jìn)入下一頁時才會重新請求數(shù)據(jù),返回上一頁時,是直接展示之前的頁面,不需要再重新請求數(shù)據(jù)。
這個可以使用虛擬任務(wù)棧的方式來緩存頁面,以達(dá)到返回上一頁時,不需要重新請求并重新渲染頁面的效果。
react-transition-group: React Transition Group
使用虛擬任務(wù)棧緩存頁面
虛擬任務(wù)棧是使用 react-activation 包來實(shí)現(xiàn)的。
安裝好后,在 main.tsx 處使用 AliveScope 將 RouterProvider 包裹起來。
ReactDOM.createRoot(document.getElementById("root")!).render(
<Provider store={store}>
<AliveScope>
{/* router */}
<RouterProvider router={router} />
</AliveScope>
</Provider>
);
然后在 BaseLayout.tsx 處給子頁面用 KeepAlive 包裹起來。
// BaseLayout.tsx
// 授權(quán)組件
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;
這樣,所有訪問過的頁面都會被緩存起來。
返回上一頁時,我們需要清理掉當(dāng)前頁面的緩存,使頁面再次進(jìn)入時,可以重新請求并渲染頁面。
封裝一個 useGoBack() 方法。
// 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 節(jié)點(diǎn)緩存
const pageNumAbs = Math.abs(pageNum);
const dropNodes = allCachingNodes.slice(
allCachingNodes.length - pageNumAbs
);
dropNodes.forEach((node) => {
dropScope(node.name!);
});
};
};
export default useGoBack;
使用 自定義 Hooks - useGoBack() 返回上一頁的頁面,當(dāng)前頁面就會從緩存中被清理掉,再將進(jìn)入頁面時,會重新走 useEffect 等生命周期。
注意
- 需要根據(jù)
useNavigationType獲取當(dāng)前頁面是push還是pop更改CSSTransition的className。 CSSTransition下面要緊挨著需要過渡的div,KeepAlive要放在這個div下面。react-activation需要配置babel。- 返回上一頁時,一定要清理掉不需要的緩存頁面,以防止緩存頁面過多,頁面使用卡頓。
- 要實(shí)現(xiàn)兩個頁面同時在頁面上展示并過渡,需要使用
TransitionGroup。
以上就是React實(shí)現(xiàn)原生APP切換效果的詳細(xì)內(nèi)容,更多關(guān)于React APP切換的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React學(xué)習(xí)筆記之高階組件應(yīng)用
這篇文章主要介紹了React 高階組件應(yīng)用,詳細(xì)的介紹了什么是React高階組件和具體使用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-06-06
VSCode 配置React Native開發(fā)環(huán)境的方法
本篇文章主要介紹了VSCode 配置React Native開發(fā)環(huán)境的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12
react自定義實(shí)現(xiàn)狀態(tài)管理詳解
這篇文章主要為大家詳細(xì)介紹了react如何自定義實(shí)現(xiàn)狀態(tài)管理,文中的示例代碼講解詳細(xì),具有一定的借鑒價值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-01-01
React事件監(jiān)聽和State狀態(tài)修改方式
這篇文章主要介紹了React事件監(jiān)聽和State狀態(tài)修改方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08
一文詳解手動實(shí)現(xiàn)Recoil狀態(tài)管理基本原理
這篇文章主要為大家介紹了一文詳解手動實(shí)現(xiàn)Recoil狀態(tài)管理基本原理實(shí)例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05

