React Navigation 5.x(一)常用知識點梳理

為了閱讀體驗,分為上下兩篇。算不上教程,主要目的還是摘取使用這個庫時的常用知識點和解決方案,便于自己記憶和查閱。

本篇梳理React Navigation 5.x 的一些基礎API、嵌套導航注意事項、如何設計合理的嵌套路由等。
第二篇主要講如何實現(xiàn)我自身App的兩個需求:1、在嵌套路由中動態(tài)配置頂部標題欄(tab導航器嵌套在根部stack導航器的首屏);2、監(jiān)聽tab點擊事件,觸發(fā)時將對應screen重置到初始狀態(tài)(數(shù)據(jù))。會把自己的代碼結構放出來。React Navigation 5.x(二)嵌套路由動態(tài)配置標題欄及自定義tabbar點擊事件

循序漸進,我們先看一些干貨,最后再來實現(xiàn)這兩個功能。

一、堆棧導航器Navigator和Screen的具體參數(shù)配置及說明

這邊只列舉比較實用的。如有需要可以去官網(wǎng)查閱API:createStackNavigator

1. 導航器組件Navigator常用參數(shù)(Props):
參數(shù)名 說明
initialRouteName 導航器初次加載時要渲染的路由的名字,對應Screen(屏幕組件)的name,默認渲染該棧內第一個Sreen
screenOptions 為該棧內所有Sreen配置通用屬性,即可把Screen的options里的屬性提到這里統(tǒng)一設置。當這些Screen有相同的屬性時,沒有必要復制多份并在它們的options上重復設置。相同的屬性,Screen的options里配置的優(yōu)先級更高。
keyboardHandlingEnabled 默認值為true,若設置為false,屏幕上的鍵盤將不會在導航到新屏幕時自動消失。
headerMode 設置該棧標題欄的形式 'float''(ios模式)|'screen'(Android模式)|'none' (不顯示頭部標題欄)
2. 導航器內的屏幕組件Screen常用參數(shù)(統(tǒng)一在options里配置):
參數(shù)名 類型 說明
title String Screen標題欄的文字
header Function 返回一個React Element作為自定義標題欄。要使用這個配置,必須先確保設置Navigator的headerMode:'screen',以及定義好標題欄高度,e.g. headerStyle: { height: 80 } 參考示例(1)
headerTitle String | Function 如果是函數(shù),返回一個接收參數(shù)的React Element,作為自定義標題欄文字組件。 參考示例(2)
headerShown Boolean 屏幕的標題欄顯示與否,在父Navigator沒有設置headerMode: 'none'的情況下,默認是true
headerTitleAlign Boolean 屏幕標題欄文字的對齊方式,可選值:leftcenter,未設置時,iOS默認居中,Android則靠左。
headerRight Function 返回一個React Element以自定義標題欄的右側。
headerLeft Function 返回一個React Element以自定義標題欄的左側。默認使用HeaderBackButton組件,你可以使用它來覆蓋后退按鈕,參考示例(3):
headerStyle Object 標題欄樣式,如背景顏色等
headerTitleStyle Object 標題欄文字組件(headerTitle)樣式
headerTintColor String 標題欄文字顏色

(1) header配置示例

header: ({ scene, previous, navigation }) => {
  const { options } = scene.descriptor;
  const title =
    options.headerTitle !== undefined
      ? options.headerTitle
      : options.title !== undefined
      ? options.title
      : scene.route.name;
  return (
    <MyHeader
      title={title}
      leftButton={
        previous ? <MyBackButton onPress={navigation.goBack} /> : undefined
      }
      style={options.headerStyle}
    />
  );
};

(2) headerTitle配置示例

