ReactRouter的实现方法

ReactRouter的实现

ReactRouterReact的核心组件,主要是作为React的路由管理器,保持UIURL同步,其拥有简单的API与强大的功能例如代码缓冲加载、动态路由匹配、以及建立正确的位置过渡处理等。

描述

React Router是建立在history对象之上的,简而言之一个history对象知道如何去监听浏览器地址栏的变化,并解析这个URL转化为location对象,然后router使用它匹配到路由,最后正确地渲染对应的组件,常用的history有三种形式: Browser HistoryHash HistoryMemory History

Browser History

Browser History是使用React Router的应用推荐的history,其使用浏览器中的History对象的pushStatereplaceStateAPI以及popstate事件等来处理URL,其能够创建一个像https://www.example.com/path这样真实的URL,同样在页面跳转时无须重新加载页面,当然也不会对于服务端进行请求,当然对于history模式仍然是需要后端的配置支持,用以支持非首页的请求以及刷新时后端返回的资源,由于应用是个单页客户端应用,如果后台没有正确的配置,当用户在浏览器直接访问URL时就会返回404,所以需要在服务端增加一个覆盖所有情况的候选资源,如果URL匹配不到任何静态资源时,则应该返回同一个index.html应用依赖页面,例如在Nginx下的配置。

location / {
 try_files $uri $uri/ /index.html;
}

Hash History

Hash符号即#原本的目的是用来指示URL中指示网页中的位置,例如https://www.example.com/index.html#print即代表exampleindex.htmlprint位置,浏览器读取这个URL后,会自动将print位置滚动至可视区域,通常使用<a>标签的name属性或者<div>标签的id属性指定锚点。
通过window.location.hash属性能够读取锚点位置,可以为Hash的改变添加hashchange监听事件,每一次改变Hash,都会在浏览器的访问历史中增加一个记录,此外Hash虽然出现在URL中,但不会被包括在HTTP请求中,即#及之后的字符不会被发送到服务端进行资源或数据的请求,其是用来指导浏览器动作的,对服务器端没有效果,因此改变Hash不会重新加载页面。
ReactRouter的作用就是通过改变URL,在不重新请求页面的情况下,更新页面视图,从而动态加载与销毁组件,简单的说就是,虽然地址栏的地址改变了,但是并不是一个全新的页面,而是之前的页面某些部分进行了修改,这也是SPA单页应用的特点,其所有的活动局限于一个Web页面中,非懒加载的页面仅在该Web页面初始化时加载相应的HTMLJavaScriptCSS文件,一旦页面加载完成,SPA不会进行页面的重新加载或跳转,而是利用JavaScript动态的变换HTML,默认Hash模式是通过锚点实现路由以及控制组件的显示与隐藏来实现类似于页面跳转的交互。

Memory History

Memory History不会在地址栏被操作或读取,这就可以解释如何实现服务器渲染的,同时其也非常适合测试和其他的渲染环境例如React Native,和另外两种History的一点不同是我们必须创建它,这种方式便于测试。

const history = createMemoryHistory(location);

实现

我们来实现一个非常简单的Browser History模式与Hash History模式的实现,因为H5pushState方法不能在本地文件协议file://运行,所以运行起来需要搭建一个http://环境,使用webpackNginxApache等都可以,回到Browser History模式路由,能够实现history路由跳转不刷新页面得益与H5提供的pushState()replaceState()等方法以及popstate等事件,这些方法都是也可以改变路由路径,但不作页面跳转,当然如果在后端不配置好的情况下路由改编后刷新页面会提示404,对于Hash History模式,我们的实现思路相似,主要在于没有使用pushStateH5API,以及监听事件不同,通过监听其hashchange事件的变化,然后拿到对应的location.hash更新对应的视图。

<!-- Browser History -->
<!DOCTYPE html>
<html lang="en">

<head>
 <meta charset="UTF-8">
 <title>Router</title>
</head>

<body>
 <ul>
  <li><a href="/home" rel="external nofollow" >home</a></li>
  <li><a href="/about" rel="external nofollow" >about</a></li>
  <div id="routeView"></div>
 </ul>
