用技術改變生活,用生活完善技術。來自攜程(旅悅)一枚向全棧方向努力奔跑的前端工程師。
微信同步:wDxKn89
React-Router基本瞭解
對於React-Router是針對React定義的路由庫,用於將URL和component進行匹配。
React Router的簡單使用教程
React-Router原始碼分析
簡單前端路由的實現
<!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事件觸發時,進行回撥處理
//3:利用addEventListener來新增事件,注意第三個引數的用處
//4:bind的使用區別於apply/call的使用
-->
Router.prototype.init = function() {
window.addEventListener(`load`, this.refresh.bind(this), false);
window.addEventListener(`hashchange`, this.refresh.bind(this), false);
}
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>
複製程式碼
上面的路由系統主要由三部分組成
- Router.protopyte.init 用於頁面初始化(load)/頁面url變化 的事件註冊
- Router.protopyte.route 對路徑(address)和回撥函式(function)的註冊並存放於Router中,為load/hashchange使用
- Router.protopyte.refresh 針對不同的路徑(address)進行回撥的處理
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);
};
// 原物件(這裡可以是H5的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的升級處理。
其中的操作也是類似的。
- Router.init(historyModule) ==> Router.protopyte.init
- Router.listen(function()) ==> Router.protopyte.route
- Router.updateLocation ==> Router.protopyte.refresh
React-Router程式碼實現分析
由於React-Router版本之間的處理方式有些差別,所以就按最新版本來進行分析。
historyModule(history)的實現
//這裡針對react-router-dom中的BrowserRouter.js進行分析
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;
複製程式碼
追蹤一下history的實現
檔案路徑在原始碼中的history中index.ts
//定義一個介面
export interface History {
length: number;
action: Action;
location: Location;
push(path: Path, state?: LocationState): void;
push(location: LocationDescriptorObject): void;
replace(path: Path, state?: LocationState): void;
replace(location: LocationDescriptorObject): void;
go(n: number): void;
goBack(): void;
goForward(): void;
block(prompt?: boolean): UnregisterCallback;
listen(listener: LocationListener): UnregisterCallback;
createHref(location: LocationDescriptorObject): Href;
}
複製程式碼
除去interface這種型別,是不是對History中定義的屬性有點熟悉。window.history
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;
複製程式碼
上面需要有幾處需要注意的地方
- React-Router是利用React的Context進行元件間通訊的。childContextTypes/getChildContext
- 需要特別主要componentWillMount,也就是說在Router元件還未載入之前,listen已經被註冊。其實這一步和第一個例子中的init道理是類似的。
- 在componentWillUnmount中將方法進行登出,用於記憶體的釋放。
- 這裡提到了 ,其實就是 用於url和元件的匹配。
瞭解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: ` +
`"${nextTo.pathname}${nextTo.search}"`
);
return;
}
this.perform();
}
computeTo({ computedMatch, to }) {
if (computedMatch) {
if (typeof to === "string") {
return generatePath(to, computedMatch.params);
} else {
return {
...to,
pathname: generatePath(to.pathname, computedMatch.params)
};
}
}
return to;
}
//進行路由的匹配操作
perform() {
const { history } = this.context.router;
const { push } = this.props;
//Router中拿到需要跳轉的路徑,然後傳遞給history
const to = this.computeTo(this.props);
if (push) {
history.push(to);
} else {
history.replace(to);
}
}
render() {
return null;
}
}
export default Redirect;
複製程式碼
note :
- 針對h5的history來講,push/replace只是將url進行改變,但是不會觸發popstate事件
generatePath函式的處理
//該方法只是對路徑進行處理
/**
* Public API for generating a URL pathname from a pattern and parameters.
*/
const generatePath = (pattern = "/", params = {}) => {
if (pattern === "/") {
return pattern;
}
const generator = compileGenerator(pattern);
return generator(params);
};
複製程式碼
針對路徑進行頁面渲染處理
需要看一個Router的結構
//這裡的Router只是一個容器元件,用於從Redux/react中獲取資料,而真正的路徑/元件資訊存放在Route中
<Router>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/topics" component={Topics}/>
</Router>
複製程式碼
看一下Route對元件的處理
/**
* The public API for matching a single path and rendering.
*/
class Route extends React.Component {
//從Router中獲取資訊
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.object.isRequired,
route: PropTypes.object.isRequired,
staticContext: PropTypes.object
})
};
//自己定義了一套Contex用於子元件的使用
static childContextTypes = {
router: PropTypes.object.isRequired
};
//自己定義了一套Contex用於子元件的使用
getChildContext() {
return {
router: {
...this.context.router,
route: {
location: this.props.location || this.context.router.route.location,
match: this.state.match
}
}
};
}
state = {
match: this.computeMatch(this.props, this.context.router)// matching a URL pathname to a path pattern.如果不匹配,返回null,也就是找不到頁面資訊
};
render() {
const { match } = this.state;
const { children, component, render } = this.props;//從Router結構中獲取對應的處理方法
const { history, route, staticContext } = this.context.router;//從Context中獲取資料
const location = this.props.location || route.location;
const props = { match, location, history, staticContext };
//如果頁面匹配成功,進行createElement的渲染。在這裡就會呼叫component的render===>頁面重新整理 這是處理第一次頁面渲染
if (component) return match ? React.createElement(component, props) : null;
//這裡針對首頁已經被渲染,在進行路由處理的時候,根據props中的資訊,進行頁面的跳轉或者重新整理
if (render) return match ? render(props) : null;
return null;
}
}
export default Route;
複製程式碼
Buzzer
針對React-Router來講,其實就是對H5的History進行了一次封裝,使能夠識別將url的變化與componet渲染進行匹配。
- 根據BrowserRouter等不同的API針對H5的history的重構
- 結構的構建,同時對history屬性進行註冊。
- 在Router的componentWillMount中註冊history的事件回撥。
- 在Redirect中進行路徑的計算,呼叫history.push/history.replace等更新history資訊。
- Route中根據計算的匹配結果,進行頁面首次渲染/頁面更新渲染處理。