react-router原理
在一個web應用中,路由系統是不可或缺的一部分。許多前端框架都會有著不同的配套的路由系統,最近也開始接觸react,瞭解了一點react-router的實現原理。
hash路由
早期的前端路由是通過hash來實現的。改變url的hash值並不會重新整理頁面,因此就可以通過hash來實現前端路由。在當前的頁面中可以通過window.location.hash
=‘test’,來改變當前頁面的hash值,執行後頁面的url會發生改變,但頁面並不會重新整理。
賦值前:http://localhost:8080 賦值後:http://localhost:8080/#test。
然後我們可以通過一個名為hashchange的事件來監聽頁面hash的變化。
window.addEventListener('hashchange',function(event){
console.log(event);
})
有了這個監聽事件我們就可以在監聽事件的回撥函式中渲染新的UI,這樣就實現了一個簡單的路由功能。
<!DOCTYPE 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>Document</title>
</head>
<body>
<a href="#/a">pageALink</a>
<a href="#/b">pageBLink</a>
<span id='body'></span>
<script>
window.addEventListener ('hashchange',(e)=>{
document.getElementById('body').innerHTML = window.location
},false)
</script>
</body>
</html>
history路由
history是H5的介面,允許我們操作瀏覽器會話歷史記錄。history提供了一些屬性和方法,主要包括:
屬性:
- history.length:返回值為會話歷史中有多少條記錄,包含當前的會話頁面。
- history.state:儲存了觸發popState事件的方法所傳遞的屬性物件。
方法:
- History.back(): 返回瀏覽器會話歷史中的上一頁,跟瀏覽器的回退按鈕功能相同
- History.forward():指向瀏覽器會話歷史中的下一頁,跟瀏覽器的前進按鈕相同。
- History.go(): 可以跳轉到瀏覽器會話歷史中的指定的某一個記錄頁。
- History.pushState():pushState可以將給定的資料壓入到瀏覽器會話歷史棧中,該方法接收3個引數,物件,title和一串url。pushState後會改變當前頁面url,但是不會伴隨著重新整理。
- History.replaceState():replaceState將當前的會話頁面的url替換成指定的資料,replaceState後也會改變當前頁面的url,但是也不會重新整理頁面。
雖然pushState和replaceState都會改變當前頁面的url,並且不會重新整理頁面,但pushState是push一條新的記錄到瀏覽器的會話歷史棧中,會使history.length+1,而replaceState是替換當前這條會話歷史記錄,history.lenth並不會增加。
每次但我們觸發history.back(), History.forward()或點選瀏覽器的前進後退按鈕時,都會觸發popstate事件。
所以如果使用history做為路由基礎,就需要使用history.pushState和history.replaceState來改變url的值而不重新整理頁面,然後通過popstate事件執行頁面的前進與後退
<!DOCTYPE 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>Document</title>
</head>
<body>
<a onClick="go('/a')">pageALink</a>
<a onClick="go('/b')">pageBLink</a>
<span id='body'></span>
<script>
function go (pathname){
window.history.pushState({},null,pathname);
document.getElementById('body').innerHTML = window.location;
}
//這裡主要處理瀏覽器前進後退功能,不加下面的程式碼就無法實現前進後退功能
window.addEventListener('popstate',(e)=>{
let pathname = window.location;
document.getElementById('body').innerHTML = window.location;
})
</script>
</body>
</html>
react-router的基礎——history
History
history是一個第三方js庫,借鑑HTML5 history物件的理念,在其基礎上又擴充套件了一些功能,用來管理歷史記錄,可以相容不同的瀏覽器和不同的環境,根據不同的環境提供了三種不同的API。
- HashHistory:針對老版本的瀏覽器,主要通過Hash實現。
- BrowserHistory:針對較高版本的瀏覽器,主要通過H5的History實現。
- MemoryHistory:主要通過記憶體中的歷史記錄實現。
History支援釋出/訂閱功能,當history發生改變的時候,可以自動觸發訂閱的函式,再對比一些兩者api的異同。以下是history庫的:
const history = {
length, // 屬性,history中記錄的state的數量
action, // 屬性,當前導航的action型別
location, // 屬性,location物件,封裝了pathname、search和hash等屬性
push, // 方法,導航到新的路由,並記錄在history中
replace, // 方法,替換掉當前記錄在history中的路由資訊
go, // 方法,前進或後退n個記錄
goBack, // 方法,後退
goForward, // 方法,前進
canGo, // 方法,是否能前進或後退n個記錄
block, // 方法,跳轉前讓使用者確定是否要跳轉
listen // 方法,訂閱history變更事件
};
以下是HTML5 history物件的:
const history = {
length, // 屬性,history中記錄的state的數量
state, // 屬性,pushState和replaceState時傳入的物件
back, // 方法,後退
forward, // 方法,前進
go, // 方法,前進或後退n個記錄
pushState, // 方法,導航到新的路由,並記錄在history中
replaceState // 方法,替換掉當前記錄在history中的路由資訊
}
// 訂閱history變更事件
window.onpopstate = function (event) {
...
}
從對比中可以看出,兩者的關係是非常密切的,history庫可以說是history物件的超集,是功能更強大的history物件。
簡單前端路由的實現
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>router</title>
</head>
<body>
<ul>
<li><a href="#/">turn white</a></li>
<li><a href="#/blue">turn blue</a></li>
<li><a href="#/green">turn green</a></li>
</ul>
<script>
function Router() {
this.routes = {};
this.currentUrl = '';
}
<!--
//針對不同的地址進行回撥的匹配
//1:使用者在呼叫Router.route('address',function),在this.routes物件中進行記錄或者說address與function的匹配
-->
Router.prototype.route = function(path, callback) {
this.routes[path] = callback || function(){};
};
<!--
//處理hash的變化,針對不同的值,進行頁面的處理
//1:在init中註冊過事件,在頁面load的時候,進行頁面的處理
//2:在hashchange變化時,進行頁面的處理
-->
Router.prototype.refresh = function() {
this.currentUrl = location.hash.slice(1) || '/';
this.routes[this.currentUrl]();
};
<!--
//1:在Router的prototype中定義init
//2:在頁面load/hashchange事件觸發時,進行回撥處理
-->
Router.prototype.init = function() {
window.addEventListener('load', this.refresh.bind(this));
window.addEventListener('hashchange', this.refresh.bind(this));
}
window.Router = new Router();//在window物件中構建一個Router物件
window.Router.init();//頁面初始化處理
var content = document.querySelector('body');
// change Page anything
function changeBgColor(color) {
content.style.backgroundColor = color;
}
Router.route('/', function() {
changeBgColor('white');
});
Router.route('/blue', function() {
changeBgColor('blue');
});
Router.route('/green', function() {
changeBgColor('green');
});
</script>
</body>
</html>
React-Router簡單實現
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>包裝方式</title>
</head>
<body>
<script>
var body = document.querySelector('body'),
newNode = null,
append = function(str){
newNode = document.createElement("p");
newNode.innerHTML = str;
body.appendChild(newNode);
};
// history庫
var historyModule = {
listener: [],
listen: function (listener) {
this.listener.push(listener);
append('historyModule listen.')
},
updateLocation: function(){
append('historyModule updateLocation tirgger.');
this.listener.forEach(function(listener){
listener('new localtion');
})
}
}
// Router 將使用 historyModule 物件,並對其包裝
var Router = {
source: {},
//複製historyModule到Router中
init: function(source){
this.source = source;
},
//處理監聽事件,在Router對頁面進行處理時,利用historyModule中處理頁面
listen: function(listener) {
append('Router listen.');
// 對 historyModule的listen進行了一層包裝
return this.source.listen(function(location){
append('Router listen tirgger.');
listener(location);
})
}
}
// 將 historyModule 注入進 Router 中
Router.init(historyModule);
// Router 註冊監聽
Router.listen(function(location){
append(location + '-> Router setState.');
})
// historyModule 觸發監聽回撥(對頁面進行渲染等處理)
historyModule.updateLocation();
</script>
</body>
</html>
其實上訴的操作就是隻是針對前端簡單路由+historyModule的升級處理。
react-router分析
BrowserRouter
用BrowserRouter 元件包裹整個App系統後,就是通過html5的history來實現無重新整理條件下的前端路由。
與BrowserRouter對應的是HashRouter,HashRouter使用url中的hash屬性來保證不重新重新整理的情況下同時渲染頁面。
import warning from "warning";
import React from "react";
import PropTypes from "prop-types";
import { createBrowserHistory as createHistory } from "history";//這裡的history就是上面第二個例子中的historyModule
import Router from "./Router"; //對應第二個例子中的Router物件
/**
* The public API for a <Router> that uses HTML5 history. //這裡是重點
*/
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default BrowserRouter;
listen函式的註冊
React-Router/Router.js
/**
* The public API for putting history on context. //這裡的道理類似於例子二中第二步
*/
class Router extends React.Component {
static childContextTypes = {
router: PropTypes.object.isRequired
};
getChildContext() {
return {
router: {
...this.context.router,
history: this.props.history,
route: {
location: this.props.history.location,
match: this.state.match
}
}
};
}
state = {
match: this.computeMatch(this.props.history.location.pathname)
};
computeMatch(pathname) {
return {
path: "/",
url: "/",
params: {},
isExact: pathname === "/"
};
}
componentWillMount() {
const { children, history } = this.props;
// Do this here so we can setState when a <Redirect> changes the
// location in componentWillMount. This happens e.g. when doing
// server rendering using a <StaticRouter>.
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
});
});
}
componentWillReceiveProps(nextProps) {
warning(
this.props.history === nextProps.history,
"You cannot change <Router history>"
);
}
componentWillUnmount() {
this.unlisten();
}
render() {
const { children } = this.props;
return children ? React.Children.only(children) : null;
}
}
export default Router;
Redirect.js
react-router/Redirect.js
//這裡省去其他庫的引用
import generatePath from "./generatePath";
/**
* The public API for updating the location programmatically
* with a component.
*/
class Redirect extends React.Component {
//這裡是從Context中拿到history等資料
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.shape({
push: PropTypes.func.isRequired,
replace: PropTypes.func.isRequired
}).isRequired,
staticContext: PropTypes.object
}).isRequired
};
isStatic() {
return this.context.router && this.context.router.staticContext;
}
componentWillMount() {
invariant(
this.context.router,
"You should not use <Redirect> outside a <Router>"
);
if (this.isStatic()) this.perform();
}
componentDidMount() {
if (!this.isStatic()) this.perform();
}
componentDidUpdate(prevProps) {
const prevTo = createLocation(prevProps.to);
const nextTo = createLocation(this.props.to);
if (locationsAreEqual(prevTo, nextTo)) {
warning(
false,
`You tried to redirect to the same route you're currently on: ` +
`"$