文章示例源码: https://github.com/youngjuning/react-navigation-best-practice
安装依赖 1 $ yarn add @react-navigation/native @react-navigation/stack @react-navigation/bottom-tabs react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view
配置 为了完成 react-native-screens
的安装,添加下面两行代码到 android/app/build.gradle
文件的 dependencies
部分中:
1 2 implementation 'androidx.appcompat:appcompat:1.1.0-rc01' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02'
为了完成 react-native-gesture-handler
的安装, 在入口文件的顶部添加下面的代码, 比如 index.js
或 App.js
:
1 import 'react-native-gesture-handler' ;
现在,我们需要把整个 App用 NavigationContainer
包裹:
1 2 3 4 5 6 7 8 9 10 11 12 import React from 'react' ;import { NavigationContainer } from '@react-navigation/native' ;const App = ( ) => { return ( <NavigationContainer > {/* Rest of your app code */} </NavigationContainer > ); }; export default App ;
App.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 import React from 'react' ;import { View , Text , StyleSheet , SafeAreaView , StatusBar , BackHandler , } from 'react-native' ; import {NavigationContainer , useFocusEffect} from '@react-navigation/native' ;import {createBottomTabNavigator} from '@react-navigation/bottom-tabs' ;import {createStackNavigator, HeaderBackButton } from '@react-navigation/stack' ;import {IconOutline } from '@ant-design/icons-react-native' ;import {Button } from '@ant-design/react-native' ;import IconWithBadge from './IconWithBadge' ;import HeaderButtons from './HeaderButtons' ;import getActiveRouteName from './getActiveRouteName' ;import getScreenOptions from './getScreenOptions' ;import {navigationRef} from './NavigationService' ;const HomeScreen = ({navigation, route} ) => { navigation.setOptions ({ headerLeft : props => ( <HeaderBackButton {...props } onPress ={() => { console.log('不能再返回了!'); }} /> ), headerRight : () => ( <HeaderButtons > {/* title、iconName、onPress、IconComponent、iconSize、color */} <HeaderButtons.Item title ="添加" iconName ="plus" onPress ={() => console.log('点击了添加按钮')} iconSize={24} color="#ffffff" /> </HeaderButtons > ), }); useFocusEffect ( React .useCallback (() => { return () => { }; }, []), ); const {author} = route.params || {}; return ( <> <StatusBar barStyle ="dark-content" /> <View style ={styles.container} > <Text > Home Screen</Text > <Text > {author}</Text > <Button type ="warning" // 使用 setOptions 更新标题 onPress ={() => navigation.setOptions({headerTitle: 'Updated!'})}> Update the title </Button > <Button type ="primary" onPress ={() => // 跳转到指定页面,并传递两个参数 navigation.navigate('DetailsScreen', { otherParam: 'anything you want here', }) }> Go to DetailsScreen </Button > <Button type ="warning" onPress ={() => navigation.navigate('SafeAreaViewScreen')}> Go SafeAreaViewScreen </Button > <Button type ="primary" onPress ={() => navigation.navigate('CustomAndroidBackButtonBehaviorScreen') }> Go CustomAndroidBackButtonBehavior </Button > </View > </> ); }; const DetailsScreen = ({navigation, route} ) => { const {itemId, otherParam} = route.params ; return ( <View style ={styles.container} > <Text > Details Screen</Text > <Text > itemId: {itemId}</Text > <Text > otherParam: {otherParam}</Text > <Button type ="primary" // 返回上一页 onPress ={() => navigation.goBack()}> Go back </Button > <Button type ="primary" // 如果返回上一个页面需要传递参数 ,请使用 navigate 方法 onPress ={() => navigation.navigate('HomeScreen', {author: '紫升'})}> Go back with Params </Button > </View > ); }; const SettingsScreen = ({navigation, route} ) => { return ( <SafeAreaView style ={{flex: 1 , justifyContent: 'space-between ', alignItems: 'center '}}> <Text > This is top text.</Text > <Text > This is bottom text.</Text > </SafeAreaView > ); }; const SafeAreaViewScreen = ( ) => { return ( <SafeAreaView style ={{flex: 1 , justifyContent: 'space-between ', alignItems: 'center '}}> <Text > This is top text.</Text > <Text > This is bottom text.</Text > </SafeAreaView > ); }; const CustomAndroidBackButtonBehaviorScreen = ({navigation, route} ) => { useFocusEffect ( React .useCallback (() => { const onBackPress = ( ) => { alert ('物理返回键被拦截了!' ); return true ; }; BackHandler .addEventListener ('hardwareBackPress' , onBackPress); return () => BackHandler .removeEventListener ('hardwareBackPress' , onBackPress); }, []), ); return ( <View style ={styles.container} > <Text > AndroidBackHandlerScreen</Text > </View > ); }; const Stack = createStackNavigator ();const BottomTab = createBottomTabNavigator ();const BottomTabScreen = ( ) => ( <BottomTab.Navigator screenOptions ={({route}) => ({ tabBarIcon: ({focused, color, size}) => { let iconName; if (route.name === 'HomeScreen') { iconName = focused ? 'apple' : 'apple'; return ( <IconWithBadge badgeCount ={90} > <IconOutline name ={iconName} size ={size} color ={color} /> </IconWithBadge > ); } else if (route.name === 'SettingsScreen') { iconName = focused ? 'twitter' : 'twitter'; } return <IconOutline name ={iconName} size ={size} color ={color} /> ; }, })} tabBarOptions={{ activeTintColor: 'tomato', inactiveTintColor: 'gray', }}> <Stack.Screen name ="HomeScreen" component ={HomeScreen} options ={{tabBarLabel: '首页 '}} /> <Stack.Screen name ="SettingsScreen" component ={SettingsScreen} options ={{tabBarLabel: '设置 '}} /> </BottomTab.Navigator > ); const App = ( ) => { const routeNameRef = React .useRef (); return ( <> <NavigationContainer ref ={navigationRef} onStateChange ={state => { const previousRouteName = routeNameRef.current; const currentRouteName = getActiveRouteName(state); if (previousRouteName !== currentRouteName) { console.log('[onStateChange]', currentRouteName); if (currentRouteName === 'HomeScreen') { StatusBar.setBarStyle('dark-content'); // 修改 StatusBar } else { StatusBar.setBarStyle('dark-content'); // 修改 StatusBar } } // Save the current route name for later comparision routeNameRef.current = currentRouteName; }}> <Stack.Navigator initialRouteName ="HomeScreen" // 页面共享的配置 screenOptions ={getScreenOptions()} > <Stack.Screen name ="BottomTabScreen" component ={BottomTabScreen} options ={{headerShown: false }} /> <Stack.Screen name ="DetailsScreen" component ={DetailsScreen} options ={{headerTitle: '详情 '}} // headerTitle 用来设置标题栏 initialParams ={{itemId: 42 }} // 默认参数 /> <Stack.Screen name ="SafeAreaViewScreen" component ={SafeAreaViewScreen} options ={{headerTitle: 'SafeAreaView '}} /> <Stack.Screen name ="CustomAndroidBackButtonBehaviorScreen" component ={CustomAndroidBackButtonBehaviorScreen} options ={{headerTitle: '拦截安卓物理返回键 '}} /> </Stack.Navigator > </NavigationContainer > </> ); }; const styles = StyleSheet .create ({ container : { flex : 1 , alignItems : 'center' , justifyContent : 'center' , }, }); export default App ;
路由名称的大小写无关紧要 – 你可以使用小写字母home
或大写字母Home
,这取决于你的喜好。 我们更喜欢将路由名称大写。 我们更喜欢利用我们的路由名称。
跳转方法有 navigate
、 push
、goBack
、popToTop
可以用 navigation.setParams
方法更新页面的参数
我们可以通过 options={({ route, navigation }) => ({ headerTitle: route.params.name })}
的方式在标题中使用参数
我们可以用 navigation.setOptions
更新页面配置
Stack.Navigator
initialRouteName
: 用来配置 Stack.Navigator
的初始路由screenOptions
: 页面共享配置对象Stack.Screen
name
: 页面名component
: 页面对应组件options
: 页面配置对象initialParams
: 默认参数使用 react-navigation-header-buttons
组件搭配任意 Icon 组件可以自定义自己的 Header Button 组件,我这里为了演示方便,使用了 @ant-design/icons-react-native
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import React from 'react' ;import { HeaderButtons as RNHeaderButtons , HeaderButton as RNHeaderButton , Item , } from 'react-navigation-header-buttons' ; import {IconOutline } from '@ant-design/icons-react-native' ;const HeaderButton = props => { return ( <RNHeaderButton {...props } IconComponent ={IconOutline} iconSize ={props.iconSize || 23 } color ={props.color || '#000000 '} /> ); }; const HeaderButtons = props => { return <RNHeaderButtons HeaderButtonComponent ={HeaderButton} {...props } /> ; }; HeaderButtons .Item = Item ;export default HeaderButtons ;
IconWithBadge.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import React from 'react' ;import {View } from 'react-native' ;import {Badge } from '@ant-design/react-native' ;const IconWithBadge = ({children, badgeCount, ...props} ) => { return ( <View style ={{width: 24 , height: 24 , margin: 5 }}> {children} <Badge {...props } style ={{position: 'absolute ', right: -6 , top: -3 }} text ={badgeCount} /> </View > ); }; export default IconWithBadge ;
getActiveRouteName.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const getActiveRouteName = state => { const route = state.routes [state.index ]; if (route.state ) { return getActiveRouteName (route.state ); } return route.name ; }; export default getActiveRouteName;
getScreenOptions.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import {TransitionPresets } from '@react-navigation/stack' ;const getScreenOptions = ( ) => { return { headerStyle : { backgroundColor : '#ffffff' , }, headerTintColor : '#000000' , headerTitleStyle : { fontWeight : 'bold' , }, headerBackTitleVisible : false , headerTitleAlign : 'center' , cardStyle : { flex : 1 , backgroundColor : '#f5f5f9' , }, ...TransitionPresets .SlideFromRightIOS , }; }; export default getScreenOptions;
NavigationService.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React from 'react' ;export const navigationRef = React .createRef ();const navigate = (name, params ) => { navigationRef.current && navigationRef.current .navigate (name, params); }; const getNavigation = ( ) => { return navigationRef.current && navigationRef.current ; }; export default { navigate, getNavigation, };
页面生命周期与React Navigation 一个包含 页面 A 和 B 的 StackNavigator ,当跳转到 A 时,componentDidMount
方法会被调用; 当跳转到 B 时,componentDidMount
方法也会被调用,但是 A 依然在堆栈中保持 被加载状态,他的 componentWillUnMount
也不会被调用。
当从 B 跳转到 A,B的 componentWillUnmount
方法会被调用,但是 A 的 componentDidMount
方法不会被调用,应为此时 A 依然是被加载状态。
React Navigation 生命周期事件 addListener 1 2 3 4 5 6 7 8 9 10 11 12 function Profile ({ navigation } ) { React .useEffect (() => { const unsubscribe = navigation.addListener ('focus' , () => { }); return unsubscribe; }, [navigation]); return <ProfileContent /> ; }
useFocusEffect 1 2 3 4 5 6 7 8 9 useFocusEffect ( React .useCallback (() => { return () => { }; }, []), );
headerMode:"none"
: hide Header for Stack.Navigator
headerShown:false
: hide Header for Stack.Screen
tabBar={() => null}
: hide TabBar for BottomTab.Navigator
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import {NavigationContainer , useFocusEffect} from '@react-navigation/native' ;import {createStackNavigator, TransitionPresets , HeaderBackButton } from '@react-navigation/stack' ;import {createBottomTabNavigator} from '@react-navigation/bottom-tabs' ;const Stack = createStackNavigator ();const BottomTab = createBottomTabNavigator ();export default App = () => { <NavigationContainer > <Stack.Navigator headerMode ="none" > <Stack.Screen ... options ={{ headerShown: false }} /> <Stack.Screen ... > {() => ( <BottomTab.Navigator ... tabBar ={() => null} > ... </BottomTab.Navigator > )} </Stack.Screen > </Stack.Navigator > </NavigationContainer > }
TabBar 的 StatusBar 不同 一般我们会对特殊的那个TabBar进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const getActiveRouteName = state => { const route = state.routes [state.index ]; if (route.state ) { return getActiveRouteName (route.state ); } return route.name ; }; const App = ( ) => { const ref = React .useRef (null ); return ( <> {/* 访问 ref.current?.navigate */} <NavigationContainer ref ={ref} onStateChange ={state => { const previousRouteName = ref.current; const currentRouteName = getActiveRouteName(state); if (previousRouteName !== currentRouteName) { console.log('[onStateChange]', currentRouteName); if (currentRouteName === 'HomeScreen') { StatusBar.setBarStyle('dark-content'); // 修改 StatusBar } else { StatusBar.setBarStyle('dark-content'); // 修改 StatusBar } } }} > </NavigationContainer > </> ) }
监听安卓物理返回键 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import {View , Text , BackHandler } from 'react-native' ;const CustomAndroidBackButtonBehaviorScreen = ({navigation, route} ) => { useFocusEffect ( React .useCallback (() => { const onBackPress = ( ) => { alert ('物理返回键被拦截了!' ); return true ; }; BackHandler .addEventListener ('hardwareBackPress' , onBackPress); return () => BackHandler .removeEventListener ('hardwareBackPress' , onBackPress); }, []), ); return ( <View style ={styles.container} > <Text > AndroidBackHandlerScreen</Text > </View > ); };
在子组件中访问 navigation
我们可以通过 useNavigation()
hook 来访问 navigation,再也不用传递多层 navigation
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import React from 'react' ;import { Button } from 'react-native' ;import { useNavigation } from '@react-navigation/native' ;function GoToButton ({ screenName } ) { const navigation = useNavigation (); return ( <Button title ={ `Go to ${screenName }`} onPress ={() => navigation.navigate(screenName)} /> ); }
给页面传递额外的属性 1 2 3 4 5 <Stack .Screen name="HomeScreen" options={{headerTitle : '首页' }}> {props => <HomeScreen {...props } extraData ={{author: '紫升 '}} /> } </Stack .Screen >
1 2 3 4 5 6 7 8 import { useHeaderHeight } from '@react-navigation/stack' const App = ( ) => { const HeaderHeight = useHeaderHeight () return (...) } export default App
继续使用类组件 考虑到对于不适应 Hooks 的但是业务又很紧急的场景,我们可以再类组件之上封装一层来支持 React Navigation 的 Hooks 组件,之所以这么做,起因是因为 React Navigation 5 中我们只能通过 useHeaderHeight()
方法获取标题栏高度。
1 2 3 4 5 6 7 8 9 10 11 class Albums extends React.Component { render ( ) { return <ScrollView ref ={this.props.scrollRef} > {/* content */}</ScrollView > ; } } export default function (props ) { const ref = React .useRef (null ); useScrollToTop (ref); return <Albums {...props } scrollRef ={ref} /> ; }