手写react-router

295 阅读3分钟

前言

react开发实践中,router是必不可少的一个库,之前对该库的使用是一知半解,所以手写一次,加深对该库的理解,学习原理和设计思想。

react-router-dom

核心库由router-dom实现,其中主要有以下几种组件。

BrowserRouter

一级组件,用于包裹router下的各类子组件,主要管理以何种方式进行跳转

import { Component } from "react";
import { createBrowserHistory } from "history";
import Router from "./Router";
class BrowserRouter extends Component {
  constructor(props) {
    super(props);
    this.history = createBrowserHistory(); // 创建跳转函数(使用插件库,磨平浏览器差异)
  }
  render() {
    const { children } = this.props;
    return <Router history={this.history} children={children} />
  }
}

export default BrowserRouter;

Router

路由的父组件,管理各类通用逻辑

import React, { Component } from "react";
import RouterContext from "./RouterContext";

// 路由中间层,处理通用逻辑和数据
class Router extends Component {
  static computeRootMatch(pathname) { // 默认path,当未传入时赋值。
    return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
  }
  constructor(props) {
    super(props);
    // console.log("props.history", props.history.location);
    // 获取父级传入的location信息
    this.state = {
      location: props.history.location
    }

    // 监听路由变化,通知对应path的子组件更新
    this.unlisten = props.history.listen(({ location }) => {
      this.setState({ location })
    })
  }

  componentWillUnmount() {
    this.unlisten();
  }

  render() {
    const { history, children } = this.props;
    // console.log("history", RouterContext.Provider);
    // 决定以何种方式跳转的history进行传递(browser, hash, memory)
    return (
      <RouterContext.Provider 
        value={{ history, location: this.state.location, match: Router.computeRootMatch(this.state.location.pathname) }}
      >
        { children }
      </RouterContext.Provider>
    )
  }
}

export default Router;

Route

真正的路由组件,接收用户传入的组件,匹配相应路径进行展示。

import React, { Component } from "react";
import RouterContext from "./RouterContext";
import matchPath from "./matchPath";
class Route extends Component {
  render() {
    // 类组件中可以使用 Consumer 获取跨层级数据传递
    return (
      <RouterContext.Consumer>
      {(context) => {
        const { location } = context;
        const { path, children, component, render } = this.props;
        const match = path ? matchPath(path, location.pathname) : context.match;

        // route props 由真正的组件使用
        const props = {
          ...context,
          match,
          location
        }

        // 匹配 按优先级进行返回 children(函数) > component(组件) > render(函数)> null
        // 不匹配 按优先级进行返回 children > null
        // 源码中使用连续三元进行匹配,不是很利于学习观看,所以这里拆分成了 if else
        const createElement = () => {
          if (match) {
            if (children) {
              if (typeof children === "function") return children(props);
              else return children;
            } else if (component) {
              return React.createElement(component, props);
            } else if (render) {
              return render(props);
            } else return null;
          } else {
            // 在react-router设计中,不匹配模式下,如果有children传入则直接返回children。
            return typeof children === "function" ? children(props) : null;
          }
        }
        // 这里再使用Provider传递一层props,用于自定义hooks获取最近的props,
        return <RouterContext.Provider value={props}>
          {createElement()}
        </RouterContext.Provider>
      }}
    </RouterContext.Consumer>
    )
  }
}

export default Route;

Switch

返回多个路由子组件中,第一个被匹配的子组件。(独占路由)

import React, { Component } from "react";
import matchPath from "./matchPath";
import RouterContext from "./RouterContext";

// 在所有子路由组件中,返回第一个匹配的。
class Switch extends Component {
  render() {
    return <RouterContext.Consumer>
      {(context) => {
        const { location } = context
        let match; // 记录是否匹配
        let element; // 记录匹配的元素
        // console.log('this.props.children', this.props.children);
        React.Children.forEach(this.props.children, child => {
          if (!match && React.isValidElement(child)) {
            element = child;
            match = child.props.path ? matchPath(child.props.path, location.pathname) : context.match;
          }
        })
        // 如果匹配成功 返回对应组件即可
        return match ? React.cloneElement(element, { computedMatch: match }) : null ;
      }}
    </RouterContext.Consumer>
  }
}

export default Switch;

Link

跳转组件,点击进入新页面(对应的path页面)

import { useContext } from "react";
import RouterContext from "./RouterContext";

// 创建link,本质就是a标签的跳转
function Link({ to, children, ...rest }) {

  const context = useContext(RouterContext); // 获取跨层级数据传递
  
  const handleClick = (e) => {
    e.preventDefault(); // 禁用默认跳转事件
    context.history.push(to); // 使用自有跳转方式进行路由跳转
  }

  return <a href={to} onClick={handleClick} { ...rest }>{children}</a>
}

export default Link;

Redirect

重定向,当匹配当前组件时,跳转你想重定向的path组件

import React, { Component } from "react";
import RouterContext from "./RouterContext";

export default class Redirect extends Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          const { history } = context;
          const { to, push = false } = this.props;
          return (
            <LifeCycle
              // 重定向跳转页面
              onMount={() => {
                push ? history.push(to) : history.replace(to);
              }}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

class LifeCycle extends Component {
  componentDidMount() {
    if (this.props.onMount) {
      this.props.onMount.call(this, this);
    }
  }
  render() {
    return null;
  }
}

结语

以上便是react-router的核心组件,还有withRouter,hooks,等没有一一写入,可前往github进行完整查看。