理解react-router 单页面应用(SPA single page application) 单页面应用是指只有一张Web页面的应用,是一种从Web服务器加载的富客户端,单页面跳转仅刷新局部资源,公共资源(js css等)仅需加载一次,常用于PC端应用、购物等网站
react-router-dom react-router history库三者关系 history可以理解为react-router的核心,也就是整个路由的核心,里面集成了popState history.pushState等底层路由实现的原理方法
react-router可以理解为是react-router-dom的核心,里面封装了Router Route Switch等核心组件,实现了从路由的改变到组件的更新的核心功能
一般在项目中只需要引入react-router-dom即可
单页面实现核心原理 history模式原理 改变路由 
1 history.pushState (state, title, path); 
 
state:一个与指定网址相关的状态对象,popState事件触发,该对象会传入回调函数,如果不需要可以填null 
title:新页面的标题,但是所有浏览器目前都忽略这个值,可填null 
path:新的网址,必须与当前页面处在同一个域,浏览器的地址栏将显示这个地址 
 
 
参数同上,这个方法会修改当前的history记录,history.length的长度不会改变
监听路由 
1 2 3 window .addEventListener ('popstate' , function (e ){	 }) 
 
 popstate事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在JS中调用history.back()方法),即在同一文档的history对象出现变化会触发该事件
 history.pushState()或者history.replaceState()不会触发popstate事件
 
hash模式原理 改变路由 
 
通过 window.location.hash属性获取hash值和设置hash值
监听路由 
1 2 3 window .addEventListener ('hashchange' , function (e ) {	  }); 
 
history库 react-router路由离不开history库,history专注于记录路由的history状态,以及path改变了我们应该做些什么?在history模式下用popstate监听路由变化,在hash模式下用hashchange监听路由变化
createBrowserHistory Browser模式下路由的运行,一切都从 createBrowserHistory开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 const  PopStateEvent  = 'popstate' ;const  HashChangeEvent  = 'hashchange' ;function  createBrowserHistory ( ) {	 	const  globalHistory = window .history ; 	 	const  transitionManager = createTransitionManage (); 	 	const  setState  = (nextState ) => { 		 		Object .assign (history, nextState); 		history.length  = globalHistory.length ; 		 		transitionManager.notifyListeners ( 			history.location , 			history.action , 		); 	}; 	 	const  handlePopState  = (event ) => { 		      		const  location = getDOMLocation (event.state )     		const  action = 'POP'      		transitionManager.confirmTransitionTo (location, action, getUserConfirmation, (ok ) =>  {         		if  (ok) {           			setState ({ action, location })         		} else  {           			revertPop (location)         		}     		}) 	}; 	 	const  push  = (path, state ) => { 		const  action = 'PUSH' ; 		 		const  loaction = createLocation (path, state, createKey (), history.location ); 		 		transitionManager.confirmTransition (location, action, getUserConfirmation, (ok ) =>  { 			if  (!ok) { 				return ; 			}; 			const  href = createHref (location)       			const  { key, state } = location       			if  (canUseHistory) {         			         			globalHistory.pushState ({ key, state }, null , href)         			if  (forceRefresh) {           				window .location .href  = href         			} else  {          				           				setState ({ action, location })         			}       			} else  {         			window .location .href  = href       			} 		}); 	}; 	 	const  listen  = (listener ) => { 		 		const  unlisten = transitionManager.appendListener (listener); 		checkDOMListeners (1 ); 		return  () =>  { 			checkDOMListeners (-1 ); 			unlisten (); 		}; 	}; 	 	const  checkDOMListeners  = (delta ) => { 		listenerCount += delta; 		if  (listenerCount === 1 ) { 			addEventListener (window , PopStateEvent , handlePopState); 			if  (needsHashChangeListener) addEventListener (window , HashChangeEvent , handleHashChange) 		} else  if  (listenerCount === 0 ) { 			removeEventListener (window , PopStateEvent , handlePopState); 			if  (needsHashChangeListener) removeEventListener (window , HashChangeEvent , handleHashChange) 		}; 	}; 	 	return  { 		push, 		listen, 		 	};  }; 
 
createHashHistory 监听哈希路由变化 hashchange监听
1 2 3 4 5 6 7 8 9 const  HashChangeEvent  = 'hashchange'  const  checkDOMListeners  = (delta ) => {  	listenerCount += delta     	if  (listenerCount === 1 ) {       		addEventListener (window , HashChangeEvent , handleHashChange)     	} else  if  (listenerCount === 0 ) {       		removeEventListener (window , HashChangeEvent , handleHashChange)     	} } 
 
改变哈希路由 history.push底层调用了window.location.href改变路由
history.replace底层调用了window.location.replace改变路由
1 2 3 4 5 6 7 8 9 10 11 12 const  pushHashPath  = (path ) =>	window .location .hash  = path 	 	const  replaceHashPath  = (path ) => {   		const  hashIndex = window .location .href .indexOf ('#' )   		window .location .replace (     			window .location .href .slice (0 , hashIndex >= 0  ? hashIndex : 0 ) + '#'  + path   		) 	} 
 
总结 
核心api Router :接收location变化,派发更新流 router的作用是把history location等路由信息传递下去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 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 ._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          }}       />     );   } } 
 
