十七、「深入React源码」--- 手写实现React基本路由

477 阅读3分钟

一般项目中,react路由是通过react-router-dom库实现的,react-router + history = react-router-dom,本文先来实现react-router。

一、基本原理

路由组件打印props的结果如下: image.png

路由基本原理: image.png

Router路由器管理每一个路由条目Route。

Router和Route是通过context通信的。在Router中创建RouterContext对象,提供value属性值为{history, location, match}。当我们去访问页面时,拿到location中的pathname属性和Routepath属性进行匹配并渲染。

二、实现思路

1. 实现react-router

image.png react-router最终暴露一个具有history属性的Router组件,history属性的值是history库里提供createHashHistory()方法,执行这个方法获得hashHistory的实例对象,作为history的值传入Router组件。

Router组件包裹所有Route并通过context通信。因为路由组件接收的props包含{history, location, match},因此我们在Router中创建context对象,通过Provider提供value属性,值就是{history, location, match}

history:  props.history // react-router中传递给Router组件的history对象
location: props.history.location // 浏览器历史对象上存放的最新的路径
match:    // 后续文章中实现

2. 实现Route

Route需要做的事情是:拿自己的pathProvider提供的location.pathname相比,如果相同就渲染对应的组件。

2-1. 获取pathname

因为Router和Route是通过context通信,因此我们从context上可以直接拿到location对象,也就可以获取pathname属性了。

2-2. 获取path

image.png 我们在Route组件内部打印props,就获取到我们在使用时给它传的pathcomponent属性。

2-3. 渲染react元素

拿到pathnamepath后比较是否相同,如果相同就渲染path相对应的component。

此时浏览器地址栏输入url,刷新后就已经可以渲染对应的组件。
但是页面不会自动刷新,为什么呢?因为我们还没有对url进行监听。

3. 实现监听

在Router中,给history添加listen监听事件(history内置对象可以拦截listen事件并执行),此事件的入参为地址栏上的新的url,因此我们可以直接把组件内的location的值,更新为这个入参即可。

三、代码实现

1. src/index.js

import React from "react";
import ReactDOM from "react-dom";
import { HashRouter as Router, Route } from "./react-router-dom";
import Home from "./components/Home";
import User from "./components/User";
import Profile from "./components/Profile";

ReactDOM.render(
  <Router>
    <div>
      {/* <Route path="/" component={Home} /> */}
      <Route path="/" component={Home} exact />
      <Route path="/user" component={User} />
      <Route path="/profile" component={Profile} />
    </div>
  </Router>,
  document.getElementById("root")
);

2. src/components

2-1. components/Home.js

import React, { Component } from 'react'

export default class Home extends Component {
  render() {
    return (
      <div>
        <p>Home</p>
      </div>
    )
  }
}

2-2. components/User.js

import React, { Component } from 'react'

export default class User extends Component {
  render() {
    return (
      <div>
        <p>User</p>
      </div>
    )
  }
}

2-3. component/profile.js

import React, { Component } from "react";

export default class Profile extends Component {
  render() {
    console.log(this.props);
    return (
      <div>
        <p>Profile</p>
      </div>
    );
  }
}

3. src/react-router-dom

3-1. react-router-dom/index.js

export * from "../react-router";
export { default as HashRouter } from "./HashRouter";
export { default as BrowserRouter } from "./BrowserRouter";

3-2. react-router-dom/HashRouter.js

import React, { Component } from "react";
import { Router } from "../react-router";
import { createHashHistory } from "history";

export default class HashRouter extends Component {
  history = createHashHistory();

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

3-3. react-router-dom/BrowserRouter.js

import React, { Component } from "react";
import { Router } from "../react-router";
import { createBrowserRouter } from "history";

export default class BrowserRouter extends Component {
  history = createBrowserRouter();

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

4. src/react-router

4-1. react-router/index.js

export { default as Router } from "./Router";
export { default as Route } from "./Route";
export { default as RouterContext } from "./RouterContext";

4-2. react-router/RouterContext.js

import React from "react";

export default React.createContext({});

4-3. react-router/Router.js

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

class Router extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      // 历史对象上有最新的路径
      location: props.history.location,
    };

    // 监听到url发生变化
    props.history.listen((location) => {
      this.setState({ location })
    })
  }

  render() {
    let value = {
      history: this.props.history,
      location: this.state.location,
    };

    return (
      <RouterContext.Provider value={value}>
        {this.props.children}
      </RouterContext.Provider>
    );
  }
}

export default Router;

4-4. react-router/Route.js

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

export default class Route extends Component {
  static contextType = RouterContext;
  render() {
    console.log(this.props);
    // 每个路由条目都要拿Provider提供的location.pathname和自己的path相比
    const { history, location } = this.context;
    const { path, component: RouteComponent } = this.props;
    const match = location.pathname === path;
    const routeProps = { history, location };
    // 要渲染的react元素
    let element = null;
    if (match) {
      element = <RouteComponent {...routeProps} />;
    }
    return element;
  }
}

四、总结

  1. 入口文件中引入Router路由容器和Route路由条目组件:Router中调用createHashHistory方法获取history对象,通过属性传入Router。
  2. Router文件中引入context,内部构建value对象:
    • history: 第一步中传过来的
    • location: 默认是浏览器的history对象提供的location 创建context后的RouterContext组件,并传入value对象并暴露组件。渲染Route条目子组件。
  3. Route文件中,进行路径的匹配渲染相应的组件。