摘要:一般情況下,都是作為等其他子路由的上層路由,使用了,接收一個屬性,傳遞給消費子組件。創建對象,兼容老瀏覽器,其他和沒有大區別總結分為四個包,分別為,其中是瀏覽器相關,是相關,是核心也是共同部分,是一些配置相關。
這篇文章主要講的是分析 react-router 源碼,版本是 v5.x,以及 SPA 路由實現的原理。
文章首發地址
單頁面應用都用到了路由 router,目前來看實現路由有兩種方法 hash 路由和 H5 History API 實現。
而 react-router 路由,則是用到了 history 庫,該庫其實是對 hash 路由、history 路由、memory 路由(客戶端)進行了封裝。
下面先看看 hash 和 history 是怎樣實現路由的。
hash routerhash 是 location 的屬性,在 URL 中是 #后面的部分。如果沒有#,則返回空字符串。
hash 路由主要實現原理是:hash 改變時,頁面不發生跳轉,即 window 對象可以對 hash 改變進行監聽(hashchange 事件),只要 url 中 hash 發生改變,就會觸發回調。
利用這個特性,下面模擬實現一個 hash 路由。
實現步驟:
初始化一個類
記錄路由、history 歷史值
添加 hashchange 事件,hash 發生改變時,觸發回調
初始添加所有路由
模擬前進和回退功能
代碼如下:
hash.html
hash router
hash.js
class Routers { constructor() { this.routes = {} this.currentUrl = "" this.history = []; // 記錄 hash 歷史值 this.currentIndex = this.history.length - 1; // 默認指向 history 中最后一個 // 默認前進、后退 this.isBack = false; this.isForward = false; this.onHashChange = this.onHashChange.bind(this) this.backOff = this.backOff.bind(this) this.forward = this.forward.bind(this) window.addEventListener("load", this.onHashChange, false); window.addEventListener("hashchange", this.onHashChange, false); // hash 變化監聽事件,support >= IE8 } route(path, callback) { this.routes[path] = callback || function () { } } onHashChange() { // 既不是前進和后退,點擊 a 標簽時觸發 if (!this.isBack && !this.isForward) { this.currentUrl = location.hash.slice(1) || "/" this.history.push(this.currentUrl) this.currentIndex++ } this.routes[this.currentUrl]() this.isBack = false this.isForward = false } // 后退功能 backOff() { this.isBack = true this.currentIndex = this.currentIndex <= 0 ? 0 : this.currentIndex - 1 this.currentUrl = this.history[this.currentIndex] location.hash = `#${this.currentUrl}` } // 前進功能 forward() { this.isForward = true this.currentIndex = this.currentIndex >= this.history.length - 1 ? this.history.length - 1 : this.currentIndex + 1 this.currentUrl = this.history[this.currentIndex] location.hash = `#${this.currentUrl}` } } // 初始添加所有路由 window.Router = new Routers(); Router.route("/", function () { changeBgColor("yellow"); }); Router.route("/blue", function () { changeBgColor("blue"); }); Router.route("/green", function () { changeBgColor("green"); }); const content = document.querySelector("body"); const buttonBack = document.querySelector("#back"); const buttonForward = document.querySelector("#forward") function changeBgColor(color) { content.style.backgroundColor = color; } // 模擬前進和回退 buttonBack.addEventListener("click", Router.backOff, false) buttonForward.addEventListener("click", Router.forward, false)
hash 路由兼容性好,缺點就是實現一套路由比較復雜些。
在線預覽
history routerhistory 是 HTML5 新增的 API,允許操作瀏覽器的曾經在標簽頁或者框架里訪問的會話歷史記錄。
history 包含的屬性和方法:
history.state 讀取歷史堆棧中最上面的值
history.length 瀏覽歷史中元素的數量
window.history.replaceState(obj, title, url) 取代當前瀏覽歷史中當前記錄
window.history.pushState(obj, title, url) 往瀏覽歷史中添加歷史記錄,不跳轉頁面
window.history.popstate(callback) 對歷史記錄發生變化進行監聽,state 發生改變時,觸發回調事件
window.history.back() 后退,等價于瀏覽器返回按鈕
window.history.forward() 前進,等價于瀏覽器前進按鈕
window.history.go(num) 前進或后退幾個頁面,num 為負數時,后退幾個頁面
實現步驟:
初始化一個類
記錄路由
添加 popstate 事件,state 發生改變時,觸發回調
初始添加所有路由
代碼如下:
history.html
h5 router
history.js
class Routers { constructor() { this.routes = {}; this._bindPopState(); } route(path, callback) { this.routes[path] = callback || function () { }; } go(path) { history.pushState({ path: path }, null, path); this.routes[path] && this.routes[path](); } _bindPopState() { window.addEventListener("popstate", e => { // 監聽 history.state 改變 const path = e.state && e.state.path; this.routes[path] && this.routes[path](); }); } } // 初始時添加所有路由 window.Router = new Routers(); Router.route("/", function () { changeBgColor("yellow"); }); Router.route("/blue", function () { changeBgColor("blue"); }); Router.route("/green", function () { changeBgColor("green"); }); const content = document.querySelector("body"); const ul = document.querySelector("ul"); function changeBgColor(color) { content.style.backgroundColor = color; } ul.addEventListener("click", e => { if (e.target.tagName === "A") { debugger e.preventDefault(); Router.go(e.target.getAttribute("href")); } });
兼容性:support >= IE10
在線預覽
react-routerreact-router 分為四個包,分別為 react-router、react-router-dom、react-router-config、react-router-native,其中 react-router-dom 是瀏覽器相關 API,react-router-native 是 React-Native 相關 API,react-router 是核心也是共同部分 API,react-router-config 是一些配置相關。
react-router 是 React指定路由,內部 API 的實現也是繼承 React 一些屬性和方法,所以 react-router 內 API 也是 React 組件。
react-router 還用到了 history 庫,這個庫主要是對 hash 路由、history 路由、memory 路由的封裝。
下面我們講到的是 react-router v5.x 版本,用到了 context,context 減少了組件之間顯性傳遞 props,具體用法可以看看官方文檔。
創建 context:
var createNamedContext = function createNamedContext(name) { var context = createContext(); context.displayName = name; return context; }; var context = createNamedContext("Router");Router
定義一個類 Router,它繼承 React.Component 屬性和方法,所以 Router 也是 React 組件。
_inheritsLoose(Router, _React$Component); // 等價于 Router.prototype = Object.create(React.Component)
在 Router 原型對象上添加 React 生命周期 componentDidMount、componentWillUnmount、render 方法。
一般情況下,Router 都是作為 Route 等其他子路由的上層路由,使用了 context.Provider,接收一個 value 屬性,傳遞 value 給消費子組件。
var _proto = Router.prototype; _proto.componentDidMount = function componentDidMount() { // todo }; _proto.componentWillUnmount = function componentWillUnmount(){ // todo }; _proto.render = function render() { return React.createElement(context.Provider, props); };
history 庫中有個方法 history.listen(callback(location)) 對 location 進行監聽,只要 location 發生變化了,就會 setState 更新 location,消費的子組件也可以拿到更新后的 location,從而渲染相應的組件。
核心源碼如下:
var Router = function (_React$Component) { // Router 從 React.Component 原型上的繼承屬性和方法 _inheritsLoose(Router, _React$Component); Router.computeRootMatch = function computeRootMatch(pathname) { return { path: "/", url: "/", params: {}, isExact: pathname === "/" }; }; function Router(props) { // 首先定義一個類 Router,也是 React 組件 var _this; _this = _React$Component.call(this, props) || this; // 繼承自身屬性和方法 _this.state = { location: props.history.location }; _this._isMounted = false; _this._pendingLocation = null; if (!props.staticContext) { // 如果不是 staticRouter,則對 location 進行監聽 _this.unlisten = props.history.listen((location) => { // 監聽 history.location 變化,如果有變化,則更新 locaiton if (_this._isMounted) { _this.setState({ location: location }); } else { _this._pendingLocation = location; } }); } return _this; } var _proto = Router.prototype; // 組件需要有生命周期,在原型對象上添加 componentDidMount、componentWillUnmount、render 方法 _proto.componentDidMount = function componentDidMount() { this._isMounted = true; if (this._pendingLocation) { this.setState({ location: this._pendingLocation }); } }; _proto.componentWillUnmount = function componentWillUnmount() { if (this.unlisten) this.unlisten(); // 停止監聽 location }; _proto.render = function render() { // 使用了 React Context 傳遞 history、location、match、staticContext,使得所有子組件都可以獲取這些屬性和方法 // const value = { // history: this.props.history, // location: this.state.location, // match: Router.computeRootMatch(this.state.location.pathname), // staticContext: this.props.staticContext // } // return ( //Route// {this.props.children} // // ) return React.createElement(context.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 // staticContext 是 staticRouter 中的 API,不是公用 API } }); }; return Router; }(React.Component);
Route 一般作為 Router 的子組件,主要是匹配 path 和渲染相應的組件。
Route 使用了 context.Consumer(消費組件),訂閱了 Router 提供的 context,一旦 location 發生改變,context 也會改變,判斷當前 location.pathname 是否與子組件的 path 是否匹配,如果匹配,則渲染對應組件,否則就不渲染。
Route 因為有些庫傳遞組件方式不同,所以有多種渲染,部分代碼如下:
const {children, render, component} = this.props let renderEle = null; // 如果有 children,則渲染 children if (children && !isEmptyChildren(children)) renderEle = children // 如果組件傳遞的是 render 及 match 是匹配的,則渲染 render if (render && match) renderEle = render(props) // 如果組件傳遞 component 及 match 是匹配的,則渲染 component if (component && match) renderEle = React.createElement(component, props) return ({renderEle} )
核心源碼如下:
var Route = function (_React$Component) { _inheritsLoose(Route, _React$Component); function Route() { return _React$Component.apply(this, arguments) || this; } var _proto = Route.prototype; _proto.render = function render() { var _this = this; // context.Consumer 每個 Route 組件都可以消費 Router 中 Provider 提供的 context return React.createElement(context.Consumer, null, function (context$$1) { var location = _this.props.location || context$$1.location; var match = _this.props.computedMatch ? _this.props.computedMatch : _this.props.path ? matchPath(location.pathname, _this.props) : context$$1.match; // 是否匹配當前路徑 var props = _extends({}, context$$1, { // 處理用 context 傳遞的還是自己傳遞的 location 和 match location: location, match: match }); var _this$props = _this.props, children = _this$props.children, component = _this$props.component, render = _this$props.render; if (Array.isArray(children) && children.length === 0) { children = null; } // let renderEle = null // 如果有 children,則渲染 children // if (children && !isEmptyChildren(children)) renderEle = children // 如果組件傳遞的是 render 及 match 是匹配的,則渲染 render // if (render && props.match) renderEle = render(props) // 如果組件傳遞 component 及 match 是匹配的,則渲染 component // if (component && props.match) renderEle = React.createElement(component, props) // return ( //Redirect// {renderEle} // // ) return React.createElement(context.Provider, { // Route 內定義一個 Provider,給 children 傳遞 props value: props }, children && !isEmptyChildren(children) ? children : props.match ? component ? React.createElement(component, props) : render ? render(props) : null : null); }); }; return Route; }(React.Component);
重定向路由:from 是從哪個組件來,to 表示要定向到哪里。
根據有沒傳屬性 push,有傳則是往 state 堆棧中新增(history.push),否則就是替代(history.replace)當前 state。
Redirect 使用了 context.Consumer(消費組件),訂閱了 Router 提供的 context,一旦 location 發生改變,context 也會改變,則也會觸發重定向。
源碼如下:
function Redirect(_ref) { var computedMatch = _ref.computedMatch, to = _ref.to, _ref$push = _ref.push, push = _ref$push === void 0 ? false : _ref$push; return React.createElement(context.Consumer, null, (context$$1) => { // context.Consumer 第三個參數是一個函數 var history = context$$1.history, staticContext = context$$1.staticContext; // method 方法:判斷是否是替換(replace)當前的 state,還是往 state 堆棧中添加(push)一個新的 state var method = push ? history.push : history.replace; // 生成新的 location var location = createLocation(computedMatch ? typeof to === "string" ? generatePath(to, computedMatch.params) : _extends({}, to, { pathname: generatePath(to.pathname, computedMatch.params) }) : to); if (staticContext) { // 當渲染一個靜態的 context 時(staticRouter),立即設置新 location method(location); return null; } // Lifecycle 是一個 return null 的空組件,但定義了 componentDidMount、componentDidUpdate、componentWillUnmount 生命周期 return React.createElement(Lifecycle, { onMount: function onMount() { method(location); }, onUpdate: function onUpdate(self, prevProps) { var prevLocation = createLocation(prevProps.to); // 觸發更新時,對比前后 location 是否相等,不相等,則更新 location if (!locationsAreEqual(prevLocation, _extends({}, location, { key: prevLocation.key }))) { method(location); } }, to: to }); }); }Switch
Switch 組件的子組件必須是 Route 或 Redirect 組件。
Switch 使用了 context.Consumer(消費組件),訂閱了 Router 提供的 context,一旦 location 發生改變,context 也會改變,判斷當前 location.pathname 是否與子組件的 path 是否匹配,如果匹配,則渲染對應的子組件,其他都不渲染。
源碼如下:
var Switch = function (_React$Component) { _inheritsLoose(Switch, _React$Component); function Switch() { return _React$Component.apply(this, arguments) || this; } var _proto = Switch.prototype; _proto.render = function render() { var _this = this; return React.createElement(context.Consumer, null, function (context$$1) { var location = _this.props.location || context$$1.location; var element, match; React.Children.forEach(_this.props.children, function (child) { if (match == null && React.isValidElement(child)) { element = child; var path = child.props.path || child.props.from; match = path ? matchPath(location.pathname, _extends({}, child.props, { path: path })) : context$$1.match; } }); return match ? React.cloneElement(element, { location: location, computedMatch: match // 加強版的 match }) : null; }); }; return Switch; }(React.Component);Link
Link 組件作用是跳轉到指定某個路由。Link 實際是對 標簽進行了封裝。
點擊時會觸發以下:
改變 url,但使用了 e.preventDefault(),所以頁面沒有發生跳轉。
根據是否傳遞屬性 replace,有傳就是替代當前 state(history.replace),否則是往 state 堆棧中新增(history.push),從而路由發生了改變。
路由發生了改變,由于 Router 中有對 location 進行監聽,從而通過 context 傳遞給消費子組件,匹配 path 是否相同,渲染相應的組件。
核心源碼如下:
function LinkAnchor(_ref) { var innerRef = _ref.innerRef, navigate = _ref.navigate, _onClick = _ref.onClick, rest = _objectWithoutPropertiesLoose(_ref, ["innerRef", "navigate", "onClick"]); // 剩余屬性 var target = rest.target; return React.createElement("a", _extends({}, rest, { // a 標簽 ref: innerRef , onClick: function onClick(event) { try { if (_onClick) _onClick(event); } catch (ex) { event.preventDefault(); // 使用 e.preventDefault() 防止跳轉 throw ex; } if (!event.defaultPrevented && // onClick prevented default event.button === 0 && ( // ignore everything but left clicks !target || target === "_self") && // let browser handle "target=_blank" etc. !isModifiedEvent(event) // ignore clicks with modifier keys ) { event.preventDefault(); navigate(); // 改變 location } } })); } function Link(_ref2) { var _ref2$component = _ref2.component, component = _ref2$component === void 0 ? LinkAnchor : _ref2$component, replace = _ref2.replace, to = _ref2.to, // to 跳轉鏈接的路徑 rest = _objectWithoutPropertiesLoose(_ref2, ["component", "replace", "to"]); return React.createElement(__RouterContext.Consumer, null, function (context) { var history = context.history; // 根據 to 生成新的 location var location = normalizeToLocation(resolveToLocation(to, context.location), context.location); var href = location ? history.createHref(location) : ""; return React.createElement(component, _extends({}, rest, { href: href, navigate: function navigate() { var location = resolveToLocation(to, context.location); var method = replace ? history.replace : history.push; method(location); // 如果有傳 replace,則是替換掉當前 location,否則往 history 堆棧中添加一個 location } })); }); }BrowserRouter
BrowserRouter 用到了 H5 history API,所以可以使用 pushState、replaceState 等方法。
源碼中主要是用到了 history 庫 createBrowserHistory 方法創建了封裝過 history 對象。
把封裝過的 history 對象傳遞給 Router 的 props。
var BrowserRouter = function (_React$Component) { _inheritsLoose(BrowserRouter, _React$Component); // BrowserRouter 繼承 React.Component 屬性和方法 function BrowserRouter() { var _this; for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } _this = _React$Component.call.apply(_React$Component, [this].concat(args)) || this; // 繼承自身屬性和方法 _this.history = createBrowserHistory(_this.props); // 創建 browser history 對象,是支持 HTML 5 的 history API return _this; } var _proto = BrowserRouter.prototype; _proto.render = function render() { return React.createElement(Router, { // 以 Router 為 element,history 和 children 作為 Router 的 props history: this.history, children: this.props.children }); }; return BrowserRouter; }(React.Component);HashRouter
HashRouter 與 BrowserRouter 的區別,主要是創建是以 window.location.hash 為對象,返回一個 history,主要是考慮到兼容性問題。
var HashRouter = function (_React$Component) { _inheritsLoose(HashRouter, _React$Component); function HashRouter() { var _this; for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } _this = _React$Component.call.apply(_React$Component, [this].concat(args)) || this; _this.history = createHashHistory(_this.props); // 創建 hash history 對象,兼容老瀏覽器 hash,其他和 BrowserRouter 沒有大區別 return _this; } var _proto = HashRouter.prototype; _proto.render = function render() { return React.createElement(Router, { history: this.history, children: this.props.children }); }; return HashRouter; }(React.Component);總結
react-router 分為四個包,分別為 react-router、react-router-dom、react-router-config、react-router-native,其中 react-router-dom 是瀏覽器相關 API,react-router-native 是 React-Native 相關 API,react-router 是核心也是共同部分 API,react-router-config 是一些配置相關。
react-router 是 React指定路由,內部 API 的實現也是繼承 React 一些屬性和方法,所以說 react-router 內 API 也是 React 組件。
react-router 還用到了 history 庫,這個庫主要是對 hash 路由、history 路由、memory 路由的封裝。
Router 都是作為 Route 等其他子路由的上層路由,使用了 context.Provider,接收一個 value 屬性,傳遞 value 給消費子組件。
history 庫中有個方法 history.listen(callback(location)) 對 location 進行監聽,點擊某個 Link 組件,改變了 location,只要 location 發生變化了,通過 context 傳遞改變后的 location,消費的子組件拿到更新后的 location,從而渲染相應的組件。
參考react-router github
history github
尋找海藍96【你了解前端路由嗎?】
History MDN
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105542.html
摘要:通過前端路由可以實現單頁應用本文首先從前端路由的原理出發,詳細介紹了前端路由原理的變遷。接著從的源碼出發,深入理解是如何實現前端路由的。執行上述的賦值后,頁面的發生改變。 ??react-router等前端路由的原理大致相同,可以實現無刷新的條件下切換顯示不同的頁面。路由的本質就是頁面的URL發生改變時,頁面的顯示結果可以根據URL的變化而變化,但是頁面不會刷新。通過前端路由可以實現...
摘要:相關配置請參考中文文檔。關于的更多使用方法及理解需要詳細具體講解,涉及篇幅較大,本文暫不涉及,有興趣可以看文檔中文文檔,我會整理后再單獨章節分享接下來我們將編寫路由組件這與有一些差別,原來的方法已經不再使用,在中或組件從中引入。 ??????相信很多剛入坑React的小伙伴們有一個同樣的疑惑,由于React相關庫不斷的再進行版本迭代,網上很多以前的技術分享變得不再適用。比如react-...
摘要:更多相關介紹請看這特點僅僅只是虛擬最大限度減少與的交互類似于使用操作單向數據流很大程度減少了重復代碼的使用組件化可組合一個組件易于和其它組件一起使用,或者嵌套在另一個組件內部。在使用后,就變得很容易維護,而且數據流非常清晰,容易解決遇到的。 歡迎移步我的博客閱讀:《React 入門實踐》 在寫這篇文章之前,我已經接觸 React 有大半年了。在初步學習 React 之后就正式應用到項...
閱讀 1760·2023-04-26 00:20
閱讀 1804·2021-11-08 13:21
閱讀 1930·2021-09-10 10:51
閱讀 1557·2021-09-10 10:50
閱讀 3249·2019-08-30 15:54
閱讀 2131·2019-08-30 14:22
閱讀 1429·2019-08-29 16:10
閱讀 3089·2019-08-26 11:50