function LogoTitle(props) {
  return (
    <>
      <Image
        style={{ width: 50, height: 50 }}
        source={require('@expo/snack-static/react-native-logo.png')}
      />
      <Text>{props.props.title}</Text>
    </>
  );
}
function StackScreen() {
  const title = '自定義標題' ;
  // 這邊標題一般都是我們通過獲取路由參數(shù)再經(jīng)過方法判斷確定的,這里寫死為了方便演示如何把title額外傳給自定義組件。
  return (
    <Stack.Navigator>
      <Stack.Screen
        name="Home"
        component={HomeScreen}
        options={{ headerTitle: props => <LogoTitle props={{...props, title}} /> }}
      />
    </Stack.Navigator>
  );
}

(3) headerLeft示例

import { HeaderBackButton } from '@react-navigation/stack';
<Screen
  name="Home"
  component={HomeScreen}
  options={{
    headerLeft: (props) => (
      <HeaderBackButton
        {...props}
        onPress={() => {
          // Do something
        }}
      />
    ),
  }}
/>;

二、navigation屬性及方法(Actions)

App里的每個screen組件都能通過props接收到一個navigation屬性,它包含了各種調度導航動作的便利功能/方法。不同的導航器接收到的navigation能執(zhí)行的方法是有區(qū)別的:
這邊只展開講了部分方法,每個藍色Action都加了API直達鏈接。

比如我們有個導航屏幕:
<Stack.Screen key="Profile-1" name="Profile" component={component} />

  • 通用方法

    • navigate - 導航到特定路由,參數(shù)必須包含name或key屬性,可選參數(shù)params(合并到目標路由的參數(shù))。通俗理解就是轉到這個name/key對應的路由。如果正在當前路由屏幕組件頁面,那么是不會有任何反應的。
      格式:navigation.navigate(name: string, [params: object])
    navigation.navigate( 'Profile',  { user: 'jane' })
    
    • goBack - 返回導航堆棧歷史記錄的上一個屏幕。navigation.goBack()
    • reset - 可以讓我們用新的導航狀態(tài)替換當前的導航狀態(tài),即移除現(xiàn)有的已經(jīng)入棧的屏幕和歷史記錄,設置新的入棧屏幕們。如果希望在更改狀態(tài)時保留現(xiàn)有的屏幕,可以使用CommonActions.set結合navigation.dispatch。像這樣:
    import { CommonActions } from '@react-navigation/native';
    navigation.dispatch(
      CommonActions.reset({
        index: 1,
        routes: [
          { name: 'Home' },
          {
            name: 'Profile',
            params: { user: 'jane' },
          },
        ],
      })
    );
    
    • setParams - 更新當前特定路由的參數(shù)。 作用就像React的setState,傳入的參數(shù)和舊的params合并對象而非覆蓋。
      如果要為特定路由更新參數(shù),則可添加source屬性,值為該路由的key。
    import { CommonActions } from '@react-navigation/native';
    
    navigation.dispatch({
      ...CommonActions.setParams({ user: 'Wojtek' }),
      source: route.key,
    });
    
    • setOptions 可以在屏幕組件內部,根據(jù)它的props、state、context,來定制我們的屏幕組件選項(screen options),比如title等。
  • 堆棧導航器

    • replace - 用新路由替換導航狀態(tài)navigation state中當前或指定的路由。以下是替換state中特定路由的示例:
    import { StackActions } from '@react-navigation/native';
    navigation.dispatch({
      ...StackActions.replace('Profile', { // 要新替換上的路由name
        user: 'jane',
      }),
      source: route.key, // 要被替換的路由的key
      target: navigation.dangerouslyGetState().key, // 要新替換上的路由的key
    });  
    
    • push - 添加一條新路由到導航堆棧頂部。格式同navigate
      和調用navigate的區(qū)別是,navigate會先嘗試查找具有目標name的現(xiàn)有路由并跳轉過去,并且僅在堆棧中還沒有這個路由時才推送新路由。而push在當目標路由已經(jīng)存在于導航堆棧時,仍然會推送新路由,因此一個路由可以多次訪問(形成多條歷史記錄)。
    • pop - 默認回到導航棧歷史記錄的上一步。有一個可選參數(shù)(count),允許你指定彈出多少個屏幕。navigation.pop(count: number)
    • popToTop - 返回堆棧中的第一個屏幕,關閉所有其他屏幕。navigation.popToTop()
  • tab選項卡導航器

    • jumpTo - 跳轉至tab導航器中的現(xiàn)有路由。
      格式:navigation.jumpTo(name: string, [params: object])
  • drawer抽屜導航器

    • jumpTo - 跳轉至drawer導航器中的現(xiàn)有路由。格式同tab導航器的jumpTo
    • openDrawer - 打開drawer導航器面板。格式:navigation.openDrawer()
    • closeDrawer - 關閉drawer面板。格式:navigation.closeDrawer()
    • toggleDrawer - 切換drawer面板開關狀態(tài)。格式:navigation.toggleDrawer()
  • 高級API參考

    • dispatch - dispatch方法允許我們發(fā)送一個導航動作對象(包含用于生成特定基于某類型導航器的操作方法),來確定導航狀態(tài)如何更新。除非實在無法直接通過navigate,goBack等方法完成我們所需的操作。不然應該避免使用它。盡量都通過navigation.[普通方法]屬性來導航。
      dispatch可調度的對象除了CommonActions,還有StackActions 、還有DrawerActions 、還有TabActions 。這仨都擴展于CommonActions。
