hippy-react 三端同構 — 路由

1. 背景介紹

Hippy 提供了 Navigator 組件,用於頁面導航、跳轉。

但是 Navigator組件有比較大的局限性, 該組件通過啟動一個新的 Hippy 實例實現,在 2.0 下實例之間可能無法互相通訊,iOS 上也必須作為根節點包裹所有子組件,使用有很大限制。

@hippy/react 以及 @hippy/react-web 中的 Navigator 組件功能相對比較欠缺,兩端都沒有比較好的實現頁面跳轉的功能。兩端的功能也存在著差異,導致無法實現原生和web的同構

以下是 @hippy/react@hippy/react-web 中的 Navigator 組件的實現方式

1.1 @hippy/react 路由實現

Navigator 組件中,通過實例化一個 Hippy 實例進行渲染展示,同時對 Android 的回退鍵進行監聽

// https://github.com/Tencent/Hippy/blob/312d0d963cac2d8cf60ff97ddd554a01e575cea0/packages/hippy-react/src/components/navigator.tsx#L125    constructor(props: NavigatorProps) {        super(props);        const { initialRoute } = props;        if (initialRoute && initialRoute.component) {          const hippy = new Hippy({            appName: initialRoute.routeName,            entryPage: initialRoute.component,          });              hippy.regist();              this.routeList[initialRoute.routeName] = true;        }        this.handleAndroidBack = this.handleAndroidBack.bind(this);    }

通過調用原生 callUIFunction 執行頁面返回

public pop(option: { animated: boolean }) {        if (this.stack.size > 1) {          const options = [option];          this.stack.pop();          callUIFunction(this.instance, 'pop', options);        }    }

這種方式,兩個頁面之間無法進行數據互通,也無法傳遞數據

1.2 @hippy/react-web 路由實現

相比於 @hippy/react@hippy/react-web 中的 Navigator 組件則沒有對應的實現功能

//https://github.com/Tencent/Hippy/blob/master/packages/hippy-react-web/src/components/navigator.tsx    /* eslint-disable class-methods-use-this */        import React from 'react';    import { formatWebStyle } from '../adapters/transfer';        /**     * Simply router component for switch in multiple Hippy page.     * @noInheritDoc     */    class Navigator extends React.Component {      pop() {        // TODO      }          push() {        // TODO      }          render() {        const { style } = this.props;        const newProps = Object.assign({}, this.props, {          style: formatWebStyle(style),        });        return (          <div {...newProps} />        );      }    }        export default Navigator;

2. hippy項目路由實現

使用 react-router 來管理多頁面,實現 Hippy 原生和web的多頁面切換

2.1 hippy router選擇

react 中,主要是由 react-router 來進行頁面切換,支援多頁面開發。同時也有native版的 react-router-native

react-router-nativereact-router 的native版本,但是其基於 react-native 中比較完善的 Navigator 組件。經過分析和實現,無法在 Hippy 中直接使用 react-router-native

react-router 中的 MemoryRouter,基於純js實現的路由,不需要依賴於 URL,這使得其可以應用在native

下面這個是關於 MemoryRouter 描述

A <Router> that keeps the history of your 「URL」 in memory (does not read or write to the address bar). Useful in tests and non-browser environments like React Native.

因此使用 react-router 可以同時支援原生和web頁面切換,進行多頁面開發

2.1 hippy中react-router使用

  1. 通過 Platform.OS 對當前平台進行判斷
  2. 在原生項目中使用 MemoryRouter, 在web中使用 HashRouter
  3. 通過 react-router 對多頁面進行切換

以下是 hippyreact-router 的使用方式

import React, { Component } from 'react';    import {      StyleSheet,      View,    } from '@hippy/react';    import {      MemoryRouter,      HashRouter,      Route,    } from "react-router-dom";        import routes from './route';    import { ISWEB } from './utils';            export default class App extends Component {          render() {        const Router = ISWEB ? HashRouter : MemoryRouter;        return (            <View>                <Router>                        {                            routes.map(item => {                                return (                                    <Route key={item.path} exact path={`${item.path}`}><item.component meta={item.meta || {}} /></Route>                                  );                            })                        }                </Router>            </View>        );      }    }

3. hippy-react三端同構router使用

3.1 使用 react-router 存在的問題

