理解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
方法,接下来的步骤就和上面一模一样了
各个路由组件之间的关系