ReactRouter的实现方法


Posted in Javascript onJanuary 25, 2021

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的实现内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
javascript入门·图片对象(无刷新变换图片)\滚动图像
Oct 01 Javascript
JavaScript中this的使用详解
Nov 08 Javascript
JS增加行复制行删除行的实现代码
Nov 09 Javascript
jQuery实现点击表格单元格就可以编辑内容的方法【测试可用】
Aug 01 Javascript
深入理解Angular2 模板语法
Aug 07 Javascript
使用bat打开多个cmd窗口执行gulp、node
Feb 17 Javascript
基于jQuery实现瀑布流页面
Apr 11 jQuery
BootStrap的两种模态框方式
May 10 Javascript
JS实现微信里判断页面是否被分享成功的方法
Jun 06 Javascript
jquery实现搜索框功能实例详解
Jul 23 jQuery
JavaScript JSON使用原理及注意事项
Jul 30 Javascript
简单了解JavaScript作用域
Jul 31 Javascript
Javascript中的奇葩知识,你知道吗?
Jan 25 #Javascript
vue 计算属性和侦听器的使用小结
Jan 25 #Vue.js
JavaScript中clientWidth,offsetWidth,scrollWidth的区别
Jan 25 #Javascript
vue keep-alive的简单总结
Jan 25 #Vue.js
JS实现简易日历效果
Jan 25 #Javascript
javascript代码实现简易计算器
Jan 25 #Javascript
js简单粗暴的发布订阅示例代码
Jan 23 #Javascript
You might like
php快速url重写 更新版[需php 5.30以上]
2010/04/20 PHP
php下正则来匹配dede模板标签的代码
2010/08/21 PHP
linux下删除7天前日志的代码(php+shell)
2011/01/02 PHP
CodeIgniter框架过滤HTML危险代码
2014/06/12 PHP
php生成随机数的三种方法
2014/09/10 PHP
php 数据结构之链表队列
2017/10/17 PHP
Laravel学习教程之request validation的编写
2017/10/25 PHP
PHP创建自己的Composer包方法
2018/04/09 PHP
laravel 输出最后执行sql 附:whereIn的使用方法
2019/10/10 PHP
基于Jquery实现的一个图片滚动切换
2012/06/21 Javascript
Jquery中ajax方法data参数的用法小结
2014/02/12 Javascript
基于jquery实现等比缩放图片
2014/12/03 Javascript
JavaScript检测实例属性, 原型属性
2015/02/04 Javascript
javascript如何实现暂停功能
2015/11/06 Javascript
AngularJS基础 ng-include 指令示例讲解
2016/08/01 Javascript
Vue.js组件使用开发实例教程
2016/11/01 Javascript
JS中关于事件处理函数名后面是否带括号的问题
2016/11/16 Javascript
vue-router 学习快速入门
2017/03/01 Javascript
NodeJS自定义模块写法(详解)
2017/06/27 NodeJs
微信小程序之判断页面滚动方向的示例代码
2018/08/30 Javascript
React路由鉴权的实现方法
2019/09/05 Javascript
JavaScript 反射和属性赋值实例解析
2019/10/28 Javascript
JavaScript链式调用原理与实现方法详解
2020/05/16 Javascript
ant design的table组件实现全选功能以及自定义分页
2020/11/17 Javascript
[01:02:53]DOTA2上海特级锦标赛主赛事日 - 5 总决赛Liquid VS Secret第二局
2016/03/06 DOTA
[38:40]2018DOTA2亚洲邀请赛 4.6淘汰赛 mineski vs LGD 第一场
2018/04/10 DOTA
Python比较2个时间大小的实现方法
2018/04/10 Python
TensorFlow 输出checkpoint 中的变量名与变量值方式
2020/02/11 Python
澳大利亚礼品卡商店:Gift Card Store
2019/06/24 全球购物
美国最大最全的亚洲购物网站:美国亚米网(Yamibuy)
2020/05/05 全球购物
英文版网络工程师求职信
2013/10/28 职场文书
店长岗位的工作内容
2013/11/12 职场文书
内乡县衙导游词
2015/02/05 职场文书
医院见习总结
2015/06/24 职场文书
机械生产实习心得体会
2016/01/22 职场文书
Promise静态四兄弟实现示例详解
2022/07/07 Javascript