源碼系列:html
這篇文章主要講的是分析 react-router 源碼,版本是 v5.x,以及 SPA 路由實現的原理。前端
單頁面應用都用到了路由 router,目前來看實現路由有兩種方法 hash 路由和 H5 History API 實現。react
而 react-router 路由,則是用到了 history 庫,該庫實際上是對 hash 路由、history 路由、memory 路由(客戶端)進行了封裝。git
下面先看看 hash 和 history 是怎樣實現路由的。github
hash 是 location 的屬性,在 URL 中是 #後面的部分。若是沒有#,則返回空字符串。redux
hash 路由主要實現原理是:hash 改變時,頁面不發生跳轉,即 window 對象能夠對 hash 改變進行監聽(hashchange 事件),只要 url 中 hash 發生改變,就會觸發回調。promise
利用這個特性,下面模擬實現一個 hash 路由。瀏覽器
實現步驟:react-router
代碼以下:app
hash.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>hash router</title>
</head>
<body>
<ul>
<li><a href="#/">turn yellow</a></li>
<li><a href="#/blue">turn blue</a></li>
<li><a href="#/green">turn green</a></li>
</ul>
<button id='back'>back</button>
<button id='forward'>forward</button>
<script src="./hash.js"></script>
</body>
</html>
複製代碼
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 是 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
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>h5 router</title>
</head>
<body>
<ul>
<li><a href="/">turn yellow</a></li>
<li><a href="/blue">turn blue</a></li>
<li><a href="/green">turn green</a></li>
</ul>
<script src='./history.js'></script>
</body>
</html>
複製代碼
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-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,它繼承 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 (
// <context.Provider value={value}>
// {this.props.children}
// </context.Provider>
// )
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 (
<context.Provider value={obj}> {renderEle} </context.Provider> ) 複製代碼
核心源碼以下:
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 (
// <context.Provider value={props}>
// {renderEle}
// </context.Provider>
// )
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 表示要定向到哪裏。
<Redirect from="home" to="dashboard" />
複製代碼
根據有沒傳屬性 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 組件的子組件必須是 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 實際是對 <a>
標籤進行了封裝。
點擊時會觸發如下:
e.preventDefault()
,因此頁面沒有發生跳轉。history.replace
),不然是往 state 堆棧中新增(history.push
),從而路由發生了改變。核心源碼以下:
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 用到了 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 與 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,從而渲染相應的組件。
下面是博客地址,以爲不錯點個Star,謝謝~