// 要先獲取特定的導航動作創(chuàng)造器
import { CommonActions } from '@react-navigation/native';

navigation.dispatch(
  // 再去觸發(fā)方法
  CommonActions.navigate({
    name: 'Profile',
    params: {
      user: 'jane',
    },
  })
);

三、導航狀態(tài)Navigation state

navigation state是React Navigation存儲應用程序的路由結構和歷史記錄的對象。
比如,在主屏幕嵌套了一個標簽導航器的堆棧導航器,可能具有如下導航狀態(tài):

const state = {
  type: 'stack',
  key: 'stack-1',
  routeNames: ['Home', 'Profile', 'Settings'],
  routes: [
    {
      key: 'home-1',
      name: 'Home',
      state: {
        key: 'tab-1',
        routeNames: ['Feed', 'Library', 'Favorites'],
        routes: [
          { key: 'feed-1', name: 'Feed', params: { sortBy: 'latest' } },
          { key: 'library-1', name: 'Library' },
          { key: 'favorites-1', name: 'Favorites' },
        ],
        index: 0,
      },
    },
    { key: 'settings-1', name: 'Settings' },
  ],
  index: 1,
};

每個導航狀態(tài)對象中包含的屬性:

  • type-這個導航狀態(tài)歸屬的導航器的類型,例如stacktabdrawer
  • key -識別導航器的唯一鍵。
  • routeName-包含所屬導航器的每個屏幕name(字符串)的數(shù)組。
  • routes-在導航器中呈現(xiàn)的路由對象(屏幕)的列表。它還在堆棧導航器中表示歷史記錄。此數(shù)組中至少應存在一項。
  • index-正獲得焦點的路由對象在routes數(shù)組中的索引。
  • history-訪問過的項目列表。這是一個可選屬性,并非在所有導航器中都存在。比如它僅存在于核心的tab和抽屜導航器中。history數(shù)組中的項目可以根據(jù)導航器而變化。此數(shù)組中至少應存在一項。
  • stale-除非顯式設置了stale屬性,否則值默認是false。也就表示導航狀態(tài)對象需要“自動補齊”

routes數(shù)組中的每個路由對象(route)都可以包含以下屬性:

  • key-屏幕的唯一鍵。會自動創(chuàng)建或在導航到此屏幕時添加。
  • name-屏幕名稱。在導航器組件層次結構中定義。
  • params-可選,一個包含參數(shù)的對象,有導航動作時定義,例如navigate('Home', { sortBy: 'latest' })
  • state -可選,嵌套在此屏幕內的子導航器的導航狀態(tài)對象。只會在導航事件發(fā)生后才掛到路由對象上。

