hippy-react 三端同构 — 路由

2,089 阅读5分钟

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/withRouter
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 操作需要终端配合,维护页面路由栈