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;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;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以更好地理解。
- 要定义导航器,我们使用
createXNavigator
函数(在此示例中为createNativeStackNavigator
),并将一个具有名为screens
的属性的对象传递给它,对象的key
为屏幕名称,value
为要渲染的组件:
const RootStack = createNativeStackNavigator({screens: {Home: HomeScreen,},});const RootStack = createNativeStackNavigator({ screens: { Home: HomeScreen, }, });const RootStack = createNativeStackNavigator({ screens: { Home: HomeScreen, }, });
等效的动态 API
const Stack = createNativeStackNavigator();function RootStack() {return (<Stack.Navigator><Stack.Screen name="Home" component={HomeScreen} /></Stack.Navigator>);}const Stack = createNativeStackNavigator(); function RootStack() { return ( <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} /> </Stack.Navigator> ); }const Stack = createNativeStackNavigator(); function RootStack() { return ( <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} /> </Stack.Navigator> ); }
- 定义导航器后,我们使用它与
createStaticNavigation
函数一起使用,以创建要呈现的组件:
const Navigation = createStaticNavigation(RootStack);function App() {return <Navigation />;}const Navigation = createStaticNavigation(RootStack); function App() { return <Navigation />; }const Navigation = createStaticNavigation(RootStack); function App() { return <Navigation />; }
等效的动态 API
function App() {return (<NavigationContainer><RootStack /></NavigationContainer>);}function App() { return ( <NavigationContainer> <RootStack /> </NavigationContainer> ); }function App() { return ( <NavigationContainer> <RootStack /> </NavigationContainer> ); }
createStaticNavigation
返回的组件类似于NavigationContainer
并接受相同的props。有关更多详细信息,请参见Static API Reference。
- 如果您使用TypeScript,则在使用
useNavigation
时需要进行自动类型检查的最后一步是:
type RootStackParamList = StaticParamList<typeof RootStack>;declare global {namespace ReactNavigation {interface RootParamList extends RootStackParamList {}}}type RootStackParamList = StaticParamList<typeof RootStack>; declare global { namespace ReactNavigation { interface RootParamList extends RootStackParamList {} } }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,},});const HomeTabs = createBottomTabNavigator({ screens: { Groups: GroupsScreen, Chats: ChatsScreen, }, }); const RootStack = createNativeStackNavigator({ screens: { Home: HomeTabs, }, });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>);}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> ); }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嵌套导航器时有几件事需要记住:
-
使用动态API时,导航器组件是常规组件,没有限制它的结构。只要在树中的某个地方呈现了导航器,它就可以用于嵌套导航。使用静态配置时,您必须传递另一个静态导航器返回的对象。
-
您可以使用使用动态API定义的组件作为静态API中屏幕的值。但是,自动链接配置和自动TypeScript类型不会为屏幕工作。
混合使用静态和动态API是可能的,但在这些情况下,您将失去静态API的好处。有关一些可以混合两个API的情况的更多详细信息,请参见组合静态和动态API。
限制
静态API是动态API的便利包装,而不是完全替代。它不适用于所有用例。在使用静态API时,重要的是要记住限制:
- 静态API的导航树是静态的,即配置无法动态更改(例如,根据外部数据更新屏幕或选项列表)。
- 静态配置无法访问上下文或 props,因此无法在静态配置中指定选项、侦听器等中使用它们。
动态API仍然是主要API,不会消失。因此,如果已经设置了类型检查和深度链接,则建议无需使用静态API重写应用程序。相反,考虑在新项目中使用静态API,其中您知道不需要动态地改变配置。
Api 参考
大部分静态配置都是使用createXNavigator
函数完成的,例如
等等。在本指南的其余部分中,我们将把这些函数称为createXNavigator
。
createXNavigator
createXNavigator
函数接受一个对象作为参数,该对象具有以下属性:
- 与导航器组件相同的属性,例如
id
、initialRouteName
、screenOptions
等。有关每个导航器接受的属性的更多详细信息,请参阅各自的文档。 screens
– 包含导航器中每个屏幕的配置的对象。groups
– 可选对象,包含屏幕组(与动态API中的Group
相当)。
例如:
const RootStack = createNativeStackNavigator({initialRouteName: 'Home',screenOptions: {headerTintColor: 'white',headerStyle: {backgroundColor: 'tomato',},},screens: {Home: HomeScreen,Profile: ProfileScreen,},});const RootStack = createNativeStackNavigator({ initialRouteName: 'Home', screenOptions: { headerTintColor: 'white', headerStyle: { backgroundColor: 'tomato', }, }, screens: { Home: HomeScreen, Profile: ProfileScreen, }, });const RootStack = createNativeStackNavigator({ initialRouteName: 'Home', screenOptions: { headerTintColor: 'white', headerStyle: { backgroundColor: 'tomato', }, }, screens: { Home: HomeScreen, Profile: ProfileScreen, }, });
screens
screens
对象可以包含键值对,其中键是屏幕的名称,值可以是以下内容之一:
- 要呈现的组件:
const RootStack = createNativeStackNavigator({screens: {Home: HomeScreen,},});const RootStack = createNativeStackNavigator({ screens: { Home: HomeScreen, }, });const RootStack = createNativeStackNavigator({ screens: { Home: HomeScreen, }, });
- 使用
createXNavigator
配置的嵌套导航器的导航器:
const HomeTabs = createBottomTabNavigator({screens: {Groups: GroupsScreen,Chats: ChatsScreen,},});const RootStack = createNativeStackNavigator({screens: {Home: HomeTabs,},});const HomeTabs = createBottomTabNavigator({ screens: { Groups: GroupsScreen, Chats: ChatsScreen, }, }); const RootStack = createNativeStackNavigator({ screens: { Home: HomeTabs, }, });const HomeTabs = createBottomTabNavigator({ screens: { Groups: GroupsScreen, Chats: ChatsScreen, }, }); const RootStack = createNativeStackNavigator({ screens: { Home: HomeTabs, }, });
- 包含屏幕配置的对象。此配置包含各种属性:
const RootStack = createNativeStackNavigator({screens: {Home: {screen: HomeScreen,linking: {path: 'home',},},},});const RootStack = createNativeStackNavigator({ screens: { Home: { screen: HomeScreen, linking: { path: 'home', }, }, }, });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: {// ...}}},});const RootStack = createNativeStackNavigator({ screens: { Home: HomeScreen, Profile: ProfileScreen, }, groups: { Guest: { if: useIsGuest, screenOptions: { headerShown: false, }, screens: { // ... } }, User: { if: useIsUser, screens: { // ... } } }, });const RootStack = createNativeStackNavigator({ screens: { Home: HomeScreen, Profile: ProfileScreen, }, groups: { Guest: { if: useIsGuest, screenOptions: { headerShown: false, }, screens: { // ... } }, User: { if: useIsUser, screens: { // ... } } }, });
groups
对象的键(例如Guest
、User
)用作组的navigationKey
– 这意味着如果一个屏幕在2个组中定义,并且组使用if
属性,则如果条件发生变化,导致一个组被删除并且使用其他组,则该屏幕将重新加载。您可以使用任何字符串作为键。
屏幕配置
屏幕的配置对象可以包含以下属性:
screen
要呈现屏幕的React组件或导航器:
const RootStack = createNativeStackNavigator({screens: {Home: {screen: HomeScreen,},},});const RootStack = createNativeStackNavigator({ screens: { Home: { screen: HomeScreen, }, }, });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',},},});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', }, }, });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
对象支持与配置链接中描述的相同的配置选项,例如parse
、stringify
和exact
。
为使本机应用程序上的深链接起作用,您还需要配置应用程序并将prefixes
传递给 createStaticNavigation
返回的导航组件:
const Navigation = createStaticNavigation(RootStack);const linking = {prefixes: ['https://example.com', 'example://'],};function App() {return <Navigation linking={linking} />;}const Navigation = createStaticNavigation(RootStack); const linking = { prefixes: ['https://example.com', 'example://'], }; function App() { return <Navigation linking={linking} />; }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,},},});const useIsLoggedIn = () => { const { isLoggedIn } = React.useContext(AuthContext); return isLoggedIn; }; const RootStack = createNativeStackNavigator({ screens: { Home: { screen: HomeScreen, if: useIsLoggedIn, }, }, });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',},},},});const RootStack = createNativeStackNavigator({ screens: { Home: { screen: HomeScreen, options: { title: 'Awesome app', }, }, }, });const RootStack = createNativeStackNavigator({ screens: { Home: { screen: HomeScreen, options: { title: 'Awesome app', }, }, }, });
当传递函数时,它将接收route
和navigation
:
const RootStack = createNativeStackNavigator({screens: {Profile: {screen: ProfileScreen,options: ({ route, navigation }) => ({title: route.params.userId,}),},},});const RootStack = createNativeStackNavigator({ screens: { Profile: { screen: ProfileScreen, options: ({ route, navigation }) => ({ title: route.params.userId, }), }, }, });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 },},},});const RootStack = createNativeStackNavigator({ screens: { Details: { screen: DetailsScreen, initialParams: { itemId: 5 }, }, }, });const RootStack = createNativeStackNavigator({ screens: { Details: { screen: DetailsScreen, initialParams: { itemId: 5 }, }, }, });
getId
回调以返回要用于屏幕的唯一ID。它接收具有路由参数的对象:
const RootStack = createNativeStackNavigator({screens: {Profile: {screen: ProfileScreen,getId: ({ params }) => params.userId,},},});const RootStack = createNativeStackNavigator({ screens: { Profile: { screen: ProfileScreen, getId: ({ params }) => params.userId, }, }, });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 actione.preventDefault();},},},},});const BottomTab = createBottomTabNavigator({ screens: { Chat: { screen: ChatScreen, listeners: { tabPress: (e) => { // Prevent default action e.preventDefault(); }, }, }, }, });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 />;}const Navigation = createStaticNavigation(RootStack); function App() { return <Navigation />; }const Navigation = createStaticNavigation(RootStack); function App() { return <Navigation />; }
此组件是NavigationContainer
组件的包装器,并接受与NavigationContainer
组件相同的 props
和 ref
。
但是有一个区别 – 此组件接受的linking
prop不接受config
属性。相反,链接配置会自动从静态配置推断出来。
这旨在类似于如何使用NavigationContainer
组件一样在应用程序的根部呈现。
createComponentForStaticNavigation
createComponentForStaticNavigation
函数接受由createXNavigator
函数返回的静态配置,并返回要呈现的React组件。第二个参数是React DevTools中将使用的组件名称:
const RootStackNavigator = createComponentForStaticNavigation(RootStack, 'RootNavigator');const RootStackNavigator = createComponentForStaticNavigation(RootStack, 'RootNavigator');const RootStackNavigator = createComponentForStaticNavigation(RootStack, 'RootNavigator');
返回的组件不接受任何props。所有配置都是从静态配置推断出来的。它本质上与使用动态API定义组件相同。
这看起来类似于createStaticNavigation
,但它们非常不同。在使用静态配置时,您永远不会直接使用此函数。您使用此函数的唯一时间是如果要迁移离开静态配置并希望重用您编写的现有代码而不是将其重写为动态API。有关详细信息,请参见将静态和动态API组合使用。
createPathConfigForStaticNavigation
createPathConfigForStaticNavigation
函数接受由createXNavigator
函数返回的静态配置,并返回可在链接配置中使用的路径配置对象。
const config = {screens: {Home: {screens: createPathConfigForStaticNavigation(HomeTabs),},},};const config = { screens: { Home: { screens: createPathConfigForStaticNavigation(HomeTabs), }, }, };const config = { screens: { Home: { screens: createPathConfigForStaticNavigation(HomeTabs), }, }, };
与createComponentForStaticNavigation
类似,这旨在在迁移离开静态配置时使用。有关详细信息,请参见将静态和动态API组合使用。
配置 TypeScript
使用静态API配置TypeScript有两个步骤:
- 每个屏幕组件需要指定它所接受的
route.params
属性的类型。StaticScreenProps
类型使其更简单:
import type { StaticScreenProps } from '@react-navigation/native';type Props = StaticScreenProps<{username: string;}>;function ProfileScreen({ route }: Props) {// ...}import type { StaticScreenProps } from '@react-navigation/native'; type Props = StaticScreenProps<{ username: string; }>; function ProfileScreen({ route }: Props) { // ... }import type { StaticScreenProps } from '@react-navigation/native'; type Props = StaticScreenProps<{ username: string; }>; function ProfileScreen({ route }: Props) { // ... }
- 生成根导航器的
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 {}}}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 {} } }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 BottomTabParamList = StaticParamList<typeof BottomTabNavigator>; type ProfileScreenNavigationProp = BottomTabNavigationProp< BottomTabParamList, 'Profile' >; // ... const navigation = useNavigation<ProfileScreenNavigationProp>();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,});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, });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>);}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> ); }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
)中加载一些身份验证状态。 - 当状态加载完成时,根据是否加载了有效的身份验证状态,向用户显示身份验证屏幕或主应用程序。
- 当用户退出时,我们清除身份验证状态并将其发送回身份验证屏幕。
注意:我们说“身份验证屏幕”,因为通常不止一个。您可能有一个主屏幕,带有用户名和密码字段,另一个用于“忘记密码”,以及另一组用于注册。
我们需要什么
这是我们想要的身份验证流的行为:当用户登录时,我们希望丢弃验证的状态并卸载与身份验证相关的所有屏幕,当我们按下硬件返回按钮时,我们希望无法返回到身份验证流。
它将如何工作
我们可以根据某些条件配置不同的屏幕。例如,如果用户已登录,我们可以定义Home
,Profile
,Settings
等。如果用户未登录,则可以定义SignIn
和SignUp
屏幕。为此,我们需要一些东西:
- 定义两个钩子:
useIsSignedIn
和useIsSignedOut
,它们返回一个布尔值,指示用户是否已登录。 - 使用
useIsSignedIn
和useIsSignedOut
以及if
属性根据条件定义可用的屏幕。
这告诉React Navigation根据已登录状态显示特定的屏幕。当已登录状态更改时,React Navigation将自动显示适当的屏幕。
当使用if
进行条件屏幕时,请勿手动导航
重要的是要注意,在使用这种设置时,您不要手动导航到Home
屏幕,例如通过调用navigation.navigate('Home')
或任何其他方法。当isSignedIn
更改时,React Navigation将自动导航到正确的屏幕 – 当isSignedIn
变为true
时,导航到Home
屏幕,当isSignedIn
变为false
时,导航到SignIn
屏幕。如果您尝试手动导航,则会收到错误。
定义钩子
要实现useIsSignedIn
和useIsSignedOut
钩子,我们可以从创建一个上下文来存储身份验证状态开始。让我们称之为SignInContext
:
import * as React from 'react';const SignInContext = React.createContext();import * as React from 'react'; const SignInContext = React.createContext();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;}function useIsSignedIn() { const isSignedIn = React.useContext(SignInContext); return isSignedIn; } function useIsSignedOut() { const isSignedIn = React.useContext(SignInContext); return !isSignedIn; }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);const RootStack = createNativeStackNavigator({ screens: { Home: { if: useIsSignedIn, screen: HomeScreen, }, SignIn: { if: useIsSignedOut, screen: SignInScreen, options: { title: 'Sign in', }, }, }, }); const Navigation = createStaticNavigation(RootStack);const RootStack = createNativeStackNavigator({ screens: { Home: { if: useIsSignedIn, screen: HomeScreen, }, SignIn: { if: useIsSignedOut, screen: SignInScreen, options: { title: 'Sign in', }, }, }, }); const Navigation = createStaticNavigation(RootStack);
请注意,此处仅定义了Home
和SignIn
屏幕,而未定义SplashScreen
。SplashScreen
应该在我们渲染任何导航器之前呈现,以便在了解用户是否已登录之前不会呈现不正确的屏幕。
在组件中使用时,它看起来像这样:
if (state.isLoading) {// We haven't finished checking for the token yetreturn <SplashScreen />;}const isSignedIn = state.userToken != null;return (<SignInContext.Provider value={isSignedIn}><Navigation /></SignInContext.Provider>);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> );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
公开登录状态,以便useIsSignedIn
和useIsSignedOut
钩子可以访问它。
在上面的示例中,我们为每种情况都有一个屏幕。但是,您也可以定义多个屏幕。例如,当用户未登录时,您可能还要定义密码重置、注册等屏幕。同样,对于登录后可访问的屏幕,您可能有多个屏幕。我们可以使用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,},},},});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, }, }, }, });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.useReducer
和React.useContext
。但是,如果您使用的是Redux或Mobx等状态管理库,则可以使用它们来代替此功能。实际上,在更大的应用程序中,全局状态管理库更适合存储身份验证令牌。您可以将相同的方法适应于您的状态管理库。
首先,我们需要创建一个上下文来公开必要的方法:
import * as React from 'react';const AuthContext = React.createContext();import * as React from 'react'; const AuthContext = React.createContext();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 placeconst 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 tokendispatch({ 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 tokendispatch({ 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>);}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> ); }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><TextInputplaceholder="Username"value={username}onChangeText={setUsername}/><TextInputplaceholder="Password"value={password}onChangeText={setPassword}secureTextEntry/><Button title="Sign in" onPress={() => signIn({ username, password })} /></View>);}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> ); }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> ); }
您可以根据需要类似地填充其他屏幕。