了解前端路由,实现一个mini-react-router


theme: nico
highlight: vs

了解前端路由,实现一个mini-react-router

前言

自从React、Vue、Angular框架盛行之后,大部分的前端工程都从由传统的多页面应用转型成单页面应用。且不讨论单页面应用和多页面应用的优势劣势,前端路由已经成为前端开发者和面试官们绕不过去话题。

为什么要有前端路由?

在单页面应用中,我们仅在首次加载的时候向服务端一次性拉取所有的静态资源。后续的页面跳转是不会再向服务端请求资源的 ,仅以切换组件重新渲染页面达到页面跳转的目的。所以我们前端就需要自己维护一套url和组件展示的对应关系来确定页面上需要展示什么组件,这就是前端路由。react-routervue-router就是用来管理前端路由的前端路由库。

前端路由的实现核心原理

前端路由主要有两种模式histroy模式和hash模式。

history模式的路由长这样:https://baidu.com/foo

hash模式的路由长这样:https://baidu.com/#/foo

history模式原理

路由改变

其实history模式就是调用浏览器提供的history.pushState这个API。该方法的作用是在历史记录中新增一条记录,同时改变浏览器地址栏的url,但是不刷新页面!

/**
* @params state. state 代表状态对象,这让我们可以给每个路由记录创建自己的状态,并且它还会序列化后保存在用户的磁盘上
*/
history.pushState(state, title[, url])
监听路由

同一个文档的 history 对象出现变化时,就会触发 popstate 事件 history.pushState 可以使浏览器地址改变,但是无需刷新页面。

window.addEventListener('popstate',function(e){
    /* 监听改变 */
})

Tips:用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件popstate 事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮或者调用 history.back()、history.forward()、history.go()方法。

hash模式原理

通过location.hash = 'foo'这样的语法来改变,路径就会从https://www.baidu.com改变成https://www.baidu.com/#foo

通过window.addEventListener('hashChange')这个时间,就可以监听到hash值的变化,但是也不刷新页面!

react-router、react-router-dom、history库

history库对浏览器原生的history对象进行了封装,是react-router的核心库。

react-router里封装了Router、Route、Switch等核心组件,实现了从路由改变到组件更新的核心功能。是react-router-dom的核心库。

react-router-dom,在react-router的基础上添加了用于跳转的Link组件。除此之外还提供了history模式下的BrowserHistory和hash模式下的HashRouter组件。

实现一个mini-react-router

经过分析,一个mini-react-router应该具有以下功能:

  1. 路由切换的时候,不能刷新浏览器
  2. 路由切换的时候,被BrowserHistory包裹的组件需要监听到路由切换事件
  3. Route中需要在监听到路由发生变化的同时,拿到当前的路由信息,以此来判断是否将当前组件渲染出来
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
// import { BrowserRouter as Router, Route } from "./react-router-dom/index";
import Home from "./pages/Home";
import Login from "./pages/Login";
import Admin from "./pages/Admin";
// import Home from './pages/Home';

function App() {
  return (
    <Router>
      <Route path="/home" component={Home} />
      <Route path="/login" component={Login} />
      <Route path="/admin" component={Admin} />
    </Router>
  );
}

export default App;

演示效果:

实现BrowserRouter

  1. 首先 BrowserRouter 在初始化的时候就需要监听popState事件,并且卸载的时候移除监听(注意,这个popState不会响应js触发的history.pushState,后面我们会讲到)。
  2. 在用户点击前进后退按钮的时候会触发handlePopstate函数,我们在函数内部通过location获取到路由名称pathname,然后更新对应的state
  3. 其次,BrowserRouter 的内部会维护一个path用来和浏览器上的path保持同步,同时需要将这个path往下透传给Route(因为考虑到Route层级可能过深,这里使用context)
export function BrowserRouter(props) {
  // 首次渲染,获取到对应的路由
  const [path, setPath] = useState(() => {
    const { pathname } = window.location;

    return pathname || "/";
  });

  // popState触发的时候更新path。(这个popState只能监听到浏览器的前进后退事件,通过js - history.pushState触发的监听不到)
  const handlePopstate = (event) => {
    const { pathname } = window.location;
    setPath(pathname);

    return () => {
      window.removeEventListener("popstate", handlePopstate);
    };
  };

  // 用来ui上点击的跳转
  const push = (path) => {
    // 这个push需要做两件事情:1. 调用 history.pushState API 改变浏览器上的路由 2. 改变browserHistory内部的路由
    // tips: 通过 hisotry.pushState 没发触发 'popstate' 事件。所以改变内部路由需要手动 setPath();
    window.history.pushState({ path }, "", path);
    setPath(path);
  };

  const goBack = function () {
    window.history.go(-1);
  };

  useEffect(() => {
    window.addEventListener("popState", () => handlePopstate);
  }, []);

  // 将路由path通过 Context 传递给子组件
  return (
    <RouterContext.Provider
      value={{
        path,
        history: {
          push,
          goBack,
        },
      }}
    >
      {props.children}
    </RouterContext.Provider>
  );
}

