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