一文了解 history 和 react-router 的实现原理

这篇具有很好参考价值的文章主要介绍了一文了解 history 和 react-router 的实现原理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:霜序

前言

在前一篇文章中,我们详细的说了 react-router@3.x 升级到 @6.x 需要注意的问题以及变更的使用方式。

react-router 版本更新非常快,但是它的底层实现原理确是万变不离其中,在本文中会从前端路由出发到 react-router 原理总结与分享。

前端路由

在 Web 前端单页面应用 SPA(Single Page Application)中,路由是描述 URL 和 UI 之间的映射关系,这种映射是单向的,即 URL 的改变会引起 UI 更新,无需刷新页面

如何实现前端路由

实现前端路由,需要解决两个核心问题

  1. 如何改变 URL 却不引起页面刷新?
  2. 如何监测 URL 变化?

在前端路由的实现模式有两种模式,hash 和 history 模式,分别回答上述两个问题

hash 模式

  1. hash 是 url 中 hash(#) 及后面的部分,常用锚点在页面内做导航,改变 url 中的 hash 部分不会引起页面的刷新
  2. 通过 hashchange 事件监听 URL 的改变。改变 URL 的方式只有以下几种:通过浏览器导航栏的前进后退、通过<a>标签、通过window.location,这几种方式都会触发hashchange事件

history 模式

  1. history 提供了 pushStatereplaceState 两个方法,这两个方法改变 URL 的 path 部分不会引起页面刷新
  2. 通过 popchange 事件监听 URL 的改变。需要注意只在通过浏览器导航栏的前进后退改变 URL 时会触发popstate事件,通过<a>标签和pushState/replaceState不会触发popstate方法。但我们可以拦截<a>标签的点击事件和pushState/replaceState的调用来检测 URL 变化,也是可以达到监听 URL 的变化,相对hashchange显得略微复杂

JS 实现前端路由

基于 hash 实现

由于三种改变 hash 的方式都会触发hashchange方法,所以只需要监听hashchange方法。需要在DOMContentLoaded后,处理一下默认的 hash 值

// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件,处理默认hash
window.addEventListener('DOMContentLoaded', onLoad);
// 监听路由变化
window.addEventListener('hashchange', onHashChange);
// 路由变化时,根据路由渲染对应 UI
function onHashChange() {
  switch (location.hash) {
    case '#/home':
      routerView.innerHTML = 'This is Home';
      return;
    case '#/about':
      routerView.innerHTML = 'This is About';
      return;
    case '#/list':
      routerView.innerHTML = 'This is List';
      return;
    default:
      routerView.innerHTML = 'Not Found';
      return;
  }
}

hash 实现 demo

基于 history 实现

因为 history 模式下,<a>标签和pushState/replaceState不会触发popstate方法,我们需要对<a>的跳转和pushState/replaceState做特殊处理。

  • <a>作点击事件,禁用默认行为,调用pushState方法并手动触发popstate的监听事件
  • pushState/replaceState可以重写 history 的方法并通过派发事件能够监听对应事件
var _wr = function (type) {
  var orig = history[type];
  return function () {
    var e = new Event(type);
    e.arguments = arguments;
    var rv = orig.apply(this, arguments);
    window.dispatchEvent(e);
    return rv;
  };
};
// 重写pushstate事件
history.pushState = _wr('pushstate');

function onLoad() {
  routerView = document.querySelector('#routeView');
  onPopState();
  // 拦截 <a> 标签点击事件默认行为
  // 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
  var linkList = document.querySelectorAll('a[href]');
  linkList.forEach((el) =>
    el.addEventListener('click', function (e) {
      e.preventDefault();
      history.pushState(null, '', el.getAttribute('href'));
      onPopState();
    }),
  );
}
// 监听pushstate方法
window.addEventListener('pushstate', onPopState());
// 页面加载完不会触发 hashchange,这里主动触发一次 popstate 事件,处理默认pathname
window.addEventListener('DOMContentLoaded', onLoad);
// 监听路由变化
window.addEventListener('popstate', onPopState);
// 路由变化时,根据路由渲染对应 UI
function onPopState() {
  switch (location.pathname) {
    case '/home':
      routerView.innerHTML = 'This is Home';
      return;
    case '/about':
      routerView.innerHTML = 'This is About';
      return;
    case '/list':
      routerView.innerHTML = 'This is List';
      return;
    default:
      routerView.innerHTML = 'Not Found';
      return;
  }
}

history 实现 demo

React-Router 的架构

一文了解 history 和 react-router 的实现原理

  • history 库给 browser、hash 两种 history 提供了统一的 API,给到 react-router-dom 使用
  • react-router 实现了路由的最核心能力。提供了<Router><Route>等组件,以及配套 hook
  • react-router-dom 是对 react-router 更上一层封装。把 history 传入<Router>并初始化成<BrowserRouter><HashRouter>,补充了<Link>这样给浏览器直接用的组件。同时把 react-router 直接导出,减少依赖

History 实现

history

在上文中说到,BrowserRouter使用 history 库提供的createBrowserHistory创建的history对象改变路由状态和监听路由变化。

❓ 那么 history 对象需要提供哪些功能讷?

  • 监听路由变化的listen方法以及对应的清理监听unlisten方法
  • 改变路由的push方法
// 创建和管理listeners的方法
export const EventEmitter = () => {
  const events = [];
  return {
    subscribe(fn) {
      events.push(fn);
      return function () {
        events = events.filter((handler) => handler !== fn);
      };
    },
    emit(arg) {
      events.forEach((fn) => fn && fn(arg));
    },
  };
};

BrowserHistory

const createBrowserHistory = () => {
  const EventBus = EventEmitter();
  // 初始化location
  let location = {
    pathname: '/',
  };
  // 路由变化时的回调
  const handlePop = function () {
    const currentLocation = {
      pathname: window.location.pathname,
    };
    EventBus.emit(currentLocation); // 路由变化时执行回调
  };
  // 定义history.push方法
  const push = (path) => {
    const history = window.history;
    // 为了保持state栈的一致性
    history.pushState(null, '', path);
    // 由于push并不触发popstate,我们需要手动调用回调函数
    location = { pathname: path };
    EventBus.emit(location);
  };

  const listen = (listener) => EventBus.subscribe(listener);

  // 处理浏览器的前进后退
  window.addEventListener('popstate', handlePop);

  // 返回history
  const history = {
    location,
    listen,
    push,
  };
  return history;
};

对于 BrowserHistory 来说,我们的处理需要增加一项,当我们触发 push 的时候,需要手动通知所有的监听者,因为 pushState 无法触发 popState 事件,因此需要手动触发

HashHistory

const createHashHistory = () => {
  const EventBus = EventEmitter();
  let location = {
    pathname: '/',
  };
  // 路由变化时的回调
  const handlePop = function () {
    const currentLocation = {
      pathname: window.location.hash.slice(1),
    };
    EventBus.emit(currentLocation); // 路由变化时执行回调
  };
  // 不用手动执行回调,因为hash改变会触发hashchange事件
  const push = (path) => (window.location.hash = path);
  const listen = (listener: Function) => EventBus.subscribe(listener);
  // 监听hashchange事件
  window.addEventListener('hashchange', handlePop);
  // 返回的history上有个listen方法
  const history = {
    location,
    listen,
    push,
  };
  return history;
};

在实现 hashHistory 的时候,我们只是对hashchange进行了监听,当该事件发生时,我们获取到最新的 location 对象,在通知所有的监听者 listener 执行回调函数

React-Router@6 丐版实现

一文了解 history 和 react-router 的实现原理

  • 绿色为 history 中的方法
  • 紫色为 react-router-dom 中的方法
  • 橙色为 react-router 中的方法

Router

🎗️ 基于 Context 的全局状态下发。Router 是一个 “Provider-Consumer” 模型

Router 做的事情很简单,接收navigatorlocation,使用 context 将数据传递下去,能够让子组件获取到相关的数据

function Router(props: IProps) {
  const { navigator, children, location } = props;

  const navigationContext = React.useMemo(() => ({ navigator }), [navigator]);

  const { pathname } = location;

  const locationContext = React.useMemo(
    () => ({ location: { pathname } }),
    [pathname],
  );

  return (
    <NavigationContext.Provider value={navigationContext}>
      <LocationContext.Provider value={locationContext} children={children} />
    </NavigationContext.Provider>
  );
}

HashRouter

基于不同的 history 调用 Router 组件。并且在 history 发生改变的时候,监听 history,能够在 location 发生改变的时候,执行回调改变 location。

在下面的代码中,能够发现监听者为 setState 函数,在上述 hashHistory 中,如果我们的 location 发生了改变,会通知到所有的监听者执行回调,也就是我们这里的 setState 函数,即我们能够拿到最新的 location 信息通过 LocationContext 传递给子组件,再去做对应的路由匹配

function HashRouter({ children }) {
  let historyRef = React.useRef();
  if (historyRef.current == null) {
    historyRef.current = createHashHistory();
  }
  let history = historyRef.current;
  let [state, setState] = React.useState({
    location: history.location,
  });

  React.useEffect(() => {
    const unListen = history.listen(setState);
    return unListen;
  }, [history]);

  return (
    <Router children={children} location={state.location} navigator={history} />
  );
}

Routes/Route

我们能够发现在 v6.0 的版本 Route 组件只是一个工具人,并没有做任何事情。

function Route(_props: RouteProps): React.ReactElement | null {
  invariant(
    false,
    `A <Route> is only ever to be used as the child of <Routes> element, ` +
      `never rendered directly. Please wrap your <Route> in a <Routes>.`,
  );
}

实际上处理一切逻辑的组件是 Routes,它内部实现了根据路由的变化,匹配出一个正确的组件。

const Routes = ({ children }) => {
  return useRoutes(createRoutesFromChildren(children));
};

useRoutes 为整个 v6 版本的核心,分为路由上下文解析、路由匹配、路由渲染三个步骤

<Routes>
  <Route path="/home" element={<Home />}>
    <Route path="1" element={<Home1 />}>
      <Route path="2" element={<Home2 />}></Route>
    </Route>
  </Route>
  <Route path="/about" element={<About />}></Route>
  <Route path="/list" element={<List />}></Route>
  <Route path="/notFound" element={<NotFound />} />
  <Route path="/navigate" element={<Navigate to="/notFound" />} />
</Routes>

上述 Routes 代码中,通过 createRoutesFromChildren 函数将 Route 组件结构化。可以把 <Route> 类型的 react element 对象,变成了普通的 route 对象结构,如下图

一文了解 history 和 react-router 的实现原理

useRoutes

useRoutes 才是真正处理渲染关系的,其代码如下:

// 第一步:获取相关的 pathname
let location = useLocation();
let { matches: parentMatches } = React.useContext(RouteContext);
// 第二步:找到匹配的路由分支,将 pathname 和 Route 的 path 做匹配
const matches = matchRoutes(routes, location);
// 第三步:渲染真正的路由组件
const renderedMatches = _renderMatches(matches, parentMatches);

return renderedMatches;

matchRoutes

matchRoutes 中通过 pathname 和路由的 path 进行匹配

因为我们在 Route 中定义的 path 都是相对路径,所以我们在 matchRoutes 方法中,需要对 routes 对象遍历,对于 children 里面的 path 需要变成完整的路径,并且需要将 routes 扁平化,不在使用嵌套结构

const flattenRoutes = (
  routes,
  branches = [],
  parentsMeta = [],
  parentPath = '',
) => {
  const flattenRoute = (route) => {
    const meta = {
      relativePath: route.path || '',
      route,
    };
    const path = joinPaths([parentPath, meta.relativePath]);

    const routesMeta = parentsMeta.concat(meta);
    if (route.children?.length > 0) {
      flattenRoutes(route.children, branches, routesMeta, path);
    }
    if (route.path == null) {
      return;
    }
    branches.push({ path, routesMeta });
  };
  routes.forEach((route) => {
    flattenRoute(route);
  });
  return branches;
};

一文了解 history 和 react-router 的实现原理

当我们访问/#/home/1/2的时候,获得的 matches 如下

一文了解 history 和 react-router 的实现原理

我们得到的 match 顺序是从 Home → Home1 → Home2

_renderMatches

_renderMatches 才会渲染所有的 matches 对象

const _renderMatches = (matches, parentMatches = []) => {
  let renderedMatches = matches;
  return renderedMatches.reduceRight((outlet, match, index) => {
    let matches = parentMatches.concat(renderedMatches.slice(0, index + 1));
    const getChildren = () => {
      let children;
      if (match.route.Component) {
        children = <match.route.Component />;
      } else if (match.route.element) {
        children = match.route.element;
      } else {
        children = outlet;
      }
      return (
        <RouteContext.Provider
          value={{
            outlet,
            matches,
          }}
        >
          {children}
        </RouteContext.Provider>
      );
    };
    return getChildren();
  }, null);
};

_renderMatches 这段代码我们能够明白 outlet 作为子路由是如何传递给父路由渲染的。matches 采用从右往左的遍历顺序,将上一项的返回值作为后一项的 outlet,那么子路由就作为 outlet 传递给了父路由

一文了解 history 和 react-router 的实现原理

Outlet

实际上就是内部渲染 RouteContext 的 outlet 属性

function Outlet(props) {
  return useOutlet(props.context);
}

function useOutlet(context?: unknown) {
  let outlet = useContext(RouteContext).outlet; // 获取上一级 RouteContext 上面的 outlet
  if (outlet) {
    return (
      <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
    );
  }
  return outlet;
}

Link

在 Link 中,我们使用<a>标签来做跳转,但是 a 标签会使页面重新刷新,所以需要阻止 a 标签的默认行为,调用 useNavigate 方法进行跳转

function Link({ to, children, onClick }) {
  const navigate = useNavigate();

  const handleClick = onClick
    ? onClick
    : (event) => {
        event.preventDefault();
        navigate(to);
      };

  return (
    <a href={to} onClick={handleClick}>
      {children}
    </a>
  );
}

Hooks

function useLocation() {
  return useContext(LocationContext).location;
}

function useNavigate() {
  const { navigator } = useContext(NavigationContext);

  const navigate = useCallback(
    (to: string) => {
      navigator.push(to);
    },
    [navigator],
  );
  return navigate;
}

本文所有的代码链接可点击查看

参考链接

  • react router v6 使用详解以及部分源码解析(新老版本对比) - 掘金
  • 「React 进阶」react-router v6 通关指南 - 掘金
  • 一文读懂 react-router 原理

最后

欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star文章来源地址https://www.toymoban.com/news/detail-652549.html

  • 大数据分布式任务调度系统——Taier
  • 轻量级的 Web IDE UI 框架——Molecule
  • 针对大数据领域的 SQL Parser 项目——dt-sql-parser
  • 袋鼠云数栈前端团队代码评审工程实践文档——code-review-practices
  • 一个速度更快、配置更灵活、使用更简单的模块打包器——ko

到了这里,关于一文了解 history 和 react-router 的实现原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • React-Router路由基础篇(简单易学)

    文章目录 系列文章目录 前言 一、pandas是什么? 二、使用步骤 1.引入库 2.读入数据 总结 提示:这里可以添加本文要记录的大概内容: 例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。 (

    2024年04月14日
    浏览(43)
  • React-学习笔记(8—react-router@5 and @6)

    目录 1、react-router@5 1-1、在项目中安装路由 1-2、一个项目使用一个路由器来管理路由即可 1-3、 路由组件和一般组件的区别 1-4、使用 NavLink 1-5、封装 NavLink —— MyNavLink 1-6、使用 Switch 标签 1-7、BrowserRouter解决多级路径匹配样式丢失问题 1-8、路由的模糊匹配和严格匹配 1-9、路

    2024年02月12日
    浏览(46)
  • react-router5: 地址栏变化,页面没变

    今天在写 react-router demo 的时候,出现了一个问题,浏览器地址栏变了,但是页面没变 手动刷新一下就变了 解决办法:

    2023年04月09日
    浏览(58)
  • 【前端知识】React 基础巩固(三十九)——React-Router的基本使用

    Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件。 Router包括两个API: BrowserRouter使用history模式 HashRouter使用hash模式(路径后面带有#号) 尝试在项目中使用HashRouter: 安装Router 在 index.js 中引入并使用HashRouter Routes:包裹所有的Route,在其中匹配一个路由(

    2024年02月14日
    浏览(66)
  • React-Router 5.0 制作导航栏+页面参数传递

    使用 React 构建 SPA 应用(单页面应用),要想实现页面间的跳转,首先想到的就是使用路由。在 React 中,常用的有两个包可以实现这个需求,那就是 react-router 和 react-router-dom 。本文主要针对 react-router-dom 进行说明。 众所周知, JS 由 DOM BOM ECMAScript 组成, React-Router-Dom 使用 BOM 提供

    2024年02月08日
    浏览(45)
  • <React-Router>超全react路由知识点(跳转、传参、子路由、重定向等)

    目录 一、路由环境配置 1.搭建一个项目名为boke(自定义),编辑文档语言为TypeScript的项目环境。 2.安装所有package.json中的依赖----安装包(注意要【cd 项目名】进入项目文件安装) 3.添加React-Router路由 4.文件、目录相关修改与搭建 二、路由的懒加载  三、路由跳转 1.标签跳转(

    2024年02月11日
    浏览(90)
  • 创建react脚手架项目——demo(react18 + react-router 6)+ react项目打包部署

    全局安装 create-react-app 说明:不建议安装全局,建议使用 npx 命令安装,具体可参考官网,如下: 官网: https://zh-hans.legacy.reactjs.org/docs/create-a-new-react-app.html. 1.2.1 问题1——npm ERR! code ENOTFOUND(网络问题clashx) 问题描述,如下: 解决问题——方式1 如果使用了clashx,可能是它

    2024年02月07日
    浏览(72)
  • 【退役之重学前端】使用vite+vue3+vue-router,重构react+react-router前后端分离的商城后台管理系统

    前言: 对前端各个技术板块,HTML、CSS、JavaScript、ES6、vue家族,整体上能“摸其大概”。笔者计划重构一个基于react的商城后台管理系统。 —— 2024年2月16日 vue3 sass bootstrap ES7 前后端分离 分层架构 模块化开发 npm vite git

    2024年02月20日
    浏览(56)
  • React和Vue 中的 router 实现原理如何

    React 和 Vue 中的路由器(Router)实现原理类似,都是基于监听 URL 变化,然后根据不同的 URL 加载相应的组件或页面。下面是它们的一般实现原理: React Router 实现原理: History API:  React Router 使用 HTML5 History API(pushState() 和 replaceState() 方法)来操作浏览器的历史记录,从而

    2024年02月20日
    浏览(49)
  • 一文带你了解React框架

    前言 由于 React的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来 Web 开发的主流工具。 这个项目本身也越滚越大,从最早的UI引擎变成了一整套前后端通吃的 Web App 解决方案。衍生的 React Nativ

    2024年02月01日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包