实现Route

  1. Route的逻辑非常简单,当path和当前的path匹配的时候,就展示出来,否则就返回null
  2. Route内部去消费Context传递下来的path和history(这也就是为什么挂载在Route上的组件的props中是有history对象的)
export function Route(props) {
  console.log("Route props", props);
  // 接受连个参数 要渲染的组件:component. 展示的路径:path
  const { component: Component, path: componentPath } = props;

  return (
    <RouterContext.Consumer>
      {({ path, history }) => {
        // 只有路径匹配上的时候才展示对应的组件
        return componentPath === path ? <Component history={history} /> : null;
      }}
    </RouterContext.Consumer>
  );
}

实现Link

  1. 我们知道,Link实际上是用来做前端路由跳转的。调用的时候传一个to表示要跳转的路径
  2. 我们Link内部需要拿到history对象,使用提前封装好的push函数进行跳转。保证即改变了浏览器的路由、又保证内部的组件重新根据最新的path重新渲染。
export function Link(props) {
  const { to, children } = props;

  const goToPage = (history) => {
    history.push(to);
  };
  return (
    <RouterContext.Consumer>
      {/* 这里的history是通过 BrowserHistory 传递下来的 */}
      {({ path, history }) => {
        // 只有路径匹配上的时候才展示对应的组件
        return React.createElement('div', {
          history,
          onClick: () => {
            goToPage(history)
          },
        }, children)
      }}
    </RouterContext.Consumer>
  );
}

完整代码

import React, { useState, useEffect, createContext } from "react";

const RouterContext = createContext();

export function Link(props) {
  const { to, children } = props;

  const goToPage = (history) => {
    history.push(to);
  };
  return (
    <RouterContext.Consumer>
      {/* 这里的history是通过 BrowserHistory 传递下来的 */}
      {({ path, history }) => {
        // 只有路径匹配上的时候才展示对应的组件
        return React.createElement('div', {
          history,
          onClick: () => {
            goToPage(history)
          },
        }, children)
      }}
    </RouterContext.Consumer>
  );
}

export function Route(props) {
  console.log("Route props", props);
  // 接受连个参数 要渲染的组件:component. 展示的路径:path
  const { component: Component, path: componentPath } = props;

  return (
    <RouterContext.Consumer>
      {({ path, history }) => {
        // 只有路径匹配上的时候才展示对应的组件
        return componentPath === path ? <Component history={history} /> : null;
      }}
    </RouterContext.Consumer>
  );
}

export function BrowserRouter(props) {
  // 首次渲染,获取到对应的路由
  const [path, setPath] = useState(() => {
    const { pathname } = window.location;

    return pathname || "/";
  });

  // popState触发的时候更新path。(这个popState只能监听到浏览器的前进后退事件,通过js - history.pushState触发的监听不到)
  const handlePopstate = (event) => {
    const { pathname } = window.location;
    setPath(pathname);

    return () => {
      window.removeEventListener("popstate", handlePopstate);
    };
  };

  // 用来ui上点击的跳转
  const push = (path) => {
    // 这个push需要做两件事情:1. 调用 history.pushState API 改变浏览器上的路由 2. 改变browserHistory内部的路由
    // tips: 通过 hisotry.pushState 没发触发 'popstate' 事件。所以改变内部路由需要手动 setPath();
    window.history.pushState({ path }, "", path);
    setPath(path);
  };

  const goBack = function () {
    window.history.go(-1);
  };

  useEffect(() => {
    window.addEventListener("popState", () => handlePopstate);
  }, []);

  // 将路由path通过 Context 传递给子组件
  return (
    <RouterContext.Provider
      value={{
        path,
        history: {
          push,
          goBack,
        },
      }}
    >
      {props.children}
    </RouterContext.Provider>
  );
}

在线演示地址

在线演示地址

参考链接

实现一个简易的react-router

「源码解析 」这一次彻底弄懂react-router路由原理

深入探索前端路由,手写 react-mini-router

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容