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">home</a></li><li><a href="/about">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) => { // 事件委托到rootif (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">home</a></li><li><a href="#/about">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.Providerchildren={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 defaultevent.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-varsreturn (<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的实现相关推荐

  1. 我们要的是一个简单的react-router路由

    我们要的是一个简单的react-router路由 习惯了 vue-router 路由的用法,再用react-router总感觉挺麻烦的. 那么react有没有用法跟vue-router一样使用简单的路 ...

  2. ‘Link‘ is not exported from ‘react-router‘

    出现这样的报错: 前面已经安装了 react-router 解决办法: 安装react-router-dom:cnpm i react-router-dom -D import { BrowserRo ...

  3. React学习笔记二——react-router

    2019独角兽企业重金招聘Python工程师标准>>> 今天写了一个关于react-router的demo,因为不喜欢下载太多的依赖包,所以就直接引得js,解析会比较慢一些. 首先先 ...

  4. react-router中离开确认组件Prompt

    在某些页面中,特别是设计到多个表单项填写的,产品需求要求:在离开页面时,需要提示用户当前页面的数据尚未保存,二次确认后才能离开页面. 大概效果如图: 幸运的是,在react-router 4中自带此类 ...

  5. webpack v3 结合 react-router v4 做 dynamic import — 按需加载(懒加载)

    为什么要做dynamic import? dynamic import不知道为什么有很多叫法,什么按需加载,懒加载,Code Splitting,代码分页等. 总之,就是在SPA,把JS代码分成N个页 ...

  6. 从无到有-在create-react-app基础上接入react-router、redux-saga

    搭建项目框架 新建项目 执行如下代码,用create-react-app来建立项目的基础框架,然后安装需要用到的依赖. $ npx create-react-app my-test-project $ ...

  7. react-router browserHistory刷新页面404问题解决

    2019独角兽企业重金招聘Python工程师标准>>> 使用React开发新项目时,遇见了刷新页面,直接访问二级或三级路由时,访问失败,出现404或资源加载异常的情况,本篇针对此问题 ...

  8. redux+react-router报错

    2019独角兽企业重金招聘Python工程师标准>>> 报错: Warning: [react-router] You cannot change <Router routes ...

  9. react-router的基础知识

    一.基本用法 React Router 安装命令如下. $ npm install -S react-router 使用时,路由器Router就是React的一个组件. import { Router ...

  10. Node.js建站笔记-使用react和react-router取代Backbone

    斟酌之后,决定在<嗨猫>项目中引入react,整体项目偏重spa模式,舍弃部分server端的模板渲染,将一部分渲染工作交给前端react实现. react拥有丰富的组件,虽然不如Back ...

最新文章

  1. linux mysql 无法识别,Linux下MySQL 5.7.23无法远程连接解决方案
  2. Overleaf-LaTex表格制作
  3. 小P寻宝记——好基友一起走 背包
  4. WinCE5.0中应用程序如何直接写屏
  5. LeetCode Integer Break(整数拆分问题)
  6. Python赋值、浅拷贝、深拷贝
  7. C++中const用法总结
  8. ios html5 不支持 flv_iExplorer v4.2.6 一款优秀强大的 iOS 资源管理工具
  9. 以太坊使用puppeth工具
  10. 在QT中使用同步阻塞式HTTP请求的方法
  11. CSDN Markdown编辑器使用教程
  12. linux五周第三次课(3月7日)笔记
  13. 21.Phabricator 安装
  14. UE4源码下载与配置sin文件
  15. java移动业务大厅案例_基于Java的SOSO移动大厅项目(功能全部实现了)
  16. Effective HPA:预测未来的弹性伸缩产品
  17. 联网时显示已连接无法连接到服务器怎么办,路由器显示已连接不可上网怎么办?...
  18. 快速可靠网络传输协议 KCP
  19. 学习笔记 吴恩达 斯坦福大学公开课 :机器学习课程-1 机器学习的动机与应用
  20. Fedora16+XAMPP+Drupal7建站,从零开始全程指导

热门文章

  1. Cookie、Session、Token、JWT区别与联系
  2. 少说话多写代码之Python学习009——字典的创建
  3. 从输入一个url到浏览器页面展示都经历了哪些过程?
  4. about x86 protected mode
  5. linux命令:watch 、lsmod、mdadm 创建软RAID
  6. tomcat session失效时间
  7. tar 命令详解 / xz 命令
  8. g​e​t​A​t​t​r​i​b​u​t​e​和​g​e​t​P​a​r​a​m​e​t​e​r​区​别...
  9. 查看当前内存使用情况---练习记录
  10. [算法设计题]输出三角形图形