背景
本文可能会有些门槛,需要读者比较了解 react-navigation
最近公司有个需求,让我与
IOS
同事各自使用React Native
和Flutter
开发一个带有TabBottom
+Header
+route
+ 项目文件目录与基础配置(就是用两种Native
开发框架各自开发一个脚手架工具),在学习了几天React Native
之后思考了一套RN
中的Route
方案,在这里记录并分享一下,如有更好的方案或者文章中有错误欢迎各位大佬讨论与斧正。
如果阅读该文章有疑惑,感兴趣可以先去了解下 React Navigation 官方文档
1. 前期准备
# 安装 `yarn` + `RN脚手架`
npm install -g yarn react-native-cli
# 安装 Android Studio (注意安装必须的 Android SDK)
# 如果需要一次性打两种移动平台安装包推荐直接使用苹果电脑
# 准备调试设备(虚拟机或真机)
# ...以上前期准备可按照官方文档逐步配置, 这里就不细讲了
# 最后执行 react-native 命令创建项目
react-native init yourProject
2. 项目结构
// RN 项目目录结构
yourProject
├-- __tests__ // 单元测试文件
├-- android // 安卓文件夹(app相关配置可在此处修改)
├-- ios // 苹果文件夹(app相关配置可在此处修改)
├-- .buckconfig
├-- .eslintrc.js // eslint 前端代码规范
├-- .gitattributes // git 相关
├-- .gitgnore // git 相关
├-- .prettierrc.js // 格式化工具, 在 VSCode 配合 Prettier 组件使用, 可按配置自动格式化文件中的代码, 与 eslint 绝配
├-- .watchmanconfig
├-- app.json // app 配置, 默认值为 app 名称, 该处配置将在 index.js 入口文件使用, "name" 字段必须与 android 和 ios 文件夹中的 app 名称相同
├-- App.tsx // 项目入口文件, 其实 index.js 文件才是真正的入口文件
├-- babel.config.js // 顾名思义 babel 配置
├-- index.js // 入口文件
├-- jest.config.js // 单元测试配置(默认使用的 jest)
├-- metro.config.js // RN 的分包配置
├-- package-lock.json // 依赖包版本锁
├-- package.json // npm 包依赖关系文件
├-- tsconfig.json // TS配置文件, 建议使用 TypeScript 开发
└-- yarn.lock // yarn 管理的依赖包版本锁
3. 开始配置
# 安装 react-navigation (推荐使用 3.x 版本, 4.x版本我在使用时会打不开 app , 直接闪退)
yarn add react-navigation@3.0.0-rc.5
这个框架可以理解为在 Native
中每个 page
都在一个 stack
, 路由切换其实就是堆栈操作, 内部自己管理堆栈的操作记录。
主要思路
在 react-navigator
中每个 page
都是调用 createStackNavigator
方法创建并返回的一个高阶 React
组件
同时在 react-navigation
中导航页是使用 createBottomTabNavigator
创建
对于 react-navigation
来讲, 不管哪个方法创建的 navigator
都是最后调用 createAppContainer
方法得到一整个 stack
# 在项目根目录新建 src 文件夹
cd yourProject && mkdir src
# 规划项目内路由、视图、组件与静态资源文件结构
src
├-- assets // 静态资源文件夹(一般为图片之类的资源)
├-- components // 组件
├-- pages // 页面
└-- route // 路由文件
├-- RouteStack
| └-- Route.tsx // 所有路由
└-- TabBarStack
└-- TabBar.tsx // 导航页路由
4. 代码编写
// TabBar.tsx
import React from 'react';
import { View, Text, Button } from 'react-native'
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons' // Icon 组件
import { fromRight, fromLeft, zoomIn } from 'react-navigation-transitions' // 动画组件
import HomeScreen from '../../pages/Home'
import {
createBottomTabNavigator,
createStackNavigator
} from 'react-navigation';
type NavigationAny = {
navigation: any
}
class SettingsScreen extends React.Component<NavigationAny> {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
{/* other code from before here */}
<Button
title="Settings go to Details"
onPress={() => this.props.navigation.navigate('List')}
/>
</View>
);
}
}
const HomeStack = createStackNavigator({
Home: {
screen: HomeScreen,
navigationOptions: {
header: null,
}
},
});
const SettingsStack = createStackNavigator({
Settings: {
screen: SettingsScreen,
navigationOptions: {
header: null,
}
},
});
const TabNavigator = createBottomTabNavigator(
{
Home: {
screen: HomeStack,
navigationOptions: {
tabBarLabel: '首页'
}
},
Settings: {
screen: SettingsStack,
navigationOptions: {
tabBarLabel: '设置'
}
},
},
{
defaultNavigationOptions: ({ navigation }) => ({ // 根据 TabBar 的路由设置 Icon 激活状态
tabBarIcon: ({ focused, horizontal, tintColor }) => {
const { routeName } = navigation.state;
let iconName = '',
color = tintColor || '';
if (routeName === 'Home') {
iconName = `home`;
} else if (routeName === 'Settings') {
iconName = `setting`;
}
iconName += `${focused ? '' : '-outline'}`
// You can return any component that you like here!
return (<MaterialCommunityIcons
name={iconName}
size={25}
color={color}
/>)
},
}),
tabBarOptions: {
activeTintColor: '#8e1bff',
inactiveTintColor: '#888',
},
}
)
export default TabNavigator
// 页面切换过渡动画的帮助方法
const handleCustomTransition = ({ scenes }: { scenes: any}) => {
const prevScene = scenes[scenes.length - 2];
const nextScene = scenes[scenes.length - 1];
if (prevScene
&& prevScene.route.routeName === 'Tabs'
&& nextScene.route.routeName === 'List') {
return zoomIn();
} else if (prevScene
&& prevScene.route.routeName === 'Tabs'
&& nextScene.route.routeName === 'Details') {
return fromRight();
}
return fromLeft();
}
export {
handleCustomTransition
}
// Route.tsx
import React from 'react';
import { View, Text } from 'react-native'
import ListScreen from '../../pages/List'
class DetailsScreen extends React.Component {
render() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Details!</Text>
</View>
);
}
}
export default {
List: ListScreen,
Details: DetailsScreen
}
App.tsx 为核心代码,统合整个 Route 文件夹中的配置,导出一个统一的 APPContainer
// App.tsx
import TabNavigator, { handleCustomTransition, } from './src/route/TabBarStack/TabBar'
import Route from './src/route/RouteStack/Route'
import {
createStackNavigator,
createAppContainer,
} from 'react-navigation';
const MainStack = createStackNavigator({
// 此处让 TabBarStack 与其他路由平级,就可不用在路由切换改变状态时判断是否显示底部 TabBar
// 该写法还有另外的好处,如果需要添加 Drawer Navigation 只需要少量的改动,添加一个新的文件就可实现,可扩展性强
// Drawer : {
// screen: DrawerNavigator,
// // code
//}
Tabs: {
screen: TabNavigator,
navigationOptions: {
header: null // 该写法为不显示 Header
}
},
...Route // 利用 ES6 的解构写法,可完美解耦该处其他的路由配置,用一个单独的文件管理其余路由利于维护
}, {
transitionConfig: (nav) => handleCustomTransition(nav)
})
export default createAppContainer(MainStack);
// index.js 入口文件(该文件无须改动,这里将代码贴出来是为了方便读者理解)
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => App);
在该方案之前, TabBarStack 的规划考虑过 React Navigation 官方给的实例,发现感觉并不优雅,官方其实也并不推荐如下写法,为方便读者理解,官方的实例如下
// 如果配置如下:
const FeedStack = createStackNavigator({
FeedHome: FeedScreen,
Details: DetailsScreen,
});
const TabNavigator = createBottomTabNavigator({
Feed: FeedStack,
Profile: ProfileScreen,
});
const AppNavigator = createSwitchNavigator({
Auth: AuthScreen,
Home: TabNavigator,
});
// 当我们从 Feed 主页面跳转到 Details 页面时,
// 如果想要隐藏 Tab Bar,我们无法在 DetailsScreen 的 navigationOptions 中配置 tabBarVisible: false,
// 因为这些选项只能在FeedStack中应用。 但是,我们可以执行以下操作:
const FeedStack = createStackNavigator({
FeedHome: FeedScreen,
Details: DetailsScreen,
});
FeedStack.navigationOptions = ({ navigation }) => {
let tabBarVisible = true;
if (navigation.state.index > 0) {
tabBarVisible = false;
}
return {
tabBarVisible,
};
};
// 只要我们离开 Feed 主页面,Tab Bar 就会被隐藏。
// 我们可以根据路由名称切换 Tab bar 的可见性, 但是在路由跳转时,反复切换 Tab Bar 的可见性,会看起来很奇怪。
// 我们应该只设置原本 Tab Bar 就是可见的页面。(这个方案官方也不推荐,写出这种例子只是为了让开发者了解该 API)
可以见到,如果以以上方式编写代码,每添加一个 page 就需要多编写一次 navigationOptions 配置,代码十分容易冗余,但是该方案也有一种优化方式,只是本人的一个猜想,并未实际实现,如果有读者感兴趣的话可以尝试一下,代码如下:
// createNavigator.js
import { createStackNavigator } from 'react-navigation'
// 该方案也有缺点,可扩展性不强,代码也有冗余,并不能满足所有需求
export default (pageName, page, predicate) => {
let screen = createStackNavigator({
[pageName]: page
})
screen.navigationOptions = predicate({ navigation })
return screen
}
// Route.js
import createNavigator from './createNavigator'
import Home from './pages/Home'
let navigationOptionHandler = (navigation) => {
let tabBarVisible = true;
if (navigation.state.index > 0) {
tabBarVisible = false;
}
return {
tabBarVisible,
};
}
const HomeScreen = createNavigator('Home', Home, navigationOptionHandler)
package.json
{
"dependencies": {
"react": "16.8.6",
"react-native": "0.60.5",
"react-native-gesture-handler": "^1.4.1",
"react-native-reanimated": "^1.2.0",
"react-native-swiper": "^1.5.14",
"react-native-vector-icons": "^6.6.0",
"react-navigation": "3.0.0-rc.5",
"react-navigation-transitions": "^1.0.12"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/runtime": "^7.5.5",
"@react-native-community/eslint-config": "^0.0.5",
"@types/jest": "^24.0.18",
"@types/react": "^16.9.2",
"@types/react-native": "^0.60.11",
"@types/react-native-vector-icons": "^6.4.1",
"@types/react-navigation": "^3.0.8",
"@types/react-test-renderer": "^16.9.0",
"babel-jest": "^24.9.0",
"eslint": "^6.3.0",
"eslint-config-prettier": "^6.2.0",
"eslint-plugin-prettier": "^3.1.0",
"jest": "^24.9.0",
"metro-react-native-babel-preset": "^0.56.0",
"prettier": "^1.18.2",
"react-test-renderer": "16.8.6",
"typescript": "^3.6.2"
},
"jest": {
"preset": "react-native"
}
}
5. 结束语
该方案并不完美,目前的架构已经足够满足需求,没有完美的架构,只有合适的架构,如果有读者对该方案感兴趣可以多交流讨论,希望我的思路能给各位一点启发,希望能帮助到大家,共勉