React實(shí)現(xiàn)原生APP切換效果
背景
最近需要使用 Hybrid 的方式開發(fā)一 個(gè) APP,交互和原生 APP 相似并且需要 IM 通信。根據(jù)目前公司已有的實(shí)現(xiàn)方案,每次進(jìn)入一個(gè)新的頁面時(shí),可以調(diào)用 Native 提供的 createWebview 方法重新創(chuàng)建一個(gè) Webview,這樣在打開一個(gè)頁面時(shí),就能像原生 APP 一樣實(shí)現(xiàn) push 或 pop 的效果。但新創(chuàng)建 Webview 后,之前 Webview 中的長鏈就會被掛起,IM 消息可能被中斷,這樣在功能使用上會存在著一些問題。
經(jīng)過討論,有想到兩種可行的解決方案。
- 長鏈下沉到
Native。我們調(diào)用Native提供的API來實(shí)現(xiàn)IM的唯一。但它有個(gè)缺點(diǎn),以后有調(diào)整,Native也需要發(fā)版,并且不太好保證不同Webview中IM信息的一致性和及時(shí)性。 - 用
H5實(shí)現(xiàn)一個(gè)原生APP的切換效果。讓系統(tǒng)成為一個(gè)單頁應(yīng)用。這樣長鏈就可以是唯一的并且不需要下沉到Native端。
最后決定嘗試使用第二種方案。
需求概覽
- 頁面進(jìn)入時(shí)從右向左推入、返回上一頁時(shí)頁面從左向右推出。
- 進(jìn)入下一頁時(shí),下一頁的數(shù)據(jù)需要重新請求。
- 返回上一頁時(shí),保持上一頁的數(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;
每個(gè)頁面都是 BaseLayout 的子節(jié)點(diǎn)。
// BaseLayout.tsx
function BaseLayout() {
// ...
return <Outlet />;
}
export default BaseLayout;
在項(xiàng)目的入口添加 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 使用時(shí),需要給每個(gè) rouer 都添加一個(gè) nodeRef(跟著官方文檔 demo 來)。
修改 router.tsx。
- 將路由展平并
export出去,方面在其他頁面調(diào)用。 - 給導(dǎo)出去的
newRouter每個(gè)路由都添加一個(gè)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;
因?yàn)?react-transition-group 是結(jié)合著 css-transition 一起使用的,使用 CSSTransition 組件,它會自動地在頁面過渡時(shí),給節(jié)點(diǎn)加上:
*-enter*-enter-active*-enter-done*-exit*-exit-active*-exit-done- ...
等 className,所以再添加一下對應(yīng)的 CSS 動畫效果,過渡的效果就實(shí)現(xiàn)了。
// style.less
/* 路由前進(jìn)時(shí)的入場/離場動畫 */
.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;
}
/* 路由后退時(shí)的入場/離場動畫 */
.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 還有一個(gè)特點(diǎn),只有進(jìn)入下一頁時(shí)才會重新請求數(shù)據(jù),返回上一頁時(shí),是直接展示之前的頁面,不需要再重新請求數(shù)據(jù)。
這個(gè)可以使用虛擬任務(wù)棧的方式來緩存頁面,以達(dá)到返回上一頁時(shí),不需要重新請求并重新渲染頁面的效果。
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;
這樣,所有訪問過的頁面都會被緩存起來。
返回上一頁時(shí),我們需要清理掉當(dāng)前頁面的緩存,使頁面再次進(jìn)入時(shí),可以重新請求并渲染頁面。
封裝一個(gè) 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)入頁面時(shí),會重新走 useEffect 等生命周期。
注意
- 需要根據(jù)
useNavigationType獲取當(dāng)前頁面是push還是pop更改CSSTransition的className。 CSSTransition下面要緊挨著需要過渡的div,KeepAlive要放在這個(gè)div下面。react-activation需要配置babel。- 返回上一頁時(shí),一定要清理掉不需要的緩存頁面,以防止緩存頁面過多,頁面使用卡頓。
- 要實(shí)現(xiàn)兩個(gè)頁面同時(shí)在頁面上展示并過渡,需要使用
TransitionGroup。
以上就是React實(shí)現(xiàn)原生APP切換效果的詳細(xì)內(nèi)容,更多關(guān)于React APP切換的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談React多個(gè)setState會調(diào)用幾次
在React的生命周期鉤子和合成事件中,多次執(zhí)行setState,會被調(diào)用幾次,本文就詳細(xì)的介紹一下,感興趣的可以了解一下2021-11-11
React項(xiàng)目打包發(fā)布到Tomcat頁面空白問題及解決
這篇文章主要介紹了React項(xiàng)目打包發(fā)布到Tomcat頁面空白問題及解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06
TS裝飾器bindThis優(yōu)雅實(shí)現(xiàn)React類組件中this綁定
這篇文章主要為大家介紹了TS裝飾器bindThis優(yōu)雅實(shí)現(xiàn)React類組件中this綁定,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
手挽手帶你學(xué)React之React-router4.x的使用
這篇文章主要介紹了手挽手帶你學(xué)React之React-router4.x的使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-02-02
React內(nèi)部實(shí)現(xiàn)cache方法示例詳解
這篇文章主要為大家介紹了React內(nèi)部實(shí)現(xiàn)cache方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
在 React 項(xiàng)目中使用 Auth0 并集成到后端服務(wù)的配置步驟詳解
這篇文章主要介紹了在 React 項(xiàng)目中使用 Auth0 并集成到后端服務(wù)的配置步驟詳解,通過本文詳細(xì)步驟,您可以將 Auth0 集成到 React 項(xiàng)目并與后端服務(wù)交互,需要的朋友可以參考下2024-07-07
ReactDOM.render在react源碼中執(zhí)行原理
這篇文章主要為大家介紹了ReactDOM.render在react源碼中執(zhí)行原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
React-hook-form-mui基本使用教程(入門篇)
react-hook-form-mui可以幫助開發(fā)人員更輕松地構(gòu)建表單,它結(jié)合了React?Hook?Form和Material-UI組件庫,使用react-hook-form-mui,開發(fā)人員可以更快速地構(gòu)建表單,并且可以輕松地進(jìn)行表單驗(yàn)證和數(shù)據(jù)處理,本文介紹React-hook-form-mui基本使用,感興趣的朋友一起看看吧2024-02-02
解決React報(bào)錯(cuò)Property?'value'?does?not?exist?on?
這篇文章主要為大家介紹了React報(bào)錯(cuò)Property?'value'?does?not?exist?on?type?EventTarget的解決方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