注:每個screen組件的props.route,就是上面說的routes數(shù)組中的路由對象,內容為這個屏幕的路由數(shù)據(jù)。

四、設計合理的導航結構

嵌套導航器就是在一個Navigator的一個Screen里渲染的Navigator,作為一個組件元素賦給Screen的component屬性。
一個應用通常都擁有底部選項卡(tabbar),一般是主頁的標準配置。同時應用中的部分頁面(比如登錄頁等)不需要tabbar。要實現(xiàn)這一點,能從導航結構入手就不要去動態(tài)設置隱藏/顯示tabbar。最簡單的方法是將選項卡導航器嵌套在堆棧導航器的第一個屏幕中,將不需要tabbar的Screen放在這個屏幕后面。
像下面的例子:一個tab導航器就被嵌套在stack導航器里。

首頁為底部tab欄的典型嵌套結構 (下面五、(3)還會用這個例子舉證)
function HomeTabs() { 
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={Home} />
      <Tab.Screen name="Feed" component={Feed} />
    </Tab.Navigator>
  );
}
function App() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeTabs} />
      <Stack.Screen name="Profile" component={Profile} />
      <Stack.Screen name="Settings" component={Settings} />
    </Stack.Navigator>
  );
}

另外一種應用中常見的導航模式,把stack導航器嵌套在drawer導航器的每個屏幕中

function Root() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Profile" component={Profile} />
      <Stack.Screen name="Settings" component={Settings} />
    </Stack.Navigator>
  );
}
function App() {
  return (
    <NavigationContainer>
      <Drawer.Navigator>
        <Drawer.Screen name="Home" component={Home} />
        <Drawer.Screen name="Root" component={Root} />
      </Drawer.Navigator>
    </NavigationContainer>
  );
}

如果我們想從Home導航到Root,這樣操作:navigation.navigate('Root'); Root的初始屏幕即Profile就會展示。如果你想展示的是Settings這一屏,就需要這樣做:navigation.navigate('Root', { screen: 'Settings' }); 如果你要帶參數(shù)跳轉,現(xiàn)在就得這樣做:

navigation.navigate('Root', {
  screen: 'Settings',
  params: { user: 'jane' },
});

五、保持嵌套導航器定義的初始路由不變(initialRouteName的值)

當你指定導航到嵌套導航器的某一屏時(navigation.navigate('Root', { screen: 'Settings' });),導航器定義初始路由就會被替換成這一屏,也就是說,下次直接導航這個嵌套導航器的時候(navigation.navigate('Root'); ),會默認顯示這個Screen(Settings)。
如果不想初始路由被改變,我們就要在跳轉的時候加一個initial: false,,如下:

navigation.navigate('Root', {
  screen: 'Settings',
  initial: false,
});

六、盡量避免深度嵌套

在能實現(xiàn)需求的基礎上,請盡可能地少嵌套導航棧,建議層數(shù)最多不要超過兩層。因為這會有很多副作用。
比如會引起低端設備的內存和性能問題。
影響代碼可讀性,過于冗余復雜變得難以維護。
Tab里再放Tab,Drawer里再放Drawer,會帶來不好的用戶體驗。
如果你為了代碼邏輯更清晰,想為Navigtor下的Screen分類,可以考慮像這樣做:

// Define multiple groups of screens in objects like this
const commonScreens = {
  Help: HelpScreen,
};

const authScreens = {
  SignIn: SignInScreen,
  SignUp: SignUpScreen,
};

const userScreens = {
  Home: HomeScreen,
  Profile: ProfileScreen,
};

// Then use them in your components by looping over the object and creating screen configs
// You could extract this logic to a utility function and reuse it to simplify your code
<Stack.Navigator>
  {Object.entries({
    // Use the screens normally
    ...commonScreens,
    // Use some screens conditionally based on some condition
    ...(isLoggedIn ? userScreens : authScreens),
  }).map(([name, component]) => (
    <Stack.Screen name={name} component={component} />
  ))}
