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 相关文章推荐
js 多种变量定义(对象直接量,数组直接量和函数直接量)
May 24 Javascript
JQuery DataTable删除行后的页面更新利用Ajax解决
May 17 Javascript
使用js Math.random()函数生成n到m间的随机数字
Oct 09 Javascript
js获取图片宽高的方法
Nov 25 Javascript
JavaScript中使用sencha gridpanel 编辑单元格、改变单元格颜色
Nov 26 Javascript
深入学习AngularJS中数据的双向绑定机制
Mar 04 Javascript
JS限制条件补全问题实例分析
Dec 16 Javascript
解析jquery easyui tree异步加载子节点问题
Mar 08 Javascript
vue单个组件实现无限层级多选菜单功能
Apr 10 Javascript
产制造追溯系统之通过微信小程序实现移动端报表平台
Jun 03 Javascript
JS实现简单的文字无缝上下滚动功能示例
Jun 22 Javascript
Vue 解决路由过渡动画抖动问题(实例详解)
Jan 05 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网页后退不再出现过期
2007/03/08 PHP
php 遍历数据表数据并列表横向排列的代码
2009/09/05 PHP
PHP用星号隐藏部份用户名、身份证、IP、手机号等实例
2014/04/08 PHP
自己写的兼容低于PHP 5.5版本的array_column()函数
2014/10/24 PHP
php生成随机颜色的方法
2014/11/13 PHP
WordPress中给文章添加自定义字段及后台编辑功能区域
2015/12/19 PHP
Symfony2实现在doctrine中内置数据的方法
2016/02/05 PHP
用JavaScript脚本实现Web页面信息交互
2006/10/11 Javascript
ExtJS如何设置与获取radio控件的选取状态
2014/01/22 Javascript
使用javascript实现雪花飘落的效果
2015/01/13 Javascript
jquery中change()用法实例分析
2015/02/06 Javascript
解决WordPress使用CDN后博文无法评论的错误
2015/12/15 Javascript
获取阴历(农历)和当前日期的js代码
2016/02/15 Javascript
详解Matlab中 sort 函数用法
2016/03/20 Javascript
BootStrap 智能表单实战系列(十)自动完成组件的支持
2016/06/13 Javascript
mvc中form表单提交的三种方式(推荐)
2016/08/10 Javascript
jQuery实现一个简单的轮播图
2017/02/19 Javascript
vue解决跨域路由冲突问题思路解析
2017/11/03 Javascript
简单的三步vuex入门
2018/05/20 Javascript
JavaScript模板引擎原理与用法详解
2018/12/24 Javascript
[02:54]辉夜杯主赛事第二日败者组 iG.V赛后采访
2015/12/26 DOTA
python append、extend与insert的区别
2016/10/13 Python
python pandas 对series和dataframe的重置索引reindex方法
2018/06/07 Python
使用Python实现跳帧截取视频帧
2019/05/31 Python
win10下python2和python3共存问题解决方法
2019/12/23 Python
Python简单实现区域生长方式
2020/01/16 Python
python3利用Axes3D库画3D模型图
2020/03/25 Python
canvas绘制文本内容自动换行的实现代码
2019/01/14 HTML / CSS
canvas 如何绘制线段的实现方法
2018/07/12 HTML / CSS
Currentbody西班牙:美容仪专家
2019/09/28 全球购物
运动鞋、街头服装、手表和手袋的实时市场:StockX
2020/11/25 全球购物
《梅兰芳学艺》教学反思
2014/02/24 职场文书
工作推荐信范文
2014/05/10 职场文书
蜗居观后感
2015/06/11 职场文书
运动会开幕式致辞
2015/07/29 职场文书
python munch库的使用解析
2021/05/25 Python