ReactNative導航設計與實現

原文地址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組件。

Navigator組件的定義和初始化

在rn首頁中的render方法中,定義一個Navigator組件,並作好如下幾件事:

  1. 實現好通用的renderScene方法,

  2. 實現好android的物理返回按鍵

  3. 初始化真正的rn首頁

實現統一路由函數renderScene

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物理返回按鍵的處理

若是你的應用須要支持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;
        });
    }

初始化真正的rn首頁

此處較爲簡單,直接使用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所有內容。

針對上述場景,解決方案以下,用僞代碼描述:

  1. page2的state至少有2個值,轉場動畫進行中=true,服務器查詢=true

  2. page2的componentWillMount方法中發起異步服務器交互請求,當請求結束setState:服務器查詢=false

  3. page2的componentWillMount方法中註冊InteractionManager.runAfterInteractions事件,當轉場結束setState:轉場動畫進行中=false

  4. page2的render方法中,先判斷(轉場動畫進行中=true || 服務器查詢=true)就返回一個loading的提示,不然返回真正的jsx,而且此時,服務器返回的數據已經可用了

也能夠參考官方文檔: http://reactnative.cn/docs/0.22/performa...

刷新的實現

目標:實現相似於html中window.reload的方法。

因爲咱們對route的規則限定,因此咱們能夠作到統一的刷新頁面的邏輯。

思路是

  1. 首先得到當前頁面對應的route對象

  2. 而後獲取route中的page屬性,page屬性多是當前頁面的jsx,也多是能夠產生當前頁面jsx的方法

  3. 最後使用官方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模塊【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;

參考地址

http://reactnative.cn/docs/0.21/navigato...

http://reactnative.cn/docs/0.22/performa...

相關文章
相關標籤/搜索