likes
comments
collection
share

react navigation7.x 中文文档青春极速mini版-Static configuration

作者站长头像
站长
· 阅读数 77

React Navigation主要使用基于动态组件的API。这提供了很大的灵活性,但也有一些缺点:

  • TypeScript类型需要手动配置,可能会变得冗长和令人厌烦。
  • Deep Linking 需要单独配置以匹配导航树的结构,可能会出现错误。
  • 组件 API 可能比必要的更冗长。

为了解决这些缺点,还有一个静态API来配置导航树,以灵活性换取便利性。这个API内置在React Navigation中,因此您不需要安装任何其他软件包。

基本用法

静态API与动态API使用相同的原则。我们有可以包含多个屏幕的导航器。


import * as React from 'react';
import { View, Text } from 'react-native';
import { createStaticNavigation } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

function HomeScreen() {
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Text>Home Screen</Text>
    </View>
  );
}

const RootStack = createNativeStackNavigator({
  screens: {
    Home: HomeScreen,
  },
});

const Navigation = createStaticNavigation(RootStack);

function App() {
  return <Navigation />;
}

export default App;

让我们详细看一下上面的代码。您还可以检查等效的动态API以更好地理解。

  1. 要定义导航器,我们使用createXNavigator函数(在此示例中为createNativeStackNavigator),并将一个具有名为screens的属性的对象传递给它,对象的 key 为屏幕名称,value 为要渲染的组件:
const RootStack = createNativeStackNavigator({
  screens: {
    Home: HomeScreen,
  },
});

等效的动态 API

const Stack = createNativeStackNavigator();

function RootStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
    </Stack.Navigator>
  );
}
  1. 定义导航器后,我们使用它与createStaticNavigation函数一起使用,以创建要呈现的组件:
const Navigation = createStaticNavigation(RootStack);

function App() {
  return <Navigation />;
}

等效的动态 API

function App() {
  return (
    <NavigationContainer>
      <RootStack />
    </NavigationContainer>
  );
}

createStaticNavigation返回的组件类似于NavigationContainer并接受相同的props。有关更多详细信息,请参见Static API Reference

  1. 如果您使用TypeScript,则在使用useNavigation时需要进行自动类型检查的最后一步是:
type RootStackParamList = StaticParamList<typeof RootStack>;

declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootStackParamList {}
  }
}

有关更多详细信息,请参见配置TypeScript

有关静态API的更多详细信息,请参见Static API Reference

嵌套导航器

要嵌套导航器,可以将使用静态API定义的导航器作为 screen 传递:

const HomeTabs = createBottomTabNavigator({
  screens: {
    Groups: GroupsScreen,
    Chats: ChatsScreen,
  },
});

const RootStack = createNativeStackNavigator({
  screens: {
    Home: HomeTabs,
  },
});

等效的动态 API

const Tab = createBottomTabNavigator();

function HomeTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Groups" component={GroupsScreen} />
      <Tab.Screen name="Chats" component={ChatsScreen} />
    </Tab.Navigator>
  );
}

const Stack = createNativeStackNavigator();

function RootStack() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeTabs} />
    </Stack.Navigator>
  );
}

在使用静态API嵌套导航器时有几件事需要记住:

  1. 使用动态API时,导航器组件是常规组件,没有限制它的结构。只要在树中的某个地方呈现了导航器,它就可以用于嵌套导航。使用静态配置时,您必须传递另一个静态导航器返回的对象。

  2. 您可以使用使用动态API定义的组件作为静态API中屏幕的值。但是,自动链接配置和自动TypeScript类型不会为屏幕工作。

混合使用静态和动态API是可能的,但在这些情况下,您将失去静态API的好处。有关一些可以混合两个API的情况的更多详细信息,请参见组合静态和动态API

限制

静态API是动态API的便利包装,而不是完全替代。它不适用于所有用例。在使用静态API时,重要的是要记住限制:

  • 静态API的导航树是静态的,即配置无法动态更改(例如,根据外部数据更新屏幕或选项列表)。
  • 静态配置无法访问上下文或 props,因此无法在静态配置中指定选项、侦听器等中使用它们。

