react-router等前端路由的原理大體相同,能夠實現無刷新的條件下切換顯示不一樣的頁面。路由的本質就是頁面的URL發生改變時,頁面的顯示結果能夠根據URL的變化而變化,可是頁面不會刷新。經過前端路由能夠實現單頁(SPA)應用,本文首先從前端路由的原理出發,詳細介紹了前端路由原理的變遷。接着從react-router4.0的源碼出發,深刻理解react-router4.0是如何實現前端路由的。html
- 經過Hash實現前端路由
- 經過H5的history實現前端路由
- React-router4.0的使用
- React-router4.0源碼分析
原文的地址,在個人博客中:https://github.com/forthealll...前端
若有幫助,您的star是對我最好的鼓勵~html5
早期的前端路由是經過hash來實現的:node
改變url的hash值是不會刷新頁面的。react
所以能夠經過hash來實現前端路由,從而實現無刷新的效果。hash屬性位於location對象中,在當前頁面中,能夠經過:git
window.location.hash='edit'
來實現改變當前url的hash值。執行上述的hash賦值後,頁面的url發生改變。github
賦值前:http://localhost:3000
賦值後:http://localhost:3000/#editchrome
在url中多了以#結尾的hash值,可是賦值先後雖然頁面的hash值改變致使頁面完整的url發生了改變,可是頁面是不會刷新的。此外,還有一個名爲hashchange的事件,能夠監聽hash的變化,咱們能夠經過下面兩種方式來監聽hash的變化:npm
window.onhashchange=function(event){ console.log(event); } window.addEventListener('hashchange',function(event){ console.log(event); })
當hash值改變時,輸出一個HashChangeEvent。該HashChangeEvent的具體值爲:redux
{isTrusted: true, oldURL: "http://localhost:3000/", newURL: "http://localhost:3000/#teg", type: "hashchange".....}
有了監聽事件,且改變hash頁面不刷新,這樣咱們就能夠在監聽事件的回調函數中,執行咱們展現和隱藏不一樣UI顯示的功能,從而實現前端路由。
此外,除了能夠經過window.location.hash來改變當前頁面的hash值外,還能夠經過html的a標籤來實現:
<a href="#edit">edit</a>
hash的兼容性較好,所以在早期的前端路由中大量的採用,可是使用hash也有不少缺點。
HTML5的History接口,History對象是一個底層接口,不繼承於任何的接口。History接口容許咱們操做瀏覽器會話歷史記錄。
History提供了一些屬性和方法。
History的屬性:
保存了會出發popState事件的方法,所傳遞過來的屬性對象(後面會在pushState和replaceState方法中詳細的介紹)
History方法:
上面的方法中,pushState和repalce的相同點:
就是都會改變當前頁面顯示的url,但都不會刷新頁面。
不一樣點:
pushState是壓入瀏覽器的會話歷史棧中,會使得History.length加1,而replaceState是替換當前的這條會話歷史,所以不會增長History.length.
history在瀏覽器的BOM對象模型中的重要屬性,history徹底繼承了History接口,所以擁有History中的全部的屬性和方法。
這裏咱們主要來看看history.length屬性以及history.pushState、history.replaceState方法。
pushState和replaceState接受3個參數,分別爲state對象,title標題,改變的url。
window.history.pushState({foo:'bar'}, "page 2", "bar.html");
此時,當前的url變爲:
執行上述方法前:http://localhost:3000
執行上述方法後:http://localhost:3000/bar.html
若是咱們輸出window.history.state:
console.log(window.history.state);
// {foo:'bar'}
window.history.state就是咱們pushState的第一個對象參數。
console.log(window.history.length);
window.history.replaceState({foo:'bar'}, "page 2", "bar.html");
console.log(window.history.length);
上述先後兩次輸出的window.history.length是相等的。
此外。
每次觸發history.back()或者瀏覽器的後退按鈕等,會觸發一個popstate事件,這個事件在後退或者前進的時候發生:
window.onpopstate=function(event){ }
注意:
history.pushState和history.replaceState方法並不會觸發popstate事件。
若是用history作爲路由的基礎,那麼須要用到的是history.pushState和history.replaceState,在不刷新的狀況下能夠改變url的地址,且若是頁面發生回退back或者forward時,會觸發popstate事件。
hisory爲依據來實現路由的優勢:
缺點:
瞭解了前端路由實現的原理以後,下面來介紹一下React-router4.0。在React-router4.0的代碼庫中,根據使用場景包含了如下幾個獨立的包:
在react-router4.0中,遵循Just Component的設計理念:
所提供的API都是以組件的形式給出。
好比BrowserRouter、Router、Link、Switch等API都是以組件的形式來使用。
下面咱們以React-router4.0中的React-router-dom包來介紹經常使用的BrowserRouter、HashRouter、Link和Router等。
用<BrowserRouter> 組件包裹整個App系統後,就是經過html5的history來實現無刷新條件下的前端路由。
<BrowserRouter>組件具備如下幾個屬性:
basename: string 這個屬性,是爲當前的url再增長名爲basename的值的子目錄。
<BrowserRouter basename="test"/>
若是設置了basename屬性,那麼此時的:
http://localhost:3000 和 http://localhost:3000/test 表示的是同一個地址,渲染的內容相同。
與<BrowserRouter>對應的是<HashRouter>,<HashRouter>使用url中的hash屬性來保證不從新刷新的狀況下同時渲染頁面。
<Route> 組件十分重要,<Route> 作的事情就是匹配相應的location中的地址,匹配成功後渲染對應的組件。下面咱們來看<Route>中的屬性。
首先來看如何執行匹配,決定<Route>地址匹配的屬性:
舉例來講,當exact不設置時:
<Route path='/home' component={Home}/> <Route path='/home/first' component={First}/>
此時url地址爲:http://localhost:3000/home/first 的時候,不只僅會匹配到 path='/home/first'時的組件First,同時還會匹配到path='home'時候的Router。
若是設置了exact:
<Route path='/home' component={Home}/>
只有http://localhost:3000/home/first 不會匹配Home組件,只有url地址徹底與path相同,只有http://localhost:3000/home才能匹配Home組件成功。
舉例來講,當不設置strict的時候:
<Route path='/home/' component={Home}/>
此時http://localhost:3000/home 和 http://localhost:3000/home/
都能匹配到組件Home。匹配對於斜線「/」比較寬鬆。若是設置了strict屬性:
<Route path='/home/' component={Home}/>
那麼此時嚴格匹配斜線是否存在,http://localhost:3000/home 將沒法匹配到Home組件。
當Route組件與某一url匹配成功後,就會繼續去渲染。那麼什麼屬性決定去渲染哪一個組件或者樣式呢,Route的component、render、children決定渲染的內容。
而且這3個屬性所接受的方法或者組件,都會有location,match和history這3個參數。若是組件,那麼組件的props中會存在從Link傳遞過來的location,match以及history。
<Route>定義了匹配規則和渲染規則,而<Link> 決定的是如何在頁面內改變url,從而與相應的<Route>匹配。<Link>相似於html中的a標籤,此外<Link>在改變url的時候,能夠將一些屬性傳遞給匹配成功的Route,供相應的組件渲染的時候使用。
to屬性的值能夠爲一個字符串,跟html中的a標籤的href同樣,即便to屬性的值是一個字符串,點擊Link標籤跳轉從而匹配相應path的Route,也會將history,location,match這3個對象傳遞給Route所對應的組件的props中。
舉例來講:
<Link to='/home'>Home</Link>
如上所示,當to接受一個string,跳轉到url爲'/home'所匹配的Route,並渲染其關聯的組件內接受3個對象history,location,match。
這3個對象會在下一小節會詳細介紹。
to屬性的值也能夠是一個對象,該對象能夠包含一下幾個屬性:pathname、seacth、hash和state,其中前3個參數與如何改變url有關,最後一個state參數是給相應的改變url時,傳遞一個對象參數。
舉例來講:
<Link to={{pathname:'/home',search:'?sort=name',hash:'#edit',state:{a:1}}}>Home</Link>
在上個例子中,to爲一個對象,點擊Link標籤跳轉後,改變後的url爲:'/home?sort=name#edit'。 可是在與相應的Route匹配時,只匹配path爲'/home'的組件,'/home?sort=name#edit'。在'/home'後所帶的參數不做爲匹配標準,僅僅是作爲參數傳遞到所匹配到的組件中,此外,state={a:1}也一樣作爲參數傳遞到新渲染的組件中。
介紹了 <BrowserRouter> 、 <Route> 和 <Link> 以後,使用這3個組件API就能夠構建一個簡單的React-router應用。這裏咱們以前說,每當點擊Link標籤跳轉或者在js中使用React-router的方法跳轉,從當前渲染的組件,進入新組件。在新組件被渲染的時候,會接受一個從舊組件傳遞過來的參數。
咱們前面提到,Route匹配到相應的改變後的url,會渲染新組件,該新組件中的props中有history、location、match3個對象屬性,其中hisotry對象屬性最爲關鍵。
一樣如下面的例子來講明:
<Link to={{pathname:'/home',search:'?sort=name',hash:'#edit',state:{a:1}}}>Home</Link> <Route exact path='/home' component={Home}/>
咱們使用了<BrowserRouter>,該組件利用了window.history對象,當點擊Link標籤跳轉後,會渲染新的組件Home,咱們能夠在Home組件中輸出props中的history:
// props中的history action: "PUSH" block: ƒ block() createHref: ƒ createHref(location) go: ƒ go(n) goBack: ƒ goBack() goForward: ƒ goForward() length: 12 listen: ƒ listen(listener) location: {pathname: "/home", search: "?sort=name", hash: "#edit", state: {…}, key: "uxs9r5"} push: ƒ push(path, state) replace: ƒ replace(path, state)
從上面的屬性明細中:
在組件props中history的go、goBack、goForward方法,分別window.history.go、window.history.back、window.history.forward對應。
action這個屬性左右很大,若是是經過Link標籤或者在js中經過this.props.push方法來改變當前的url,那麼在新組件中的action就是"PUSH",不然就是"POP".
action屬性頗有用,好比咱們在作翻頁動畫的時候,前進的動畫是SlideIn,後退的動畫是SlideOut,咱們能夠根據組件中的action來判斷採用何種動畫:
function newComponent (props)=>{ return ( <ReactCSSTransitionGroup transitionAppear={true} transitionAppearTimeout={600} transitionEnterTimeout={600} transitionLeaveTimeout={200} transitionName={props.history.action==='PUSH'?'SlideIn':'SlideOut'} > <Component {...props}/> </ReactCSSTransitionGroup> ) }
在新組件的location屬性中,就記錄了從就組件中傳遞過來的參數,從上面的例子中,咱們看到此時的location的值爲:
hash: "#edit" key: "uxs9r5" pathname: "/home" search: "?sort=name" state: {a:1}
除了key這個用做惟一表示外,其餘的屬性都是咱們從上一個Link標籤中傳遞過來的參數。
在第三節中咱們介紹了React-router的大體使用方法,讀一讀React-router4.0的源碼。
這裏咱們主要分析一下React-router4.0中是如何根據window.history來實現前端路由的,所以設計到的組件爲BrowserRouter、Router、Route和Link
從上一節的介紹中咱們知道,點擊Link標籤傳遞給新渲染的組件的props中有一個history對象,這個對象的內容很豐富,好比:action、goBack、go、location、push和replace方法等。
React-router構建了一個History類,用於在window.history的基礎上,構建屬性更爲豐富的實例。該History類實例化後具備action、goBack、location等等方法。
React-router中將這個新的History類的構建方法,獨立成一個node包,包名爲history。
npm install history -s
能夠經過上述方法來引入,咱們來看看這個History類的實現。
const createBrowserHistory = (props = {}) => { const globalHistory = window.history; ...... //默認props中屬性的值 const { forceRefresh = false, getUserConfirmation = getConfirmation, keyLength = 6, basename = '', } = props; const history = { length: globalHistory.length, action: "POP", location: initialLocation, createHref, push, replace, go, goBack, goForward, block, listen }; ---- (1) const basename = props.basename; const canUseHistory = supportsHistory(); ----(2) const createKey = () =>Math.random().toString(36).substr(2, keyLength); ----(3) const transitionManager = createTransitionManager(); ----(4) const setState = nextState => { Object.assign(history, nextState); history.length = globalHistory.length; transitionManager.notifyListeners(history.location, history.action); }; ----(5) const handlePopState = event => { handlePop(getDOMLocation(event.state)); }; const handlePop = location => { if (forceNextPop) { forceNextPop = false; setState(); } else { const action = "POP"; transitionManager.confirmTransitionTo( location, action, getUserConfirmation, ok => { if (ok) { setState({ action, location }); } else { revertPop(location); } } ); } }; ------(6) const initialLocation = getDOMLocation(getHistoryState()); let allKeys = [initialLocation.key]; ------(7) // 與pop相對應,相似的push和replace方法 const push ... replace ... ------(8) return history ------ (9) }
export const supportsHistory = () => { const ua = window.navigator.userAgent; if ( (ua.indexOf("Android 2.") !== -1 || ua.indexOf("Android 4.0") !== -1) && ua.indexOf("Mobile Safari") !== -1 && ua.indexOf("Chrome") === -1 && ua.indexOf("Windows Phone") === -1 ) return false; return window.history && "pushState" in window.history; };
從上述判別式咱們能夠看出,window.history在chrome、mobile safari和windows phone下是絕對支持的,但不支持安卓2.x以及安卓4.0
(4)中 createTransitionManager方法,返回一個集成對象,對象中包含了關於history地址或者對象改變時候的監聽函數等,具體代碼以下:
const createTransitionManager = () => { const setPrompt = nextPrompt => { }; const confirmTransitionTo = ( location, action, getUserConfirmation, callback ) => { if (typeof getUserConfirmation === "function") { getUserConfirmation(result, callback); } else { callback(true); } } }; let listeners = []; const appendListener = fn => { let isActive = true; const listener = (...args) => { if (isActive) fn(...args); }; listeners.push(listener); return () => { isActive = false; listeners = listeners.filter(item => item !== listener); }; }; const notifyListeners = (...args) => { listeners.forEach(listener => listener(...args)); }; return { setPrompt, confirmTransitionTo, appendListener, notifyListeners };
};
setPrompt函數,用於設置url跳轉時彈出的文字提示,confirmTransaction函數,會將當前生成新的history對象中的location,action,callback等參數,做用就是在回調的callback方法中,根據要求,改變傳入的location和action對象。
接着咱們看到有一個listeners數組,保存了一系列與url相關的監聽事件數組,經過接下來的appendListener方法,能夠往這個數組中增長事件,經過notifyListeners方法能夠遍歷執行listeners數組中的全部事件。
其實最難弄懂的是React-router中如何從新構建了一個history工廠函數,在第一小節中咱們已經詳細的介紹了history生成函數createBrowserHistory的源碼,接着來看Link組件就很容易了。
首先Link組件相似於HTML中的a標籤,目的也很簡單,就是去主動觸發改變url的方法,主動改變url的方法,從上述的history的介紹中可知爲push和replace方法,所以Link組件的源碼爲:
class Link extends React.Component { handleClick = event => { ... const { history } = this.context.router; const { replace, to } = this.props; if (replace) { history.replace(replace); } else { history.push(to); } } }; render(){ const { replace, to, innerRef, ...props } = this.props; <a {...props} onClick={this.handleClick}/> } }
上述代碼很簡單,從React的context API全局對象中拿到history,而後若是傳遞給Link組件的屬性中有replace爲true,則執行history.replace(to),to 是一個包含pathname的對象,若是傳遞給Link組件的replace屬性爲false,則執行history.push(to)方法。
Route組件也很簡單,其props中接受一個最主要的屬性path,Route作的事情只有一件:
當url改變的時候,將path屬性與改變後的url作對比,若是匹配成功,則渲染該組件的componet或者children屬性所賦值的那個組件。
具體源碼以下:
class Route extends React.Component { .... constructor(){ } render() { const { match } = this.state; const { children, component, render } = this.props; const { history, route, staticContext } = this.context.router; const location = this.props.location || route.location; const props = { match, location, history, staticContext }; if (component) return match ? React.createElement(component, props) : null; if (render) return match ? render(props) : null; if (typeof children === "function") return children(props); if (children && !isEmptyChildren(children)) return React.Children.only(children); return null; } }
state中的match就是是否匹配的標記,若是匹配當前的Route的path,那麼根據優先級順序component屬性、render屬性和children屬性來渲染其所指向的React組件。
Router組件中,是BrowserRouter、HashRouter等組件的底層組件。該組件中,定義了包含匹配規則match函數,以及使用了新history中的listener方法,來監聽url的改變,從而,當url改變時,更改Router下不一樣path組件的isMatch結果。
class Router extends React.Component { componentWillMount() { const { children, history } = this.props //調用history.listen監聽方法,該方法的返回函數是一個移除監聽的函數 this.unlisten = history.listen(() => { this.setState({ match: this.computeMatch(history.location.pathname) }); }); } componentWillUnmount() { this.unlisten(); } render() { } }
上述首先在組件建立前調用了listener監聽方法,來監聽url的改變,實時的更新isMatch的結果。
本文從前端路由的原理出發,前後介紹了兩種前端路由經常使用的方法,接着介紹了React-router的基本組件API以及用法,詳細介紹了React-router的組件中新構建的history對象,最後結合React-router的API閱讀了一下React-router的源碼。