</body>
<script>
 function Router() {
  this.routeView = null; // 组件承载的视图容器
  this.routes = Object.create(null); // 定义的路由
 }

 // 绑定路由匹配后事件
 Router.prototype.route = function (path, callback) {
  this.routes[path] = () => this.routeView.innerHTML = callback() || "";
 };

 // 初始化
 Router.prototype.init = function(root, rootView) {
  this.routeView = rootView; // 指定承载视图容器
  this.refresh(); // 初始化即刷新视图
  root.addEventListener("click", (e) => { // 事件委托到root
   if (e.target.nodeName === "A") {
    e.preventDefault();
    history.pushState(null, "", e.target.getAttribute("href"));
    this.refresh(); // 触发即刷新视图
   }
  })
  // 监听用户点击后退与前进
  // pushState与replaceState不会触发popstate事件
  window.addEventListener("popstate", this.refresh.bind(this), false);
 };

 // 刷新视图
 Router.prototype.refresh = function () {
  let path = location.pathname;
  console.log("refresh", path);
  if(this.routes[path]) this.routes[path]();
  else this.routeView.innerHTML = "";
 };

 window.Router = new Router();

 Router.route("/home", function() {
  return "home";
 });
 Router.route("/about", function () {
  return "about";
 });

 Router.init(document, document.getElementById("routeView"));

</script>
</html>
<!-- Hash History -->
<!DOCTYPE html>
<html lang="en">

<head>
 <meta charset="UTF-8">
 <title>Router</title>
</head>

<body>
 <ul>
  <li><a href="#/home" rel="external nofollow" >home</a></li>
  <li><a href="#/about" rel="external nofollow" >about</a></li>
  <div id="routeView"></div>
 </ul>
</body>
<script>
 function Router() {
  this.routeView = null; // 组件承载的视图容器
  this.routes = Object.create(null); // 定义的路由
 }

 // 绑定路由匹配后事件
 Router.prototype.route = function (path, callback) {
  this.routes[path] = () => this.routeView.innerHTML = callback() || "";
 };

 // 初始化
 Router.prototype.init = function(root, rootView) {
  this.routeView = rootView; // 指定承载视图容器
  this.refresh(); // 初始化触发
  // 监听hashchange事件用以刷新
  window.addEventListener("hashchange", this.refresh.bind(this), false);
 };

 // 刷新视图
 Router.prototype.refresh = function () {
  let hash = location.hash;
  console.log("refresh", hash);
  if(this.routes[hash]) this.routes[hash]();
  else this.routeView.innerHTML = "";
 };

 window.Router = new Router();

 Router.route("#/home", function() {
  return "home";
 });
 Router.route("#/about", function () {
  return "about";
 });

 Router.init(document, document.getElementById("routeView"));

</script>
</html>

分析

  • 我们可以看一下ReactRouter的实现,commit ideef79d5TAG4.4.0,在这之前我们需要先了解一下history库,history库,是ReactRouter依赖的一个对window.history加强版的history库,其中主要用到的有match对象表示当前的URLpath的匹配的结果,location对象是history库基于window.location的一个衍生。
  • ReactRouter将路由拆成了几个包: react-router负责通用的路由逻辑,react-router-dom负责浏览器的路由管理,react-router-native负责react-native的路由管理。
  • 我们以BrowserRouter组件为例,BrowserRouterreact-router-dom中,它是一个高阶组件,在内部创建一个全局的history对象,可以监听整个路由的变化,并将history作为props传递给react-routerRouter组件,Router组件再会将这个history的属性作为context传递给子组件。
// packages\react-router-dom\modules\HashRouter.js line 10
class BrowserRouter extends React.Component {
 history = createHistory(this.props);

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

接下来我们到Router组件,Router组件创建了一个React Context环境,其借助contextRoute传递context,这也解释了为什么Router要在所有Route的外面。在RoutercomponentWillMount中,添加了history.listen,其能够监听路由的变化并执行回调事件,在这里即会触发setState。当setState时即每次路由变化时 -> 触发顶层Router的回调事件 -> Router进行setState -> 向下传递 nextContext此时context中含有最新的location -> 下面的Route获取新的nextContext判断是否进行渲染。

// line packages\react-router\modules\Router.js line 10
class Router extends React.Component {
 static computeRootMatch(pathname) {
 return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
 }