动态API仍然是主要API,不会消失。因此,如果已经设置了类型检查和深度链接,则建议无需使用静态API重写应用程序。相反,考虑在新项目中使用静态API,其中您知道不需要动态地改变配置。

Api 参考

大部分静态配置都是使用createXNavigator函数完成的,例如

等等。在本指南的其余部分中,我们将把这些函数称为createXNavigator

createXNavigator

createXNavigator函数接受一个对象作为参数,该对象具有以下属性:

  • 与导航器组件相同的属性,例如idinitialRouteNamescreenOptions等。有关每个导航器接受的属性的更多详细信息,请参阅各自的文档。
  • screens - 包含导航器中每个屏幕的配置的对象。
  • groups - 可选对象,包含屏幕组(与动态API中的Group相当)。

例如:

const RootStack = createNativeStackNavigator({
  initialRouteName: 'Home',
  screenOptions: {
    headerTintColor: 'white',
    headerStyle: {
      backgroundColor: 'tomato',
    },
  },
  screens: {
    Home: HomeScreen,
    Profile: ProfileScreen,
  },
});

screens

screens对象可以包含键值对,其中键是屏幕的名称,值可以是以下内容之一:

  • 要呈现的组件:
const RootStack = createNativeStackNavigator({
  screens: {
    Home: HomeScreen,
  },
});
  • 使用createXNavigator配置的嵌套导航器的导航器:
const HomeTabs = createBottomTabNavigator({
  screens: {
    Groups: GroupsScreen,
    Chats: ChatsScreen,
  },
});

const RootStack = createNativeStackNavigator({
  screens: {
    Home: HomeTabs,
  },
});
  • 包含屏幕配置的对象。此配置包含各种属性:
const RootStack = createNativeStackNavigator({
  screens: {
    Home: {
      screen: HomeScreen,
      linking: {
        path: 'home',
      },
    },
  },
});

有关更多详细信息,请参见屏幕配置

groups

groups对象可以包含键值对,其中键是组的名称,值是组配置。组配置可以包含以下属性:

  • if - 可以用于有条件地呈现组,并与屏幕配置中的if属性相同。
  • screenOptions - 适用于该组下所有屏幕的默认选项。
  • screens - 包含组中每个屏幕的配置的对象。配置与导航器配置中的screens对象相同。

例如:

const RootStack = createNativeStackNavigator({
  screens: {
    Home: HomeScreen,
    Profile: ProfileScreen,
  },
  groups: {
    Guest: {
      if: useIsGuest,
      screenOptions: {
        headerShown: false,
      },
      screens: {
        // ...
      }
    },
    User: {
      if: useIsUser,
      screens: {
        // ...
      }
    }
  },
});

groups对象的键(例如GuestUser)用作组的navigationKey - 这意味着如果一个屏幕在2个组中定义,并且组使用if属性,则如果条件发生变化,导致一个组被删除并且使用其他组,则该屏幕将重新加载。您可以使用任何字符串作为键。

屏幕配置

屏幕的配置对象可以包含以下属性:

screen

要呈现屏幕的React组件或导航器:

const RootStack = createNativeStackNavigator({
  screens: {
    Home: {
      screen: HomeScreen,
    },
  },
});

使用静态配置定义的屏幕组件接收route prop。与动态API不同,必须通过useNavigation钩子访问navigation对象。

linking

屏幕的链接配置。它可以是路径的字符串,也可以是具有链接配置的对象:

const RootStack = createNativeStackNavigator({
  screens: {
    Profile: {
      screen: ProfileScreen,
      linking: {
        path: 'u/:userId',
        parse: {
          userId: (id) => id.replace(/^@/, ''),
        },
        stringify: {
          userId: (id) => `@${id}`,
        },
      },
    },
    Chat: {
      screen: ChatScreen,
      linking: 'chat/:chatId',
    },
  },
});

linking对象支持与配置链接中描述的相同的配置选项,例如parsestringifyexact

为使本机应用程序上的深链接起作用,您还需要配置应用程序并将prefixes传递给 createStaticNavigation 返回的导航组件:

const Navigation = createStaticNavigation(RootStack);

