从零实现react-router部分组件

229 阅读8分钟

介绍

react-route react-router包含3个库:

  • react-router;
  • react-router-dom;
  • react-router-native。

react-router提供最基本的路由功能,实际使用的时候我们不会直接安装react-router,而是根据应用运行的环境选择安装react-router-dom(在浏览器器中使⽤用)或react-router-native(在rn中使⽤用)。react-router-dom和react-router-native都依赖react-router,所以在安装时,react-router也会自动安装。
yarn add react-router-dom 或者 npm install --save react-router-dom。

react-router中奉行一切皆组件的思想,路由器-Router、链接-Link、路由-Route、独占-Switch、重定向-Redirect都以组件形式存在。
ReactRouter中提供了以下三大组件:

  • Router是所有路由组件共用的底层接口组件,它是路由规则制定的最外层的容器;
  • Route路由规则匹配,并显示当前的规则对应的组件;
  • Link路由跳转的组件。

HashRouter

URL格式为Hash路由组件,当前项目一旦使用HASH-ROUTER,则默认在页面的地址后面加上“#/”,也就是HASH默认值是一个斜杠,我们一般让其显示首页组件信息内容。HASH-ROUTER机制中,我们需要根据哈希地址不同,展示不同的组件内容。
代码实现:

// context文件里面的内容,就是新的设置context信息的方式。
import React from "react";

// react16.3新增的
let { Provider, Consumer } = React.createContext();

export { Provider, Consumer };

HashRouter代码:

import React from "react";
import { Provider } from "./context";

export default class HashRouter extends React.Component {
  constructor(props) {
    super(props);
    // 初始化状态信息,通过window.location.hash.slice(1)获取hash值,没有的话,默认为"/"
    this.state = {
      location: { pathname: window.location.hash.slice(1) || "/" },
    };
  }

  componentDidMount() {
    // 通过hashchange事件监听hash值变化,重新设置hash值,并放到state信息中。
    window.addEventListener("hashchange", () => {
      this.setState({
        location: {
          ...this.state.location,
          pathname: window.location.hash.slice(1) || "/",
        },
      });
    });
  }

  render() {
    // 设置要挂载到context上下文的信息,包括state里面的hash值构成的对象
    // 以及传递给子组件更新hash值得方法,这里用的是window.location.hash = to,因为window.location可读可写。
    let value = {
      location: this.state.location,
      history: {
        push(to) {
          if (to) {
            window.location.hash = to;
          }
          return "hash";
        },
      },
    };
    // 通过react16.3版本的生产者消费者模式设置context信息,this.props.children就是HashRouter包含的元素。
    return <Provider value={value}>{this.props.children}</Provider>;
  }
}

BrowserRouter

BrowserRouter主要使用在浏览器中,也就是WEB应用中。它利用HTML5 的history API来同步URL和UI的变化。当我们点击了程序中的一个链接之后,BrowserRouter就会找出与这个URL匹配的Route,并将他们对应的组件渲染出来。

在实现BrowserRouter组件之前,我们先来了解一下History对象,History对象包含用户(在浏览器窗口中)访问过的 URL。 传统方法:

  • back() 加载 history 列表中的前一个 URL。
  • forward() 加载 history 列表中的下一个 URL。
  • go() 加载 history 列表中的某个具体页面。

H5新增加的方法:

  • pushState(state, title, url)
    state: 要传递的数据;
    title: 可选参数,暂时没有用,建议传个短标题;
    url: 改变过后的url地址。

    改变url地址后,浏览器不会向服务端请求数据,可以类似的理解为变相版的hash;但不像hash一样,浏览器会记录pushState的历史记录,可以使用浏览器的前进、后退功能作用。

  • replaceState(state, title, url)
    和pushState用法一样,只是修改url,不会讲修改的url放在history列表里面。

  • popstate
    window新增的一个事件,当用户在浏览器点击进行后退、前进,或者在js中调用histroy.back(),history.go(),history.forward()等,会触发popstate事件;但pushState、replaceState不会触发这个事件。

怎么在pushState方法更改url后,获取最新的url值?
思路:调用两次pushState方法,更改同一个url两次,这样会在history列中存放两条记录,在通过back方法回退一次,就可以在通过popstate监听url变化,变化后,通过window.location.pathname拿到最新的url路径。

BrowserRouter代码:

import React from "react";
import { createBrowserHistory } from "history";
import { Provider } from "./context";

