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

Yukino 1,593 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>();
}