摘要:還是先來(lái)一段官方的基礎(chǔ)使用案例,熟悉一下整體的代碼流程中使用了端常用到的等一些常用組件,作為的頂層組件來(lái)獲取的和設(shè)置回調(diào)函數(shù)來(lái)更新。
react-router是react官方推薦并參與維護(hù)的一個(gè)路由庫(kù),支持瀏覽器端、app端、服務(wù)端等常見(jiàn)場(chǎng)景下的路由切換功能,react-router本身不具備切換和跳轉(zhuǎn)路由的功能,這些功能全部由react-router依賴的history庫(kù)完成,history庫(kù)通過(guò)對(duì)url的監(jiān)聽(tīng)來(lái)觸發(fā) Router 組件注冊(cè)的回調(diào),回調(diào)函數(shù)中會(huì)獲取最新的url地址和其他參數(shù)然后通過(guò)setState更新,從而使整個(gè)應(yīng)用進(jìn)行rerender。所以react-router本身只是封裝了業(yè)務(wù)上的眾多功能性組件,比如Route、Link、Redirect 等等,這些組件通過(guò)context api可以獲取到Router傳遞history api,比如push、replace等,從而完成頁(yè)面的跳轉(zhuǎn)。
還是先來(lái)一段react-router官方的基礎(chǔ)使用案例,熟悉一下整體的代碼流程
import React from "react"; import { BrowserRouter as Router, Route, Link } from "react-router-dom"; function BasicExample() { return (); } function Home() { return (
- Home
- About
- Topics
); } function About() { return (Home
); } function Topics({ match }) { return (About
); } function Topic({ match }) { return (Topics
- Rendering with React
- Components
- Props v. State
Please select a topic.
} />); } export default BasicExample;{match.params.topicId}
Demo中使用了web端常用到的BrowserRouter、Route、Link等一些常用組件,Router作為react-router的頂層組件來(lái)獲取 history 的api 和 設(shè)置回調(diào)函數(shù)來(lái)更新state。這里引用的組件都是來(lái)自react-router-dom 這個(gè)庫(kù),那么react-router 和 react-router-dom 是什么關(guān)系呢。
說(shuō)的簡(jiǎn)單一點(diǎn),react-router-dom 是對(duì)react-router所有組件或方法的一個(gè)二次導(dǎo)出,并且在react-router組件的基礎(chǔ)上添加了新的組件,更加方便開(kāi)發(fā)者處理復(fù)雜的應(yīng)用業(yè)務(wù)。
1.react-router 導(dǎo)出的所有內(nèi)容
統(tǒng)計(jì)一下,總共10個(gè)方法
1.MemoryRouter.js、2.Prompt.js、3.Redirect.js、4.Route.js、5.Router.js、6.StaticRouter.js、7.Switch.js、8.generatePath.js、9.matchPath.js、10.withRouter.js
2.react-router-dom 導(dǎo)出的所有內(nèi)容
統(tǒng)計(jì)一下,總共14個(gè)方法
1.BrowserRouter.js、2.HashRouter.js、3.Link.js、4.MemoryRouter.js、5.NavLink.js、6.Prompt.js、7.Redirect.js、8.Route.js、9.Router.js、10.StaticRouter.js、11.Switch.js、12.generatePath.js、13.matchPath.js、14.withRouter.js
react-router-dom在react-router的10個(gè)方法上,又添加了4個(gè)方法,分別是BrowserRouter、HashRouter、Link、以及NavLink。
所以,react-router-dom將react-router的10個(gè)方法引入后,又加入了4個(gè)方法,再重新導(dǎo)出,在開(kāi)發(fā)中我們只需要引入react-router-dom這個(gè)依賴即可。
下面進(jìn)入react-router-dom的源碼分析階段,首先來(lái)看一下react-router-dom的依賴庫(kù)
React, 要求版本大于等于15.x
history, react-router的核心依賴庫(kù),注入組件操作路由的api
invariant, 用來(lái)拋出異常的工具庫(kù)
loose-envify, 使用browserify工具進(jìn)行打包的時(shí)候,會(huì)將項(xiàng)目當(dāng)中的node全局變量替換為對(duì)應(yīng)的字符串
prop-types, react的props類型校驗(yàn)工具庫(kù)
react-router, 依賴同版本的react-router
warning, 控制臺(tái)打印警告信息的工具庫(kù)
①.BrowserRouter.js, 提供了HTML5的history api 如pushState、replaceState等來(lái)切換地址,源碼如下
import warning from "warning"; import React from "react"; import PropTypes from "prop-types"; import { createBrowserHistory as createHistory } from "history"; import Router from "./Router"; /** * The public API for athat uses HTML5 history. */ class BrowserRouter extends React.Component { static propTypes = { basename: PropTypes.string, // 當(dāng)應(yīng)用為某個(gè)子應(yīng)用時(shí),添加的地址欄前綴 forceRefresh: PropTypes.bool, // 切換路由時(shí),是否強(qiáng)制刷新 getUserConfirmation: PropTypes.func, // 使用Prompt組件時(shí) 提示用戶的confirm確認(rèn)方法,默認(rèn)使用window.confirm keyLength: PropTypes.number, // 為了實(shí)現(xiàn)block功能,react-router維護(hù)創(chuàng)建了一個(gè)訪問(wèn)過(guò)的路由表,每個(gè)key代表一個(gè)曾經(jīng)訪問(wèn)過(guò)的路由地址 children: PropTypes.node // 子節(jié)點(diǎn) }; // 核心api, 提供了push replace go等路由跳轉(zhuǎn)方法 history = createHistory(this.props); // 提示用戶 BrowserRouter不接受用戶自定義的history方法, // 如果傳遞了history會(huì)被忽略,如果用戶使用自定義的history api, // 需要使用 Router 組件進(jìn)行替代 componentWillMount() { warning( !this.props.history, " ignores the history prop. To use a custom history, " + "use `import { Router }` instead of `import { BrowserRouter as Router }`." ); } // 將history和children作為props傳遞給Router組件 并返回 render() { return ; } } export default BrowserRouter;
**總結(jié):BrowserRouter組件非常簡(jiǎn)單,它本身其實(shí)就是對(duì)Router組件的一個(gè)包裝,將HTML5的history api封裝好再賦予 Router 組件。BrowserRouter就好比一個(gè)容器組件,由它來(lái)決定Router的最終api,這樣一個(gè)Router組件就可以完成多種api的實(shí)現(xiàn),比如HashRouter、StaticRouter 等,減少了代碼的耦合度
②. Router.js, 如果說(shuō)BrowserRouter是Router的容器組件,為Router提供了html5的history api的數(shù)據(jù)源,那么Router.js 亦可以看作是子節(jié)點(diǎn)的容器組件,它除了接收BrowserRouter提供的history api,最主要的功能就是組件本身會(huì)響應(yīng)地址欄的變化進(jìn)行setState進(jìn)而完成react本身的rerender,使應(yīng)用進(jìn)行相應(yīng)的UI切換,源碼如下**
import warning from "warning"; import invariant from "invariant"; import React from "react"; import PropTypes from "prop-types"; /** * The public API for putting history on context. */ class Router extends React.Component { // react-router 4.x依然使用的使react舊版的context API // react-router 5.x將會(huì)作出升級(jí) static propTypes = { history: PropTypes.object.isRequired, children: PropTypes.node }; // 此處是為了能夠接收父級(jí)容器傳遞的context router,不過(guò)父級(jí)很少有傳遞router的 // 存在的目的是為了方便用戶使用這種潛在的方式,來(lái)傳遞自定義的router對(duì)象 static contextTypes = { router: PropTypes.object }; // 傳遞給子組件的context api router, 可以通過(guò)context上下文來(lái)獲得 static childContextTypes = { router: PropTypes.object.isRequired }; // router 對(duì)象的具體值 getChildContext() { return { router: { ...this.context.router, history: this.props.history, // 路由api等,會(huì)在history庫(kù)進(jìn)行講解 route: { location: this.props.history.location, // 也是history庫(kù)中的內(nèi)容 match: this.state.match // 對(duì)當(dāng)前地址進(jìn)行匹配的結(jié)果 } } }; } // Router組件的state,作為一個(gè)頂層容器組件維護(hù)的state,存在兩個(gè)目的 // 1.主要目的為了實(shí)現(xiàn)自上而下的rerender,url改變的時(shí)候match對(duì)象會(huì)被更新 // 2.Router組件是始終會(huì)被渲染的組件,match對(duì)象會(huì)隨時(shí)得到更新,并經(jīng)過(guò)context api // 傳遞給下游子組件route等 state = { match: this.computeMatch(this.props.history.location.pathname) }; // match 的4個(gè)參數(shù) // 1.path: 是要進(jìn)行匹配的路徑可以是 "/user/:id" 這種動(dòng)態(tài)路由的模式 // 2.url: 地址欄實(shí)際的匹配結(jié)果 // 3.parmas: 動(dòng)態(tài)路由所匹配到的參數(shù),如果path是 "/user/:id"匹配到了,那么 // params的內(nèi)容就是 {id: 某個(gè)值} // 4.isExact: 精準(zhǔn)匹配即 地址欄的pathname 和 正則匹配到url是否完全相等 computeMatch(pathname) { return { path: "/", url: "/", params: {}, isExact: pathname === "/" }; } componentWillMount() { const { children, history } = this.props; // 當(dāng) 子節(jié)點(diǎn)并非由一個(gè)根節(jié)點(diǎn)包裹時(shí) 拋出錯(cuò)誤提示開(kāi)發(fā)者 invariant( children == null || React.Children.count(children) === 1, "Amay have only one child element" ); // Do this here so we can setState when a changes the // location in componentWillMount. This happens e.g. when doing // server rendering using a . // 使用history.listen方法,在Router被實(shí)例化時(shí)注冊(cè)一個(gè)回調(diào)事件, // 即location地址發(fā)生改變的時(shí)候,會(huì)重新setState,進(jìn)而rerender // 這里使用willMount而不使用didMount的原因時(shí)是因?yàn)?,服?wù)端渲染時(shí)不存在dom, // 故不會(huì)調(diào)用didMount的鉤子,react將在17版本移除此鉤子,那么到時(shí)候router應(yīng)該如何實(shí)現(xiàn)此功能? this.unlisten = history.listen(() => { this.setState({ match: this.computeMatch(history.location.pathname) }); }); } // history參數(shù)不允許被更改 componentWillReceiveProps(nextProps) { warning( this.props.history === nextProps.history, "You cannot change " ); } // 組件銷(xiāo)毀時(shí) 解綁history對(duì)象中的監(jiān)聽(tīng)事件 componentWillUnmount() { this.unlisten(); } // render的時(shí)候使用React.Children.only方法再驗(yàn)證一次 // children 必須是一個(gè)由根節(jié)點(diǎn)包裹的組件或dom render() { const { children } = this.props; return children ? React.Children.only(children) : null; } } export default Router;
總結(jié):Router組件職責(zé)很清晰就是作為容器組件,將上層組件的api進(jìn)行向下的傳遞,同時(shí)組件本身注冊(cè)了回調(diào)方法,來(lái)滿足瀏覽器環(huán)境下或者服務(wù)端環(huán)境下location發(fā)生變化時(shí),重新setState,達(dá)到組件的rerender。那么history對(duì)象到底是怎么實(shí)現(xiàn)對(duì)地址欄進(jìn)行監(jiān)聽(tīng)的,又是如何對(duì)location進(jìn)行push 或者 replace的,這就要看history這個(gè)庫(kù)做了啥。
createBrowserHistory.js 使用html5 history api封裝的路由控制器
createHashHistory.js 使用hash方法封裝的路由控制器
createMemoryHistory.js 針對(duì)native app這種原生應(yīng)用封裝的路由控制器,即在內(nèi)存中維護(hù)一份路由表
createTransitionManager.js 針對(duì)路由切換時(shí)的相同操作抽離的一個(gè)公共方法,路由切換的操作器,攔截器和訂閱者都存在于此
DOMUtils.js 針對(duì)web端dom操作或判斷兼容性的一個(gè)工具方法集合
LocationUtils.js 針對(duì)location url處理等抽離的一個(gè)工具方法的集合
PathUtils.js 用來(lái)處理url路徑的工具方法集合
這里主要分析createBrowserHistory.js文件
import warning from "warning" import invariant from "invariant" import { createLocation } from "./LocationUtils" import { addLeadingSlash, stripTrailingSlash, hasBasename, stripBasename, createPath } from "./PathUtils" import createTransitionManager from "./createTransitionManager" import { canUseDOM, addEventListener, removeEventListener, getConfirmation, supportsHistory, supportsPopStateOnHashChange, isExtraneousPopstateEvent } from "./DOMUtils" const PopStateEvent = "popstate" const HashChangeEvent = "hashchange" const getHistoryState = () => { // ... } /** * Creates a history object that uses the HTML5 history API including * pushState, replaceState, and the popstate event. */ const createBrowserHistory = (props = {}) => { invariant( canUseDOM, "Browser history needs a DOM" ) const globalHistory = window.history const canUseHistory = supportsHistory() const needsHashChangeListener = !supportsPopStateOnHashChange() const { forceRefresh = false, getUserConfirmation = getConfirmation, keyLength = 6 } = props const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : "" const getDOMLocation = (historyState) => { // ... } const createKey = () => Math.random().toString(36).substr(2, keyLength) const transitionManager = createTransitionManager() const setState = (nextState) => { // ... } const handlePopState = (event) => { // ... } const handleHashChange = () => { // ... } let forceNextPop = false const handlePop = (location) => { // ... } const revertPop = (fromLocation) => { // ... } const initialLocation = getDOMLocation(getHistoryState()) let allKeys = [ initialLocation.key ] // Public interface const createHref = (location) => basename + createPath(location) const push = (path, state) => { // ... } const replace = (path, state) => { // ... } const go = (n) => { globalHistory.go(n) } const goBack = () => go(-1) const goForward = () => go(1) let listenerCount = 0 const checkDOMListeners = (delta) => { // ... } let isBlocked = false const block = (prompt = false) => { // ... } const listen = (listener) => { // ... } const history = { length: globalHistory.length, action: "POP", location: initialLocation, createHref, push, replace, go, goBack, goForward, block, listen } return history } export default createBrowserHistory
createBrowserHistory.js 總共300+行代碼,其原理就是封裝了原生的html5 的history api,如pushState,replaceState,當(dāng)這些事件被觸發(fā)時(shí)會(huì)激活subscribe的回調(diào)來(lái)進(jìn)行響應(yīng)。同時(shí)也會(huì)對(duì)地址欄進(jìn)行監(jiān)聽(tīng),當(dāng)history.go等事件觸發(fā)history popstate事件時(shí),也會(huì)激活subscribe的回調(diào)。
由于代碼量較多,而且依賴的方法較多,這里將方法分成幾個(gè)小節(jié)來(lái)進(jìn)行梳理,對(duì)于依賴的方法先進(jìn)行簡(jiǎn)短闡述,當(dāng)實(shí)際調(diào)用時(shí)在深入源碼內(nèi)部去探究實(shí)現(xiàn)細(xì)節(jié)
1. 依賴的工具方法
import warning from "warning" // 控制臺(tái)的console.warn警告 import invariant from "invariant" // 用來(lái)拋出異常錯(cuò)誤信息 // 對(duì)地址參數(shù)處理,最終返回一個(gè)對(duì)象包含 pathname,search,hash,state,key 等參數(shù) import { createLocation } from "./LocationUtils" import { addLeadingSlash, // 對(duì)傳遞的pathname添加首部`/`,即 "home" 處理為 "/home",存在首部`/`的不做處理 stripTrailingSlash, // 對(duì)傳遞的pathname去掉尾部的 `/` hasBasename, // 判斷是否傳遞了basename參數(shù) stripBasename, // 如果傳遞了basename參數(shù),那么每次需要將pathname中的basename統(tǒng)一去除 createPath // 將location對(duì)象的參數(shù)生成最終的地址欄路徑 } from "./PathUtils" import createTransitionManager from "./createTransitionManager" // 抽離的路由切換的公共方法 import { canUseDOM, // 當(dāng)前是否可使用dom, 即window對(duì)象是否存在,是否是瀏覽器環(huán)境下 addEventListener, // 兼容ie 監(jiān)聽(tīng)事件 removeEventListener, // 解綁事件 getConfirmation, // 路由跳轉(zhuǎn)的comfirm 回調(diào),默認(rèn)使用window.confirm supportsHistory, // 當(dāng)前環(huán)境是否支持history的pushState方法 supportsPopStateOnHashChange, // hashChange是否會(huì)觸發(fā)h5的popState方法,ie10、11并不會(huì) isExtraneousPopstateEvent // 判斷popState是否時(shí)真正有效的 } from "./DOMUtils" const PopStateEvent = "popstate" // 針對(duì)popstate事件的監(jiān)聽(tīng) const HashChangeEvent = "hashchange" // 針對(duì)不支持history api的瀏覽器 啟動(dòng)hashchange監(jiān)聽(tīng)事件 // 返回history的state const getHistoryState = () => { try { return window.history.state || {} } catch (e) { // IE 11 sometimes throws when accessing window.history.state // See https://github.com/ReactTraining/history/pull/289 // IE11 下有時(shí)會(huì)拋出異常,此處保證state一定返回一個(gè)對(duì)象 return {} } }
creareBrowserHistory的具體實(shí)現(xiàn)
const createBrowserHistory = (props = {}) => { // 當(dāng)不在瀏覽器環(huán)境下直接拋出錯(cuò)誤 invariant( canUseDOM, "Browser history needs a DOM" ) const globalHistory = window.history // 使用window的history // 此處注意android 2. 和 4.0的版本并且ua的信息是 mobile safari 的history api是有bug且無(wú)法解決的 const canUseHistory = supportsHistory() // hashChange的時(shí)候是否會(huì)進(jìn)行popState操作,ie10、11不會(huì)進(jìn)行popState操作 const needsHashChangeListener = !supportsPopStateOnHashChange() const { forceRefresh = false, // 默認(rèn)切換路由不刷新 getUserConfirmation = getConfirmation, // 使用window.confirm keyLength = 6 // 默認(rèn)6位長(zhǎng)度隨機(jī)key } = props // addLeadingSlash 添加basename頭部的斜杠 // stripTrailingSlash 去掉 basename 尾部的斜杠 // 如果basename存在的話,保證其格式為 ‘/xxx’ const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : "" const getDOMLocation = (historyState) => { // 獲取history對(duì)象的key和state const { key, state } = (historyState || {}) // 獲取當(dāng)前路徑下的pathname,search,hash等參數(shù) const { pathname, search, hash } = window.location // 拼接一個(gè)完整的路徑 let path = pathname + search + hash // 當(dāng)傳遞了basename后,所有的pathname必須包含這個(gè)basename warning( (!basename || hasBasename(path, basename)), "You are attempting to use a basename on a page whose URL path does not begin " + "with the basename. Expected path "" + path + "" to begin with "" + basename + ""." ) // 去掉path當(dāng)中的basename if (basename) path = stripBasename(path, basename) // 生成一個(gè)自定義的location對(duì)象 return createLocation(path, state, key) } // 使用6位長(zhǎng)度的隨機(jī)key const createKey = () => Math.random().toString(36).substr(2, keyLength) // transitionManager是history中最復(fù)雜的部分,復(fù)雜的原因是因?yàn)? // 為了實(shí)現(xiàn)block方法,做了對(duì)路由攔截的hack,雖然能實(shí)現(xiàn)對(duì)路由切時(shí)的攔截功能 // 比如Prompt組件,但同時(shí)也帶來(lái)了不可解決的bug,后面在討論 // 這里返回一個(gè)對(duì)象包含 setPrompt、confirmTransitionTo、appendListener // notifyListeners 等四個(gè)方法 const transitionManager = createTransitionManager() const setState = (nextState) => { // nextState包含最新的 action 和 location // 并將其更新到導(dǎo)出的 history 對(duì)象中,這樣Router組件相應(yīng)的也會(huì)得到更新 // 可以理解為同react內(nèi)部所做的setState時(shí)相同的功能 Object.assign(history, nextState) // 更新history的length, 實(shí)實(shí)保持和window.history.length 同步 history.length = globalHistory.length // 通知subscribe進(jìn)行回調(diào) transitionManager.notifyListeners( history.location, history.action ) } // 當(dāng)監(jiān)聽(tīng)到popState事件時(shí)進(jìn)行的處理 const handlePopState = (event) => { // Ignore extraneous popstate events in WebKit. if (isExtraneousPopstateEvent(event)) return // 獲取當(dāng)前地址欄的history state并傳遞給getDOMLocation // 返回一個(gè)新的location對(duì)象 handlePop(getDOMLocation(event.state)) } const handleHashChange = () => { // 監(jiān)聽(tīng)到hashchange時(shí)進(jìn)行的處理,由于hashchange不會(huì)更改state // 故此處不需要更新location的state handlePop(getDOMLocation(getHistoryState())) } // 用來(lái)判斷路由是否需要強(qiáng)制 let forceNextPop = false // handlePop是對(duì)使用go方法來(lái)回退或者前進(jìn)時(shí),對(duì)頁(yè)面進(jìn)行的更新,正常情況下來(lái)說(shuō)沒(méi)有問(wèn)題 // 但是如果頁(yè)面使用Prompt,即路由攔截器。當(dāng)點(diǎn)擊回退或者前進(jìn)就會(huì)觸發(fā)histrory的api,改變了地址欄的路徑 // 然后彈出需要用戶進(jìn)行確認(rèn)的提示框,如果用戶點(diǎn)擊確定,那么沒(méi)問(wèn)題因?yàn)榈刂窓诟淖兊牡刂肪褪菍⒁D(zhuǎn)到地址 // 但是如果用戶選擇了取消,那么地址欄的路徑已經(jīng)變成了新的地址,但是頁(yè)面實(shí)際還停留再之前,這就產(chǎn)生了bug // 這也就是 revertPop 這個(gè)hack的由來(lái)。因?yàn)轫?yè)面的跳轉(zhuǎn)可以由程序控制,但是如果操作的本身是瀏覽器的前進(jìn)后退 // 按鈕,那么是無(wú)法做到真正攔截的。 const handlePop = (location) => { if (forceNextPop) { forceNextPop = false setState() } else { const action = "POP" transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (ok) { setState({ action, location }) } else { // 當(dāng)攔截器返回了false的時(shí)候,需要把地址欄的路徑重置為當(dāng)前頁(yè)面顯示的地址 revertPop(location) } }) } } // 這里是react-router的作者最頭疼的一個(gè)地方,因?yàn)殡m然用hack實(shí)現(xiàn)了表面上的路由攔截 // ,但也會(huì)引起一些特殊情況下的bug。這里先說(shuō)一下如何做到的假裝攔截,因?yàn)楸旧韍tml5 history // api的特性,pushState 這些操作不會(huì)引起頁(yè)面的reload,所有做到攔截只需要不手懂調(diào)用setState頁(yè)面不進(jìn)行render即可 // 當(dāng)用戶選擇了取消后,再將地址欄中的路徑變?yōu)楫?dāng)前頁(yè)面的顯示路徑即可,這也是revertPop實(shí)現(xiàn)的方式 // 這里貼出一下對(duì)這個(gè)bug的討論:https://github.com/ReactTraining/history/issues/690 const revertPop = (fromLocation) => { // fromLocation 當(dāng)前地址欄真正的路徑,而且這個(gè)路徑一定是存在于history歷史 // 記錄當(dāng)中某個(gè)被訪問(wèn)過(guò)的路徑,因?yàn)槲覀冃枰獙⒌刂窓诘倪@個(gè)路徑重置為頁(yè)面正在顯示的路徑地址 // 頁(yè)面顯示的這個(gè)路徑地址一定是還再history.location中的那個(gè)地址 // fromLoaction 用戶原本想去但是后來(lái)又不去的那個(gè)地址,需要把他換位history.location當(dāng)中的那個(gè)地址 const toLocation = history.location // TODO: We could probably make this more reliable by // keeping a list of keys we"ve seen in sessionStorage. // Instead, we just default to 0 for keys we don"t know. // 取出toLocation地址再allKeys中的下標(biāo)位置 let toIndex = allKeys.indexOf(toLocation.key) if (toIndex === -1) toIndex = 0 // 取出formLoaction地址在allKeys中的下標(biāo)位置 let fromIndex = allKeys.indexOf(fromLocation.key) if (fromIndex === -1) fromIndex = 0 // 兩者進(jìn)行相減的值就是go操作需要回退或者前進(jìn)的次數(shù) const delta = toIndex - fromIndex // 如果delta不為0,則進(jìn)行地址欄的變更 將歷史記錄重定向到當(dāng)前頁(yè)面的路徑 if (delta) { forceNextPop = true // 將forceNextPop設(shè)置為true // 更改地址欄的路徑,又會(huì)觸發(fā)handlePop 方法,此時(shí)由于forceNextPop已經(jīng)為true則會(huì)執(zhí)行后面的 // setState方法,對(duì)當(dāng)前頁(yè)面進(jìn)行rerender,注意setState是沒(méi)有傳遞參數(shù)的,這樣history當(dāng)中的 // location對(duì)象依然是之前頁(yè)面存在的那個(gè)loaction,不會(huì)改變history的location數(shù)據(jù) go(delta) } } // 返回一個(gè)location初始對(duì)象包含 // pathname,search,hash,state,key key有可能是undefined const initialLocation = getDOMLocation(getHistoryState()) let allKeys = [ initialLocation.key ] // Public interface // 拼接上basename const createHref = (location) => basename + createPath(location) const push = (path, state) => { warning( !(typeof path === "object" && path.state !== undefined && state !== undefined), "You should avoid providing a 2nd state argument to push when the 1st " + "argument is a location-like object that already has state; it is ignored" ) const action = "PUSH" const location = createLocation(path, state, createKey(), history.location) transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (!ok) return const href = createHref(location) // 拼接basename const { key, state } = location if (canUseHistory) { globalHistory.pushState({ key, state }, null, href) // 只是改變地址欄路徑 此時(shí)頁(yè)面不會(huì)改變 if (forceRefresh) { window.location.href = href // 強(qiáng)制刷新 } else { const prevIndex = allKeys.indexOf(history.location.key) // 上次訪問(wèn)的路徑的key const nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1) nextKeys.push(location.key) // 維護(hù)一個(gè)訪問(wèn)過(guò)的路徑的key的列表 allKeys = nextKeys setState({ action, location }) // render頁(yè)面 } } else { warning( state === undefined, "Browser history cannot push state in browsers that do not support HTML5 history" ) window.location.href = href } }) } const replace = (path, state) => { warning( !(typeof path === "object" && path.state !== undefined && state !== undefined), "You should avoid providing a 2nd state argument to replace when the 1st " + "argument is a location-like object that already has state; it is ignored" ) const action = "REPLACE" const location = createLocation(path, state, createKey(), history.location) transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (!ok) return const href = createHref(location) const { key, state } = location if (canUseHistory) { globalHistory.replaceState({ key, state }, null, href) if (forceRefresh) { window.location.replace(href) } else { const prevIndex = allKeys.indexOf(history.location.key) if (prevIndex !== -1) allKeys[prevIndex] = location.key setState({ action, location }) } } else { warning( state === undefined, "Browser history cannot replace state in browsers that do not support HTML5 history" ) window.location.replace(href) } }) } const go = (n) => { globalHistory.go(n) } const goBack = () => go(-1) const goForward = () => go(1) let listenerCount = 0 // 防止重復(fù)注冊(cè)監(jiān)聽(tīng),只有l(wèi)istenerCount == 1的時(shí)候才會(huì)進(jìn)行監(jiān)聽(tīng)事件 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) } } // 默認(rèn)情況下不會(huì)阻止路由的跳轉(zhuǎn) let isBlocked = false // 這里的block方法專門(mén)為Prompt組件設(shè)計(jì),開(kāi)發(fā)者可以模擬對(duì)路由的攔截 const block = (prompt = false) => { // prompt 默認(rèn)為false, prompt可以為string或者func // 將攔截器的開(kāi)關(guān)打開(kāi),并返回可關(guān)閉攔截器的方法 const unblock = transitionManager.setPrompt(prompt) // 監(jiān)聽(tīng)事件只會(huì)當(dāng)攔截器開(kāi)啟時(shí)被注冊(cè),同時(shí)設(shè)置isBlock為true,防止多次注冊(cè) if (!isBlocked) { checkDOMListeners(1) isBlocked = true } // 返回關(guān)閉攔截器的方法 return () => { if (isBlocked) { isBlocked = false checkDOMListeners(-1) } return unblock() } } const listen = (listener) => { const unlisten = transitionManager.appendListener(listener) // 添加訂閱者 checkDOMListeners(1) // 監(jiān)聽(tīng)popState pushState 等事件 return () => { checkDOMListeners(-1) unlisten() } } const history = { length: globalHistory.length, action: "POP", location: initialLocation, createHref, push, replace, go, goBack, goForward, block, listen } return history }
由于篇幅過(guò)長(zhǎng),所以這里抽取push方法來(lái)梳理整套流程
const push = (path, state) => { // push可接收兩個(gè)參數(shù),第一個(gè)參數(shù)path可以是字符串,或者對(duì)象,第二個(gè)參數(shù)是state對(duì)象 // 里面是可以被瀏覽器緩存的數(shù)據(jù),當(dāng)path是一個(gè)對(duì)象并且path中的state存在,同時(shí)也傳遞了 // 第二個(gè)參數(shù)state,那么這里就會(huì)給出警告,表示path中的state參數(shù)將會(huì)被忽略 warning( !(typeof path === "object" && path.state !== undefined && state !== undefined), "You should avoid providing a 2nd state argument to push when the 1st " + "argument is a location-like object that already has state; it is ignored" ) const action = "PUSH" // 動(dòng)作為push操作 //將即將訪問(wèn)的路徑path, 被緩存的state,將要訪問(wèn)的路徑的隨機(jī)生成的6位隨機(jī)字符串, // 上次訪問(wèn)過(guò)的location對(duì)象也可以理解為當(dāng)前地址欄里路徑對(duì)象, // 返回一個(gè)對(duì)象包含 pathname,search,hash,state,key const location = createLocation(path, state, createKey(), history.location) // 路由的切換,最后一個(gè)參數(shù)為回調(diào)函數(shù),只有返回true的時(shí)候才會(huì)進(jìn)行路由的切換 transitionManager.confirmTransitionTo(location, action, getUserConfirmation, (ok) => { if (!ok) return const href = createHref(location) // 拼接basename const { key, state } = location // 獲取新的key和state if (canUseHistory) { // 當(dāng)可以使用history api時(shí)候,調(diào)用原生的pushState方法更改地址欄路徑 // 此時(shí)只是改變地址欄路徑 頁(yè)面并不會(huì)發(fā)生變化 需要手動(dòng)setState從而rerender // pushState的三個(gè)參數(shù)分別為,1.可以被緩存的state對(duì)象,即刷新瀏覽器依然會(huì)保留 // 2.頁(yè)面的title,可直接忽略 3.href即新的地址欄路徑,這是一個(gè)完整的路徑地址 globalHistory.pushState({ key, state }, null, href) if (forceRefresh) { window.location.href = href // 強(qiáng)制刷新 } else { // 獲取上次訪問(wèn)的路徑的key在記錄列表里的下標(biāo) const prevIndex = allKeys.indexOf(history.location.key) // 當(dāng)下標(biāo)存在時(shí),返回截取到當(dāng)前下標(biāo)的數(shù)組key列表的一個(gè)新引用,不存在則返回一個(gè)新的空數(shù)組 // 這樣做的原因是什么?為什么不每次訪問(wèn)直接向allKeys列表中直接push要訪問(wèn)的key // 比如這樣的一種場(chǎng)景, 1-2-3-4 的頁(yè)面訪問(wèn)順序,這時(shí)候使用go(-2) 回退到2的頁(yè)面,假如在2 // 的頁(yè)面我們選擇了push進(jìn)行跳轉(zhuǎn)到4頁(yè)面,如果只是簡(jiǎn)單的對(duì)allKeys進(jìn)行push操作那么順序就變成了 // 1-2-3-4-4,這時(shí)候就會(huì)產(chǎn)生一悖論,從4頁(yè)面跳轉(zhuǎn)4頁(yè)面,這種邏輯是不通的,所以每當(dāng)push或者replace // 發(fā)生的時(shí)候,一定是用當(dāng)前地址欄中path的key去截取allKeys中對(duì)應(yīng)的訪問(wèn)記錄,來(lái)保證不會(huì)push // 連續(xù)相同的頁(yè)面 const nextKeys = allKeys.slice(0, prevIndex === -1 ? 0 : prevIndex + 1) nextKeys.push(location.key) // 將新的key添加到allKeys中 allKeys = nextKeys // 替換 setState({ action, location }) // render頁(yè)面 } } else { warning( state === undefined, "Browser history cannot push state in browsers that do not support HTML5 history" ) window.location.href = href } }) }
createLocation的源碼
export const createLocation = (path, state, key, currentLocation) => { let location if (typeof path === "string") { // Two-arg form: push(path, state) // 分解pathname,path,hash,search等,parsePath返回一個(gè)對(duì)象 location = parsePath(path) location.state = state } else { // One-arg form: push(location) location = { ...path } if (location.pathname === undefined) location.pathname = "" if (location.search) { if (location.search.charAt(0) !== "?") location.search = "?" + location.search } else { location.search = "" } if (location.hash) { if (location.hash.charAt(0) !== "#") location.hash = "#" + location.hash } else { location.hash = "" } if (state !== undefined && location.state === undefined) location.state = state } // 嘗試對(duì)pathname進(jìn)行decodeURI解碼操作,失敗時(shí)進(jìn)行提示 try { location.pathname = decodeURI(location.pathname) } catch (e) { if (e instanceof URIError) { throw new URIError( "Pathname "" + location.pathname + "" could not be decoded. " + "This is likely caused by an invalid percent-encoding." ) } else { throw e } } if (key) location.key = key if (currentLocation) { // Resolve incomplete/relative pathname relative to current location. if (!location.pathname) { location.pathname = currentLocation.pathname } else if (location.pathname.charAt(0) !== "/") { location.pathname = resolvePathname(location.pathname, currentLocation.pathname) } } else { // When there is no prior location and pathname is empty, set it to / // pathname 不存在的時(shí)候返回當(dāng)前路徑的根節(jié)點(diǎn) if (!location.pathname) { location.pathname = "/" } } // 返回一個(gè)location對(duì)象包含 // pathname,search,hash,state,key return location }
createTransitionManager.js的源碼
import warning from "warning" const createTransitionManager = () => { // 這里使一個(gè)閉包環(huán)境,每次進(jìn)行路由切換的時(shí)候,都會(huì)先進(jìn)行對(duì)prompt的判斷 // 當(dāng)prompt != null 的時(shí)候,表示路由的上次切換被阻止了,那么當(dāng)用戶confirm返回true // 的時(shí)候會(huì)直接進(jìn)行地址欄的更新和subscribe的回調(diào) let prompt = null // 提示符 const setPrompt = (nextPrompt) => { // 提示prompt只能存在一個(gè) warning( prompt == null, "A history supports only one prompt at a time" ) prompt = nextPrompt // 同時(shí)將解除block的方法返回 return () => { if (prompt === nextPrompt) prompt = null } } // const confirmTransitionTo = (location, action, getUserConfirmation, callback) => { // TODO: If another transition starts while we"re still confirming // the previous one, we may end up in a weird state. Figure out the // best way to handle this. if (prompt != null) { // prompt 可以是一個(gè)函數(shù),如果是一個(gè)函數(shù)返回執(zhí)行的結(jié)果 const result = typeof prompt === "function" ? prompt(location, action) : prompt // 當(dāng)prompt為string類型時(shí) 基本上就是為了提示用戶即將要跳轉(zhuǎn)路由了,prompt就是提示信息 if (typeof result === "string") { // 調(diào)用window.confirm來(lái)顯示提示信息 if (typeof getUserConfirmation === "function") { // callback接收用戶 選擇了true或者false getUserConfirmation(result, callback) } else { // 提示開(kāi)發(fā)者 getUserConfirmatio應(yīng)該是一個(gè)function來(lái)展示阻止路由跳轉(zhuǎn)的提示 warning( false, "A history needs a getUserConfirmation function in order to use a prompt message" ) // 相當(dāng)于用戶選擇true 不進(jìn)行攔截 callback(true) } } else { // Return false from a transition hook to cancel the transition. callback(result !== false) } } else { // 當(dāng)不存在prompt時(shí),直接執(zhí)行回調(diào)函數(shù),進(jìn)行路由的切換和rerender callback(true) } } // 被subscribe的列表,即在Router組件添加的setState方法,每次push replace 或者 go等操作都會(huì)觸發(fā) let listeners = [] // 將回調(diào)函數(shù)添加到listeners,一個(gè)發(fā)布訂閱模式 const appendListener = (fn) => { let isActive = true // 這里有個(gè)奇怪的地方既然訂閱事件可以被解綁就直接被從數(shù)組中刪除掉了,為什么這里還需要這個(gè)isActive // 再加一次判斷呢,其實(shí)是為了避免一種情況,比如注冊(cè)了多個(gè)listeners: a,b,c 但是在a函數(shù)中注銷(xiāo)了b函數(shù) // 理論上來(lái)說(shuō)b函數(shù)應(yīng)該不能在執(zhí)行了,但是注銷(xiāo)方法里使用的是數(shù)組的filter,每次返回的是一個(gè)新的listeners引用, // 故每次解綁如果不添加isActive這個(gè)開(kāi)關(guān),那么當(dāng)前循環(huán)還是會(huì)執(zhí)行b的事件。加上isActive后,原始的liteners中 // 的閉包b函數(shù)的isActive會(huì)變?yōu)閒alse,從而阻止事件的執(zhí)行,當(dāng)循環(huán)結(jié)束后,原始的listeners也會(huì)被gc回收 const listener = (...args) => { if (isActive) fn(...args) } listeners.push(listener) return () => { isActive = false listeners = listeners.filter(item => item !== listener) } } // 通知被訂閱的事件開(kāi)始執(zhí)行 const notifyListeners = (...args) => { listeners.forEach(listener => listener(...args)) } return { setPrompt, confirmTransitionTo, appendListener, notifyListeners } } export default createTransitionManager
由于篇幅太長(zhǎng),自己都看的蒙圈了,現(xiàn)在就簡(jiǎn)單做一下總結(jié),描述router工作的原理。
1.首先BrowserRouter通過(guò)history庫(kù)使用createBrowserHistory方法創(chuàng)建了一個(gè)history對(duì)象,并將此對(duì)象作為props傳遞給了Router組件
2.Router組件使用history對(duì)的的listen方法,注冊(cè)了組件自身的setState事件,這樣一樣來(lái),只要觸發(fā)了html5的popstate事件,組件就會(huì)執(zhí)行setState事件,完成整個(gè)應(yīng)用的rerender
3.history是一個(gè)對(duì)象,里面包含了操作頁(yè)面跳轉(zhuǎn)的方法,以及當(dāng)前地址欄對(duì)象的location的信息。首先當(dāng)創(chuàng)建一個(gè)history對(duì)象時(shí)候,會(huì)使用props當(dāng)中的四個(gè)參數(shù)信息,forceRefresh、basename、getUserComfirmation、keyLength 來(lái)生成一個(gè)初始化的history對(duì)象,四個(gè)參數(shù)均不是必傳項(xiàng)。首先會(huì)使用window.location對(duì)象獲取當(dāng)前路徑下的pathname、search、hash等參數(shù),同時(shí)如果頁(yè)面是經(jīng)過(guò)rerolad刷新過(guò)的頁(yè)面,那么也會(huì)保存之前向state添加過(guò)數(shù)據(jù),這里除了我們自己添加的state,還有history這個(gè)庫(kù)自己每次做push或者repalce操作的時(shí)候隨機(jī)生成的六位長(zhǎng)度的字符串key
拿到這個(gè)初始化的location對(duì)象后,history開(kāi)始封裝push、replace、go等這些api。
以push為例,可以接收兩個(gè)參數(shù)push(path, state)----我們常用的寫(xiě)法是push("/user/list"),只需要傳遞一個(gè)路徑不帶參數(shù),或者push({pathname: "/user", state: {id: "xxx"}, search: "?name=xxx", hash: "#list"})傳遞一個(gè)對(duì)象。任何對(duì)地址欄的更新都會(huì)經(jīng)過(guò)confirmTransitionTo 這個(gè)方法進(jìn)行驗(yàn)證,這個(gè)方法是為了支持prompt攔截器的功能。正常在攔截器關(guān)閉的情況下,每次調(diào)用push或者replace都會(huì)隨機(jī)生成一個(gè)key,代表這個(gè)路徑的唯一hash值,并將用戶傳遞的state和key作為state,注意這部分state會(huì)被保存到 瀏覽器 中是一個(gè)長(zhǎng)效的緩存,將拼接好的path作為傳遞給history的第三個(gè)參數(shù),調(diào)用history.pushState(state, null, path),這樣地址欄的地址就得到了更新。
地址欄地址得到更新后,頁(yè)面在不使用foreceRefrsh的情況下是不會(huì)自動(dòng)更新的。此時(shí)需要循環(huán)執(zhí)行在創(chuàng)建history對(duì)象時(shí),在內(nèi)存中的一個(gè)listeners監(jiān)聽(tīng)隊(duì)列,即在步驟2中在Router組件內(nèi)部注冊(cè)的回調(diào),來(lái)手動(dòng)完成頁(yè)面的setState,至此一個(gè)完整的更新流程就算走完了。
在history里有一個(gè)block的方法,這個(gè)方法的初衷是為了實(shí)現(xiàn)對(duì)路由跳轉(zhuǎn)的攔截。我們知道瀏覽器的回退和前進(jìn)操作按鈕是無(wú)法進(jìn)行攔截的,只能做hack,這也是history庫(kù)的做法。抽離出了一個(gè)路徑控制器,方法名稱叫做createTransitionManager,可以理解為路由操作器。這個(gè)方法在內(nèi)部維護(hù)了一個(gè)prompt的攔截器開(kāi)關(guān),每當(dāng)這個(gè)開(kāi)關(guān)打開(kāi)的時(shí)候,所有的路由在跳轉(zhuǎn)前都會(huì)被window.confirm所攔截。注意此攔截并非真正的攔截,雖然頁(yè)面沒(méi)有改變,但是地址欄的路徑已經(jīng)改變了。如果用戶沒(méi)有取消攔截,那么頁(yè)面依然會(huì)停留在當(dāng)前頁(yè)面,這樣和地址欄的路徑就產(chǎn)生了悖論,所以需要將地址欄的路徑再重置為當(dāng)前頁(yè)面真正渲染的頁(yè)面。為了實(shí)現(xiàn)這一功能,不得不創(chuàng)建了一個(gè)用隨機(jī)key值的來(lái)表示的訪問(wèn)過(guò)的路徑表allKeys。每次頁(yè)面被攔截后,都需要在allKeys的列表中找到當(dāng)前路徑下的key的下標(biāo),以及實(shí)際頁(yè)面顯示的location的key的下標(biāo),后者減前者的值就是頁(yè)面要被回退或者前進(jìn)的次數(shù),調(diào)用go方法后會(huì)再次觸發(fā)popstate事件,造成頁(yè)面的rerender。
正式因?yàn)橛辛薖rompt組件才會(huì)使history不得不增加了key列表,prompt開(kāi)關(guān),導(dǎo)致代碼的復(fù)雜度成倍增加,同時(shí)很多開(kāi)發(fā)者在開(kāi)發(fā)中對(duì)此組件的濫用也導(dǎo)致了一些特殊的bug,并且這些bug都是無(wú)法解決的,這也是作者為什么想要在下個(gè)版本中移除此api的緣由。討論地址在鏈接描述
。下篇將會(huì)進(jìn)行對(duì)Route Switch Link等其他組件的講解
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/53901.html
摘要:還是先來(lái)一段官方的基礎(chǔ)使用案例,熟悉一下整體的代碼流程中使用了端常用到的等一些常用組件,作為的頂層組件來(lái)獲取的和設(shè)置回調(diào)函數(shù)來(lái)更新。 react-router是react官方推薦并參與維護(hù)的一個(gè)路由庫(kù),支持瀏覽器端、app端、服務(wù)端等常見(jiàn)場(chǎng)景下的路由切換功能,react-router本身不具備切換和跳轉(zhuǎn)路由的功能,這些功能全部由react-router依賴的history庫(kù)完成,his...
摘要:如果將添加到當(dāng)前組件,并且當(dāng)前組件由包裹,那么將引用最外層包裝組件的實(shí)例而并非我們期望的當(dāng)前組件,這也是在實(shí)際開(kāi)發(fā)中為什么不推薦使用的原因,使用一個(gè)回調(diào)函數(shù)是一個(gè)不錯(cuò)的選擇,也同樣的使用的是回調(diào)函數(shù)來(lái)實(shí)現(xiàn)的。 回顧:上一篇講了BrowserRouter 和 Router之前的關(guān)系,以及Router實(shí)現(xiàn)路由跳轉(zhuǎn)切換的原理。這一篇來(lái)簡(jiǎn)短介紹react-router剩余組件的源碼,結(jié)合官方文...
摘要:如果將添加到當(dāng)前組件,并且當(dāng)前組件由包裹,那么將引用最外層包裝組件的實(shí)例而并非我們期望的當(dāng)前組件,這也是在實(shí)際開(kāi)發(fā)中為什么不推薦使用的原因,使用一個(gè)回調(diào)函數(shù)是一個(gè)不錯(cuò)的選擇,也同樣的使用的是回調(diào)函數(shù)來(lái)實(shí)現(xiàn)的。 回顧:上一篇講了BrowserRouter 和 Router之前的關(guān)系,以及Router實(shí)現(xiàn)路由跳轉(zhuǎn)切換的原理。這一篇來(lái)簡(jiǎn)短介紹react-router剩余組件的源碼,結(jié)合官方文...
閱讀 662·2021-11-24 09:39
閱讀 2315·2021-11-22 13:54
閱讀 2197·2021-09-23 11:46
閱讀 3246·2019-08-30 15:55
閱讀 2679·2019-08-30 15:54
閱讀 2403·2019-08-30 14:18
閱讀 1546·2019-08-29 14:15
閱讀 2732·2019-08-29 13:49