手写一个react-router-dom简易版(一)

背景


自从接触处前端路由以来,一直想对前端路由深入理解,由于只会在项目中使用,无法深入其原理,遇到一些疑难问题容易只知其一不知其二,所以本文主要从零到一实现一个简易react-router-dom(纯底层实现)

路由原理


后端路由

路由这个概念最先是后端出现的。
响应的过程:

  • 浏览器发出请求
  • 服务器监听到80端口(或443)有请求过来,并解析url路径
  • 根据服务器的路由配置,返回相应信息(可以是 html 字串,也可以是 json 数据,图片等)
  • 浏览器根据数据包的Content-Type来决定如何解析数据

简单来说路由就是用来跟后端服务器进行交互的一种方式

前端路由

对 url 进行改变和监听,来让某个 dom 节点显示对应的视图。

所以并没有那么神秘。

具体功能


  • 路由模式:browser,hash
  • 组件:BrowserRouter,HashRouter,Route,Switch,Redirect,Link
  • 当前路由信息:路径pathname,参数query
  • 路由跳转:push,replace,go,goBack

具体实现


知识背景

BrowserRouter

具体思路:

  • 这个就是React组件,使用Context的包装组件,负责往消费组件传递路由信息路由跳转方法
  • 监听路由变化,实时更新传递路由信息
import React from "react";
import { Provider} from "./context";
import History from "./history";
const url = require("url");

class BrowserRouter extends React.Component {
  constructor() {
    super();
    this.state = {
      pathname: window.location.pathname || "/",
      count: 0
    };
  }
  componentDidMount() {
    window.addEventListener("pushState", () => {
      console.log("pushState");
      this.setPathname();
    });
  }
  setPathname = () => {
    this.setState(
      {
        pathname: window.location.pathname || "/",
        count: ++this.state.count,
      },
      (v) => {
        console.log(this.state);
      }
    );
  };
  render() {
    let value = {
      type: "BrowserRouter",
      history: History,
      location: Object.assign(
        {
          pathname: "/",
        },
        url.parse(this.state.pathname, true)
      ),
      count: this.state.count,
      cb:this.setPathname,
    };
    console.log(this.props.children);
    return (
      <Provider value={value}>
       {this.props.children}
      </Provider>
    );
  }
}

export default BrowserRouter;

Context

  • Provider:包装组件
  • Consumer:消费组件
import React from 'react';

let { Provider, Consumer } = React.createContext();

export { Provider, Consumer };

Route

React的一个消费组件,主要匹配正确的组件,渲染对应的组件

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

class Route extends React.Component {
  render() {
    return (
      <div>
        <Consumer>
          {(state) => {
            let { path, component: View } = this.props;
            if (path === state.location.pathname) {
              return <View {...state}></View>;
            }
            return null;
          }}
        </Consumer>
      </div>
    );
  }
}
export default Route;
  • state包装组件传递给消费组件的value
  • props父组件传来的数据

Link

实现思路

  • 返回一个阻止默认事件的a标签
  • 然后通过实践跳转的方式
import React from 'react';
import { Consumer } from "./context";

class Link extends React.Component {
  render() {
    return (
      <Consumer>
        {(state) => {
          let to = this.props.to || "/";
          to = to.indexOf("/") === 0 ? to : "/" + to;
          return (
            <a
              href={to}
              onClick={(e) => {
                if (e && e.preventDefault) {
                  e.preventDefault();
                } else {
                  window.event.returnValue = false;
                }
                state.history.push(to);
              }}
            >
              {this.props.children}
            </a>
          );
        }}
      </Consumer>
    );
  }
}
export default Link;

history

由于路由跳转有hashbrowser两种方式,每种模式有push,replace,go,goBack 4种方式

hash

通过 location.hash = ‘foo’ 这样的语法来改变,路径就会由 baidu.com 变更为 baidu.com/#foo
通过 window.addEventListener(‘hashchange’) 这个事件,就可以监听到 hash 值的变化。

history

通过 history.pushState({}, ‘’, foo),可以让 baidu.com 变化为 baidu.com/foo。
history 路由的监听也有点坑,浏览器提供了 window.addEventListener(‘popstate’) 事件,但是它只能监听到浏览器回退和前进所产生的路由变化,对于主动的 pushState 却监听不到。需要自己封装一个 listen API

const url = require("url");

class History {
  static push(path) {
    if (typeof path === "string") {
      window.history.pushState(null, "", path);
      return;
    }
    if (typeof path === "object") {
      let obj = {
        pathname: path.path || "/",
        query: path.query || {},
      };
      const formatUrl = url.format(obj);
      window.history.pushState(null, "", formatUrl);
    }
  }
}

export default History;

listen

利用函数劫持的方式,来重写window.history.pushState, window.history.replaceState方法

(function () {
  if (typeof window === undefined) {
    return;
  }
  var _wr = function (type) {
    var origin = window.history[type];
    return function () {
      var rv = origin.apply(this, arguments);
      var e = new Event(type);
      window.dispatchEvent(e);
      return rv;
    };
  };
  window.history.pushState = _wr("pushState");
  window.history.replaceState = _wr("replaceState");
})();

验证demo

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>MyRouter</title>
    <script
      crossorigin
      src="https://unpkg.com/react@16/umd/react.production.min.js"
    ></script>
    <script
      crossorigin
      src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"
    ></script>
    <script src="https://cdn.bootcss.com/babel-standalone/6.22.1/babel.min.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script src="./dist/main.js"></script>
  </body>
  <script type="text/babel">
    let { BrowserRouter,Route,Link } = MyRouter;

    class Home extends React.Component {
      render() {
        return (
          <div>
            <Link to="/demo">跳转demo</Link>
            <h1>Home</h1>
          </div>
        )
    }
  }

    class Demo extends React.Component {
      render() {
        return (
          <div>
            <Link to="/">跳转主页</Link>
            <h1>Demo</h1>
          </div>
        )
        }
    }

    class App extends React.Component {
      render() {
        return (
          <div>
          <BrowserRouter>
            <Route path="/" component={Home}></Route>
            <Route path="/demo" component={Demo}></Route>
          </BrowserRouter>
        </div>
        );
      }
    }

    ReactDOM.render(<App />, document.getElementById("root"));
  </script>
</html>

完整代码地址:html-app

后续内容,我们在手写一个react-router-dom简易版(二)继续实现

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值