 constructor(props) {
 super(props);

 this.state = {
  location: props.history.location
 };

 // This is a bit of a hack. We have to start listening for location
 // changes here in the constructor in case there are any <Redirect>s
 // on the initial render. If there are, they will replace/push when
 // they mount and since cDM fires in children before parents, we may
 // get a new location before the <Router> is mounted.
 this._isMounted = false;
 this._pendingLocation = null;

 if (!props.staticContext) {
  this.unlisten = props.history.listen(location => {
  if (this._isMounted) {
   this.setState({ location });
  } else {
   this._pendingLocation = location;
  }
  });
 }
 }

 componentDidMount() {
 this._isMounted = true;

 if (this._pendingLocation) {
  this.setState({ location: this._pendingLocation });
 }
 }

 componentWillUnmount() {
 if (this.unlisten) this.unlisten();
 }

 render() {
 return (
  <RouterContext.Provider
  children={this.props.children || null}
  value={{
   history: this.props.history,
   location: this.state.location,
   match: Router.computeRootMatch(this.state.location.pathname),
   staticContext: this.props.staticContext
  }}
  />
 );
 }
}

我们在使用时都是使用Router来嵌套Route,所以此时就到Route组件,Route的作用是匹配路由,并传递给要渲染的组件propsRoute接受上层的Router传入的contextRouter中的history监听着整个页面的路由变化,当页面发生跳转时,history触发监听事件,Router向下传递nextContext,就会更新Routepropscontext来判断当前Routepath是否匹配location,如果匹配则渲染,否则不渲染,是否匹配的依据就是computeMatch这个函数,在下文会有分析,这里只需要知道匹配失败则matchnull,如果匹配成功则将match的结果作为props的一部分,在render中传递给传进来的要渲染的组件。Route接受三种类型的render props<Route component><Route render><Route children>,此时要注意的是如果传入的component是一个内联函数,由于每次的props.component都是新创建的,所以Reactdiff的时候会认为进来了一个全新的组件,所以会将旧的组件unmountre-mount。这时候就要使用render,少了一层包裹的component元素,render展开后的元素类型每次都是一样的,就不会发生re-mount了,另外children也不会发生re-mount

// \packages\react-router\modules\Route.js line 17
class Route extends React.Component {
 render() {
 return (
  <RouterContext.Consumer>
  {context => {
   invariant(context, "You should not use <Route> outside a <Router>");

   const location = this.props.location || context.location;
   const match = this.props.computedMatch
   ? this.props.computedMatch // <Switch> already computed the match for us
   : this.props.path
    ? matchPath(location.pathname, this.props)
    : context.match;

   const props = { ...context, location, match };

   let { children, component, render } = this.props;

   // Preact uses an empty array as children by
   // default, so use null if that's the case.
   if (Array.isArray(children) && children.length === 0) {
   children = null;
   }

   if (typeof children === "function") {
   children = children(props);
   // ...
   }

   return (
   <RouterContext.Provider value={props}>
    {children && !isEmptyChildren(children)
    ? children
    : props.match
     ? component
     ? React.createElement(component, props)
     : render
      ? render(props)
      : null
     : null}
   </RouterContext.Provider>
   );
  }}
  </RouterContext.Consumer>
 );
 }
}

我们实际上我们可能写的最多的就是Link这个标签了,所以我们再来看一下<Link>组件,我们可以看到Link最终还是创建一个a标签来包裹住要跳转的元素,在这个a标签的handleClick点击事件中会preventDefault禁止默认的跳转,所以实际上这里的href并没有实际的作用,但仍然可以标示出要跳转到的页面的URL并且有更好的html语义。在handleClick中,对没有被preventDefault、鼠标左键点击的、非_blank跳转的、没有按住其他功能键的单击进行preventDefault,然后pushhistory中,这也是前面讲过的路由的变化与 页面的跳转是不互相关联的,ReactRouterLink中通过history库的push调用了HTML5 historypushState,但是这仅仅会让路由变化,其他什么都没有改变。在Router中的listen,它会监听路由的变化,然后通过context更新propsnextContext让下层的Route去重新匹配,完成需要渲染部分的更新。

// packages\react-router-dom\modules\Link.js line 14
class Link extends React.Component {
 handleClick(event, history) {
 if (this.props.onClick) this.props.onClick(event);

 if (
  !event.defaultPrevented && // onClick prevented default
  event.button === 0 && // ignore everything but left clicks
  (!this.props.target || this.props.target === "_self") && // let browser handle "target=_blank" etc.
  !isModifiedEvent(event) // ignore clicks with modifier keys
 ) {
  event.preventDefault();

  const method = this.props.replace ? history.replace : history.push;

  method(this.props.to);
 }
 }

