React Native项目中如何定制自己的Navigator

「这是我参与2022首次更文挑战的第N天,活动详情查看:2022首次更文挑战」。

我好像吐槽过RN很多次,哈哈,不过国内确实很多项目组在用RN。众所周知,RN官方的库不够多,甚至不够好,比如说导航库,现在比较推荐的导航库不是RN官方的,而是第三方的React Navigation

关于React Navigation,今天我们主说导航方式。React Navigation本身提供了丰富的导航方式,如stack、tab、drawer等。

image.png

但是如果这么多的导航还是不满足你的需求,你需要定制自己的导航,怎么办呢?如果你想从0造轮子,那也太复杂了,毕竟客户端不比web端,客户端不仅仅需要考虑兼容性,底层涉及太多太复杂。这个时候我们就不要从0早轮子了,还是用零件组装吧~

React Navigation就给我们完美提供了相关零件。接下来我们就来说下,如何利用React Navigation定制自己的Navigator。

环境准备

我用的是React Native cli,前面诸多环境搭建就不说了,直接说安装了:

npx react-native init lesson3
cd lesson3
yarn ios
yarn android
复制代码

除了默认的包,我们还需要再安装@react-navigation/bottom-tabs、@react-navigation/native、react-native-safe-area-context等,具体直接上package.json吧,

{
  "name": "lesson3",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint ."
  },
  "dependencies": {
    "@react-navigation/bottom-tabs": "^6.0.9",
    "@react-navigation/native": "^6.0.6",
    "@react-navigation/native-stack": "^6.2.4",
    "react": "17.0.2",
    "react-native": "0.66.0",
    "react-native-elements": "^3.4.2",
    "react-native-safe-area-context": "^3.3.2",
    "react-native-screens": "^3.8.0",
    "react-native-vector-icons": "^8.1.0",
  },
  "devDependencies": {
    "@babel/core": "^7.12.9",
    "@babel/runtime": "^7.12.5",
    "@react-native-community/eslint-config": "^2.0.0",
    "babel-jest": "^26.6.3",
    "eslint": "7.14.0",
    "jest": "^26.6.3",
    "metro-react-native-babel-preset": "^0.66.2",
    "react-test-renderer": "17.0.2"
  },
  "jest": {
    "preset": "react-native"
  }
}
复制代码

注意安装完上面的这些React Navigation等库之后,不要忘记进入ios文件下再装下ios相关的依赖:

cd ios && pod install
复制代码

接下来,我们就可以来定制自己的Navigator了,其实就是实现createMyNavigator创建函数的过程,先看用法:

const {Navigator, Screen, Group} = createMyNavigator();
复制代码

接下来我们来实现一个Tab导航,我们一步步来实现:

代码实现与原理

createNavigatorFactory

这是一个HOC,亲们还记得什么是HOC吗?HOC,Higher Order Component,高阶组件,是指一个参数和返回值都是组件的函数。 此函数用于创建NavigatorScreen,因此接下我们要实现的就是TabNavigator组件了

const createMyNavigator = createNavigatorFactory(TabNavigator);

export default createMyNavigator;
复制代码

TabNavigator

这是一个组件,控制导航显示方式与页面显示,也就是说,这个组件显示两部分内容:导航与页面。 那么TabNavigator组件架子就是:

function TabNavigator({
  initialRouteName,
  children,
  screenOptions,
  tabBarStyle,
  contentStyle,
}) {
  return (
    <NavigationContent>
        <View>
            <Text>导航</Text>
        </View>
        <View>
            <Text>页面</Text>
        </View>
    </NavigationContent>
  );
}
复制代码

另外,TabNavigator接收props参数,包括children、screenOptions等,用作子节点、Screen参数等,这些都是来自组件Navigator的子节点:

const {Navigator, Screen, Group} = createMyTabNavigator();

export default function HomeRouterScreen() {
  return (
    <Navigator>
    
      <Screen name="home" component={HomeScreen} options={{title: '首页'}} />
      <Screen
        name="user"
        component={UserScreen}
        options={{title: '用户中心'}}
      />

      <Screen
        name="setting"
        component={SettingScreen}
        options={{title: '设置'}}
      />

    </Navigator>
  );
}
复制代码

接下来我们现导航和页面组件之前,还要再了解一个自定义Hook:useNavigationBuilder。它接收子组件children和页面配置信息screenOptions作为参数。返回Navigator组件的子组件的信息,或者叫子路由的信息,如子路由信息数组等。useNavigationBuilder使用如下:

function TabNavigator({children, screenOptions}) {
  const {state, descriptors, navigation, NavigationContent} =
    useNavigationBuilder(TabRouter, {
      children,
      screenOptions,
    });
     return (
    <NavigationContent>
         // 省略一万字
    </NavigationContent>
  );
}
复制代码

好了,接下来我们就可以分别来实现导航组件和页面组件部分了。

导航组件

说是导航组件,其实有时候是没有组件的,只是导航方式,比如stack导航只是进栈出栈的数据信息改变,并没有显示出来的组件。但是我们现在写的Tab导航是有导航组件的。

好,那接下来我们就来实现这里的Tab导航组件,其实就是个数组遍历的过程,把所有的导航配置遍历显示,然后添加切换页面的点击事件:

     // 导航
      <View style={[{flexDirection: 'row', paddingTop: 50}, tabBarStyle]}>
        {state.routes.map((route, index) => (
          <Pressable
            title={descriptors[route.key].options.title || route.name}
            key={route.key}
            onPress={() => {
              const event = navigation.emit({
                type: 'tabPress',
                target: route.key,
                canPreventDefault: true,
              });

              if (!event.defaultPrevented) {
                navigation.dispatch({
                  ...TabActions.jumpTo(route.name),
                  target: state.key,
                });
              }
            }}
            style={{
              flex: 1,
              justifyContent: 'space-between',
              alignItems: 'center',
            }}>
            {/* 显示title或者route.name */}
            <Text>{descriptors[route.key].options.title || route.name}</Text>
          </Pressable>
        ))}
      </View>
复制代码

页面

而页面显示,同导航组件,遍历所有的页面数据,匹配到当前的页面则显示,否则隐藏,显示页面则是通过descriptor.render()函数的执行。匹配页面可以通过state.index判断,即页面索引,每次导航切换,都会更新state.index,因此我们可以通过state.index===index判断是否显示页面。

    // 页面
    <View style={[{flex: 1}, contentStyle]}>
        {state.routes.map((route, index) => {
          const descriptor = descriptors[route.key];
          const isFocused = state.index === index;
          return (
            <Screen
              key={route.key}
              focused={isFocused}
              route={descriptor.route}
              navigation={descriptor.navigation}
              header={<Header title={descriptor.options.title || route.name} />}
              headerShown={descriptor.options.headerShown}
              style={[
                StyleSheet.absoluteFill,
                {display: index === state.index ? 'flex' : 'none'},
              ]}>
              {descriptor.render()}
              {/* <Text>{JSON.stringify(state)}</Text> */}
            </Screen>
          );
        })}
      </View>
复制代码

以上,自定义导航我们便实现完啦,代码非常详细了,大家可以自己尝试下~

如果想要完整可运行的代码,查看这里

おすすめ

転載: juejin.im/post/7054205618176917517