原文地址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;