React Native系列(二) - 路由配置

Yukino 780 2022-09-07

上一篇说到项目初始化配置,这一篇记录下Route和Store的用法

一、技术选型

根据官方文档的推荐,路由的库就直接选用React Navigation了;作为一个前端应用,那么基本的权限管控,登录和未登录页面区分开是最基本的,目前没有需要根据权限显示页面的需求,就暂不考虑,我习惯于将token等用户信息持久化存在cookie/localStorage中,在APP渲染时,将其取出放在store中,所以使用了Redux,作为状态容器,结合实现路由配置。

二、安装依赖

React Navigation的包里包含了许多模块,但有些东西我们暂时还用不到,只安装一些基础的东西即可;

yarn add @react-navigation/native
yarn add @react-navigation/native-stack
yarn add react-native-screens react-native-safe-area-context

然后是Redux:

yarn add @reduxjs/toolkit

三、Store配置

先来创建store的目录:

cd js
mkdir store
cd store
mkdir reducer
cd reducer

在reducer目录下创建我们的userReducer.ts:

import { createSlice, PayloadAction } from '@reduxjs/toolkit';

interface UserState {
  username: string;
  token: string;
}

const initialState: UserState = {
  username: '',
  token: ''
};

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    login: (state, action: PayloadAction<UserState>) => {
      console.log(action);
      state.token = action.payload.token;
      state.username = action.payload.username;
    },
    logout: (state) => {
      state.token = '';
      state.username = '';
    }
  }
});

export const { login, logout } = userSlice.actions;

export default userSlice.reducer;

这个reducer非常简单,包含usernametokenloginlogout两个actionlogin将payload中的信息赋给state,logout则是简单的将state的重置,接下回到store目录,来创建store对象;

// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import userReducer from './reducer/userReducer';

export const store = configureStore({
  reducer: {
    user: userReducer
  }
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

再回到源代码目录(我的是js),创建hooks目录,来编写store简单的的hooks;

// hooks/useAppStore.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from '~/store';

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

最后,将store对象通过Provider挂载在根节点(App.tsx)下

import { Provider } from 'react-redux';
import { store } from '~/store';

const App = () => {
  return (
    <SafeAreaProvider>
      <Provider store={store}>
        <Text>Hello world!</Text>
      </Provider>
    </SafeAreaProvider>
  );
};

export default App;

这样我们在其他的组件中就可以使用useAppSelector来读取state、useAppDispatch来提交变更了,如下:

// test.tsx
import { useAppDispatch, useAppSelector } from '~/hooks/useAppStore';

function Test () {
  // 读取token
  const isLogin = useAppSelector((state) => state.user.token);
  // 设置用户信息
  dispatch(login({ username: 'admin', token: 'xxxxx' }));
}

四、Navigation配置

基础路由

先来创建一个简单的navigation:

// route/index.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { View, Text } from 'react-native';

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

const BaseStack = createNativeStackNavigator();
export function Navigation() {
  return (
    <NavigationContainer>
      <BaseStack.Navigator>
        <BaseStack.Screen name="Home" component={HomeScreen} options={{ title="首页" }} />
      </BaseStack.Navigator>
    </NavigationContainer>
  );
}

Auth路由

上面的Screen就是对应的一个个路由,name为唯一的标识,component是对应的页面组件,options为路由的配置对象,下面我们来加上登录的校验:

// route/index.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { View, Text } from 'react-native';
import { useAppSelector, useAppDispatch } from '~/hooks/useAppStore';

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

function LoginScreen() {
  const dispatch = useAppDispatch();
  return <Button 
           title="login" 
           onPress={() => dispatch(login({ username: 'admin', token: 'xxxxx' }))}
}

const BaseStack = createNativeStackNavigator();
export function Navigation() {
  const isLogin = useAppSelector((state) => state.user.token);
  return (
    <NavigationContainer>
      <BaseStack.Navigator>
        {isLogin ? 
          <BaseStack.Screen name="Home" component={HomeScreen} options={{ title="首页" }} /> :
          <BaseStack.Screen name="Login" component={LoginScreen} />
        }
      </BaseStack.Navigator>
    </NavigationContainer>
  );
}

上面是官方推荐的做法,在此基础上我们可以再用Group做一层包装:

function AuthGroup() {
  return (
    <BaseStack.Group>
      <BaseStack.Screen name="Home" component={HomeScreen} options={{ title="首页" }} />
    </BaseStack.Group>
  )
}

function NoAuthGroup() {
  return (
    <BaseStack.Group>
      <BaseStack.Screen name="Login" component={LoginScreen} />
    </BaseStack.Group>
  )
}

export function BaseStackNavigation() {
  const isLogin = useAppSelector((state) => state.user.token);
  return (
    <NavigationContainer>
      <BaseStack.Navigator>
        {isLogin ? AuthGroup() : NoAuthGroup()}
      </BaseStack.Navigator>
    </NavigationContainer>
  );
}

通用配置

基础的创建路由就完成了,我们还可以再Navigator上进行一些通用的配置,比如header标题的位置、切换的动画等等;

export function BaseStackNavigation() {
  const isLogin = useAppSelector((state) => state.user.token);
  return (
    <NavigationContainer>
      <BaseStack.Navigator
      	screenOptions={{
          headerShadowVisible: false,
          headerTitleAlign: 'center',
          headerTitleStyle: head,
          animation: 'slide_from_right'
        }}  
      >
        {isLogin ? AuthGroup() : NoAuthGroup()}
      </BaseStack.Navigator>
    </NavigationContainer>
  );
}

screenOptions还支持function的参数,具体可见官方文档;

组件中的使用

下面讲讲如何在组件中使用navigation和route,进行页面跳转、传参、读参;

首先是获取navigation和route方式,一般有两种种常用的方式:

// 第一种,通过Screen Props
export default function ({navigation, route}) {
  // ...
}

// 第二种,通过hooks
export default function () {
  const navigation = useNavigation();
  const route = useRoute();
}

推荐使用第二种,useNavigationuseRoute在Screen的子组件中也是可以调用的,使用起来比较方便;

然后是参数

export default function () {
  // 跳转传参
  const navigation = useNavigation();
  navigation.navigate('Home', {
    a: 1
  });
  // 读取参数
  const route = useRoute();
  const params = route.params;
}

最后补充一下关于结合TypeScript的使用;

TS的话首先需要定义Screen 的参数类型:

const BaseStack = createNativeStackNavigator();

export type BaseNavigationProps = {
  Home: {
    a: number;
  };
  Login: undefined;
};

然后在获取navigation和route的时候使用rn navigation提供的辅助类型定义对应的类型;

// Props的类型
type HomeScreenProps = NativeStackScreenProps<BaseNavigationProps, 'Home'>;
export default function ({navigation, route}: HomeScreenProps) {
  
}

// navigation的类型
type HomeScreenNavigation = NativeStackNavigationProp<BaseNavigationProps, RouteNames.HOME>;
export default function () {
	const navigation = useNavigation<HomeScreenNavigation>();  
}

// route的类型
type HomeRouteProps = RouteProp<BaseNavigationProps, 'Home'>;
export default function () {
  const route = useRoute<ProjectDetailRouteProps>();
}