const linking = {
  prefixes: ['https://example.com', 'example://'],
};

function App() {
  return <Navigation linking={linking} />;
}

if

用于确定是否应呈现屏幕的回调。它不接收任何参数。这对于有条件地呈现屏幕很有用,例如 - 如果您想为已登录的用户呈现不同的屏幕。

您可以使用自定义钩子使用自定义逻辑确定返回值:

const useIsLoggedIn = () => {
  const { isLoggedIn } = React.useContext(AuthContext);

  return isLoggedIn;
};

const RootStack = createNativeStackNavigator({
  screens: {
    Home: {
      screen: HomeScreen,
      if: useIsLoggedIn,
    },
  },
});

上面的示例仅在用户登录时呈现HomeScreen

有关详细信息,请参见使用静态API进行身份验证流程

options

配置屏幕在导航器中如何呈现的选项。它接受一个对象或返回对象的函数:

const RootStack = createNativeStackNavigator({
  screens: {
    Home: {
      screen: HomeScreen,
      options: {
        title: 'Awesome app',
      },
    },
  },
});

当传递函数时,它将接收routenavigation

const RootStack = createNativeStackNavigator({
  screens: {
    Profile: {
      screen: ProfileScreen,
      options: ({ route, navigation }) => ({
        title: route.params.userId,
      }),
    },
  },
});

有关详细信息和示例,请参见屏幕选项

initialParams

要用于屏幕的初始参数。如果屏幕用作initialRouteName,则它将包含来自initialParams的参数。如果您导航到新屏幕,则传递的参数将与初始参数进行浅合并。

const RootStack = createNativeStackNavigator({
  screens: {
    Details: {
      screen: DetailsScreen,
      initialParams: { itemId: 5 },
    },
  },
});

getId

回调以返回要用于屏幕的唯一ID。它接收具有路由参数的对象:

const RootStack = createNativeStackNavigator({
  screens: {
    Profile: {
      screen: ProfileScreen,
      getId: ({ params }) => params.userId,
    },
  },
});

默认情况下,调用navigate('ScreenName', params)使用其名称标识屏幕,并导航到现有屏幕,而不是添加新屏幕。如果指定了getId并且它不返回undefined,则屏幕由屏幕名称和返回的ID标识。

这对于防止导航器中多个相同屏幕的实例很有用,例如 - 当params.userId用作ID时,后续导航到具有相同userId的屏幕将导航到现有屏幕,而不是将新屏幕添加到堆栈中。如果导航使用不同的userId,则它将添加新屏幕。

listeners

要订阅的事件侦听器。它采用具有事件名称作为键和侦听器回调作为值的对象:

const BottomTab = createBottomTabNavigator({
  screens: {
    Chat: {
      screen: ChatScreen,
      listeners: {
        tabPress: (e) => {
          // Prevent default action
          e.preventDefault();
        },
      },
    },
  },
});

createStaticNavigation

createStaticNavigation函数接受由createXNavigator函数返回的静态配置,并返回一个要呈现的React组件:

const Navigation = createStaticNavigation(RootStack);

function App() {
  return <Navigation />;
}

此组件是NavigationContainer组件的包装器,并接受与NavigationContainer组件相同的 propsref

但是有一个区别 - 此组件接受的linking prop不接受config属性。相反,链接配置会自动从静态配置推断出来。

这旨在类似于如何使用NavigationContainer组件一样在应用程序的根部呈现。

createComponentForStaticNavigation

createComponentForStaticNavigation函数接受由createXNavigator函数返回的静态配置,并返回要呈现的React组件。第二个参数是React DevTools中将使用的组件名称:

const RootStackNavigator = createComponentForStaticNavigation(RootStack, 'RootNavigator');

返回的组件不接受任何props。所有配置都是从静态配置推断出来的。它本质上与使用动态API定义组件相同。

这看起来类似于createStaticNavigation,但它们非常不同。在使用静态配置时,您永远不会直接使用此函数。您使用此函数的唯一时间是如果要迁移离开静态配置并希望重用您编写的现有代码而不是将其重写为动态API。有关详细信息,请参见将静态和动态API组合使用

createPathConfigForStaticNavigation