</Stack.Navigator>;

七、使用嵌套導航器,其他要注意并弄清的點

(1) 每個導航器保管它自己的導航歷史

比如,當你在一個被嵌套在Screen里的堆棧導航器上點擊返回按鈕的時候,它會返回到本導航器(就是被嵌套的stack導航器)導航歷史中的上一頁,而不是返回到上級導航器中。

(2) 每個導航器中的屏幕有它自己的參數(shù)

比如,傳遞給嵌套導航器中的screen的任何參數(shù)都在該屏幕的route prop中,且不能被它的父或子導航器中的屏幕訪問。
如果要從子屏幕訪問父屏幕的參數(shù),可以使用React Context將參數(shù)暴露給子屏幕。

(3) 導航action會優(yōu)先由當前導航器處理,如果當前導航器不能處理則通過冒泡的方式由上一級導航器處理

比如,你在一個被嵌套的導航器的screen中調用navigation.goBack(),那么只有當你在該導航器的首頁時你才會返回到父導航器中。其他的action像navigate工作原理相同。也就是說,只有當被嵌套的導航器不能處理這個action時,父導航器才會試圖去處理它。
在上面的例子(首頁為底部tab欄的典型嵌套結構)中,當你在Feed頁調用navigate('Messages'),嵌套的tab導航器會處理這個action,但當你在這里調用navigate('Settings'),就會由它的父導航器來處理了。

(4) 導航器的一些特定方法可以在子導航器中使用

比如,如果一個stack導航器嵌套在drawer導航器中,那么drawer導航器的openDrawercloseDrawertoggleDrawer等方法在被嵌套的stack導航器傳遞給屏幕的navigation屬性中依然是可用的。但是如果stack導航器是drawer的父導航器,那么它里面的screen是不能訪問這些方法的,因為它沒有被嵌套在drawer導航器里。
同樣,如果一個tab導航器被嵌套在stack導航器中,那么tab導航器screen中的navigation屬性會新得到pushreplace這兩個方法。

如果你想從父導航器中分派動作給嵌套的子導航器,可以使用 navigation.dispatch
具體語句:navigation.dispatch(DrawerActions.toggleDrawer());

(5) 被嵌套的導航器不會響應父級導航器的事件

比如說,你有一個嵌套在tab導航器中的stack導航器,那么stack導航器的screen在用navigation.addListener綁定監(jiān)聽事件時,不能接收到由父tab導航器觸發(fā)出的事件,比如tabPress。為了能夠響應父導航器的事件,你可以用navigation.dangerouslyGetParent().addListener來顯式地監(jiān)聽父級導航器事件。

useEffect(() => {
  const unsubscribe = navigation
    .dangerouslyGetParent()
    .addListener('tabPress', (e) => {
      // ...
  });
  return unsubscribe;
}, [navigation]);
(6) 父級導航器的UI先于子導航器被渲染

例如,將stack導航器嵌套在drawer導航器內部時,你會看到drawer顯示在stack導航器標題的上方。但是如果將drawer導航器嵌套在stack導航器中,則drawer將出現(xiàn)在stack標題下方。這是在決定如何嵌套導航器時要考慮的一個要點。

在開發(fā)應用時,你可能會根據(jù)需求來選用下面這些模式:

  • 在根stack導航器的首屏嵌套tab導航器——當你通過push跳轉頁面的時候,新的頁面會覆蓋掉標簽欄。
  • 在drawer導航器的每個頁面嵌套stack導航器----即先渲染抽屜效果再渲染stack導航器的頭部
  • tab導航器的每個頁面都嵌套stack導航器----tab導航器的標簽欄仍然可見。常見的就是點擊tab將stack置頂。
    詳細官方說明:Nesting navigators
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容