记RN路由系统重构-react-navigation
简单说明
目前react-native项目中使用的路由是react-navigation,官方的路由只是简单的示例,页面过多后,App.tsx
主页面需要复制粘贴许多RootStack.Screen
,路由navigation
需要逐层传递,当然也可以是实用useDispatch
和useNavigation
等hooks
获取,但为了方便管理和升级路由系统,封装了routers
以及MRouters
管理类,部分完整代码直接拖到最后可以查看。
路由系统改造主要是包括路由配置src/router.js
、路由管理src/framework/MRouter.ts
、页面使用src/app.tsx
三部分
路由配置src/router.js
1.路由动画枚举animationType
const animationType = {
default: 'default',
fade: 'fade',
fade_from_bottom: 'fade_from_bottom',
flip: 'flip',
none: 'none',
simple_push: 'simple_push',
slide_from_bottom: 'slide_from_bottom',
slide_from_right: 'slide_from_right',
slide_from_left: 'slide_from_left'
}
2.路由屏幕屏幕方向枚举orientationType
const orientationType = {
default: 'default',
all: 'all',
portrait: 'portrait',
portrait_up: 'portrait_up',
portrait_down: 'portrait_down',
landscape: 'landscape',
landscape_left: 'landscape_left',
landscape_right: 'landscape_right'
}
3.默认路由配置screenConfig
const screenConfig: NativeStackNavigationOptions = {
headerShown: true,
headerShadowVisible: false,
headerTitleAlign: 'center',
headerBackTitle: '',
headerBackVisible: false,
headerBackTitleVisible: false,
headerTintColor: '#3D3F43',
animation: animationType.slide_from_right,
orientation: orientationType.portrait,
headerStyle: {
backgroundColor: '#fff',
// @ts-ignore
borderBottomWidth: 0
},
headerTitleStyle: {
fontSize: 18,
fontWeight: '500',
color: '#3D3F43'
}
};
自定义HeaderTitlegetHeaderTitle
const getHeaderTitle = (text) => {
return (
<View style={{
height: 28,
marginBottom: 8,
// backgroundColor: '#f00',
justifyContent: 'center',
alignItems: 'center'
}}>
<UIText style={[{
fontSize: 18,
fontWeight: '500'
}, { color: '#3D3F43' }]}>{text}</UIText>
</View>
);
};
screen option生成getOptions
const getOptions = (param: NativeStackNavigationOptions) => {
const options: NativeStackNavigationOptions = {
...screenConfig,
...param,
headerLeft: () => <HeaderBackArrow />,
headerTitle: () => getHeaderTitle(param.title || '')
};
return options;
};
路由配置对象routers
路由中包括name
、options
、compenent
三部分
name
代表当前路由名称
options
代表当前路由的配置,主要包括title``anmimation``headerShown``gestureEnabled
等配置项,具体有哪些配置项参考NativeStackNavigationOptions
类
component
代表当前路由的组件
const routers = {
Login: { name: 'Login', options: getOptions({ animation: animationType.slide_from_bottom, headerShown: false }), component: Login },
Info: { name: 'Info', options: getOptions({}), component: Info },
Tabbar: { name: 'Tabbar', options: getOptions({ animation: animationType.slide_from_right }), component: Tabbar },
Agreement: { name: 'Agreement', options: getOptions({}), component: Agreement },
...
}
路由管理src/framework/MRouter.ts
1.路由ref设置
let _navigator: any;
/**
* 设置路由ref
* @param navigatorRef 路由ref
*/
function setNavigator(navigatorRef: any) {
_navigator = navigatorRef;
logInfo('setNavigator');
}
2.打开新页面
主要newPage
参数使用,可开启新页面不会回跳旧页面
/**
* 打开一个新页面
* @param name 路由名称
* @param params 路由参数
* @param newPage 是否开启新页面 默认false
*/
function open(name: string, params?: any, newPage?: boolean) {
let index = indexOfRouteByName(name);
let route = getRouteInfoByName(name);
let item: any = {
key: route?.key || name,
name: name,
path: name,
params: params || null
};
console.log('newPage', newPage, index);
if (newPage || index === -1) item.key = name + generateRandom();
_navigator.dispatch(CommonActions.navigate(item));
logInfo('open', name);
}
3.重置路由栈
/**
* 将路由name重置到首页 并且清空路由栈
* @param name 路由名称
* @param params 路由参数
*/
function home(name: string, params?: object) {
try {
let item = {
name: name,
path: name,
params: params,
key: name + generateRandom()
};
_navigator.dispatch(
CommonActions.reset({
index: 0,
routes: [item]
})
);
} catch (e) {
errorHandler.noRoute(name);
}
logInfo('home', name);
}
4.路由替换
/**
* 将栈顶的路由替换为${name}路由
* @param name 路由名称
* @param params 路由参数
*/
function replace(name: string, params?: any) {
let routers = _navigator.getRootState().routes || [];
let index = indexOfRouteByName(name);
if (name && index > -1) {
let item = {
index,
name: name,
path: name,
key: routers[index].key
};
if (index > -1) routers = routers.slice(0, index);
routers.push(item);
_navigator.dispatch(
CommonActions.reset({
index: 0,
routes: routers
})
);
} else {
routers.pop();
let item = {
name: name,
params: params,
path: name,
key: name + generateRandom()
};
routers.push(item);
_navigator.dispatch(
CommonActions.reset({
index: 0,
routes: routers
})
);
}
logInfo('replace', name);
}
路由返回
主要name
参数,不传就默认往回跳转一页,传了就调用open
方法跳转到原来就有的那一页
/**
* 路由回退
* @param name 路由名称
* @param params 回调参数
*/
function back(name?: string, params?: any) {
let index = -1;
let route = null;
if (name) {
index = indexOfRouteByName(name);
route = getRouteInfoByName(name);
} else {
route = _navigator.getRootState().routes[_navigator.getRootState().routes.length - 1];
}
if (index > -1) {
open(route.name, params);
} else {
close();
}
params &&
_navigator.dispatch(
CommonActions.setParams({
params: params,
key: route && route.key,
source: route && route.key
})
);
logInfo('back', name);
}
关闭当前路由
/**
* 关闭当前路由
*/
function close() {
if (_navigator.canGoBack()) {
_navigator.dispatch(CommonActions.goBack());
}
logInfo('close');
}
获取路由name在路由栈中当前的位置
查找的顺序是从后往前找
/**
* 获取路由name在路由栈中当前的位置 从后往前查找
* @param name 路由名称
* @returns 返回index
*/
function indexOfRouteByName(name: string) {
let routers = _navigator.getRootState().routes || [];
let index = -1;
let routeIndex = routers.length;
console.log('indexOfRouteByName', name, routeIndex, routers);
while (routeIndex > 0) {
routeIndex--;
let route = routers[routeIndex];
if (route.name === name) {
index = routeIndex;
break;
}
}
return index;
}
根据页面名字获取页面所在路由的信息
/**
* 根据页面名字获取页面所在路由的信息
* @param {String} name 页面名字
* @return {{name,key,path,params}} 页面所在路由,如果没有则返回 null
*/
function getRouteInfoByName(name: string) {
let routers = _navigator.getRootState().routes || [];
let routeIndex = routers.length;
while (routeIndex) {
routeIndex--;
let route = routers[routeIndex];
if (route.name === name) {
return route;
}
}
return routers[routers.length];
}
App.tsx配置
1.路由navigationRef
对象获取传递
function App() {
const navigationRef = useNavigationContainerRef();
return (
<SafeAreaProvider>
<NavigationContainer ref={navigationRef}>
<NavigatorFn navigationRef={navigationRef} />
</NavigationContainer>
</SafeAreaProvider>
);
}
2.MRouter
初始化
useEffect(() => {
MRouter.setNavigator(navigationRef);
...
}, []);
3.路由动态生成
<RootStack.Navigator
// initialRouteName="Root"
// backBehavior="history"
screenOptions={{
headerShown: false,
orientation: 'portrait'
}}>
{routers &&
Object.keys(routers).map((key, index) => {
return (
<RootStack.Screen
key={index}
name={routers[key].name}
options={routers[key].options}
getComponent={() => routers[key].component}
/>
);
})}
</RootStack.Navigator>
4.根据现有项目改造
useEffect(() => {
AsyncStorage.getItem('token')
.then(token => {
dispatch(setToken({ token }));
if (!token) {
MRouter.home(routers.Login.name);
}
})
.catch(err => {
MRouter.home(routers.Login.name);
});
}, [token]);
// if (isLoading) {
// return <></>;
// }
useEffect(() => {
switch (status) {
case 'login': // 进入登陆
MRouter.home(routers.Login.name);
break;
default:
MRouter.home(routers.Loading.name);
break;
}
}, [status]);
MRouter
使用补充说明
MRouter
使用在任何页面和组件都可以直接使用,不用层层传递
MRouter
提供了基本所有场景的使用方式
MRouter
与原来navigation
的navigate
、replace
等方法兼容,路由栈是同一个可以配合使用
MRouter
使用说明
//MRouter打开新页面
//通过字符串Login
MRouter.open(‘Login’)
//通过路由系统的name
MRouter.open(routers.Login.name)
//通过第二参数传递params
MRouter.open(routers.Login.name, { name: 'abc', key: 1 })
//通过第二参数传递params 通过第三参数确认是否开启新页面
//第三参数默认false,当前路由栈存在该页面时会自动跳转回存在的页面 设置为true,每次都开启新页面
MRouter.open(routers.Login.name, { name: 'abc', key: 1 }, true)
//参数获取
//Component获取 直接从this.props中获取
const { params } = this.props;
const { name, key } = params;
//从hooks中获取
const route = useRoute();
const name = _.get(route, 'params.name', 'Demo Params');
//MRouter重置当前路由栈 并将页面为某个页面 同open可设置参数
MRouter.home(routers.Login.name)
MRouter.home(routers.Login.name, { key: 2, name: 'bcd' })
//MRouter替换路由栈顶的路由
MRouter.replace(routers.Login.name)
MRouter.replace(routers.Login.name, { key: 2, name: 'bcd' })
//MRouter关闭页面
//默认返回上一页
MRouter.back()
//返回路由名称为name的页面 可传递参数
//默认调用的是open方法 如果页面存在就返回到存在的页面 可传递参数
MRouter.back(routers.Login.name)
MRouter.back(routers.Login.name, { key: 2, name: 'bcd' })
部分代码示例
MRouter.ts
import { CommonActions } from '@react-navigation/native';
import errorHandler from './error/errorHandler';
import { routers } from 'router/router';
import { generateRandom } from 'utils';
import { NativeModules } from 'react-native';
let _navigator: any;
/**
* 设置路由ref
* @param navigatorRef 路由ref
*/
function setNavigator(navigatorRef: any) {
_navigator = navigatorRef;
logInfo('setNavigator');
}
/**
* 将栈顶的路由替换为${name}路由
* @param name 路由名称
* @param params 路由参数
*/
function replace(name: string, params?: any) {
let routers = _navigator.getRootState().routes || [];
let index = indexOfRouteByName(name);
if (name && index > -1) {
let item = {
index,
name: name,
path: name,
params: params,
key: name + generateRandom()
};
if (index > -1) routers = routers.slice(0, index);
routers.push(item);
_navigator.dispatch(
CommonActions.reset({
index: 0,
routes: routers
})
);
} else {
routers.pop();
let item = {
name: name,
params: params,
path: name,
key: name + generateRandom()
};
routers.push(item);
_navigator.dispatch(
CommonActions.reset({
index: 0,
routes: routers
})
);
}
logInfo('replace', name);
}
/**
* 打开一个新页面
* @param name 路由名称
* @param params 路由参数
* @param newPage 是否开启新页面 默认false
*/
function open(name: string, params?: object, newPage?: boolean) {
let index = indexOfRouteByName(name);
let route = getRouteInfoByName(name);
let item: any = {
key: route?.key || name,
name: name,
path: name,
params: params || null
};
// console.log('newPage', newPage, index);
if (newPage || index === -1) item.key = name + generateRandom();
_navigator.dispatch(CommonActions.navigate(item));
logInfo('open', name);
}
/**
* 将路由name重置到首页 并且清空路由栈
* @param name 路由名称
* @param params 路由参数
*/
function home(name: string, params?: object) {
try {
let item = {
name: name,
path: name,
params: params,
key: name + generateRandom()
};
_navigator.dispatch(
CommonActions.reset({
index: 0,
routes: [item]
})
);
} catch (e) {
errorHandler.noRoute(name);
}
logInfo('home', name);
}
/**
* 路由回退
* @param name 路由名称
* @param params 回调参数
*/
function back(name?: string, params?: any) {
let index = -1;
let route = null;
if (name) {
index = indexOfRouteByName(name);
route = getRouteInfoByName(name);
} else {
route = _navigator.getRootState().routes[_navigator.getRootState().routes.length - 1];
}
if (index > -1) {
open(route.name, params || route.params);
} else {
close();
}
params &&
_navigator.dispatch(
CommonActions.setParams({
params: params,
key: route && route.key,
source: route && route.key
})
);
logInfo('back', name);
}
//顶部路由名称
function topPath() {
return _navigator.getRootState().routes[0] || {};
}
//顶部路由名称
function isTopPath(name: string) {
const topRoute = _navigator.getRootState().routes[0] || {};
return topRoute.name === name;
}
/**
* 关闭当前路由
*/
function close() {
if (_navigator.canGoBack()) {
_navigator.dispatch(CommonActions.goBack());
}
logInfo('close');
}
/**
* 获取路由name在路由栈中当前的位置 从后往前查找
* @param name 路由名称
* @returns 返回index
*/
function indexOfRouteByName(name: string, isPrev?: boolean) {
let routers = _navigator.getRootState().routes || [];
let index = -1;
if (isPrev) {
return routers.findIndex((item: any) => item.name === name);
} else {
let routeIndex = routers.length;
// console.log('indexOfRouteByName', name, routeIndex, routers);
while (routeIndex > 0) {
routeIndex--;
let route = routers[routeIndex];
if (route.name === name) {
index = routeIndex;
break;
}
}
}
return index;
}
/**
* 根据页面名字获取页面所在路由的信息
* @param {String} name 页面名字
* @return {{name,key,path,params}} 页面所在路由,如果没有则返回 null
*/
function getRouteInfoByName(name: string, isPrev?: boolean) {
let routers = _navigator.getRootState().routes || [];
let routeIndex = routers.length;
if (isPrev) {
return routers.find((item: any) => item.name === name);
}
while (routeIndex) {
routeIndex--;
let route = routers[routeIndex];
if (route.name === name) {
return route;
}
}
return routers[routers.length];
}
/**
* 获取当前的路由栈
* @returns 返回路由栈
*/
function getRouters() {
return _navigator?.getRootState()?.routes;
}
/**
* 打开android app设置页面
*/
function openAppSetting() {
return new Promise((resolve, reject) => {
NativeModules.NativeRouter.openAppSetting();
});
}
/**
* 重新生成路由栈-只保留栈顶的路由和${name}路由
* @param name 路由名称
* @param params 路由参数
*/
function replaceOthers(name: string, params?: object) {
let routers = _navigator.getRootState().routes || [];
const newRouters = routers.slice(0, 1);
try {
let item = {
name: name,
path: name,
params: params,
key: name + generateRandom()
};
_navigator.dispatch(
CommonActions.reset({
index: 0,
routes: [...newRouters, item]
})
);
} catch (e) {
errorHandler.noRoute(name);
}
logInfo('replaceOthers', name);
}
/**
* 回到路由栈中的第一个,并关闭所有其他路由
*/
function popToTop() {
let routers = _navigator.getRootState().routes || [];
const newRouters = routers.slice(0, 1);
try {
_navigator.dispatch(
CommonActions.reset({
index: 0,
routes: [...newRouters]
})
);
} catch (e) {
errorHandler.noRoute('popToTop');
}
logInfo('popToTop');
}
const logInfo = (type:string, msg?:string) => {
console.log('msg', type, msg)
}
export default {
home,
open,
replace,
back,
close,
setNavigator,
topPath,
getRouters,
openAppSetting,
isTopPath,
replaceOthers,
popToTop,
};
ErrorHandler.js
const noop = (e:Error,isFatal:Boolean) => { };
export const setJSExceptionHandler = (customHandler = noop, allowedInDevMode = false) => {
if (typeof allowedInDevMode !== "boolean" || typeof customHandler !== "function") {
return;
}
const allowed = allowedInDevMode ? true : !__DEV__;
if (allowed) {
// !!! 关键代码
// 设置错误处理函数
global.ErrorUtils.setGlobalHandler(customHandler);
// 改写 console.error,保证报错能被 ErrorUtils 捕获并调用错误处理函数处理
console.error = (message, error) => global.ErrorUtils.reportError(error);
}
};
export const getJSExceptionHandler = () => global.ErrorUtils.getGlobalHandler();
export default {
setJSExceptionHandler,
getJSExceptionHandler,
};
generateRandom
方法
export const generateRandom = () => {
return Math.random().toString(16).slice(2);
};
router.tsx
import React from 'react';
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
import Login from 'pages/login/index';
import HeaderBackArrow from 'components/HeaderBackArrow';
import { UITitleText } from 'components/Text/Texts';
import Loading from 'pages/loading';
import { GestureResponderEvent } from 'react-native-modal';
import MRouter from 'framework/MRouter';
//屏幕动画枚举
const animationType: any = {
default: 'default',
fade: 'fade',
fade_from_bottom: 'fade_from_bottom',
flip: 'flip',
none: 'none',
simple_push: 'simple_push',
slide_from_bottom: 'slide_from_bottom',
slide_from_right: 'slide_from_right',
slide_from_left: 'slide_from_left'
};
//屏幕方向枚举
const orientationType = {
default: 'default',
all: 'all',
portrait: 'portrait',
portrait_up: 'portrait_up',
portrait_down: 'portrait_down',
landscape: 'landscape',
landscape_left: 'landscape_left',
landscape_right: 'landscape_right'
};
//屏幕默认config
// const screenConfig: NativeStackNavigationOptions = {
const screenConfig: any = {
headerShown: true,
headerShadowVisible: false,
headerTitleAlign: 'center',
headerBackTitle: '',
headerBackVisible: false,
headerBackTitleVisible: false,
headerTintColor: '#3D3F43',
animation: animationType.slide_from_right,
orientation: orientationType.portrait,
headerStyle: {
backgroundColor: '#fff',
// @ts-ignore
borderBottomWidth: 0
},
headerTitleStyle: {
fontSize: 18,
fontWeight: '500',
color: '#3D3F43'
}
};
let tempTime = 0;
let clickNum = 0;
const onWelcome = (e: GestureResponderEvent) => {
// console.log('onWelcome', e.timeStamp);
const time = e.timeStamp;
if (tempTime === 0) {
tempTime = e.timeStamp;
} else {
if (time - tempTime > 300) {
tempTime = time;
clickNum = 0;
return;
}
clickNum++;
tempTime = time;
if (clickNum === 11) {
// Toast.info('连续点击8次,进入隐藏页面');
// if (REACT_NATIVE_ENV !== 'PRODUCTION') {
// }
MRouter.open(routers.Login.name);
clickNum = 0;
tempTime = 0;
return;
}
}
};
//路由option
const getOptions = (param: NativeStackNavigationOptions) => {
const options: NativeStackNavigationOptions = {
...screenConfig,
...param,
headerLeft: () => <HeaderBackArrow />,
headerTitle: () => (
<UITitleText onPress={onWelcome} suppressHighlighting={true}>
{param.title || ''}
</UITitleText>
)
};
return options;
};
//在这里可以填写我们所需要的路由(页面) options可以配置我们对应的navigation配置
const routers = {
Loading: { name: 'Loading', options: getOptions({ headerShown: false }), component: Loading },
Login: { name: 'Login', options: getOptions({ title: '关于我们' }), component: Login },
};
//导出路由
export { routers };
App.tsx
// In App.js in a new project
import React, { useEffect} from 'react';
import { useAppSelector, useAppDispatch } from 'store/hook';
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import MyStyleSheet from 'components/MyStyleSheet';
import MRouter from 'framework/MRouter';
import { routers } from 'router/router';
const RootStack = createNativeStackNavigator();
const NavigatorFn: React.FC<{ navigationRef: any }> = ({ navigationRef }) => {
const dispatch = useAppDispatch();
const { action, action_params } = useAppSelector(state => state.account);
const openPage = (pageName: string) => {
const params = action_params ? action_params : {};
MRouter.home(pageName, params);
};
useEffect(() => {
MRouter.setNavigator(navigationRef?.current);
}, []);
useEffect(() => {
console.log('action', action);
switch (action) {
case 'login':
openPage(routers.Tabbar.name);
break;
default:
openPage(routers.Loading.name);
// openPage(routers.Tabbar.name);
break;
}
}, [action]);
return (
<>
<RootStack.Navigator
// initialRouteName="Root"
// backBehavior="history"
screenOptions={{
headerShown: false,
orientation: 'portrait'
}}>
{routers &&
Object.keys(routers).map((key, index) => {
return (
<RootStack.Screen
key={index}
name={routers[key].name}
options={routers[key].options}
getComponent={() => routers[key].component}
/>
);
})}
</RootStack.Navigator>
</>
);
};
function App() {
const navigationRef = useNavigationContainerRef();
const dispatch = useAppDispatch();
return (
<SafeAreaProvider>
<NavigationContainer
ref={navigationRef}
onStateChange={state => {
// 全局路由埋点
// if (state) {
// const { routes } = state;
// if (routes?.length > 0) {
// let data: any = {};
// if (routes.length > 1) {
// const { name = '', key = '' } = routes[1];
// const { name: preName = '', key: preKey = '' } = routes[0];
// data.page_from = preName;
// data.page = name;
// data.page_from_key = preKey;
// data.page_to = name;
// data.page_to_key = key;
// } else {
// const { name, key } = routes[0];
// data.page_to = name;
// data.page_key_to = key;
// }
// TrackEventManager.trackEvent(TrackEventEnum.page_jump, {
// ...data,
// event: TrackEventEnum.page_jump,
// event_desc: '页面跳转',
// page: '页面'
// });
// }
// }
}}>
<NavigatorFn navigationRef={navigationRef} />
</NavigationContainer>
</SafeAreaProvider>
);
}
const styles = MyStyleSheet.create({
header: {
height: 28,
marginBottom: 8,
// backgroundColor: '#f00',
justifyContent: 'center',
alignItems: 'center'
},
headerTitle: {
fontSize: 18,
fontWeight: '500'
},
});
export default App;
结束语
懂的都懂
网友评论