ReactRouter
是React
的核心组件,主要是做为React
的路由管理器,保持UI
与URL
同步,其拥有简单的API
与强大的功能例如代码缓冲加载、动态路由匹配、以及创建正确的位置过渡处理等。javascript
React Router
是创建在history
对象之上的,简而言之一个history
对象知道如何去监听浏览器地址栏的变化,并解析这个URL
转化为location
对象,而后router
使用它匹配到路由,最后正确地渲染对应的组件,经常使用的history
有三种形式: Browser History
、Hash History
、Memory History
。html
Browser History
是使用React Router
的应用推荐的history
,其使用浏览器中的History
对象的pushState
、replaceState
等API
以及popstate
事件等来处理URL
,其可以建立一个像https://www.example.com/path
这样真实的URL
,一样在页面跳转时无须从新加载页面,固然也不会对于服务端进行请求,固然对于history
模式仍然是须要后端的配置支持,用以支持非首页的请求以及刷新时后端返回的资源,因为应用是个单页客户端应用,若是后台没有正确的配置,当用户在浏览器直接访问URL
时就会返回404
,因此须要在服务端增长一个覆盖全部状况的候选资源,若是URL
匹配不到任何静态资源时,则应该返回同一个index.html
应用依赖页面,例如在Nginx
下的配置。java
location / { try_files $uri $uri/ /index.html; }
Hash
符号即#
本来的目的是用来指示URL
中指示网页中的位置,例如https://www.example.com/index.html#print
即表明example
的index.html
的print
位置,浏览器读取这个URL
后,会自动将print
位置滚动至可视区域,一般使用<a>
标签的name
属性或者<div>
标签的id
属性指定锚点。
经过window.location.hash
属性可以读取锚点位置,能够为Hash
的改变添加hashchange
监听事件,每一次改变Hash
,都会在浏览器的访问历史中增长一个记录,此外Hash
虽然出如今URL
中,但不会被包括在HTTP
请求中,即#
及以后的字符不会被发送到服务端进行资源或数据的请求,其是用来指导浏览器动做的,对服务器端没有效果,所以改变Hash
不会从新加载页面。
ReactRouter
的做用就是经过改变URL
,在不从新请求页面的状况下,更新页面视图,从而动态加载与销毁组件,简单的说就是,虽然地址栏的地址改变了,可是并非一个全新的页面,而是以前的页面某些部分进行了修改,这也是SPA
单页应用的特色,其全部的活动局限于一个Web
页面中,非懒加载的页面仅在该Web
页面初始化时加载相应的HTML
、JavaScript
、CSS
文件,一旦页面加载完成,SPA
不会进行页面的从新加载或跳转,而是利用JavaScript
动态的变换HTML
,默认Hash
模式是经过锚点实现路由以及控制组件的显示与隐藏来实现相似于页面跳转的交互。node
Memory History
不会在地址栏被操做或读取,这就能够解释如何实现服务器渲染的,同时其也很是适合测试和其余的渲染环境例如React Native
,和另外两种History
的一点不一样是咱们必须建立它,这种方式便于测试。react
const history = createMemoryHistory(location);
咱们来实现一个很是简单的Browser History
模式与Hash History
模式的实现,由于H5
的pushState
方法不能在本地文件协议file://
运行,因此运行起来须要搭建一个http://
环境,使用webpack
、Nginx
、Apache
等均可以,回到Browser History
模式路由,可以实现history
路由跳转不刷新页面得益与H5
提供的pushState()
、replaceState()
等方法以及popstate
等事件,这些方法都是也能够改变路由路径,但不做页面跳转,固然若是在后端不配置好的状况下路由改编后刷新页面会提示404
,对于Hash History
模式,咱们的实现思路类似,主要在于没有使用pushState
等H5
的API
,以及监听事件不一样,经过监听其hashchange
事件的变化,而后拿到对应的location.hash
更新对应的视图。webpack
<!-- 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) => { // 事件委托到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">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 id
为eef79d5
,TAG
是4.4.0
,在这以前咱们须要先了解一下history
库,history
库,是ReactRouter
依赖的一个对window.history
增强版的history
库,其中主要用到的有match
对象表示当前的URL
与path
的匹配的结果,location
对象是history
库基于window.location
的一个衍生。
ReactRouter
将路由拆成了几个包: react-router
负责通用的路由逻辑,react-router-dom
负责浏览器的路由管理,react-router-native
负责react-native
的路由管理。
咱们以BrowserRouter
组件为例,BrowserRouter
在react-router-dom
中,它是一个高阶组件,在内部建立一个全局的history
对象,能够监听整个路由的变化,并将history
做为props
传递给react-router
的Router
组件,Router
组件再会将这个history
的属性做为context
传递给子组件。git
// 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
环境,其借助context
向Route
传递context
,这也解释了为何Router
要在全部Route
的外面。在Router
的componentWillMount
中,添加了history.listen
,其可以监听路由的变化并执行回调事件,在这里即会触发setState
。当setState
时即每次路由变化时 ->
触发顶层Router
的回调事件 ->
Router
进行setState
->
向下传递 nextContext
此时context
中含有最新的location
->
下面的Route
获取新的nextContext
判断是否进行渲染。github
// 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
的做用是匹配路由,并传递给要渲染的组件props
,Route
接受上层的Router
传入的context
,Router
中的history
监听着整个页面的路由变化,当页面发生跳转时,history
触发监听事件,Router
向下传递nextContext
,就会更新Route
的props
和context
来判断当前Route
的path
是否匹配location
,若是匹配则渲染,不然不渲染,是否匹配的依据就是computeMatch
这个函数,在下文会有分析,这里只须要知道匹配失败则match
为null
,若是匹配成功则将match
的结果做为props
的一部分,在render
中传递给传进来的要渲染的组件。Route
接受三种类型的render props
,<Route component>
、<Route render>
、<Route children>
,此时要注意的是若是传入的component
是一个内联函数,因为每次的props.component
都是新建立的,因此React
在diff
的时候会认为进来了一个全新的组件,因此会将旧的组件unmount
再re-mount
。这时候就要使用render
,少了一层包裹的component
元素,render
展开后的元素类型每次都是同样的,就不会发生re-mount
了,另外children
也不会发生re-mount
。web
// \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
,而后push
进history
中,这也是前面讲过的路由的变化与 页面的跳转是不互相关联的,ReactRouter
在Link
中经过history
库的push
调用了HTML5 history
的pushState
,可是这仅仅会让路由变化,其余什么都没有改变。在Router
中的listen
,它会监听路由的变化,而后经过context
更新props
和nextContext
让下层的Route
去从新匹配,完成须要渲染部分的更新。segmentfault
// 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