 render() {
 const { innerRef, replace, to, ...rest } = this.props; // eslint-disable-line no-unused-vars

 return (
  <RouterContext.Consumer>
  {context => {
   invariant(context, "You should not use <Link> outside a <Router>");

   const location =
   typeof to === "string"
    ? createLocation(to, null, null, context.location)
    : to;
   const href = location ? context.history.createHref(location) : "";

   return (
   <a
    {...rest}
    onClick={event => this.handleClick(event, context.history)}
    href={href}
    ref={innerRef}
   />
   );
  }}
  </RouterContext.Consumer>
 );
 }
}

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://zhuanlan.zhihu.com/p/44548552 https://github.com/fi3ework/blog/issues/21 https://juejin.cn/post/6844903661672333326 https://juejin.cn/post/6844904094772002823 https://juejin.cn/post/6844903878568181768 https://segmentfault.com/a/1190000014294604 https://github.com/youngwind/blog/issues/109 http://react-guide.github.io/react-router-cn/docs/guides/basics/Histories.html

到此这篇关于ReactRouter的实现方法的文章就介绍到这了,更多相关ReactRouter的实现内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • react router4+redux实现路由权限控制的方法

    总体概述 一个完善的路由系统应该是这样子的,当链接到的组件是需要登录后才能查看,要能够跳转到登录页,然后登录成功后又跳回来之前想访问的页面.这里主要是用一个权限控制类来定义路由路由信息,同时用redux把登录成功后要访问的路由地址给保存起来,登录成功时看redux里面有没有存地址,如果没有存地址就跳转到默认路由地址. 路由权限控制类 在这个方法里面,通过sessionStorage判断是否登录了,如果没有登录,就保存一下当前想要跳转的路由到redux里面.然后跳转到我们登录页. import R

  • React-router4路由监听的实现

    React-router 4 React Router4是一个纯React重写的包,现在的版本中已不需要路由配置,一切皆组件. 问题出发点 最近在一个新的H5项目中使用了react router 4 ("react-router-dom": "^4.2.2"),项目中的一部分页面是需要给app客户端的同学使用,这样H5项目中的title就不能一成不变,需要显示对应页面的title,所以,我们就需要去监听路由变动来更改title. 思路 在react中,例如:在父路由

  • React从react-router路由上做登陆验证控制的方法