createPathConfigForStaticNavigation函数接受由createXNavigator函数返回的静态配置,并返回可在链接配置中使用的路径配置对象。

const config = {
  screens: {
    Home: {
      screens: createPathConfigForStaticNavigation(HomeTabs),
    },
  },
};

createComponentForStaticNavigation类似,这旨在在迁移离开静态配置时使用。有关详细信息,请参见将静态和动态API组合使用

配置 TypeScript

使用静态API配置TypeScript有两个步骤:

  1. 每个屏幕组件需要指定它所接受的route.params属性的类型。StaticScreenProps类型使其更简单:
import type { StaticScreenProps } from '@react-navigation/native';

type Props = StaticScreenProps<{
  username: string;
}>;

function ProfileScreen({ route }: Props) {
  // ...
}
  1. 生成根导航器的ParamList类型,并将其指定为RootParamList类型的默认类型:
import type { StaticParamList } from '@react-navigation/native';

const HomeTabs = createBottomTabNavigator({
  screens: {
    Feed: FeedScreen,
    Profile: ProfileScreen,
  },
});

const RootStack = createNativeStackNavigator({
  screens: {
    Home: HomeTabs,
  },
});

type RootStackParamList = StaticParamList<typeof RootStack>;

declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootStackParamList {}
  }
}

这是为了对useNavigation钩子进行类型检查。

导航器特定类型

通常,我们建议使用默认类型来访问导航对象,以实现与导航器无关的方式。但是,如果您需要使用特定于导航器的API,则需要手动注释useNavigation

type BottomTabParamList = StaticParamList<typeof BottomTabNavigator>;
type ProfileScreenNavigationProp = BottomTabNavigationProp<
  BottomTabParamList,
  'Profile'
>;

// ...

const navigation = useNavigation<ProfileScreenNavigationProp>();

这遵循与Type checking with TypeScript中描述的类型相同的原则。

请注意,以这种方式注释useNavigation不是类型安全的,因为我们无法保证您提供的类型与导航器的类型匹配。

使用动态API嵌套导航器

考虑以下示例:

const Tab = createBottomTabNavigator();

function HomeTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Feed" component={FeedScreen} />
      <Tab.Screen name="Profile" component={ProfileScreen} />
    </Tab.Navigator>
  );
}

const RootStack = createStackNavigator({
  Home: HomeTabs,
});

在这里,HomeTabs组件是使用动态API定义的。这意味着当我们使用StaticParamList<typeof RootStack>创建根导航器的参数列表时,它不会知道嵌套导航器中定义的屏幕。为了解决这个问题,我们需要显式指定嵌套导航器的参数列表。

这可以通过使用屏幕组件接收的route属性的类型来完成:

type HomeTabsParamList = {
  Feed: undefined;
  Profile: undefined;
};

type HomeTabsProps = StaticScreenProps<
  NavigatorScreenParams<HomeTabsParamList>
>;

function HomeTabs(_: HomeTabsProps) {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Feed" component={FeedScreen} />
      <Tab.Screen name="Profile" component={ProfileScreen} />
    </Tab.Navigator>
  );
}

现在,使用StaticParamList<typeof RootStack>时,它将包括在嵌套导航器中定义的屏幕。

身份验证

大多数应用程序需要用户以某种方式进行身份验证,以访问与用户相关的数据或其他私有内容。通常流程如下:

  • 用户打开应用程序。
  • 应用程序从加密的持久存储(例如SecureStore)中加载一些身份验证状态。
  • 当状态加载完成时,根据是否加载了有效的身份验证状态,向用户显示身份验证屏幕或主应用程序。
  • 当用户退出时,我们清除身份验证状态并将其发送回身份验证屏幕。

注意:我们说“身份验证屏幕”,因为通常不止一个。您可能有一个主屏幕,带有用户名和密码字段,另一个用于“忘记密码”,以及另一组用于注册。

我们需要什么

这是我们想要的身份验证流的行为:当用户登录时,我们希望丢弃验证的状态并卸载与身份验证相关的所有屏幕,当我们按下硬件返回按钮时,我们希望无法返回到身份验证流。

它将如何工作

