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