定制一款功能强大的react-native导航组件-ANavig
ANavigator稍微复杂点的移动应用都会有页面跳转的场景,即用户在页面A上点击某个功能,比如查看内容详情或者帮助信息,跳转到下个页面B,在用户处理完B后,通过滑动返回,或者返回按钮返(android中的返回键)回到原页面A。有页面跳转的地方就会涉及到页面栈的管理,在IOS原生应用中使用UINavigationController来管理页面栈,react-native(RN)中也提供了Navigator来管理页面栈。目前RN中提供了3中Navigator:Navigator,NavigatorIOS, NavigationExperimental。NavigatorIOS是基于IOS原生NavigationController的,一般RN代码都考虑夸平台,所以不太会用它。NavigationExperimental目前项目中还未大量使用,如果需要,以后可以定制一个基于此的ANavigator。本文主要是基于Navigator的创建定制一个通用的ANavigator,除了有基本的页面切换功能外,还希望实现以下功能:1. 便捷的控制左右按钮,title2. 统一的RN页面返回原生页面3. 处理android的返回按键4. 支持滑动返回5. 页面进入,离开的通知消息6. 可定制化的行为如导航栏,页面切换方式等## 一,初步了解Navigator1,Navigator除了push,pop外,还有几个方法贯穿Navigator的活动周期,理解这些方法对可以更好地使用Navigator完成需要实现的功能。* configureScene:Navigator render之前调用,可以用来设置页面切换的动画,滑动返回参数* renderScene:Navigator render的时候调用,在此构建新页面,可以传递参数给新页面* onDidFocus:RN页面显示的时候,不论push到新页面,还是pop回老页面,会被调用,参数是显示页面的route* 除此之外,Navigator还接受一个navigationBar参数,该NavigatorBar可以提供LeftButton,RightButton,Title三个函数设置新页面的左右按钮,以及Title,可以提供不同的参数定制NavigatorBar显示。不传的时候,不使用Navigator自带的导航栏。2,为实现上面的功能,需要扩展Navigator的功能,那问题来了,选择‘继承’还是‘组合’呢?本来打算通过继承Navigator,再在此基础上重载一些方法来定制化其功能,但后来发现行不通。主要原因:Navigator在.30版本还是通过React.CreateClass创建的,与ES6支持的extends并不能很好地兼容,继承的时候不能覆盖相应的方法,没法扩张,所以只能放弃继承。可以参考:[http://stackoverflow.com/questions/35909476/how-to-extend-react-component-class-created-via-react-createclass](http://)(You can't extend an old style component with an ES6 one, at least not with the ES6 extends syntax)。所以只能新建一个类,在内部‘组合使用’Navigator类,新建的ANavigator大致这样:```import Navigator from ‘Navigator';class ANavigator extends Component {reder() {…return (}```在此情况下如何扩张Navigator的功能呢?覆盖Navigator的方法在使用到Navigator的时候,判断对应的方法是否被重载过,如果没有重载这些方法。可以放在每次push, pop页面的时候,renderScene被调用。改进后的renderScene如下:```renderScene(route, navigator) {…if(navigator.overrideMethod)return;let {pop} = navigator;navigator.pop = function(route){…pop.apply(this,arguments);…};navigator.overrideMethod = true;…}```3,组件与route 组件是什么,route又是什么,两者的关系如何? Navigator中的组件是负责页面功能的对象,是真正承担显示,逻辑工作的对象。route是用来给Navigator管理组件的映射,可能会带一些参数,用来控制组件的具体功能。两者没有直接的关系。为了后续的一些功能,需要建立两者的联系。从上面讨论的Navigator生命周期函数可以看到,renderScene是创建组件的地方,而且有route信息,可以在此建立两者关系。renderScene创建组件的时候把route传给组件```renderScene(route, navigator) {…let Screen = route.screen;route.props = route.props || {};return ();}```这样在组件内部可以将组件的this传给route,这样route就可以通过这个对象访问组件的所有参数,方法了。```constructor(props) {...props.route.component = this;...}```组件访问route的方法:props.routeroute访问组件中变量,方法:route.component.***## 二,实现预定的功能准备工作差不多了,下面考虑如何实现预定的几个功能。1)便捷的控制左右按钮,titlenavigatorBar在定义Navigator的时候就要提供,如果把导航栏显示逻辑都放在这里而具体显示的内容又要根据不同的组件有所不同,则navigatorBar就成了一个耦合点,关联了所有页面,不利于解耦。为了能够便捷地控制左右按钮等信息,希望把设置内容的逻辑放到当前显示的组件中,在Navigator的navigatorBar中仅仅读取这些信息并显示,而前面的准备工作3中关联了route和组件,因此可以在组件中设置信息供以后在route中使用。在组件中可以这样定制左按钮:```constructor(props) {…this.leftButton = this.leftButton.bind(this);props.route.leftView = this.leftButton;…}```具体的设置逻辑在leftButton中:leftButton(route, navigator) { …}navigationBar的leftButton的实现如下:const NavigationBarRouteMapper = { // 左边Button LeftButton(route, navigator, index) { let leftView; if (route.leftView) { leftView = route.leftView(route, navigator); return leftView; }….}这样的话可以在组件中定义左右按钮,title等,navigationBar仅仅负责调用各自的方法。组件之间,组件与Navigator相互独立,navigator功能单一,便于维护。2)从RN页面退回到原生页面统一到pop中处理,如果pop的时候发现当前是最后一页,Android调用BackAndroid.exitApp(),IOS调用原生的pop。```navigator.pop = function(route){…let length = navigator.getCurrentRoutes().length;if(length>1){pop.apply(this,arguments);} else {if (Platform.OS === 'android') {BackAndroid.exitApp();} else if (Platform.OS === 'ios') {UIManagerNativeModule.popViewControllerAnimated(animated);}}};```3)处理android的返回按键接受hardwareBackPress事件,转化为Navigator的pop操作,实际的控制逻辑放在pop中处理,包括判断是否需要返回原生页面。```BackAndroid.addEventListener('hardwareBackPress', handleBackEvent);handleBackEvent() {…navigator.pop();…}```4)支持滑动返回根据当前RN页面在Navigator栈中位置,滑动返回分两种情况处理:* 第一个RN页面,即Navigator的根页面,这时没法通过Navigator返回,可以通过原生层支持滑动返回* 不是第一个RN页面,可以通过控制RN的Navigator的滑动返回,为了原生滑动不影响RN返回,需要禁止原生层的滑动返回这里主要介绍RN中滑动返回的控制,原生的滑动返回就不详细介绍了,由于苹果默认的滑动返回行为限制,我们使用的是FDFullscreenPopGesture控制原生的滑动返回。配置RN的滑动返回Navigator不仅可以配置push,pop的动画,也可以配置滑动返回的参数。具体配置是在Navigator的SceneConfigs中,它提供了多种页面切换的方式,如PushFromRight, HorizontalSwipeJump等。在IOS中常用的PushFromRight,新页面会从右侧进入,它使用的pop手势默认参数值如下为:```var BaseLeftToRightGesture = {…// Region that can trigger swipe. iOS default is 30px from the left edge edgeHitWidth: 30,…direction: 'left-to-right',};```可以通过设置edgeHitWidth控制是否可以滑动返回。如果设置为0,则表示禁止滑动返回,如果设置屏幕宽度,则相当于支持全屏幕滑动返回手势。可以通过一个[0,1]值来配置支持的滑动范围。```configLeftToRightPopRange (range) {let width = 0;if (range >= 0 && range <= 1) {width = range * kScreenWidth;}const pushFromRight = Navigator.SceneConfigs.PushFromRight;pushFromRight.gestures.pop.edgeHitWidth = width;return pushFromRight;}```enable/disable原生滑动返回功能根据navigator中管理的route数判断是否是第一个RN页面```handleLeftPanGesture(navigator) {const routes = navigator.getCurrentRoutes();const len = routes.length;// open native LefToRight GestureUIManagerNativeModule.configNativeLeftToRightGesture(findNodeHandle(navigator),len === 1);}```5)页面进入,离开的通知消息页面的进入离开根据参与的页面是RN的还是原生的可以分* RN页面间的页面间的切换* RN与原生页面间的切换RN页面间的进入离开不论是第一次显示,还是push,pop,所有页面显示的时候都会调起onDidFocus事件,onDidFocus的参数是正在显示的新页面的route,因此可以在此作页面进入,离开(进入新页面就意味着离开老页面)通知。```_onDidFocus(route) {… this.pageOut(); this.pageIn(pageName); …}```RN与原生页面间的切换除了纯RN页面间的切换外,实际产品中往往会有RN管理的页面与原生的页面发生交互,大致有两种情况* 在RN A页面切换到原生B页面* 在原生B页面完成操作回到页面A(原生页面打开RN页面,也算交互之一,在RN第一次显示中解决了,不需要这里再处理了。)这两种情况发生时,RN的Navigator不会有任何通知发出,所以仅仅依靠RN代码没法做页面进入/离开的处理工作。必须借助原生代码通知RN页面。其实任何一个RN页面都是通过原生ViewController加载的,只不过View是通过RN的RootView提供的。在这两种页面切换情况发生时,RN所在的原生页面都会发生viewWillAppear,viewWillDisappear,会被正确调起,可以在对应的地方发送通知给RN做对应的处理工作。考虑到可能有多个Navigator同时存在(比如一个TabController容器中有多个RN Navgator管理各自页面),所以需要一个标志区分各个Navigator,在页面显示/隐藏时通过原生VC传给RN,RN的Navigator根据这个标志判断自己是否需要处理该通知。具体做法可以从原生代码生成一个UUID,在创建RN页面的时候传入,最后由Navigator保存。如果该VC是第一次打开,这是RN初次显示,如果不是第一次显示,则是原生页面返回,RN页面显示,应该通知对应的Navigator,向其管理的顶级页面发送顶级页面进入通知```- (void)viewWillAppear:(BOOL)animated {…if (!self.firstTimeLoad) {[((RCTRootView *)self.view).bridge.eventDispatcher sendAppEventWithName:@"_pop_" body:@{@"tag":self.tag}];}…}```当该VC不管因为何种原因disappear时,应该通知对应的Navigator,向其管理的顶级页面发送页面离开通知```- (void)viewWillDisappear:(BOOL)animated {…[((RCTRootView *)self.view).bridge.eventDispatchersendAppEventWithName:@"_push_" body:@{@"tag":self.tag}];…}```6)可定制化的行为如导航栏,页面切换方式等另外。为了提供一定的定制化功能,Navigator接受外部的renderScene, configureScene,navigationBar,如果有外部传入值的话,使用外部的值所以改进后的ANavigator是这样的:```class ANavigator extends Component {reder() {…return (} _configureScene(route) {const customizedConfig = route.SceneConfigs || this.props.configureScene;if (customizedConfig) {return customizedConfig(route);}return this.configureScene();}configureScene() {…}_renderScene(route, navigator) {this.navigator = navigator;return this.renderScene(route, navigator);}renderScene(route, navigator) {if (this.props.renderScene) {return this.props.renderScene(route, navigator);}…}_onDidFocus(route) {if (this.props.onDidFocus) {this.props.onDidFocus();}…}```至此,ANavigator实现完成,除了完成页面跳转,配合原生代码还实行了前文所列的几个常用功能,功能完备,便于维护,使用方便。如果还有别的功能可以通过route传入,甚至继承ANavigator覆盖其方法。sample代码见:[https://github.com/PingAnYee/ANavigator](http://)参考:[http://bbs.reactnative.cn/topic/20/新手理解navigator的教程](http://)[https://facebook.github.io/react-native/releases/next/docs/using-navigators.html](http://)[https://facebook.github.io/react-native/releases/next/docs/navigation.html](http://)