router做爲當前盛行的單頁面應用必不可少的部分,今天咱們就以React-Router V4爲例,來解開他的神祕面紗。本文並不專一於講解 Reacr-Router V4 的基礎概念,能夠前往官方文檔瞭解更多基礎知識java
本文以RRV4代指Reacr-Router V4react
RRV4依賴的history庫git
Q1. 爲何咱們有時看到的寫法是這樣的github
import {
Switch,
Route, Router,
BrowserRouter, Link
} from 'react-router-dom';
複製代碼
或是這樣的?web
import {Switch, Route, Router} from 'react-router';
import {BrowserRouter, Link} from 'react-router-dom';
複製代碼
react-router-dom和react-router有什麼關係和區別?數組
Q2. 爲何v4版本中支持div等標籤的嵌套了?緩存
Q3. Route 會在當前 url 與 path 屬性值相符的時候渲染相關組件,他是如何作到的呢?bash
Q4. 爲何用Link組件,而不是a標籤?服務器
進入RR V4以前,先想一想路由的做用,路由的做用就是同步url與其對應的回調函數。通常基於history,經過history.pushstate和replacestate方法修改url,經過window.addEventListener('popstate', callback) 來監聽前進後退,對於hash路由,經過window.location.hash修改hash,經過window.addEventListener('hashchange', callback) 監聽變化react-router
爲了便於理解原理, 先來一段關於RRV4的簡單代碼
<BrowserRouter>
<div>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
</ul>
<hr/>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</div>
</BrowserRouter>
複製代碼
在看看Route的子組件的props
{
match: {
path: "/", // 用來匹配的 path
url: "/", // 當前的 URL
params: {}, // 路徑中的參數
isExact:true // 是否爲嚴格匹配 pathname === "/"
},
location: {
hash: "" // hash
key: "nyi4ea" // 惟一的key
pathname: "/" // URL 中路徑部分
search: "" // URL 參數
state: undefined // 路由跳轉時傳遞的參數 state
}
history: {...} // history庫提供
staticContext: undefined // 用於服務端渲染
}
複製代碼
咱們帶着問題去看源碼,發現react-router-dom基於react-router,Router, Route, Switch等都是引用的react-router,而且加入了Link,BrowserRouter,HashRouter組件,這裏解釋了Q1,react-router負責通用的路由管理, react-router-dom負責web,固然還有react-router-native負責rn的管理,咱們從BrowserRouter開始看
rrv4的做者提倡Just Components 概念,BrowserRouter很簡單,以組件的形式包裝了Router,history傳遞下去,固然HashRouter也是同理
BrowserRouter源碼
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
複製代碼
看完BrowserRouter,其實本質就是Router組件嘛,在下已經忍不住先去看react-router的Router源碼了。
Router做爲Route的根組件,負責監聽url的變化和傳遞數據(props), 這裏使用了history.listen監聽url,使用react context的Provider和Consumer模式,最初的數據來自history,並將 history, location, match, staticContext做爲props傳遞
router源碼+註釋
// 構造props
function getContext(props, state) {
return {
history: props.history,
location: state.location,
match: Router.computeRootMatch(state.location.pathname),
staticContext: props.staticContext
};
}
class Router extends React.Component {
static computeRootMatch(pathname) {
return { path: "/", url: "/", params: {}, isExact: pathname === "/" };
}
constructor(props) {
super(props);
this.state = {
// browserRouter的props爲history
location: props.history.location
};
this._isMounted = false;
this._pendingLocation = null;
// staticContext爲true時,爲服務器端渲染
// staticContext爲false
if (!props.staticContext) {
// 監聽listen,location改變觸發
this.unlisten = props.history.listen(location => {
// _isMounted爲true表示經歷過didmount,能夠setState,防止在構造函數中setstate
if (this._isMounted) {
// 更新state location
this.setState({ location });
} else {
// 不然存儲到_pendingLocation, 等到didmount再setState避免可能報錯
this._pendingLocation = location;
}
});
}
}
componentDidMount() {
// 賦值爲true,且不會再改變
this._isMounted = true;
// 更新location
if (this._pendingLocation) {
this.setState({ location: this._pendingLocation });
}
}
componentWillUnmount() {
// 取消監聽
if (this.unlisten) this.unlisten();
}
render() {
const context = getContext(this.props, this.state);
return (
<RouterContext.Provider
children={this.props.children || null}
value={context}
/>
);
}
}
複製代碼
rrv4中Router組件爲context中的Pirover, children能夠是任何div等元素,這裏解釋了問題Q2,react-router的v4版本直接推翻了以前的v2,v3版本,在v2,v3的版本中Router組件根據子組件的Route,生成全局的路由表,路由表中記錄了path與UI組件的映射關係,Router監聽path變化,當path變化時,根據新的path找出對應所需的全部UI組件,按必定層級將這些UI渲染出來.而在rrv4中做者提倡Just Components思想,這也符合react中一切皆組件的思想。
在v4中,Route只是一個Consumer包裝的react組件,無論path是什麼,Route組件總會渲染,在Route內部判斷請求路徑與當前的path是否匹配,匹配會繼續渲染Route中的children或者component或者render中的子組件,若是不匹配,渲染null
route源碼+註釋
function getContext(props, context) {
const location = props.location || context.location;
const match = props.computedMatch
? props.computedMatch // <Switch> already computed the match for us
: props.path // <Route path='/xx' ... >
? matchPath(location.pathname, props)
: context.match; // 默認 { path: "/", url: "/", params: {}, isExact: pathname === "/" }
return { ...context, location, match };
}
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
invariant(context, "You should not use <Route> outside a <Router>");
// 經過path生成props
// this.props = {exact, path, component, children, render, computedMatch, ...others }
// context = { history, location, staticContext, match }
const props = getContext(this.props, context);
// 結構Route的props
let { children, component, render } = this.props;
// 空數組用null代替
if (Array.isArray(children) && children.length === 0) {
children = null;
}
if (typeof children === "function") {
// 無狀態組件時
children = children(props);
if (children === undefined) {
children = null;
}
}
return (
<RouterContext.Provider value={props}>
{children && !isEmptyChildren(children) // children && React.Children.count > 0
? children
: props.match // match爲true,查找到了匹配的<Route ... >
? component
? React.createElement(component, props) //建立react組件,傳遞props{ ...context, location, match }
: render
? render(props) // 執行render方法
: null
: null}
</RouterContext.Provider>
// 優先級 children > component > render
);
}}
</RouterContext.Consumer>
);
}
}
複製代碼
Route是一個組件,每個Route都會監聽本身context並執行從新的渲染,爲子組件提供了新的props, props.match用來決定是否渲染component和render,props.match由matchPath生成, 這裏咱們不得不看一下matchPath這個很重要的方法,他決定當前Route的path與url的匹配。
matchPath方法依賴path-to-regexp庫, 舉個小🌰
var pathToRegexp = require('path-to-regexp')
var keys = []
var re = pathToRegexp('/foo/:bar', keys)
// re = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
複製代碼
matchPath源碼+註釋
/** * Public API for matching a URL pathname to a path. * @param {*} pathname history.location.pathname * @param {*} options * 默認配置,是否全局匹配 exact,末尾加/ strict, 大小寫 sensitive, path <Route path="/xx" ...> */
function matchPath(pathname, options = {}) {
// use <Switch />, options = location.pathname
if (typeof options === "string") options = { path: options };
const { path, exact = false, strict = false, sensitive = false } = options;
// path存入paths數組
const paths = [].concat(path);
return paths.reduce((matched, path) => {
if (matched) return matched;
// compilePath內部使用path-to-regexp庫,並作了緩存處理
const { regexp, keys } = compilePath(path, {
end: exact,
strict,
sensitive
});
// 在pathname中查找path
const match = regexp.exec(pathname);
// 匹配失敗
if (!match) return null;
// 定義查找到的path爲url
const [url, ...values] = match;
// 判斷pathname與url是否相等 eg: '/' === '/home'
const isExact = pathname === url;
// 精準匹配時, 保證查找到的url === pathname
if (exact && !isExact) return null;
// 返回match object
return {
path, // the path used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
}, null);
}
複製代碼
不知道看官老爺有沒有注意到這裏
這就是Switch組件渲染與位置匹配的第一個子組件Route或Redirect的緣由。Switch利用React.Children.forEach(this.props.children, child => {...})方法匹配第一個子組件, 若是匹配成功添加computedMatch props,props值爲match。從而改變了matchPath的邏輯
switch部分源碼
class Switch extends React.Component {
render() {
...省略無關代碼
let element, match;
React.Children.forEach(this.props.children, child => {
// child爲react elemnet
// match若是沒有匹配到這爲context.match
if (match == null && React.isValidElement(child)) {
element = child;
// form用於<redirect form="..." ... >
const path = child.props.path || child.props.from;
// 匹配的match
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match; // path undefind爲默認mactch
// note: path爲undefined 時,會默認爲'/'
}
});
return match // 添加computedMatch props爲match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}
}
....
複製代碼
到這裏咱們瞭解了RRV4的基本工做流程和源碼,解決了Q3,最後來看一下Link。
Link組件的主要用於處理理用戶經過點擊錨標籤進行跳轉,之因此不用a標籤是由於要避免每次用戶切換路由時都進行頁面的總體刷新,而是使用histoy庫中的push和replace。解決Q4,當點擊Link組件時,點擊的是頁面上渲染出來的 a 標籤,經過preventDefault阻止默認行爲,經過history的push或repalce跳轉。
Link部分源碼+註釋
class Link extends React.Component {
....
handleClick(event, context) {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // 忽略不是左鍵的點擊
(!this.props.target || this.props.target === "_self") && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
// 阻止默認行爲
event.preventDefault();
const method = this.props.replace
? context.history.replace
: context.history.push;
method(this.props.to);
}
...
render() {
....
return(
<a {...rest} onClick={event => this.handleClick(event, context)} href={href} ref={innerRef} /> ) } '''' } 複製代碼
到這裏,本文結束,歡迎看官老爺們的到來。