    本文介绍了React从react-router路由上做登陆验证控制的方法,分享给大家,具体如下: 验证代码 import React from 'react' import {connect} from 'react-redux'; function requireAuthentication(Component) { // 组件有已登陆的模块 直接返回 (防止从新渲染) if (Component.AuthenticatedComponent) { return Component.Authe

  • 详解React-Router中Url参数改变页面不刷新的解决办法

    问题 今天在写页面的时候发现一个问题,就是在React Router中使用了Url传参的功能,像这样: export class MainRouter extends React.Component { render() { return ( <BrowserRouter> <Switch> ... <Route exact path={'/channel/:channelId'} component={ChannelPerPage}/> ... </Switch

  • React路由管理之React Router总结

    React项目通常都有很多的URL需要管理,最常使用的解决方案就是React Router了,最近学习了一下,主要是看了一下官方的英文文档,加以总结,以备后查. React Router是做什么的呢,官方的介绍是: A complete routing library for React,keeps your UI in sync with the URL. It has a simple API with powerful features like lazy code loading, dy

  • ReactRouter的实现方法

    ReactRouter的实现 ReactRouter是React的核心组件,主要是作为React的路由管理器,保持UI与URL同步,其拥有简单的API与强大的功能例如代码缓冲加载.动态路由匹配.以及建立正确的位置过渡处理等. 描述 React Router是建立在history对象之上的,简而言之一个history对象知道如何去监听浏览器地址栏的变化,并解析这个URL转化为location对象,然后router使用它匹配到路由,最后正确地渲染对应的组件,常用的history有三种形式: Brow

  • react同构实践之实现自己的同构模板

    一开始想学学服务端渲染,脑海中第一个浮现出来的就是next.js这种成熟的方案.看了一两天,有趣,优雅,但是封装好了,原理不甚清楚,也感觉无法灵活嵌合到老项目上去.于是看各种资料,想整理出同构的线索,一步一步地实现自己的同构模板.相关代码可查看我的GitHub.感谢阅读!! TODO List 数据:如何保持前后端应用状态一致 路由:路由在服务端和客户端中的匹配方案 代码:同构,哪些地方可以共享,哪些地方需要差异化 静态资源:服务端如何引入css/图片等 ssr直出资源:服务端在渲染路由页面时如

  • react最流行的生态替代antdpro搭建轻量级后台管理

    目录 前言 项目初始化 数据请求 + mock 配置 axios 配置 react-query mock 路由权限配置 路由文件 main.tsx App.tsx 页面编写 login 页面 BasicLayout 动态菜单栏 封装页面通用面包屑 总结 前言 你是否经历过公司的产品和 ui 要求左侧菜单栏要改成设计图上的样子? 苦恼 antd-pro 强绑定的 pro-layout 菜单栏不能自定义?你可以使用 umi,但是就要根据它的约定来开发,捆绑全家桶等等.手把手教你搭一个轻量级的后台模版

  • React-router v4 路由配置方法小结

    本文主要介绍了React-router v4 路由配置方法小结,分享给大家,也给自己留个笔记 一. Switch .Router .Route三者的区别 1.Route Route 是建立location 和 ui的最直接联系 2.Router react-router v4 中,Router被拆分成了StaticRouter.MemoryRouter.BrowserRouter.HashRouter.NativeRouter. MemoryRouter.BrowserRouter.HashRo

  • 浅谈react-router@4.0 使用方法和源码分析

    react-router-dom@4.3.0 || react-router@4.4.1 react-router 使用方法 配置 router.js import React, { Component } from 'react'; import { Switch, Route } from 'react-router-dom'; const router = [{ path: '/', exact: true, component:importPath({ loader: () => imp

  • react-router实现跳转传值的方法示例

    前言 本文主要给大家介绍了关于react-router跳转传值的相关内容,分享出来供大家参考学习,下面来一起看看详细的介绍: react-router跳转传值 1.引入包 import {hashHistory} from 'React-router' 2.跳转传值 handleClick = (value) => { hashHistory.push({ pathname: 'message/detailMessage', query: { title:value.title, time:va

  • react-router browserHistory刷新页面404问题解决方法

    使用React开发新项目时,遇见了刷新页面,直接访问二级或三级路由时,访问失败,出现404或资源加载异常的情况,本篇针对此问题进行分析并总结解决方案. 背景 使用webpack-dev-server做本地开发服务器时,正常情况只需要简单使用webpack-dev-server指令启动即可,但是当项目处于以下两种情况时,往往需要有嵌套路由和异步加载路由: 我们使用react-router这种路由库构建单页面应用路由: 使用html-webpack-plugin插件动态将加载js的<script>

  • React-Router如何进行页面权限管理的方法

    前言 在一个复杂的SAP应用中,我们可能需要根据用户的角色控制用户进行页面的权限,甚至在用户进入系统之前就进行权限的控制.本文就此一权限控制进行讨论.本文假设读者了解React和React-Router的相关使用. 从传统的Router开始 一个传统的路由大概长下边这个样式,这是没有添加任何权限限制的. export default (store) => { const history = syncHistoryWithStore(hashHistory, store); return ( <

  • 模块化react-router配置方法详解

    react-router模块化配置 因为公司的需要最近踏进了react坑,一直在挖坑填坑,在路由这一块折腾得不行. 直接进入主题,配置react-router模块化 1.先下载react-router-dom npm install react-router-dom --save 2.在相应的文件引入react-router-dom相应的模块 import { BrowserRouter as Router, Route, Link } from "react-router-dom";

随机推荐