React 路由之 “道”

1,147 阅读7分钟

React中学会使用路由

基础知识 Location 对象

location 对象

Nodejs URL 模块 Jest 测试 这是 nodejs 的 url 的学习是用 jest 作为测试的学习方式,对比学习可以更好的理解 location 对象

属性/方法 说明
href :---:
protocol :---:
host :---:
hostname :---:
port :---:
pathname :---:
search :---:
hash :---:
usename :---:
password :---:
origin :---:
assign() :---:
reload() :---:
replace() :---:
toString() :---:

基础知识 History 对象

history 对象

属性/方法 说明
back()
forward()
go()
length
pushState() h5
replaceState() h5

也就是在 H5 中有 history 的概念。HTML5引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate 配合使用。 使用 history.pushState() 可以改变referrer,它在用户发送 XMLHttpRequest 请求时在HTTP头部使用,改变state后创建的 XMLHttpRequest 对象的referrer都会被改变。因为referrer是标识创建 XMLHttpRequest 对象时 this 所代表的window对象中document的URL。

let stateObj = {
    foo: "bar",
};

history.pushState(stateObj, "page 2", "bar.html");

history.replaceState(stateObj, "page 3", "bar2.html");

React 路由社区解决方法

  • React-Router
  • Reach-Route (号称下一代 😸 React-Router)

这里使用 react-router/react-router-dom 来进行说明,静态配置路由虽然是比较好的路由方式,但是在 react-router 中并不成熟。

react-router 对外暴露的 API

  1. 其次依赖于 react-router 包,它向外暴露的包:
序号 组件 说明
1 MemoryRouter :--- :---
2 Prompt 提示组件 :---
3 <Redirect /> 路由重定向 :---
4 <Route /> :--- :---
5 <Router /> :--- :---
6 StaticRouter :--- :---
7 <Switch /> :--- :---
8 __RouterContext 私有的 Router 上下文 :---
9 generatePath :--- :---
10 matchPath :--- :---

装饰器,高阶函数

序号 react-router API 说明
11 withRouter :---
12 withHistory 获取现在location的对象, hook 方便我们使用

React 钩子函数

序号 钩子名 说明
13 useLocation 使用动态路由的时候,我们要写参数/cards/:id,hook 方便我们使用
14 useParams 获取现在location的对象,hook 方便我们使用
14 useRouteMatch 使用动态路由的时候,我们要写参数/cards/:id,hook 方便我们使用

功能

  1. 嵌套路由
  2. 重定向

react-router 中的对象

  • history, 与浏览器中的 History 对象有所不同
  • location 与浏览器中的 location 对象有所不同
  • match
// history 对象
history = {
    length: globalHistory.length,
    action: 'POP',
    location: initialLocation,
    createHref,
    push,
    replace,
    go,
    goBack,
    goForward,
    block,
    listen
  };
// location 对象
location = {
  pathname, // 当前路径,即 Link 中的 to 属性
  search, // search
  hash, // hash
  state, // state 对象
  action, // location 类型,在点击 Link 时为 PUSH,浏览器前进后退时为 POP,调用 replaceState 方法时为 REPLACE
  key, // 用于操作 sessionStorage 存取 state 对象
};

路由按需加载

  • 使用 @loadable/component,@loadable/component 支持服务端渲染。
  • 不考虑服务端渲染的情况使用使用 React.Suspense 和 React.lazy,即可完成同样的任务。

react-router-dom 对外暴露的 API

针对浏览器平台的路由

序号 组价 说明
1 <BrowserRouter /> 浏览器 h5路由
2 <HashRouter /> Hash 路由
3 <Link /> 导航链接,相当于 <a /> 标签
4 <NavLink /> 路由导航链接

以上各个 API 是 react-router-dom 经过修该之后,单独暴露出来,在浏览器中,我们可以使用这些 API。其他组件其实是从 react-router 中直接拿过来的,这样做是为了考虑跨端的抽象能力,不同的平台最后实现的内容都是不一样的。

为了方便使用,通常 BrowserRouterHashRouter 都重新定义为 Router 。在切换不同的路由的时候,更加方便使用。其次 BrowserRouterHashRouter 是包裹性质中的组件,Route 对象都放在 BrowserRouterHashRouter 内部作用 child 进行渲染。

下面是 react-router-domBrowserRouter 的部分 源码

import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";

class BrowserRouter extends React.Component {
  history = createHistory(this.props);

