从路由原理出发,深入阅读理解react-router 4.0的源码
??react-router等前端路由的原理大致相同,可以實現(xiàn)無刷新的條件下切換顯示不同的頁面。路由的本質(zhì)就是頁面的URL發(fā)生改變時,頁面的顯示結(jié)果可以根據(jù)URL的變化而變化,但是頁面不會刷新。通過前端路由可以實現(xiàn)單頁(SPA)應用,本文首先從前端路由的原理出發(fā),詳細介紹了前端路由原理的變遷。接著從react-router4.0的源碼出發(fā),深入理解react-router4.0是如何實現(xiàn)前端路由的。
- 通過Hash實現(xiàn)前端路由
- 通過H5的history實現(xiàn)前端路由
- React-router4.0的使用
- React-router4.0源碼分析
原文的地址,在我的博客中:https://github.com/forthealll...
如有幫助,您的star是對我最好的鼓勵~
一、通過Hash實現(xiàn)前端路由
1、hash的原理
??早期的前端路由是通過hash來實現(xiàn)的:
改變url的hash值是不會刷新頁面的。
??因此可以通過hash來實現(xiàn)前端路由,從而實現(xiàn)無刷新的效果。hash屬性位于location對象中,在當前頁面中,可以通過:
window.location.hash='edit'來實現(xiàn)改變當前url的hash值。執(zhí)行上述的hash賦值后,頁面的url發(fā)生改變。
賦值前:http://localhost:3000
賦值后:http://localhost:3000/#edit
在url中多了以#結(jié)尾的hash值,但是賦值前后雖然頁面的hash值改變導致頁面完整的url發(fā)生了改變,但是頁面是不會刷新的。此外,還有一個名為hashchange的事件,可以監(jiān)聽hash的變化,我們可以通過下面兩種方式來監(jiān)聽hash的變化:
window.onhashchange=function(event){console.log(event); } window.addEventListener('hashchange',function(event){console.log(event); })當hash值改變時,輸出一個HashChangeEvent。該HashChangeEvent的具體值為:
{isTrusted: true, oldURL: "http://localhost:3000/", newURL: "http://localhost:3000/#teg", type: "hashchange".....}??有了監(jiān)聽事件,且改變hash頁面不刷新,這樣我們就可以在監(jiān)聽事件的回調(diào)函數(shù)中,執(zhí)行我們展示和隱藏不同UI顯示的功能,從而實現(xiàn)前端路由。
此外,除了可以通過window.location.hash來改變當前頁面的hash值外,還可以通過html的a標簽來實現(xiàn):
<a href="#edit">edit</a>2、hash的缺點
hash的兼容性較好,因此在早期的前端路由中大量的采用,但是使用hash也有很多缺點。
- 搜索引擎對帶有hash的頁面不友好
- 帶有hash的頁面內(nèi)難以追蹤用戶行為
二、通過history實現(xiàn)前端路由
HTML5的History接口,History對象是一個底層接口,不繼承于任何的接口。History接口允許我們操作瀏覽器會話歷史記錄。
(1)History的屬性和方法
History提供了一些屬性和方法。
History的屬性:
- History.length: 返回在會話歷史中有多少條記錄,包含了當前會話頁面。此外如果打開一個新的Tab,那么這個length的值為1
- History.state:
保存了會出發(fā)popState事件的方法,所傳遞過來的屬性對象(后面會在pushState和replaceState方法中詳細的介紹)
History方法:
- History.back(): 返回瀏覽器會話歷史中的上一頁,跟瀏覽器的回退按鈕功能相同
- History.forward():指向瀏覽器會話歷史中的下一頁,跟瀏覽器的前進按鈕相同
- History.go(): 可以跳轉(zhuǎn)到瀏覽器會話歷史中的指定的某一個記錄頁
- History.pushState():pushState可以將給定的數(shù)據(jù)壓入到瀏覽器會話歷史棧中,該方法接收3個參數(shù),對象,title和一串url。pushState后會改變當前頁面url,但是不會伴隨著刷新
- History.replaceState():replaceState將當前的會話頁面的url替換成指定的數(shù)據(jù),replaceState后也會改變當前頁面的url,但是也不會刷新頁面。
上面的方法中,pushState和repalce的相同點:
就是都會改變當前頁面顯示的url,但都不會刷新頁面。
不同點:
pushState是壓入瀏覽器的會話歷史棧中,會使得History.length加1,而replaceState是替換當前的這條會話歷史,因此不會增加History.length.
(2)BOM對象history
history在瀏覽器的BOM對象模型中的重要屬性,history完全繼承了History接口,因此擁有History中的所有的屬性和方法。
這里我們主要來看看history.length屬性以及history.pushState、history.replaceState方法。
- history.pushState(stateObj,title,url) or history.replaceState(stateObj,title,url)
pushState和replaceState接受3個參數(shù),分別為state對象,title標題,改變的url。
window.history.pushState({foo:'bar'}, "page 2", "bar.html");
此時,當前的url變?yōu)?#xff1a;
執(zhí)行上述方法前:http://localhost:3000
執(zhí)行上述方法后:http://localhost:3000/bar.html
如果我們輸出window.history.state:
console.log(window.history.state);
// {foo:'bar'}
window.history.state就是我們pushState的第一個對象參數(shù)。
- history.replaceState()方法不會改變hitroy的長度
console.log(window.history.length);
window.history.replaceState({foo:'bar'}, "page 2", "bar.html");
console.log(window.history.length);
上述前后兩次輸出的window.history.length是相等的。
此外。
每次觸發(fā)history.back()或者瀏覽器的后退按鈕等,會觸發(fā)一個popstate事件,這個事件在后退或者前進的時候發(fā)生:
window.onpopstate=function(event){}注意:
history.pushState和history.replaceState方法并不會觸發(fā)popstate事件。
如果用history做為路由的基礎,那么需要用到的是history.pushState和history.replaceState,在不刷新的情況下可以改變url的地址,且如果頁面發(fā)生回退back或者forward時,會觸發(fā)popstate事件。
hisory為依據(jù)來實現(xiàn)路由的優(yōu)點:
- 對搜索引擎友好
- 方便統(tǒng)計用戶行為
缺點:
- 兼容性不如hash
- 需要后端做相應的配置,否則直接訪問子頁面會出現(xiàn)404錯誤
三、React-router4.0的使用
了解了前端路由實現(xiàn)的原理之后,下面來介紹一下React-router4.0。在React-router4.0的代碼庫中,根據(jù)使用場景包含了以下幾個獨立的包:
- react-router : react-router4.0的核心代碼
- react-router-dom : 構建網(wǎng)頁應用,存在DOM對象場景下的核心包
- react-router-native : 適用于構建react-native應用
- react-router-config : 配置靜態(tài)路由
- react-router-redux : 結(jié)合redux來配置路由,已廢棄,不推薦使用。
在react-router4.0中,遵循Just Component的設計理念:
所提供的API都是以組件的形式給出。
比如BrowserRouter、Router、Link、Switch等API都是以組件的形式來使用。
1、React-router-dom常用的組件API
下面我們以React-router4.0中的React-router-dom包來介紹常用的BrowserRouter、HashRouter、Link和Router等。
(1) <BrowserRouter>
用<BrowserRouter> 組件包裹整個App系統(tǒng)后,就是通過html5的history來實現(xiàn)無刷新條件下的前端路由。
<BrowserRouter>組件具有以下幾個屬性:
-
basename: string 這個屬性,是為當前的url再增加名為basename的值的子目錄。
<BrowserRouter basename="test"/>
如果設置了basename屬性,那么此時的:
http://localhost:3000 和 http://localhost:3000/test 表示的是同一個地址,渲染的內(nèi)容相同。
- getUserConfirmation: func 這個屬性,用于確認導航的功能。默認使用window.confirm
- forceRefresh: bool 默認為false,表示改變路由的時候頁面不會重新刷新,如果當前瀏覽器不支持history,那么當forceRefresh設置為true的時候,此時每次去改變url都會重新刷新整個頁面。
- keyLength: number 表示location的key屬性的長度,在react-router中每個url下都有為一個location與其對應,并且每一個url的location的key值都不相同,這個屬性一般都使用默認值,設置的意義不大。
- children: node children的屬性必須是一個ReactNode節(jié)點,表示唯一渲染一個元素。
與<BrowserRouter>對應的是<HashRouter>,<HashRouter>使用url中的hash屬性來保證不重新刷新的情況下同時渲染頁面。
(2) <Route>
<Route> 組件十分重要,<Route> 做的事情就是匹配相應的location中的地址,匹配成功后渲染對應的組件。下面我們來看<Route>中的屬性。
首先來看如何執(zhí)行匹配,決定<Route>地址匹配的屬性:
- path:當location中的url改變后,會與Route中的path屬性做匹配,path決定了與路由或者url相關的渲染效果。
- exact: 如果有exact,只有url地址完全與path相同,才會匹配。如果沒有exact屬性,url的地址不完全相同,也會匹配。
舉例來說,當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 :與exact不同,strict屬性僅僅是對exact屬性的一個補充,設置了strict屬性后,嚴格限制了但斜線“/”。
舉例來說,當不設置strict的時候:
<Route path='/home/' component={Home}/>此時http://localhost:3000/home 和 http://localhost:3000/home/
都能匹配到組件Home。匹配對于斜線“/”比較寬松。如果設置了strict屬性:
那么此時嚴格匹配斜線是否存在,http://localhost:3000/home 將無法匹配到Home組件。
當Route組件與某一url匹配成功后,就會繼續(xù)去渲染。那么什么屬性決定去渲染哪個組件或者樣式呢,Route的component、render、children決定渲染的內(nèi)容。
- component:該屬性接受一個React組件,當url匹配成功,就會渲染該組件
- render:func 該屬性接受一個返回React Element的函數(shù),當url匹配成功,渲染覆該返回的元素
- children:與render相似,接受一個返回React Element的函數(shù),但是不同點是,無論url與當前的Route的path匹配與否,children的內(nèi)容始終會被渲染出來。
并且這3個屬性所接受的方法或者組件,都會有l(wèi)ocation,match和history這3個參數(shù)。如果組件,那么組件的props中會存在從Link傳遞過來的location,match以及history。
(3) <Link>
<Route>定義了匹配規(guī)則和渲染規(guī)則,而<Link> 決定的是如何在頁面內(nèi)改變url,從而與相應的<Route>匹配。<Link>類似于html中的a標簽,此外<Link>在改變url的時候,可以將一些屬性傳遞給匹配成功的Route,供相應的組件渲染的時候使用。
- to: string
to屬性的值可以為一個字符串,跟html中的a標簽的href一樣,即使to屬性的值是一個字符串,點擊Link標簽跳轉(zhuǎn)從而匹配相應path的Route,也會將history,location,match這3個對象傳遞給Route所對應的組件的props中。
舉例來說:
<Link to='/home'>Home</Link>如上所示,當to接受一個string,跳轉(zhuǎn)到url為'/home'所匹配的Route,并渲染其關聯(lián)的組件內(nèi)接受3個對象history,location,match。
這3個對象會在下一小節(jié)會詳細介紹。
- to: object
to屬性的值也可以是一個對象,該對象可以包含一下幾個屬性:pathname、seacth、hash和state,其中前3個參數(shù)與如何改變url有關,最后一個state參數(shù)是給相應的改變url時,傳遞一個對象參數(shù)。
舉例來說:
<Link to={{pathname:'/home',search:'?sort=name',hash:'#edit',state:{a:1}}}>Home</Link>在上個例子中,to為一個對象,點擊Link標簽跳轉(zhuǎn)后,改變后的url為:'/home?sort=name#edit'。 但是在與相應的Route匹配時,只匹配path為'/home'的組件,'/home?sort=name#edit'。在'/home'后所帶的參數(shù)不作為匹配標準,僅僅是做為參數(shù)傳遞到所匹配到的組件中,此外,state={a:1}也同樣做為參數(shù)傳遞到新渲染的組件中。
(4) React-router中傳遞給組件props的history對象
介紹了 <BrowserRouter> 、 <Route> 和 <Link> 之后,使用這3個組件API就可以構建一個簡單的React-router應用。這里我們之前說,每當點擊Link標簽跳轉(zhuǎn)或者在js中使用React-router的方法跳轉(zhuǎn),從當前渲染的組件,進入新組件。在新組件被渲染的時候,會接受一個從舊組件傳遞過來的參數(shù)。
我們前面提到,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標簽跳轉(zhuǎn)后,會渲染新的組件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)從上面的屬性明細中:
- push:f 這個方法用于在js中改變url,之前在Link組件中可以類似于HTML標簽的形式改變url。push方法映射于window.history中的pushState方法。
- replace: f 這個方法也是用于在js中改變url,replace方法映射于window.history中的replaceState方法。
- block:f 這個方法也很有用,比如當用戶離開當前頁面的時候,給用戶一個文字提示,就可以采用history.block("你確定要離開當前頁嗎?")這樣的提示。
- go / goBack / goForward
在組件props中history的go、goBack、goForward方法,分別window.history.go、window.history.back、window.history.forward對應。
- action: "PUSH" || "POP"
action這個屬性左右很大,如果是通過Link標簽或者在js中通過this.props.push方法來改變當前的url,那么在新組件中的action就是"PUSH",否則就是"POP".
action屬性很有用,比如我們在做翻頁動畫的時候,前進的動畫是SlideIn,后退的動畫是SlideOut,我們可以根據(jù)組件中的action來判斷采用何種動畫:
function newComponent (props)=>{return (<ReactCSSTransitionGrouptransitionAppear={true}transitionAppearTimeout={600}transitionEnterTimeout={600}transitionLeaveTimeout={200}transitionName={props.history.action==='PUSH'?'SlideIn':'SlideOut'}><Component {...props}/></ReactCSSTransitionGroup>) }- location:object
在新組件的location屬性中,就記錄了從就組件中傳遞過來的參數(shù),從上面的例子中,我們看到此時的location的值為:
hash: "#edit"key: "uxs9r5"pathname: "/home"search: "?sort=name"state: {a:1}除了key這個用作唯一表示外,其他的屬性都是我們從上一個Link標簽中傳遞過來的參數(shù)。
四、React-router4.0源碼分析
在第三節(jié)中我們介紹了React-router的大致使用方法,讀一讀React-router4.0的源碼。
這里我們主要分析一下React-router4.0中是如何根據(jù)window.history來實現(xiàn)前端路由的,因此設計到的組件為BrowserRouter、Router、Route和Link
1、React-router中的history
從上一節(jié)的介紹中我們知道,點擊Link標簽傳遞給新渲染的組件的props中有一個history對象,這個對象的內(nèi)容很豐富,比如: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類的實現(xiàn)。
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)}- (1) 中指明了新的構建方法History所返回的history對象中所具有的屬性。
- (2)中的supportsHistory的方法判斷當前的瀏覽器對于window.history的兼容性,具體方法如下:
從上述判別式我們可以看出,window.history在chrome、mobile safari和windows phone下是絕對支持的,但不支持安卓2.x以及安卓4.0
- (3)中用于創(chuàng)建與history中每一個url記錄相關聯(lián)的指定位數(shù)的唯一標識key, 默認的keyLength為6位
-
(4)中 createTransitionManager方法,返回一個集成對象,對象中包含了關于history地址或者對象改變時候的監(jiān)聽函數(shù)等,具體代碼如下:
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函數(shù),用于設置url跳轉(zhuǎn)時彈出的文字提示,confirmTransaction函數(shù),會將當前生成新的history對象中的location,action,callback等參數(shù),作用就是在回調(diào)的callback方法中,根據(jù)要求,改變傳入的location和action對象。
接著我們看到有一個listeners數(shù)組,保存了一系列與url相關的監(jiān)聽事件數(shù)組,通過接下來的appendListener方法,可以往這個數(shù)組中增加事件,通過notifyListeners方法可以遍歷執(zhí)行l(wèi)isteners數(shù)組中的所有事件。
- (5) setState方法,發(fā)生在history的url或者history的action發(fā)生改變的時候,此方法會更新history對象中的屬性,同時會觸發(fā)notifyListeners方法,傳入當前的history.location和history.action。遍歷并執(zhí)行所有監(jiān)聽url改變的事件數(shù)組listeners。
- (6)這個getDOMLocation方法就是根據(jù)當前在window.state中的值,生成新history的location屬性對象,allKeys這是始終保持了在url改變時候的歷史url相關聯(lián)的key,保存在全局,allKeys在執(zhí)行生“POP”或者“PUSH”、“Repalce”等會改變url的方法時,會保持一個實時的更新。
- (7) handlePop方法,用于處理“POP”事件,我們知道在window.history中點擊后退等會觸發(fā)“POP”事件,這里也是一樣,執(zhí)行action為“POP”,當后退的時候就會觸發(fā)該函數(shù)。
- (8)中包含了與pop方法類似的,push和replace方法,push方法同樣做的事情就是執(zhí)行action為“PUSH”(“REPLACE”),該變allKeys數(shù)組中的值,唯一不同的是actio為“PUSH”的方法push是往allKeys數(shù)組中添加,而action為“REPLACE”的方法replace則是替換掉當前的元素。
- (9)返回這個新生成的history對象。
2、React-router中Link組件
其實最難弄懂的是React-router中如何重新構建了一個history工廠函數(shù),在第一小節(jié)中我們已經(jīng)詳細的介紹了history生成函數(shù)createBrowserHistory的源碼,接著來看Link組件就很容易了。
首先Link組件類似于HTML中的a標簽,目的也很簡單,就是去主動觸發(fā)改變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,則執(zhí)行history.replace(to),to 是一個包含pathname的對象,如果傳遞給Link組件的replace屬性為false,則執(zhí)行history.push(to)方法。
3、React-router中Route組件
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,那么根據(jù)優(yōu)先級順序component屬性、render屬性和children屬性來渲染其所指向的React組件。
4、React-router中Router組件
Router組件中,是BrowserRouter、HashRouter等組件的底層組件。該組件中,定義了包含匹配規(guī)則match函數(shù),以及使用了新history中的listener方法,來監(jiān)聽url的改變,從而,當url改變時,更改Router下不同path組件的isMatch結(jié)果。
class Router extends React.Component {componentWillMount() {const { children, history } = this.props//調(diào)用history.listen監(jiān)聽方法,該方法的返回函數(shù)是一個移除監(jiān)聽的函數(shù)this.unlisten = history.listen(() => {this.setState({match: this.computeMatch(history.location.pathname)});});}componentWillUnmount() {this.unlisten();}render() {} }上述首先在組件創(chuàng)建前調(diào)用了listener監(jiān)聽方法,來監(jiān)聽url的改變,實時的更新isMatch的結(jié)果。
5、總結(jié)
本文從前端路由的原理出發(fā),先后介紹了兩種前端路由常用的方法,接著介紹了React-router的基本組件API以及用法,詳細介紹了React-router的組件中新構建的history對象,最后結(jié)合React-router的API閱讀了一下React-router的源碼。
總結(jié)
以上是生活随笔為你收集整理的从路由原理出发,深入阅读理解react-router 4.0的源码的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: P2801 教主的魔法
- 下一篇: ThreadLocal实践