react-router-dom源码学习之实现一个自定义的路由组件

现代前端应用,几乎已经离不开前端路由这个概念了,只要稍微复杂一点的前端应用,都会选择引入前端路由的方式,使我们的不同的模块能够有机组合并通过浏览器的历史进行管理。今天,本人学习了一下react-router-dom的底层实现原理,尝试实现一个简易版的react-router(仅实现基于浏览器History的路由方式)

// 路由容器组件
import React, { Component } from "react";

// 通过这个第三方库中的createBrowserHistory创建浏览器history对象
// 也可以通过createHashHistory创建基于hash的历史对象
import { createBrowserHistory } from "history";
// 当前组件作为路由容器,需要向他的子组件全局提供上下文(上下文中包括创建的history、location等),
// 因此需要使用React.createContext创建上下问对象,为了方便在路由容器和其他子组件如Route和Link组件中使用同一个上下文
// 我们将其独立到RouterContext文件中,此处单独引入其中的Provider作为内容提供者
import { Provider } from "./RouterContext";

/**
 * 路由容器对象,为子组件提供必要的上下文,并对浏览器的历史进行监听,一旦history发生改变,则触发重新渲染达到切换路由的目的
 */
class BorrowerRouter extends Component<any, any> {
  // 浏览器history对象
  private history: any = null;
  // 取消监听history的方法,当我们的路由组件准备卸载时调用
  private unListener: () => void;
  constructor(props: any) {
    super(props);

    // 创建浏览器历史对象
    this.history = createBrowserHistory();
    // 将histrory中的location放入组件的状态中进行管理,一旦监听到location发生改变,
    // 便可以通过setState的方式触发组件重新渲染,达到切换子路由的目的
    this.state = {
      location: this.history.location
    };

    // 对history进行监听,发生改变时重新设置location
    this.unListener = this.history.listen((location: any) => {
      this.setState({ location });
    });
  }

  // 卸载前取消对history的监听
  public componentWillUnmount() {
    if (this.unListener) {
      this.unListener();
    }
  }

  public render() {
    return (
      // 通过Provider向所有的子组件传递创建的history和loaction对象
      <Provider
        value={{ history: this.history, location: this.state.location }}
      >
        {this.props.children}
      </Provider>
    );
  }
}

export default BorrowerRouter;

// Route组件
import React, { Component } from "react";

// 路由组件需要接受由路由容器组件BorrowerRouter传递过来的上下文,
// 从中获取到history和location对象才能对路由进行匹配,
// 因此此处需引入公共的上下文,并将其消费者Consumer结构出来使用
import { Consumer } from "./RouterContext";

// 使用第三方库path-to-regexp将路径解析成正则表达式,方便匹配路由
import pathToRegexp from "path-to-regexp";

/**
 * 路由组件,只有满足当前location的pathname满足当前组件配置的path转化的正则表达式的要求,才会渲染目标组件
 */
class Route extends Component<any, any> {
  public render() {
    return (
      // 通过Consumer获取路由容器获取的上下文中的history和location对象,用于路由匹配
      <Consumer>
        {(value: any) => {
          const { path, component: Comp, exact = false, render } = this.props;

          const pathname = value.location.pathname;

          let keys: any[] = [];
          // 将传入组件的path属性通过第三方库pathToRegexp进行解析,将解析出来的路由参数,
          // 如/list/:type/:id中的type和id放入keys数组中,用于之后解析路由参数是使用
          // 通过指定end:true|false来确定是否使用精确匹配路由的方式来匹配路由
          const exp = pathToRegexp(path, keys, { end: exact });
          // 对location中的pathname解析,看是否匹配,如果不匹配,将不会执行render或显示Comp组件
          const res = pathname.match(exp);

          // 将解析出来的动态路由参数的名称获取出来
          keys = keys.map(item => item.name);

          // 将动态路由参数名和当前location中的参数值结合组成路由参数对象params并放到match对象中
          // 格式如:match={params:{type:"react",id:"123"}}
          const match = {
            params: {}
          };
          keys.forEach((key, index) => {
            match.params[key] = res[index + 1];
          });

          const props = {
            history: value.history,
            location: value.location,
            match
          };
          // 若存在render方法,则采用render方法进行构建目标组件,否则采用传递过来的component进行构建,并将props传递下去
          if (render) {
            return res && render(props);
          } else {
            return res && <Comp {...props} />;
          }
        }}
      </Consumer>
    );
  }
}

export default Route;

// Link和Redirect组件
import React, { Component, MouseEvent } from "react";

import { Consumer } from "./RouterContext";
/**
 * 实现Link方式跳转路由,原理就是通过history的push或replace方法对当前历史进行改变,
 * 从而触发路由容器中的history监听执行,导致路由容器重新渲染,最终展示目标路由组件
 */
export class Link extends Component<any, any> {
  public handleClick(e: MouseEvent, ctx: any, replace: boolean) {
    // 阻止a链接的默认行为
    e.preventDefault();
    // 根据用户指定的方式跳转路由
    ctx.history[replace ? "replace" : "push"](this.props.to);
  }
  public render() {
    const { to, replace = false, ...rest } = this.props;
    return (
      <Consumer>
        {ctx => {
          return (
            <a
              {...rest}
              href={to}
              onClick={e => this.handleClick(e, ctx, replace)}
            >
              {this.props.children}
            </a>
          );
        }}
      </Consumer>
    );
  }
}
/**
 * 实现Redirect功能
 * @param props
 */
export const Redirect = (props: any) => {
  const { to } = props;
  return (
    <Consumer>
      {(ctx: any) => {
        return (
          <a
            onClick={e => {
              e.preventDefault();
              ctx.history.replace(to);
            }}
          >
            {props.children}
          </a>
        );
      }}
    </Consumer>
  );
};

export default {
  Link,
  Redirect
};

// RouterContext.tsx
import React from "react";

const ctx = React.createContext({});

export const Provider = ctx.Provider;
export const Consumer = ctx.Consumer;

简易版的Router大概就分为以上几个部分:

  • 路由容器BorrowerRouter: 为子组件提供统一的包含history和location的上下文,并实现对history的监听操作
  • Route组件:指定路由匹配规则和目标组件
  • Link和Redirect: 使用不同的方式跳转路由
  • RouterContext为路由组件提供上下文

下面我们来尝试一下实现的路由组件是否达到我们的预期

import * as React from "react";
import "./App.css";
import BorrowerRouter from "./BorrowerRouter";
import Route from "./Route";
import { Link, Redirect } from "./Link";

class App extends React.Component {
  public render() {
    return (
      <div className="App">
        <BorrowerRouter>
          <Link to="/home/vue/1">vue</Link>|
          <Link to="/home/react/2" replace={true}>
            react
          </Link>
          |<Redirect to="/home/angular/3">angular</Redirect>
          <Route
            path="/home/:type/:id"
            component={(props: any) => {
              console.log("--->", props);
              const params = props.match.params;
              return (
                <div>
                  测试路由{params.type}-{params.id}
                </div>
              );
            }}
            exact={false}
          />
        </BorrowerRouter>
      </div>
    );
  }
}

export default App;

你可能感兴趣的:(tsx,源码,自定义路由组件,react-router,源码学习,自定义路由)