我们可以根据某些条件配置不同的屏幕。例如,如果用户已登录,我们可以定义HomeProfileSettings等。如果用户未登录,则可以定义SignInSignUp屏幕。为此,我们需要一些东西:

  1. 定义两个钩子:useIsSignedInuseIsSignedOut,它们返回一个布尔值,指示用户是否已登录。
  2. 使用useIsSignedInuseIsSignedOut以及if属性根据条件定义可用的屏幕。

这告诉React Navigation根据已登录状态显示特定的屏幕。当已登录状态更改时,React Navigation将自动显示适当的屏幕。

当使用if进行条件屏幕时,请勿手动导航

重要的是要注意,在使用这种设置时,您不要手动导航Home屏幕,例如通过调用navigation.navigate('Home')或任何其他方法。当isSignedIn更改时,React Navigation将自动导航到正确的屏幕 - 当isSignedIn变为true时,导航到Home屏幕,当isSignedIn变为false时,导航到SignIn屏幕。如果您尝试手动导航,则会收到错误。

定义钩子

要实现useIsSignedInuseIsSignedOut钩子,我们可以从创建一个上下文来存储身份验证状态开始。让我们称之为SignInContext

import * as React from 'react';

const SignInContext = React.createContext();

稍后我们将讨论如何公开上下文值。

function useIsSignedIn() {
  const isSignedIn = React.useContext(SignInContext);
  return isSignedIn;
}

function useIsSignedOut() {
  const isSignedIn = React.useContext(SignInContext);
  return !isSignedIn;
}

定义我们的屏幕

对于我们的情况,假设我们有3个屏幕:

  • SplashScreen - 当我们恢复令牌时,这将显示闪屏或加载屏幕。
  • SignIn - 这是我们在用户尚未登录时显示的屏幕(我们找不到令牌)。
  • Home - 这是我们在用户已登录时显示的屏幕。

我们将使用以下方式定义我们的导航树:

const RootStack = createNativeStackNavigator({
  screens: {
    Home: {
      if: useIsSignedIn,
      screen: HomeScreen,
    },
    SignIn: {
      if: useIsSignedOut,
      screen: SignInScreen,
      options: {
        title: 'Sign in',
      },
    },
  },
});

const Navigation = createStaticNavigation(RootStack);

请注意,此处仅定义了HomeSignIn屏幕,而未定义SplashScreenSplashScreen应该在我们渲染任何导航器之前呈现,以便在了解用户是否已登录之前不会呈现不正确的屏幕。

在组件中使用时,它看起来像这样:

if (state.isLoading) {
  // We haven't finished checking for the token yet
  return <SplashScreen />;
}

const isSignedIn = state.userToken != null;

return (
  <SignInContext.Provider value={isSignedIn}>
    <Navigation />
  </SignInContext.Provider>
);

在上面的片段中,isLoading表示我们仍在检查是否有令牌。通常可以通过检查SecureStore中是否有令牌并验证令牌来完成此操作。在获取令牌并验证其有效性后,我们需要设置userToken。我们还有另一个状态称为isSignout,以便在注销时有不同的动画。

接下来,我们通过SignInContext公开登录状态,以便useIsSignedInuseIsSignedOut钩子可以访问它。

在上面的示例中,我们为每种情况都有一个屏幕。但是,您也可以定义多个屏幕。例如,当用户未登录时,您可能还要定义密码重置、注册等屏幕。同样,对于登录后可访问的屏幕,您可能有多个屏幕。我们可以使用groups定义多个屏幕:

const RootStack = createNativeStackNavigator({
  screens: {
    // Common screens
  },
  groups: {
    SignedIn: {
      if: useIsSignedIn,
      screens: {
        Home: HomeScreen,
        Profile: ProfileScreen,
      },
    },
    SignedOut: {
      if: useIsSignedOut,
      screens: {
        SignIn: SignInScreen,
        SignUp: SignUpScreen,
        ResetPassword: ResetPasswordScreen,
      },
    },
  },
});

如果您的登录相关屏幕和其他屏幕在堆栈导航器中,我们建议使用单个堆栈导航器并将条件放在其中,而不是使用2个不同的导航器。这使得在登录/注销期间可以拥有适当的转换动画。

