лучшие практики реакции-навигации 5.x

React Native
лучшие практики реакции-навигации 5.x

Исходный код примера статьи:На GitHub.com/ есть этот гигантский нин...

Установить зависимости

$ 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раздел:

implementation 'androidx.appcompat:appcompat:1.1.0-rc01'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha02'

чтобы завершитьreact-native-gesture-handler, добавьте следующий код вверху файла записи, напримерindex.jsилиApp.js:

import 'react-native-gesture-handler';

Теперь нам нужно использовать все приложениеNavigationContainerпакет:

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

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(() => {
      // Do something when the screen is focused
      return () => {
        // Do something when the screen is unfocused
        // Useful for cleanup functions
      };
    }, []),
  );
  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}) => {
  // 通过 props.route.params 接收参数
  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: параметр по умолчанию

HeaderButtons.js

использоватьreact-navigation-header-buttonsВы можете настроить свой собственный компонент Header Button с любым компонентом Icon.Для удобства демонстрации я использую@ant-design/icons-react-native:

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

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

/**
 * Gets the current screen from navigation state
 * @param state
 */
const getActiveRouteName = state => {
  const route = state.routes[state.index];

  if (route.state) {
    // Dive into nested navigators
    return getActiveRouteName(route.state);
  }

  return route.name;
};

export default getActiveRouteName;

getScreenOptions.js

import {TransitionPresets} from '@react-navigation/stack';

const getScreenOptions = () => {
  return {
    headerStyle: {
      backgroundColor: '#ffffff',
    }, // 一个应用于 header 的最外层 View 的 样式对象
    headerTintColor: '#000000', // 返回按钮和标题都使用这个属性作为它们的颜色
    headerTitleStyle: {
      fontWeight: 'bold',
    },
    headerBackTitleVisible: false,
    headerTitleAlign: 'center',
    cardStyle: {
      flex: 1,
      backgroundColor: '#f5f5f9',
    },
    ...TransitionPresets.SlideFromRightIOS,
  };
};

export default getScreenOptions;

NavigationService.js

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

StackNavigator, содержащий страницы A и B, при переходе к A,componentDidMountбудет вызван метод; при переходе к B,componentDidMountМетод также будет вызван, но A все еще остается загруженным в стеке, егоcomponentWillUnMountтоже не позовут.

При прыжке из B в A, BcomponentWillUnmountметод будет вызван, но метод AcomponentDidMountМетод не будет вызываться, так как A в это время все еще загружен.

Реагировать на события жизненного цикла навигации

addListener

function Profile({ navigation }) {
  React.useEffect(() => {
    const unsubscribe = navigation.addListener('focus', () => {
      // Screen was focused
      // Do something
    });

    return unsubscribe;
  }, [navigation]);

  return <ProfileContent />;
}

useFocusEffect

useFocusEffect(
    React.useCallback(() => {
      // Do something when the screen is focused
      return () => {
        // Do something when the screen is unfocused
        // Useful for cleanup functions
      };
    }, []),
  );

Скрыть заголовок/вкладку

  • headerMode:"none": hide Header for Stack.Navigator
  • headerShown:false: hide Header for Stack.Screen
  • tabBar={() => null}: hide TabBar for BottomTab.Navigator
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 отличается

Как правило, мы рассмотрим специальный Tabbar.

const getActiveRouteName = state => {
  const route = state.routes[state.index];

  if (route.state) {
    // Dive into nested navigators
    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>
    </>
	)
}

Прослушайте физический ключ возврата Android

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()крюк для доступа к навигации, больше не нужно проходить несколько слоевnavigation

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)}
    />
  );
}

Передать дополнительные свойства на страницу

<Stack.Screen
  name="HomeScreen"
  options={{headerTitle: '首页'}}>
  {props => <HomeScreen {...props} extraData={{author: '杨俊宁'}} />}
</Stack.Screen>

Получить высоту заголовка

import { useHeaderHeight } from '@react-navigation/stack'

const App = () => {
    const HeaderHeight = useHeaderHeight() // 获取Header Height
    return(...)
}

export default App

Продолжайте использовать компоненты класса

Учитывая, что сцена не подходит для хуков, но дело очень срочное, мы можем инкапсулировать слой поверх компонента класса для поддержки компонента хуков в React Navigation, Причина этого в том, что в React Navigation 5 мы можем передать толькоuseHeaderHeight()способ получить высоту строки заголовка.

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} />;
}