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-native
是 react-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使用
- 通过
Platform.OS
对当前平台进行判断 - 在原生项目中使用
MemoryRouter
, 在web中使用HashRouter
- 通过
react-router
对多页面进行切换
以下是 hippy
中 react-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
中多页面跳转的功能,是也存在一些问题
- 原生切换没有动画,体验与web的一样
- 无法使用
react-router-transition
动画 - 原生的返回操作,直接回关闭
hippy
项目 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}
/>
);
- 在终端中,重写路由跳转函数,调用原生提供的跳转方法
// 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)
}
}
}
- 提供额外的
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. 遗留问题
- 页面切换动画
hippy
项目内页面跳转适配系统返回上一页动作replace
操作需要终端配合,维护页面路由栈