  render() {
    return <Router history={this.history} children={this.props.children} />;
  }
}

export default BrowserRouter;

看到 react-router-dom 其实是依赖于 react-router, <BrowserRouter /> 组件中的 history 属性是从 history 中创建的。

  • history
  • children,所以我们的 Route 等等,都是放在 Router 里面作为 children渲染的。

先看创建的 history 中到底包含了什么?

function createBrowserHistory(historyState) {
  const globalHistory = window.history;
  const canUseHistory = supportsHistory();
  const needsHashChangeListener = !supportsPopStateOnHashChange();

  const {
    forceRefresh = false,
    getUserConfirmation = getConfirmation,
    keyLength = 6
  } = props;

  const basename = props.basename
    ? stripTrailingSlash(addLeadingSlash(props.basename))
    : '';

    // other functions

  const history = {
    length: globalHistory.length,
    action: 'POP',
    location: initialLocation,
    createHref,
    push,
    replace,
    go,
    goBack,
    goForward,
    block,
    listen
  };

  return history;
}

对于 HashHistory 其实和 BrowserHistory 是一样的,在实现的上有些不同罢了, 所暴露出的方法也是一样的,这里就不在重复,可以下载源码对比。

接下俩是 <Link />, 它是通过 forwardRef 实现的, forwardRef 是一个穿透 ref 的 api, forwardRef 是通过 ref 属性在 react 元素上传递的,访问的方式是 ref.current. <Link /> 组件最终渲染内容给很简单,内容如下。

return <a {...props} />;

再来看 <NavLink />, 它其实是对 <Link /> 的一层封装, 可以理解为 <Link /> 一个有激活导航组件自定义样式的特别版本。

再来看 <Switch />, 其实就是根据 match 对象进行切换:

class Switch extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          invariant(context, "You should not use <Switch> outside a <Router>");

          const location = this.props.location || context.location;

          let element, match;
          React.Children.forEach(this.props.children, child => {
            if (match == null && React.isValidElement(child)) {
              element = child;

              const path = child.props.path || child.props.from;

              match = path
                ? matchPath(location.pathname, { ...child.props, path })
                : context.match;
            }
          });

          return match
            ? React.cloneElement(element, { location, computedMatch: match })
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}

match 对象的形式是如下:

{
      path, // the path used to match
      url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
      isExact, // whether or not we matched exactly
      params: keys.reduce((memo, key, index) => {
        memo[key.name] = values[index];
        return memo;
      }, {})
    }

matchPath 函数的根据 pathname 作为参数和一些选项作为参数,就可以生成一个匹配对象。最后根绝这个匹配对象 match 来生成组件:

React.cloneElement(element, { location, computedMatch: match })

withRouter 其实是一个一直困扰初学者的问题,我们来看它的源码的实现:

import React from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";
import invariant from "tiny-invariant";

import RouterContext from "./RouterContext.js";

/**
 * A public higher-order component to access the imperative API
 */