总结:
初始化绑定listen, 路由变化,通知改变 location,改变组件。 react的history路由状态是保存在 React.Content上下文之间, 状态更新。
一个项目应该有一个根 Router , 来产生切换路由组件之前的更新作用。如果存在多个 Router会造成,会造成切换路由,页面不更新的情况。 
Switch: 匹配正确的唯一的路由 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class  Switch  extends  React.Component  {  render ( ) {     return  (       <RouterContext.Consumer>         {/* 含有 history location 对象的 context */}         {context => {           invariant(context, 'You should not use <Switch> outside a <Router>');           const location = this.props.location || context.location;           let element, match;           //我们使用React.Children.forEach而不是React.Children.toArray().find()           //这里是因为toArray向所有子元素添加了键,我们不希望           //为呈现相同的两个<Route>s触发卸载/重新装载           //组件位于不同的URL。           //这里只需然第一个 含有 match === null 的组件           React.Children.forEach(this.props.children, child => {             if (match == null && React.isValidElement(child)) {               element = child;               // 子组件 也就是 获取 Route中的 path 或者 rediect 的 from               const path = child.props.path || child.props.from;               match = path                 ? matchPath(location.pathname, { ...child.props, path })                 : context.match;             }           });           return match             ? React.cloneElement(element, { location, computedMatch: match })             : null;         }}       </RouterContext.Consumer>     );   } } 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 function  matchPath (pathname, options = {} ) {  if  (typeof  options === "string"  || Array .isArray (options)) {     options = { path : options };   }   const  { path, exact = false , strict = false , sensitive = false  } = options;   const  paths = [].concat (path);   return  paths.reduce ((matched, path ) =>  {     if  (!path && path !== "" ) return  null ;     if  (matched) return  matched;     const  { regexp, keys } = compilePath (path, {       end : exact,       strict,       sensitive     });     const  match = regexp.exec (pathname);          if  (!match) return  null ;     const  [url, ...values] = match;     const  isExact = pathname === url;     if  (exact && !isExact) return  null ;     return  {       path,        url : path === "/"  && url === ""  ? "/"  : url,        isExact,        params : keys.reduce ((memo, key, index ) =>  {         memo[key.name ] = values[index];         return  memo;       }, {})     };   }, null ); } 
 
Route:组件页面承载容器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class  Route  extends  React.Component  {  render ( ) {     return  (       <RouterContext.Consumer>         {context => {           /* router / route 会给予警告警告 */           invariant(context, "You should not use <Route> outside a <Router>");           // computedMatch 为 经过 swich处理后的 path           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;           if (Array.isArray(children) && children.length === 0) {             children = null;           }           return (             <RouterContext.Provider value={props}>               {props.match                 ? children                   ? typeof children === "function"                     ? __DEV__                       ? evalChildrenDev(children, props, this.props.path)                       : children(props)                     : children                   : component                   ? React.createElement(component, props)                   : render                   ? render(props)                   : null                 : typeof children === "function"                 ? __DEV__                   ? evalChildrenDev(children, props, this.props.path)                   : children(props)                 : null}             </RouterContext.Provider>           );         }}       </RouterContext.Consumer>     );   } } 
 
Redirect:重定向 
初始化页面跳转 
组件更新的时候location不相等 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 function  Redirect ({ computedMatch, to, push = false  } ) {  return  (     <RouterContext.Consumer >        {context => {         const { history, staticContext } = context;         /* method就是路由跳转方法。 */         const method = push ? history.push : history.replace;         /* 找到符合match的location ,格式化location */         const location = createLocation(           computedMatch             ? typeof to === 'string'               ? generatePath(to, computedMatch.params)               : {                   ...to,                   pathname: generatePath(to.pathname, computedMatch.params)                 }             : to         )         /* 初始化的时候进行路由跳转,当初始化的时候,mounted执行push方法,当组件更新的时候,如果location不相等。同样会执行history方法重定向 */         return (           <Lifecycle                 onMount ={()  =>  {              method(location);             }}               onUpdate={(self, prevProps) => {               const prevLocation = createLocation(prevProps.to);               if (                 !locationsAreEqual(prevLocation, {                   ...location,                   key: prevLocation.key                 })               ) {                 method(location);               }              }}               to={to}           />         );       }}     </RouterContext.Consumer >    ); }; 
 
总结 
history提供了核心api,如监听路由,更改路由的方法,已经保存路由状态state 
react-router提供路由渲染组件,路由唯一性匹配组件,重定向组件等功能组件 
 
流程分析:
1. 当地址栏改变url,组件的更新渲染都经历了什么? 
拿history模式做参考。当url改变,首先触发histoy,调用事件监听 popstate事件, 触发回调函数 handlePopState,触发history下面的 setstate方法,产生新的location对象,然后通知Router组件更新 location并通过context上下文传递,switch通过传递的更新流,匹配出符合的Route组件渲染,最后有 Route组件取出 context内容,传递给渲染页面,渲染更新
2.当我们调用 history.push方法,切换路由,组件的更新渲染又都经历了什么呢? 
当我们调用 history.push方法,首先调用history的 push方法,通过 history.pushState来改变当前 url,接下来触发history下面的 setState方法,接下来的步骤就和上面一模一样了
各个路由组件之间的关系