react-router 能夠在一定層度上解決 hippy 中多頁面跳轉的功能,是也存在一些問題

  1. 原生切換沒有動畫,體驗與web的一樣
  2. 無法使用 react-router-transition 動畫
  3. 原生的返回操作,直接回關閉 hippy 項目
  4. Link 的使用過程中,需要傳入 component。 原因是 Link 組件默認使用 a 標籤,而 hippy 中不支援 a 標籤
// hippy 中 Link的使用方式    import { View } from '@hippy/react';        <Link to="/about" component={View}>About</Link>

3.2 頁面切換兼容

hippy 項目中頁面切換除了項目中的頁面切換,還存在著與客戶端或者瀏覽器的交互

hippy 頁面切換到客戶端原生頁面,需要客戶端提供偽協議跳轉支援。在web環境下,需要使用瀏覽器基礎能力。因此需要進行兼容處理

hippy 項目中的頁面切換主要有一下三種場景

場景 | 處理方式

—|—

hippy 項目內 | react-router

hippy -> 原生 | 原生偽協議支援

hippy -> web頁面 | window.location 或者 window.open

3.2.1 頁面切換兼容

* **原理分析**

react-router 通過 Context 將跳轉路由的函數, 如 goback, push,傳遞給組件

當組件需要使用到 react-router 功能時,通過 withRouter 高階組件,向組件注入路由跳轉函數

// withRouter 使用方式    // https://reacttraining.com/react-router/web/api/withRoute    import React from "react";    import PropTypes from "prop-types";    import { withRouter } from "react-router";    class ShowTheLocation extends React.Component {      static propTypes = { // 聲明propTypes,從而獲取router方法        match: PropTypes.object.isRequired,        location: PropTypes.object.isRequired,        history: PropTypes.object.isRequired      };    }        const ShowTheLocationWithRouter = withRouter(ShowTheLocation);

withRouter 實現原理

// https://github.com/ReactTraining/react-router/blob/402ecabdc94e5aeb657c593d8af200625a09cdfe/packages/react-router/modules/withRouter.js#L11    <RouterContext.Consumer>        {context => {          return (            <Component              {...remainingProps}              {...context}              ref={wrappedComponentRef}            />          );        }}     </RouterContext.Consumer>

withRouter 的源碼分析來看,其中 context 包含了 router 所有的方式,提供給組件使用,因此可以在 context 這一層,按照不同的平台,進行個性化處理

* **解決方案**

通過實現 withRouter 的邏輯,在 context 進行劫持處理

import { Platform } from '@hippy/react';    import { withHippyHistory } from './history.hippy';    import { withWebHistory } from './history.web';        const ISWEB = Platform.OS === 'web';        const wrapper = ISWEB ? withWebHistory : withHippyHistory      return (        <Component          {...remainingProps}          {...wrapper(context)}          ref={wrappedComponentRef}        />    );
  1. 在終端中,重寫路由跳轉函數,調用原生提供的跳轉方法
// history.hippy.js    import { callNativeWithPromise } from "@hippy/react";    import { parsePath } from './util';    const createHook = (history, ) => {        function push (path, state) {}        function replace () {}        function go () {}        function goBack () {}        function goForward () {}        function open ({ hippyUrl }) {            return callNativeWithPromise('TgclubJsModule', 'callNativeAction', hippyUrl)        }        return { push, go, goBack, goForward, replace, open }    }        export function withHippyHistory (context) {        const { history } = context;        return {            ...context,            history: {                ...history,                ...createHook(history)            }        }    }
  1. 提供額外的 open 函數,用於跳轉到非本項目的頁面,使得在對業務層無感知。
// history.web.js    const createHook = (history) => {        function open ({ webUrl, config }) {            if (webUrl) {                window.open(webUrl);            }        }        return { open }    }    export function withWebHistory (context) {        const { history } = context;        return {            ...context,            history: {                ...history,                ...createHook(history)            }        }    }

通過對 context 中函數的改造,統一不同平台的頁面切換調用方式。而在 wrapper 裡面針對平台進行特殊處理

4. 遺留問題

  1. 頁面切換動畫
  2. hippy 項目內頁面跳轉適配系統返回上一頁動作
  3. replace 操作需要終端配合,維護頁面路由棧