原文地址html
关于reactnaitve的导航,官方提供了2个组件,NavigatorIOS和Navigator,其中官方并不推荐使用NavigatorIOS,它不是官方维护的,不能保证及时的更新和维护。react
因此本文中是以Navigator组件为基础,进行导航的设计和实现。android
Navigator的劣势:Navigator组件是纯js的实现,因此在页面进行转场动画的过程当中,若是js不能保证在16ms内完成其它操做的话,转场动画会有卡顿现象,后面会介绍优化的方案。git
官方的Navigator组件使用方式较为灵活,本文的目的是选取一种最佳用法,并提取出通用功能应对经常使用场景,规范和设计项目中导航的使用。github
rn应用:全站rn应用,简称rn应用。算法
rn模块:部分模块使用rn,简称rn模块。react-native
rn首页:不管是rn应用仍是rn模块,进入rn页面的第一屏,简称rn首页。数组
nav:Navigator组件对象的简称,注意是实例化好的对象,不是类。页面间传递的导航对象统一使用此命名。服务器
Header:自定义的导航栏组件。app
一个rn应用或者一个rn模块,有且只有一个Navigator组件被定义。
在rn首页定义Navigator组件。
各个子页面统一使用首页定义的Navigator组件对象nav。
不要使用Navigator的navigationBar,请自定义导航栏组件,例如Header组件。
在rn首页中的render方法中,定义一个Navigator组件,并作好如下几件事:
实现好通用的renderScene方法,
实现好android的物理返回按键
初始化真正的rn首页
renderScene函数是Navigator组件的必填函数,入参是route对象和当前的nav对象,返回值是jsx。
此函数的意思是根据传入的route,返回一个做为新页面的jsx,也就是说全部的路由算法都是在此函数中实现的。
其中route对象是一个自定义的对象,是nav.push方法中传入的对象。
此函数设计至关于门面模式,此函数是路由的统一处理器,全部的页面跳转请求都会经过此函数的计算来得到具体的jsx页面。
既然是统一的路由处理器,必然要求传入的route对象要知足统一的规则,不然没法实现统一的算法。
在此,设计route对象以下:
{ name: 'page2', //名字用来作上下文判断和日志输出 page: <Page2 />, //jsx形式的page,做为新页面的jsx // page: () => <Page2 />, //或者函数形式的page,此函数必须返回jsx,此jsx做为新页面的jsx }
根据route对象设计,设计统一的renderScene方法以下:
_renderPage(route, nav) { if (!route.page) { console.error('页面导航请求没有传入page参数.'); return null; } let page; if (typeof route.page === 'function') { page = route.page(); } else { page = route.page; } let name = route.name; if (!name) { if (page) { name = page.type.name; } } console.log(`in render page ${name}`); return page; }
业务代码中,页面跳转的时候,只须要以下代码
nav.push({ name: 'page2', page: <Page2 nav={nav}/>, });
若是你的应用须要支持android的话,那就要实现andorid的物理返回按键的对应处理。
通常按物理返回按键要么是返回上一页面,要么是返回页面的上一状态【例如,有打开的弹窗,按返回是关闭这个弹窗】。
返回上一页面由于有通用路由器的存在,因此能够通用处理,直接使用nav.pop()便可。
可是返回页面上一状态,并不容易统一处理,因此使用基于事件扩展的方式,交给业务代码自行实现。
在此重构route对象的规则,添加事件onHardwareBackPress,以下
{ name: 'page2', //名字用来作上下文判断和日志输出 page: <Page2 />, //jsx形式的page,做为新页面的jsx // page: () => <Page2 />, //或者函数形式的page,此函数必须返回jsx,此jsx做为新页面的jsx onHardwareBackPress: () => alert('点物理按键会触发我'), // 返回false就终止统一路由器的默认动做,即终止页面返回动做,能够在此方法中实现返回页面上一状态的相关实现 }
android物理返回按键的统一处理代码以下,
componentWillMount() { BackAndroid.addEventListener('hardwareBackPress', () => { if (this.refs.nav) { let routes = this.refs.nav.getCurrentRoutes(); let lastRoute = routes[routes.length - 1]; // 当前页面对应的route对象 if (lastRoute.onHardwareBackPress) {// 先执行route注册的事件 let flag = lastRoute.onHardwareBackPress(); if (flag === false) {// 返回值为false就终止后续操做 return true; } } if (routes.length === 1) {// 在第一页了 // 此处能够根据状况实现 点2次就退出应用,或者弹出rn视图等 } else { this.refs.nav.pop(); } } return true; }); }
此处较为简单,直接使用Navigator组件的initialRoute属性来指定初始化的route对象。
<Navigator initialRoute={{ page: <Home />, // Home为伪代码,自定义的首页组件 name: 'home', }} />
根据前面设计好的renderScene方法,直接使用以下代码,便可跳转到Page2,并将nav对象传递给了Page2.
nav.push({ name: 'page2', page: <Page2 nav={nav}/>, });
页面返回直接使用
nav.pop();
前面提到,Navigator组件彻底使用js实现,因为js的单线程特色,若是在页面转场动画过程当中,js干其余事情【好比渲染个某个jsx】超过了16ms,那么转场动画将不足60帧,给用户的感受就是动画有卡顿。
为了不这种状况,一种简单粗暴的办法就是在转场动画中不要让js来干别的事情。
那么咱们如何知道转场动画何时结束呢,官方提供了动画交互管理器InteractionManager,示例伪代码以下:
InteractionManager.runAfterInteractions(() => { alert('哈哈 转场动画结束了!'); });
大多数的场景:点击page1的某个按钮,要跳转到page2,而且page2要和服务器请求数据,根据返回的数据来渲染page2的部分or所有内容。
针对上述场景,解决方案以下,用伪代码描述:
page2的state至少有2个值,转场动画进行中=true,服务器查询=true
page2的componentWillMount方法中发起异步服务器交互请求,当请求结束setState:服务器查询=false
page2的componentWillMount方法中注册InteractionManager.runAfterInteractions事件,当转场结束setState:转场动画进行中=false
page2的render方法中,先判断(转场动画进行中=true || 服务器查询=true)就返回一个loading的提示,不然返回真正的jsx,而且此时,服务器返回的数据已经可用了
也能够参考官方文档: http://reactnative.cn/docs/0.22/performa...
目标:实现相似于html中window.reload的方法。
因为咱们对route的规则限定,因此咱们能够作到统一的刷新页面的逻辑。
思路是
首先得到当前页面对应的route对象
而后获取route中的page属性,page属性多是当前页面的jsx,也多是能够产生当前页面jsx的方法
最后使用官方Navigator组件提供的replace方法,来用新的route替换掉原有的route
示例参考代码以下:
/** * 刷新页面,route能够为空,会刷新当前页面 * @param nav * @param route */ refresh(nav, route) { if (!route) { let routes = nav.getCurrentRoutes(); let length = routes.length; route = routes[length - 1]; // 使用当前页对应的route } // todo 最好的方式是直接使用route.page,可是很差使,这种写法只支持一层节点,若是有多层会有问题 // todo 暂时未处理page是function的状况 let Tag = route.page.type; nav.replace({ page: <Tag {...route.page.props} />, }); }
而后业务代码中这样调用,当前页面就被刷新了。
Util.refresh(nav); //Util是伪代码,是你定义refresh方法的对应对象
若是你开发的是rn模块【rn模块嵌入到已有app中,定义能够参考前面定义一节】,可能进入rn模块的入口会不少,好比,用rn开发一个论坛模块,正常入口进来是直接展示帖子列表,也可能会有点击某个其它按钮【此按钮是否是rn的】会直接跳转到某个帖子的详情页。
使用官方Navigator组件提供的initialRouteStack属性,能够完美的解决此问题,官方文档对此属性的说明以下:提供一个路由集合用来初始化。若是没有设置初始路由的话则必须设置该属性。若是没有提供该属性,它将被默认设置成一个只含有initialRoute的数组。
说白了就是,initialRouteStack要定义一个数组,里面是不少route对象,而后Navigator对象会展示到最后一个,并且数组中的其余route也都被初始化过了,你想返回到任何一个route都是能够的,是否是爽歪歪了。
给个示例代码吧,这是我项目中真正的代码,请当伪代码来阅读:
getInitialRouteStack() { let props = this.getProps(); let detailId = props.detailId; if (detailId) { // 若是传入了详情id,那么跳转到详情页 return [{name: 'home', }, { page: <AskDetail data={{id: detailId, }}/>, backIsClose: true, }]; } let wantAsk = props.wantAsk; if (wantAsk === true || wantAsk === 'true') { // 若是传入了提问属性=true,那么直接跳转到提问页面 return [{name: 'home', }, { page: <WantAsk backIsClose={true}/>, backIsClose: true, }]; } // 跳转到首页 return [{name: 'home', }]; }
根据以上设计思路,笔者封装了一个Navigator组件,是对官方的navigator组件进行了一层封装,供你们参考:
import React from "react-native"; const { Platform, Animated, View, DeviceEventEmitter, Dimensions, Navigator, BackAndroid, } = React; class Navigator2 extends React.Component { componentWillMount() { BackAndroid.addEventListener('hardwareBackPress', () => { if (this.refs.nav) { let routes = this.refs.nav.getCurrentRoutes(); let lastRoute = routes[routes.length - 1]; if (lastRoute.onHardwareBackPress) {// 先执行route注册的事件 let flag = lastRoute.onHardwareBackPress(); if (flag === false) {// 返回值为false就终止后续操做 return true; } } if (routes.length === 1) {// 在第一页了 if (this.props.nav) {// 父页面仍有nav this.props.nav.pop(); } if (this.props.onHardwareBackPressInFirstPage) { this.props.onHardwareBackPressInFirstPage(); } } else { if (lastRoute.backIsClose === true) { if (this.props.onHardwareBackPressInFirstPage) { this.props.onHardwareBackPressInFirstPage(); } } else { this.refs.nav.pop(); } } } return true; }); } getLastRoute() { if (this.refs.nav) { let routes = this.getCurrentRoutes(); let lastRoute = routes[routes.length - 1]; return lastRoute; } return null; } render() { return <Navigator renderScene={this._renderPage.bind(this)} {...this.props} ref='nav' />; } _renderPage(route, nav) { if (!route.page) { console.error('页面导航请求没有传入page参数.'); return null; } let page; if (typeof route.page === 'function') { page = route.page(); } else { page = route.page; } let name = route.name; if (!name) { if (page) { name = page.type.name; } } console.log(`in render page ${name}`); return page; } // todo 如下的方法为实现原版navigator的方法,这样作很差,可是没想到其它好办法 getCurrentRoutes() { return this.refs.nav.getCurrentRoutes(...arguments); } jumpBack() { return this.refs.nav.jumpBack(...arguments); } jumpForward() { return this.refs.nav.jumpForward(...arguments); } jumpTo(route) { return this.refs.nav.jumpTo(...arguments); } push(route) { return this.refs.nav.push(...arguments); } pop() { return this.refs.nav.pop(...arguments); } replace(route) { return this.refs.nav.replace(...arguments); } replaceAtIndex(route, index) { return this.refs.nav.replaceAtIndex(...arguments); } replacePrevious(route) { return this.refs.nav.replacePrevious(...arguments); } immediatelyResetRouteStack(routeStack) { return this.refs.nav.immediatelyResetRouteStack(...arguments); } popToRoute(route) { return this.refs.nav.popToRoute(...arguments); } popToTop() { return this.refs.nav.popToTop(...arguments); } } module.exports = Navigator2;