Overview
NavigationExperimental是react native的一个新的导航系统,重点是改进<Navigator/>组件.
- 单向数据流, 它使用reducers 来操作最顶层的state 对像,而在<Navigator/>中,当你在子导航页中,不可能操作到app最初打开页面时的state对像,除非,一级级的通过props传递过方法名或函数名,然后在子页面中调用这些方法或者函数,来修改某个顶层的数据。
- 为了允许存在本地和基于 js的导航视图,导航的逻辑和路由,必须从视图逻辑中独立出来。
- 改进了切换时的场景动画,手势和导航栏
如果你对react native 中的三个导航感到困惑,可以查看导航比较的文章
三个导航系统的比较
Navigator 和 NavigatorIOS 对于新人来说,不知道如何区别它们。一个是基于JS的,而NavigatorIOS则是第三方的开发的只针对ios的本地组件. 而Facebook正在将Navigator 过渡到NavigationExperimental. NavigationExperimental向前兼容navigation库。
NavigationExperimental 通常称为”新的导航”, 但其实它是以一种新的方法实现导航逻辑,这样允许作何的视图都可以作为导航的视图 。它包含了一个预编异的组件NavigationAnimatedView来管理场景间的动画。它内部的每一个视图都可以有自己的手势和动画。这些预编译的场景和overlay组件,看起来就会跟平台相一致(ios, android)
Navigator and NavigatorIOS两个都是有状态(即保存各个导航的序顺)的组件,允许你的APP在多个不同的场景(屏幕)之间管理你的导航。这两个导航管理了一个路由栈(route stack),这样就允许我们使用pop(), psh(), and replace()来管理状态。这类似于html5的history API. 这两者的主要区别在于NavigatorIOS是使用了iOS的 UINavigationController类,而Navigator都是基于Javascript。 Navigator适用于两个平台,而NavigatorIOS只能适用于iOS. 如果在一个APP中应用了多个导航组件(Navigator and NavigatorIOS一起使用). 那么在两者之间进行导航过渡,会变得非常困难.
NavigationExperimental
- NavigationRootContainer允许导航的各个状态(屏幕)保存在app的最顶层.
- 使用reducer在导航状态中声明设置转换过渡
- 可以将state永久保存存到硬盘,这样刷新和app更新后,还能获得之前的导航状态
- 监听打开中的url链接,BackAndroid便于支持返回按纽
- NavigationReducers 包含了预置的reducers, 用来管理导航状态之间的转换过渡。
- Reducers可以彼此之前进行组合,设置更高级的导航逻辑
- 导航逻辑可以用于任何的视图
- NavigationAnimatedView 是一个用来管理不同场景动画的组件,也可以用于Navigator和NavigatorIOS组件
- 每一个scene可以完全自定义,并且管理它自己的动画和手势
- 可以有一个Overlay/header, 用于跟场景的动画同步
- NavigationCard 和NavigationHeader可以作为预编译的scenes和overlays. 然后跟NavigationAnimatedView一起使用
Navigator
- Facebook会慢慢不支持Navigator, 重点会放在NavigationExperimental
- 它有自己的navigations state和API,这违返了React的单向数据流原则
- Scene animations and gestures很难自定义
- 手势可以通过Navigator处理,但是不能基于每一个预编译scene进行自定义
- Animation 的自定义是模糊的,因为它在Animated库出来之前就有了
- 可以用于iOS和Android
- 跟NavigatorIOS一样,只有一个简单的导航条:Navigator.NavigatorBar, 和一个breadcrumbs Navigator.BreadcrumbNavigatorBar. 可以看看React Native的官方UIExplorer demo 看看如何使用它们。
- 动画不如Apple的精致,你可以使用NavigatorIOS.
- 你可以通过navigationBar属性,提供你自己的navigation bar
NavigatorIOS
- 包含一 个专有的API, 不能很好的兼容其它的app
- API很小,所以限制了它对Navigator or NavigationStackView的自定义
- 开发这个组件不是React Native团队,而是属于开源的社区
- 有很多积压的bug
- 如果社区将它重构为声明性的(declarative), 它将跟NavigationExperimental一起使用的很好
- 它在iOS UIKit的基础上包装的, 所以它跟其它的本地app是一样的。
- 仅支持iOS
- 包含一个默认的navigation bar. 这个navigation bar不是一个React Native view组件,它的样式只能轻微调整
NavigationExperimental Guide
我们通过一个简单的聊天APP开始,学习一个使用NavigationExperimental. 首先,我们需要确应用程序state的结构,因为我们的app有多个屏幕栈组成(类似于一个网站由多个网页组成). 所以我们需要在state中定义一个数组,用来保存场景列表。
class MyApp extends React.Component {
constructor(props) {
super(props);
this.state = {
scenes: [
{key: ‘home‘}, // 表示应用程序主页
],
};
}
对于应用程序的render function, 我们想要显示scene stack中最顶层/当前(scenes数组中最后一项)的scene.
render() {
const scene = this.state.scenes[this.state.scenes.length - 1];
if (scene.key === ‘home‘) {
return <HomeView />;
}
if (scene.type === ‘chat‘) {
return <ChatView id={scene.key} />;
}
return null;
}
为了打开聊天页,我们要添加一个openChat的方法
render() {
const scene = this.state.scenes[this.state.scenes.length - 1];
if (scene.key === ‘home‘) {
return <HomeView />;
}
if (scene.type === ‘chat‘) {
return <ChatView id={scene.key} />;
}
return null;
}
如果我们想要返回,我们需要实现一个back方法
goBack() {
if (this.state.scenes.length > 1) {
this.setState({
scenes: this.state.scenes.slice(0, this.state.scenes.length - 1),
});
}
}
可是,这会变得难以维护,因为在你的应用中,每次导航都依赖于具体的方法。为此,我们需要将所有的导航逻辑都委托给一个reducer进行处理, 所以我们需要修改上面的代码。上面的代码修改如下。
constructor(props) {
super(props);
this.state = AppReducer(null, { type: ‘init‘ });
}
dispatch(action) {
this.setState(AppReducer(this.state, action));
}
我们的reducer看起来如下所示,一个Reducer接受上一次的状态,以及一个action, 同时返回一个state. 可以在NavigationExperimental文档中,查看Reducer的定义
function AppReducer(lastState, action) {
let state = lastState;
if (!state) {
state = {
scenes: [
{key: ‘home‘}
],
};
}
if (action.type === ‘back‘ && state.scenes.length > 1) {
return {
scenes: state.scenes.slice(0, this.state.scenes.length - 1),
};
}
if (action.type === ‘openChat‘) {
return {
scenes: [
...state.scenes,
{
type: ‘chat‘,
key: action.id
}
],
};
}
return state;
}
现在,我们可以非常容易的实现我们的导航的方法,如下所示
openChat(id) {
this.dispatch({ type: ‘openChat‘, id });
}
goBack() {
this.dispatch({ type: ‘back‘ });
}
我们现在实现了this.dispatch方法,所以通过props,将dispatch方法传定给子组件(子页面). 那么就可以在子页面中访问到dispatch action.
render() {
const scene = this.state.scenes[this.state.scenes.length - 1];
if (scene.key === ‘home‘) {
return (
<HomeView
dispatch={this.dispatch.bind(this)}
/>
);
}
if (scene.type === ‘chat‘) {
return (
<ChatView
id={scene.key}
dispatch={this.dispatch.bind(this)}
/>
);
}
return null;
}
function HomeView(props) {
return
<Text
onPress={() => {
props.dispatch({ type: ‘openChat‘, id: ‘A‘ });
}}>
This is the home screen. Tap to open Chat A.
</Text>;
}
function ChatView(props) {
return
<Text
onPress={() => {
props.dispatch({ type: ‘back‘ });
}}>
This is chat {props.id}. Tap to go back home.
</Text>;
}
现在我们的应用就可以打开一个chat view 和一个主页,完成的代码如下
function MyChatAppReducer(lastState, action) {
let state = lastState;
if (!state) {
state = {
scenes: [
{key: ‘home‘}
],
};
}
if (action.type === ‘back‘ && state.scenes.length > 1) {
return {
scenes: state.scenes.slice(0, state.scenes.length - 1),
};
}
if (action.type === ‘openChat‘) {
return {
scenes: [
...state.scenes,
{
type: ‘chat‘,
key: action.id
}
],
};
}
return state;
}
function HomeView(props) {
return (
<Text
onPress={() => {
props.dispatch({ type: ‘openChat‘, id: ‘A‘ });
}}>
This is the home screen. Tap to open Chat A.
</Text>
);
}
function ChatView(props) {
return (
<Text
onPress={() => {
props.dispatch({ type: ‘back‘ });
}}>
This is chat {props.id}. Tap to go back home.
</Text>
);
}
class MyChatApp extends React.Component {
constructor(props) {
super(props);
this.state = MyChatAppReducer(null, { type: ‘init‘ });
}
dispatch(action) {
this.setState(MyChatAppReducer(this.state, action));
}
render() {
return (
<View style={styles.container}>
{this.renderCurrentScene()}
</View>
);
}
renderCurrentScene() {
const scene = this.state.scenes[this.state.scenes.length - 1];
if (scene.key === ‘home‘) {
return (
<HomeView
dispatch={this.dispatch.bind(this)}
/>
);
}
if (scene.type === ‘chat‘) {
return (
<ChatView
id={scene.key}
dispatch={this.dispatch.bind(this)}
/>
);
}
return null;
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: ‘white‘,
padding: 10,
paddingTop: 30,
},
});
NavigationExperimental 文档
Navigation State
你整个app的导航状态(state)可以被NavigationStates模式化, 一个NavigationState是一个对像。
const myState = {
key: ‘myPage0‘,
myType: ‘ExamplePage‘,
myParams: {foo:‘bar‘},
}
一个NavigationParentState 包含一组路由(routes), 并且有一个index字段,表示当前的路由
const myState = {
key: ‘myAppTabs‘,
routes: [
{key: ‘home‘},
{key: ‘notifs‘},
{key: ‘settings‘},
],
index: 1, // points to the ‘notifs‘ tab
}
navigation state types在NavigationStateUtils中保存,同时在NavigationStateUtils还有一些函数,通过这些函数可以改变NavigationParentState。
Containers
在NavigationExperimental中提供了一个最顶级的组件,用于维护导航的状态以及处理永久性(将导航保存到硬盘或者从硬盘中读取导航的状态数据)。
如果你使用redux 或者flux, 你可以不需要NavigationContainer. 你可以使用现有的stores and providers.
NavigationRootContainer
开发者可以为根容器设置一个reducer, reducer会包含整个app的导航逻辑。我们的navigation reducers将会接受最后的导航状态,一个我们需要处理的action. 然后它为我们的app输出一个新的导航装态。为了获得初始化的state, reducers可以在调用时,不需要上一个状态或者action.
<NavigationRootContainer
reducer={MyReducer}
renderNavigation={(navigationState, onNavigate) => (
<Text>Currently at {navigationState.routes[navigationState.index]}</Text>
它也提供了一个针对navigation action的处理器,并且允许reducer被自定义.
NavigationContainer.create
在整个应用中,都要传递onNavigate会非常繁锁,因此我们可以提供一个更高阶的”container”组件。
<NavigationRootContainer
reducer={MyReducer}
renderNavigation={(navigationState) => <ExampleComponent />}
...
class ExampleComponent {
render() {
<Text onPress={() => { this.props.onNavigate(new ExampleAction()) }}>
This action will work, even though `onNavigate` was not directly passed in
</Text>
}
}
ExampleComponent = NavigationContainer.create(ExampleComponent);
如果onNavigation作为一个属性被传递给container, 它会覆盖处理程序中包含的组件和所有的子容器.
Reducers
一个导航的reducer是一个action 处理器,它返回当前的navigation state.当调用navigation reducers, 你要提供一个可选的previous state和一个字符串类型的 navigation action.
let state = MyReducer(null, { type: ‘InitialAction‘ });
//output
> {
key: ‘Root‘,
index: 0,
routes: [
{key: ‘Home‘},
]
}
state = MyReducer(state, { type: ‘PushPerson‘, name: ‘Christopher‘ });
//output
> {
key: ‘Root‘,
index: 1,
routes: [
{key: ‘Home‘},
{key: ‘Person0‘, name: ‘Christopher‘},
]
}
Stack Reducer
常见的导航逻辑是一个’stack’(栈), 这可以通过stack reducer来处理
const MyReducer = NavigationStackReducer({
// First, define the initial parent state that will be used if there was no previous state.
initialState: {
key: ‘Root‘,
index: 0,
routes: [
{key: ‘Home‘},
]
},
getPushedReducerForAction: (action) => {
if (action.type === ‘PushPerson‘) {
// We need to push some additional state, that will be defined by this reducer:
return () => ({
key: ‘Person‘+(i++),
name: action.name,
});
}
// In this case we do not need to push, so our reducer for this action is nothing
return null;
},
});
let state = MyReducer(null, { type: ‘InitAction‘ });
> {
key: ‘Root‘,
index: 0,
routes: [
{key: ‘Home‘},
]
}
state = MyReducer(state, { type: ‘PushPerson‘, name: ‘Christopher‘ });
> {
key: ‘Root‘,
index: 1,
routes: [
{key: ‘Home‘},
{key: ‘Person0‘, name: ‘Christopher‘},
]
}
// The back action can be used to pop:
state = MyReducer(state, NavigationRootContainer.getBackAction());
> {
key: ‘Root‘,
index: 0,
routes: [
{key: ‘Home‘},
]
}
stack reducer中也可以包含sub-reducers, 它需要你实现getReducerForState. 它会为sub-state 返回一个sub-reducer. 当前的sub-state的sub-reducer将会被使用.
Tabs Reducer
Tabs reducer允许你有多个子sub-reducers, 但有一个是激活状态。对于每一个action, 都会被发送给tabs reducer, 它会首先使用active状态的sub-reducer. 如果reducers没有返回一个新的sub-state, 则另外的reducers将会获得机会,并进行处理。如果一个不同的tab reducer处理了它,tabs reducer将返回一个新的new sub-state, 并且交换active tab.
Find Reducer
Reducers的一个常见模式是组合了多个reducers, 当其中一个reducer返回一个新的state时停止。Find Reducer会接受一个reducers数组,然后遍历数据中的每一个元素,直到state改变时,返回这个reducer. 如果这些reducers没有返回一个新的state, find reducer将返回默认的state.
Views
NavigationView
最简单的视图是render当前sub-state的场景(scene). 常用于tabs, 因为它不需要转换。
NavigationAnimateView
NavigationAnimateView 采用声明API, 它使用Animate library向scenes委派动画和手势
NavigationCard和NavigationHeader就是场景和叠加的NavigationAnimateView。这是为了看起来跟iOS或android一样。
NavigationCard
<NavigationAnimatedView
navigationState={navigationState}
renderScene={(props) => (
<NavigationCard
key={props.navigationState.key}
index={props.index}
navigationState={props.navigationParentState}
position={props.position}
layout={props.layout}>
<MyInnerView info={props.navigationState} />
</NavigationCard>
)}
/>
NavigationHeader
<NavigationAnimatedView
navigationState={navigationState}
renderOverlay={(props) => (
<NavigationHeader
navigationState={props.navigationParentState}
position={props.position}
getTitle={state => state.key}
/>
)}
renderScene={this._renderScene}
/>
NavigationCardStack
包装了NavigationAnimateView,可以为每一个Scene,render一个NavigationCard. 类似于过时的Navigator. 这是因为它内置了animations和gestures
使用:
render() {
return (
<NavigationCardStack
style={styles.main}
renderScene={props =>
<MyPetView
name={props.navigationState.key}
species={props.navigationState.species}
/>
}
renderOverlay={props => <NavigationHeader {...props} />}
navigationState={{
key: ‘MyPetStack‘,
index: 2,
routes: [
{key: ‘Pluto‘, species: ‘dog‘},
{key: ‘Snoopy‘, species: ‘dog‘},
{key: ‘Garfield‘, species: ‘cat‘},
]
}}
/>
);
}
NavigationExperimental 实际例子
UIExplorer Example中的入口文件为UIExplorerApp.android.js,在这个文件中,主要的也是App的一些初始化操作,比如state, reducer等导航相关 设置。