export default class BrowserRouter extends React.Component {
  constructor(props) {
    super(props);
    this.history = createBrowserHistory();
    // window.location.pathname拿到url路径
    this.state = {
      location: { pathname: window.location.pathname || "/" },
    };
  }

  componentDidMount() {
    // 监听location值变化,重新设置变化
    window.onpopstate = () => {
      this.setState({
        location: { pathname: window.location.pathname || "/" },
      });
    };
  }

  render() {
    const { children } = this.props;
    // 将最新的state信息和push方法传递给子组件
    let value = {
      location: this.state.location,
      history: {
        push(to) {
          if (to) {
            window.history.pushState(null, "", to);
            window.history.pushState(null, "", to);
            window.history.back();
          }
          return "pathname";
        },
      },
    };
    return <Provider value={value}>{children}</Provider>;
  }
}

Route

Route应该是react-route中最重要的组件了,它的作用是当location与Route的path匹配时渲染Route中的Component。如果有多个Route匹配,那么这些Route的Component都会被渲染。Route有一个exact属性,作用也是要求location与Route的path绝对匹配。但如果有多层组件最好设置exact为false,不设置默认为false。

  • path:设置匹配地址,但是默认不是严格匹配,当前页面哈希地址只要包含完整的它(内容是不变的),都能被匹配上。
  • component:一但哈希值和当前ROUTE的PATH相同了,则渲染COMPONENT指定的组件
  • exact:让PATH的匹配严谨和严格一些(只有URL哈希值和PATH设定的值相等才可以匹配到)。

在编写该代码之前,需要些安装"path-to-regexp"做路由匹配校验:

yarn add path-to-regexp

Route代码:

import React from "react";
import { Consumer } from "./context";
import { pathToRegexp } from "path-to-regexp";

export default class Route extends React.Component {
  render() {
    return (
      // 以消费者模式拿到context
      <Consumer>
        {(state) => {
          // 从this.props获取我们需要的值,包括自带的path,组件和exact值
          // 给path设定一个默认值为"",这样可以匹配所有的pathname,实际设计也是这样,不给path值,默认匹配所有,一般放在最后。
          let { path = "", component: Cmp, exact = false } = this.props;

          //  pathname是location中的
          let pathname = state.location.pathname;

          //   根据path实现一个正则,通过正则匹配,需要安装path-to-regexp
          let keys = [];
          let reg = pathToRegexp(path, keys, { end: exact });
          // 这里的keys用来匹配动态路由的:id,比如path="/user/detail/:id"中的id
          // 返回keys = [id];
          keys = keys.map((item) => item.name);

          // 将location中的pathname和自带的path进行比对,匹配后就返回一个数组。
          // ["/user/detail/3", "3", index: 0, input: "/user/detail/3", groups: undefined]
          // 第二个3便是符合动态id的值,在进行详情页选择时,可以利用这个值来选择。
          let result = pathname.match(reg);

          let [url, ...values] = result || [];
          url = null;
          let props = {
            location: state.location,
            history: state.history,
            // 通过reduce方法将动态路由的id和我们在url传入的值组成一个对象。
            // 结构为 match:{params:{id: "3"}}
            match: {
              params: keys.reduce((obj, current, idx) => {
                obj[current] = values[idx];
                return obj;
              }, {}),
            },
          };
          // result存在表示当前路由的path和传入的url路径匹配。
          // 就渲染它携带的组件,并将动态路由值和state信息传入过去
          if (result) {
            return <Cmp {...props}></Cmp>;
          }
          return null;
        }}
      </Consumer>
    );
  }
}

Link

是REACT-ROUTER中提供的路由切换组件,基于它可以实现点击时候路由的切换。参数to支持两种写法:

  • to [STRING]:跳转到指定的路由地址
  • to [OBJECT]:可以提供一些参数配置项(和REDIRECT类似)
    {
        PATHNAME:跳转地址
        SERACH:问号传参
        STATE:基于这种方式传递信息
    }
    

但是我在源码中没有实现,其实很简单,遍历对象,取出每一项的值,拼成一个字符串放到push函数中执行就可以了。

原理:基于LINK组件渲染,渲染后的结果就是一个A标签,TO对应的信息最后变为HREF中的内容,例如:

<Link to="/home">首页</Link>

会变成:

<a href="/home">首页</a>

Link代码实现:

import React from "react";
import { Consumer } from "./context";