function withRouter(Component) {
  const displayName = `withRouter(${Component.displayName || Component.name})`;
  const C = props => {
    const { wrappedComponentRef, ...remainingProps } = props;

    return (
      <RouterContext.Consumer>
        {context => {
          invariant(
            context,
            `You should not use <${displayName} /> outside a <Router>`
          );
          return (
            <Component
              {...remainingProps}
              {...context}
              ref={wrappedComponentRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  };

  C.displayName = displayName;
  C.WrappedComponent = Component;

  if (__DEV__) {
    C.propTypes = {
      wrappedComponentRef: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.func,
        PropTypes.object
      ])
    };
  }

  return hoistStatics(C, Component);
}

export default withRouter;
  • hoistStatics 将非 react 特定的静态信息从子组件复制到父组件。来看源码

import { ForwardRef, Memo, isMemo } from 'react-is';

const REACT_STATICS = {
    childContextTypes: true,
    contextType: true,
    contextTypes: true,
    defaultProps: true,
    displayName: true,
    getDefaultProps: true,
    getDerivedStateFromError: true,
    getDerivedStateFromProps: true,
    mixins: true,
    propTypes: true,
    type: true
};

const KNOWN_STATICS = {
    name: true,
    length: true,
    prototype: true,
    caller: true,
    callee: true,
    arguments: true,
    arity: true
};

const FORWARD_REF_STATICS = {
    '?typeof': true,
    render: true,
    defaultProps: true,
    displayName: true,
    propTypes: true
};

const MEMO_STATICS = {
    '?typeof': true,
    compare: true,
    defaultProps: true,
    displayName: true,
    propTypes: true,
    type: true,
}

const TYPE_STATICS = {};
TYPE_STATICS[ForwardRef] = FORWARD_REF_STATICS;
TYPE_STATICS[Memo] = MEMO_STATICS;

function getStatics(component) {
    // React v16.11 and below
    if (isMemo(component)) {
        return MEMO_STATICS;
    }

    // React v16.12 and above
    return TYPE_STATICS[component['?typeof']] || REACT_STATICS;
}

const defineProperty = Object.defineProperty;
const getOwnPropertyNames = Object.getOwnPropertyNames;
const getOwnPropertySymbols = Object.getOwnPropertySymbols;
const getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const getPrototypeOf = Object.getPrototypeOf;
const objectPrototype = Object.prototype;

export default function hoistNonReactStatics(targetComponent, sourceComponent, blacklist) {
    if (typeof sourceComponent !== 'string') { // don't hoist over string (html) components

        if (objectPrototype) {
            const inheritedComponent = getPrototypeOf(sourceComponent);
            if (inheritedComponent && inheritedComponent !== objectPrototype) {
                hoistNonReactStatics(targetComponent, inheritedComponent, blacklist);
            }
        }

        let keys = getOwnPropertyNames(sourceComponent);

        if (getOwnPropertySymbols) {
            keys = keys.concat(getOwnPropertySymbols(sourceComponent));
        }

        const targetStatics = getStatics(targetComponent);
        const sourceStatics = getStatics(sourceComponent);

        for (let i = 0; i < keys.length; ++i) {
            const key = keys[i];
            if (!KNOWN_STATICS[key] &&
                !(blacklist && blacklist[key]) &&
                !(sourceStatics && sourceStatics[key]) &&
                !(targetStatics && targetStatics[key])
            ) {
                const descriptor = getOwnPropertyDescriptor(sourceComponent, key);
                try { // Avoid failures from read-only properties
                    defineProperty(targetComponent, key, descriptor);
                } catch (e) {}
            }
        }
    }

    return targetComponent;
};

一个后台管理系统的路由设计

在一个后台管理系统中,我们应该如何设计自己路由方案呢?

先思考业务模型:

  1. 进入网站,登录/login,后台管理系统一般是直接就是登录路由/login。不考虑角色分配问题。
  2. 登录请求成功之后,就要决定路由应该如何跳转。登录成功直接进入 app 的仪表盘界路由 /app/dashboard界面。
  3. 下面有一个概念:精确匹配 exact 和模糊匹配。精确匹配是只匹配唯一指定路径。
    • 例如 /app,有 exact 属性的路由,使得只能匹配 /app 路由对应的组件。
    • 没有 exact 属性的路径,是可以匹配 /app/a/app/b 等等模糊的路径。

下面是将后台系统简单的抽象:

说明: 当登录成功之后,跳转的应该是 exact /app 路径。进入布局页面 Layout, Layout 中具有二级路由,用于存放 app/*下的路由内容(用 views 视图表示其实更加合适, 因为本质是一个单应用。也用 pages,页面更加的如何 html 的编程习惯)。

登录成功之后,访问 / 或者/app 路径也是,会重新的跳转到 /app/dashboard, 因为 dashboard 仪表盘才是主界面。

import { HashRouter, Route, Switch, Redirect } from "react-router-dom";

<HashRouter>
    <Switch>
        <Route exact path="/" render={() => <Redirect to="/app/dashboard" />} />
        <Route
            exact
            path="/app"
            render={() => <Redirect to="/app/dashboard" />}
        />
        <Route path="/app" component={Layout} />
        <Route path="/login" component={Login} />
        <Route component={Error} />
    </Switch>
</HashRouter>

当然可以增加特殊的提示结果页面:

  • 404
  • 400
  • 500
  • ...

导航方式

  1. 组件导航:Link、NavLink 组件进行导航
  2. 编程式导航

函数组件

  • react-router-5.0 使用 react-hooks, 这种方式简单方便
import { useHistory } from "react-router-dom";

function HomeButton() {
  let history = useHistory();
  history.push('/your/path')
};

class 组件

  • 在4.0及更高版本中,将历史记录用作组件的支持, class 组件中,通过 props 拿到 history 对象
class Example extends React.Component {
   // use `this.props.history.push('/some/path')` here
}

参考