实现恢复令牌的逻辑

注意:以下仅是您可能在应用程序中实现身份验证逻辑的示例。您不需要完全按照此示例进行操作。

从前面的片段中,我们可以看到我们需要2个状态变量:

  • isLoading - 当我们尝试检查是否已在SecureStore中保存令牌时,我们将其设置为true
  • userToken - 用户的令牌。如果它非空,则我们假设用户已登录,否则未登录。

所以我们需要:

  • 添加一些逻辑以恢复令牌、登录和注销
  • 向其他组件公开登录和注销方法

在本指南中,我们将使用React.useReducerReact.useContext。但是,如果您使用的是Redux或Mobx等状态管理库,则可以使用它们来代替此功能。实际上,在更大的应用程序中,全局状态管理库更适合存储身份验证令牌。您可以将相同的方法适应于您的状态管理库。

首先,我们需要创建一个上下文来公开必要的方法:

import * as React from 'react';

const AuthContext = React.createContext();

在我们的组件中,我们将:

  • useReducer中存储令牌和加载状态
  • 在应用程序启动时将其保存到SecureStore并从中读取
  • 使用AuthContext将登录和注销方法公开给子组件

因此,我们将拥有以下内容:

import * as React from 'react';
import * as SecureStore from 'expo-secure-store';

export default function App({ navigation }) {
  const [state, dispatch] = React.useReducer(
    (prevState, action) => {
      switch (action.type) {
        case 'RESTORE_TOKEN':
          return {
            ...prevState,
            userToken: action.token,
            isLoading: false,
          };
        case 'SIGN_IN':
          return {
            ...prevState,
            userToken: action.token,
          };
        case 'SIGN_OUT':
          return {
            ...prevState,
            userToken: null,
          };
      }
    },
    {
      isLoading: true,
      userToken: null,
    }
  );

  React.useEffect(() => {
    // Fetch the token from storage then navigate to our appropriate place
    const bootstrapAsync = async () => {
      let userToken;

      try {
        userToken = await SecureStore.getItemAsync('userToken');
      } catch (e) {
        // Restoring token failed
      }

      // After restoring token, we may need to validate it in production apps
      // ...

      // This will switch to the App screen or Auth screen and this loading
      // screen will be unmounted and thrown away.
      dispatch({ type: 'RESTORE_TOKEN', token: userToken });
    };

    bootstrapAsync();
  }, []);

  const authContext = React.useMemo(
    () => ({
      signIn: async (data) => {
        // In a production app, we need to send some data (usually username, password) to server and get a token
        // We will also need to handle errors if sign in failed
        // After getting token, we need to persist the token using `SecureStore`
        // In the example, we'll use a dummy token

        dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
      },
      signOut: () => dispatch({ type: 'SIGN_OUT' }),
      signUp: async (data) => {
        // In a production app, we need to send user data to server and get a token
        // We will also need to handle errors if sign up failed
        // After getting token, we need to persist the token using `SecureStore`
        // In the example, we'll use a dummy token

        dispatch({ type: 'SIGN_IN', token: 'dummy-auth-token' });
      },
    }),
    []
  );

  if (state.isLoading) {
    return <SplashScreen />;
  }

  return (
    <AuthContext.Provider value={authContext}>
      <SignInContext.Provider value={isSignedIn}>
        <Navigation />
      </SignInContext.Provider>
    </AuthContext.Provider>
  );
}

填充其他组件

我们将不讨论如何为身份验证屏幕实现文本输入和按钮,这超出了导航的范围。我们只会填充一些占位符内容。

function SignInScreen() {
  const [username, setUsername] = React.useState('');
  const [password, setPassword] = React.useState('');

  const { signIn } = React.useContext(AuthContext);

  return (
    <View>
      <TextInput
        placeholder="Username"
        value={username}
        onChangeText={setUsername}
      />
      <TextInput
        placeholder="Password"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <Button title="Sign in" onPress={() => signIn({ username, password })} />
    </View>
  );
}

您可以根据需要类似地填充其他屏幕。

转载自:https://juejin.cn/post/7244788137409740855
评论
请登录