年初做了 React Native 的 Android 小应用,里面使用到了 React Navigation 4.x,完成后 2、3 个月 React Navigation 发布了 5.x,又过了小半年才来尝鲜。
首先接触到最大的改变是路由配置不再是函数创建的,而是组件化,有点像 React Route 3.x 到 4.x 的改变。
// 4.x 创建路由
const defaultConfig = (s: any) => {
return {
screen: s,
navigationOptions: {
headerShown: false,
},
};
};
export default createStackNavigator(
{
[BOTTOM_TAB_KEY]: {
screen: TabContainer,
navigationOptions: {
headerShown: false,
},
},
[RouteKeyMap.myApp]: defaultConfig(PageMyApp),
}
)
// 5.x 创建路由
const NestingNavigators: React.FC = () => {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home" headerMode="none">
<Stack.Screen name="Home" component={TabsView} />
<Stack.Screen name="List" component={ListView} />
<Stack.Screen name="Details" component={DetailsView} />
</Stack.Navigator>
</NavigationContainer>
);
};
export default NestingNavigators;
新版本在做沉浸式布局更容易了。
在 4.x 的时候,组件内通过 StatusBar
可以修改状态栏的背景颜色、文字颜色等,但是在路由嵌套/层级多了,多个状态栏的样式就会有问题。
例如: a(红色) -> b(蓝色) -> c(灰色),从 a 到 c 一层一层进入,状态栏显示正常,但是一层一层返回,状态栏只会显示最新(c 灰色)的那个。在 BottomTab
的页面直接切换,也是同样的状况。
在原文档中也给出了对应的方案,通过手动监听组件 didFocus
事件,手动设置/重置状态栏。当时我们写了一个高阶组件处理这个问题,ILayout.widthStatusBar({ full: true, backgroundColor: 'transparent'})(Home)
。
import { View, StatusBar, StatusBarStyle } from 'react-native';
interface WidthStatusBarParam {
/** 状态栏的背景色 */
backgroundColor?: string;
/** 设置状态栏文本的颜色。 */
barStyle?: StatusBarStyle;
/** 是否隐藏状态栏。 */
hidden?: boolean;
/** 全屏 */
full?: boolean;
/** 背景颜色 */
pageBackgroundColor?: string;
}
/**
* 设置状态栏
* @param barStyle 设置状态栏文本的颜色。
* @param backgroundColor 状态栏的背景色
* @param hidden 是否隐藏状态栏。
*/
export const setBarStyle = (barStyle: StatusBarStyle, backgroundColor: string, hidden: boolean) => {
StatusBar.setBarStyle(barStyle as StatusBarStyle);
StatusBar.setHidden(hidden);
if (Helpers.iOS) {
StatusBar.setNetworkActivityIndicatorVisible(true);
} else {
StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor(backgroundColor);
}
};
/**
* 设置当前页面的状态栏
* @param {Object} statusbarProps 参数
* @param {string} statusbarProps.backgroundColor 状态栏背景色
*
* @example widthStatusBar({})(YourNode);
*/
const widthStatusBar = ({
backgroundColor = Configs.Colors.primary,
barStyle = 'light-content',
hidden = false,
full = false,
pageBackgroundColor = Configs.Colors.pageBackgroundColor,
}: WidthStatusBarParam) => <T, P, C>(WrappedComponent: React.ComponentType<T>) => {
type Props = T & NavigationStackScreenProps;
return class WidthStatusBar extends React.PureComponent<Props, P, C> {
_navListener!: NavigationEventSubscription;
constructor(props: Props) {
super(props);
this._navListener = props.navigation.addListener('willFocus', () => {
setBarStyle(barStyle, 'transparent', hidden);
});
}
componentWillUnmount() {
this._navListener.remove();
}
render() {
// TODO 优化 iOS SafeAreaView
return (
<View style={{ flex: 1, backgroundColor: pageBackgroundColor }}>
<View
style={{ height: full ? 0 : StatusBar.currentHeight, backgroundColor: backgroundColor }}
/>
<WrappedComponent {...this.props} />
</View>
);
}
};
};
export default widthStatusBar;
现在嵌套中已经能显示正常了,但是 BottomTab
还是有点小问题,不过现在已经不需要这样麻烦了。
import React from 'react';
import { StatusBar, StatusBarProps } from 'react-native';
import { useIsFocused } from '@react-navigation/native';
const FocusAwareStatusBar: React.FC<StatusBarProps> = (props) => {
const isFocused = useIsFocused();
return isFocused ? <StatusBar {...props} /> : null;
};
export default FocusAwareStatusBar;
使用这个组件轻松搞定。
文档:Different status bar configuration based on route
不过,目前最让我感到有趣的是对 TypeScript 的支持越来越好了。
以前在路由组件中需要从 route 拿值需要针对单个组件进行 NavigationStackScreenProps
配置,但是在 navigation.push
的时候并不知道具体有哪些参数。
现在通过配置一个根组件状态,就能实现在 navigation.push
提示可以跳转到哪个路由,并且必填的参数也会提醒。
import React from 'react';
import { NavigationContainer, CompositeNavigationProp } from '@react-navigation/native';
import {
createStackNavigator,
StackScreenProps,
StackNavigationProp,
} from '@react-navigation/stack';
import { BottomTabNavigationProp as BottomTabNavigationPropOriginal } from '@react-navigation/bottom-tabs';
import ListView from '@~/pages/list/list';
import DetailsView from '@~/pages/details/details';
import TabsView, { BottomTabParamList } from './bottom-tab';
/** 当前所有 Stack 路由的参数 */
type RootStackParamList = {
Home: undefined;
List: undefined;
Details: {
id: number;
};
};
/** Stack 路由的 props */
export type RootStackScreenProps<T extends keyof RootStackParamList> = StackScreenProps<
RootStackParamList,
T
>;
/** BottomTab 路由的 navigation prop */
export type BottomTabNavigationProp<T extends keyof BottomTabParamList> = CompositeNavigationProp<
BottomTabNavigationPropOriginal<BottomTabParamList, T>,
StackNavigationProp<RootStackParamList>
>;
/** BottomTab 路由的 props */
export type BottomTabScreenProps<T extends keyof BottomTabParamList> = {
navigation: BottomTabNavigationProp<T>;
};
const Stack = createStackNavigator<RootStackParamList>();
const NestingNavigators: React.FC = () => {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home" headerMode="none">
<Stack.Screen name="Home" component={TabsView} />
<Stack.Screen name="List" component={ListView} />
<Stack.Screen name="Details" component={DetailsView} />
</Stack.Navigator>
</NavigationContainer>
);
};
export default NestingNavigators;
import React from 'react';
import { View, Text } from 'react-native';
import FocusAwareStatusBar from '@~/components/focus-aware-status-bar/focus-aware-status-bar';
import * as Routes from '@~/routes';
type UserCenterProps = {} & Routes.BottomTabScreenProps<'UserCenter'>;
const UserCenter: React.FC<UserCenterProps> = ({ navigation }) => {
return (
<View>
<FocusAwareStatusBar backgroundColor="#f99" />
<View>
<Text>UserCenter</Text>
</View>
<View>
<Text
onPress={() => {
navigation.push('Details', {
id: 3333,
});
}}
>
Go Details
</Text>
</View>
</View>
);
};
export default UserCenter;
文档:Type checking with TypeScript
到目前为止,体验已经比以前好太多了,单单路由跳转的代码提示就已经提升很多幸福感了。