export default class Link extends React.Component {
  render() {
    return (
      <Consumer>
        {(state) => {
          const { push } = state.history;
          const { to, children } = this.props;
          // 先执行方法push方法,根据返回值,来判断是BrowserRouter还是HashRouter。
          const routeType = push();
          return (
            <a
              // 如果是为hash”,href=#to”,如果是“pathname”,则“href=to”
              href={
                routeType === "hash"
                  ? `#${to}`
                  : routeType === "pathname"
                  ? to
                  : null
              }
              // 添加一个click事件阻止a标签默认行为并且执行push方法
              onClick={(ev) => {
                ev.preventDefault();
                push(to);
              }}
            >
            // 渲染Link组件包裹的内容
              {children}
            </a>
          );
        }}
      </Consumer>
    );
  }
}

Switch

原理:渲染匹配地址(location)的第一个 或者,使用Switch组件后,它会拿到context中的state.location.pathname,再通过props.children拿到所有的Route组件并遍历,取到props里面的path值,和pathname比对,如果遇到第一个匹配的,就将它返回出去,结束循环,不在遍历其他的组件。

Switch代码实现:

import React from "react";
import { Consumer } from "./context";
import { pathToRegexp } from "path-to-regexp";

export default class Switch extends React.Component {
  render() {
    return (
      <Consumer>
        {(state) => {
          // 拿到拿到context中的state.location.pathname。
          let { pathname } = state.location;
          // 通过props.children拿到所有的Route组件并遍历
          let { children } = this.props;
          for (let i = 0; i < children.length; i++) {
            // 取到props里面的path值,通过pathToRegexp这个工具和pathname比对,没有path值或者path值不存在的话,就给它一个"",这样它就可以和所有的pathname匹配,一般用于Redirect或者默认Router上。
            let path = children[i].props.path || "";
            let reg = pathToRegexp(path, [], { end: false });
            //遇到第一个匹配的,就将它返回出去,结束循环,不在遍历其他的组件。
            if (reg.test(pathname)) return children[i];
          }
          return null;
        }}
      </Consumer>
    );
  }
}

REDIRCT

重定向组件,一般和Switch组件配合使用,一般是没有通过Switch找到匹配的组件,就将它返回。 to [STRING]:重新定向到新的地址 to [OBJECT]:重新定向到新的地址,只不过指定了更多的信息

  {
    PATHNAME:定向的地址
    SEARCH:给定向的地址问号传参(结合当前案例,真实项目中,我们有时候会根据是否存在问号参数值来统计是正常进入首页还是非正常跳转过来的,也有可能根据问号传参值做不同的事情)
    STATE:给定向后的组件传递一些信息
 }

同样,没有实现第二种写法,有兴趣的可以自己写。

REDIRCT的代码实现:

import React from "react";
import { Consumer } from "./context";

export default class Redirect extends React.Component {
  render() {
    return (
      <Consumer>
        {(state) => {
          const { push } = state.history;
          const { to } = this.props;
          // 重定向就是通过Switch匹配不到后直接跳转到redirect中的to路径
          push(to);
          return null;
        }}
      </Consumer>
    );
  }
}

实际效果

编写一个简单的页面: MyRouter组件:

import React from "react";
import Home from "./Home";
import User from "./User";
import {
  HashRouter,
  Route,
  Link,
  Redirect,
  Switch,
  BrowserRouter,
} from "./index";

export default class MyRouter extends React.Component {
  render() {
    return (
      <section className="MyRouterPage">
        <h2>MyRouterPage页面</h2>
        <BrowserRouter>
          {/* <HashRouter> */}
          <nav style={{ color: "blue" }}>
            <Link to="/home">首页</Link>
            <br />
            <Link to="/user">用户中心</Link>
          </nav>
          {/* switch的作用就是匹配一个组件 */}
          <Switch>
            {/* exact表示严格匹配 */}
            <Route exact path="/home/123" component={Home} />
            <Route path="/home" component={Home} />
            <Route path="/user" component={User} />
            <Redirect to="/user" />
          </Switch>
          {/* </HashRouter> */}
        </BrowserRouter>
      </section>
    );
  }
}

App组件:

import React from "react";
import "./styles.less";
import MyRouter from "./react-router-dom/MyRouterPage";

export default function App() {
  return (
    <div className="App">
      <h1>Hello App!</h1>
      {/* <Provider store={store}>
        <RouterPage />
        <MyRouterPage />
      </Provider> */}

      <MyRouter />
    </div>
  );
}

BrowserRouter组件效果: HashRouter组件效果: