前端路由
- 路由這個(gè)概念最早出現(xiàn)在后端,通過?戶請求的url導(dǎo)航到具體的html??。
- 現(xiàn)在的前端路由不同 于傳統(tǒng)路由,它不需要服務(wù)器解析,?是可以通過hash函數(shù)或者h(yuǎn)istory API來實(shí)現(xiàn)。
- 在前端開發(fā)中,可以使?路由設(shè)置訪問路徑,并根據(jù)路徑與組件的映射關(guān)系切換組件的顯示
- 這整個(gè)過程都是在同 ?個(gè)??中實(shí)現(xiàn)的,不涉及??間的跳轉(zhuǎn),這也就是常說的單?應(yīng)?(spa)。

React-Router.png
1、查看源碼姿勢
1.1 代碼倉庫
https://github.com/ReactTraining/react-router
2.2 包說明
- react-router 公共基礎(chǔ)包
- react-router-dom 在瀏覽器中使?,依賴react-router
- react-router-native 在react-native中使用,依賴react-router
2.3 源碼位置
- react-router 源碼位置
react-router/packages/react-router/modules/ - react-router-dom 源碼位置
react-router/packages/react-router-dom/modules/
2.4 文件結(jié)構(gòu)
-
Hooks暫不關(guān)注 -
<Link><NavLink>
-
<Prompt>暫不關(guān)注 <Redirect><Route>-
<Router><HashRouter><BrowserRouter>-
<MemoryRouter>暫不關(guān)注 -
<StaticRouter>暫不關(guān)注
<Switch>- generatePath
暫不關(guān)注 - history
- location
暫不關(guān)注 - match
暫不關(guān)注 - matchPath
暫不關(guān)注 - withRouter
3、React-Router源碼分析
3.1 四種 Router 源碼對比
<!--HashRouter-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";
/**
* The public API for a <Router> that uses window.location.hash.
*/
class HashRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default HashRouter;
<!--BrowserRouter-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
/**
* 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;
<!--MemoryRouter-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
import React from "react";
import { createMemoryHistory as createHistory } from "history";
import Router from "./Router.js";
/**
* The public API for a <Router> that stores location in memory.
*/
class MemoryRouter extends React.Component {
history = createHistory(this.props);
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default MemoryRouter;
<!--StaticRouter-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
import React from "react";
import { createLocation, createPath } from "history";
import Router from "./Router.js";
/**
* The public top-level API for a "static" <Router>, so-called because it
* can't actually change the current location. Instead, it just records
* location changes in a context object. Useful mainly in testing and
* server-rendering scenarios.
*/
class StaticRouter extends React.Component {
render() {
const { basename = "", context = {}, location = "/", ...rest } = this.props;
const history = {
createHref: path => addLeadingSlash(basename + createURL(path)),
action: "POP",
location: stripBasename(basename, createLocation(location)),
push: this.handlePush,
replace: this.handleReplace,
go: staticHandler("go"),
goBack: staticHandler("goBack"),
goForward: staticHandler("goForward"),
listen: this.handleListen,
block: this.handleBlock
};
return <Router {...rest} history={history} staticContext={context} />;
}
}
export default StaticRouter;
都是基于<Router>組件實(shí)現(xiàn),區(qū)別就是傳遞不同的參數(shù)。重點(diǎn)關(guān)注 <HashRouter> 和 <BrowserRouter>
3.2 <Router> 源碼分析
<!--Router-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
class Router extends React.Component {
render() {
return (
<RouterContext.Provider
value={{
history: this.props.history,
location: this.state.location,
match: Router.computeRootMatch(this.state.location.pathname),
//只有StaticRouter在使用,暫不考慮
staticContext: this.props.staticContext
}}
>
{/*HistoryContext在hooks中使用,暫不考慮*/}
<HistoryContext.Provider
children={this.props.children || null}
value={this.props.history}
/>
</RouterContext.Provider>
);
}
}
通過源碼可以看出Router的核心功能就是提供以下數(shù)據(jù)
- history,父組件傳入,由history庫生成
- location,組件內(nèi)計(jì)算生成
- match,組件內(nèi)靜態(tài)方法計(jì)算生成
3.3 <Route> 源碼分析
<!--Route-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
class Route extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
return (
<RouterContext.Provider value={props}>
{props.match
? children
? typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: children
: component
? React.createElement(component, props)
: render
? render(props)
: null
: typeof children === "function"
? __DEV__
? evalChildrenDev(children, props, this.props.path)
: children(props)
: null}
</RouterContext.Provider>
);
}}
</RouterContext.Consumer>
);
}
}
由于三項(xiàng)表達(dá)式嵌套不便于閱讀,代碼可以轉(zhuǎn)換成
/*
* 1、檢測是否 match
* 1.1 不匹配,children 為函數(shù)則返回 children(props),否則返回 null
* 1.2 匹配,進(jìn)行第2步
*
* 2、檢查 children
* 2.1 存在, children 為函數(shù)則返回 children(props),否則返回 children
* 2.2 不存在,進(jìn)行第3步
*
* 3、檢查component
* 3.1 存在,返回React.createElement(component, props)
* 3.2 不在存,進(jìn)行第4步
*
* 4、檢查render
* 4.1 存在,返回 render(props)
* 4.2 不存在,返回 null
* */
getComponent(props, children, component, render) {
if (props.match) {
if (children) {
if (typeof children === "function") {
return children(props);
} else {
return children;
}
} else {
if (component) {
return React.createElement(component, props);
} else {
if (render) {
return render(props);
} else {
return null;
}
}
}
} else {
if (typeof children === "function") {
return children(props);
} else {
return null;
}
}
}
通過源碼可以看出Reoute的核心功能是渲染組件,并有以下特點(diǎn)
- 三者優(yōu)先級(jí) children > component > render
- children 為函數(shù)可以做到匹配與否都顯示
- children 為組件只能在匹配顯示
- component 只能為組件
- render 只能為函數(shù)
3.4 <Redirect> 源碼分析
<!--Switch-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
function Redirect({ computedMatch, to, push = false }) {
const method = push ? history.push : history.replace;
return (
<RouterContext.Consumer>
{context => {
return (
<Lifecycle
onMount={() => {
method(location);
}}
onUpdate={(self, prevProps) => {
method(location);
}}
to={to}
/>
);
}}
</RouterContext.Consumer>
);
}
通過源碼可以看出 Redirect 的核心功能是跳轉(zhuǎn)到 to 指向的頁面。
3.5 <Switch> 源碼分析
<!--Switch-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
class Switch extends React.Component {
render() {
return (
<RouterContext.Consumer>
{context => {
const location = this.props.location || context.location;
let element, match;
// 匹配第一個(gè)Route
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
//匹配則返回React生成的元素,否則返回null
return match
? React.cloneElement(element, { location, computedMatch: match })
: null;
}}
</RouterContext.Consumer>
);
}
}
通過源碼可以看出 Switch 的核心功能是渲染匹配到第一個(gè) Route 組件。
3.6 withRouter 源碼分析
<!--withRouter-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
function withRouter(Component) {
const C = props => {
const { wrappedComponentRef, ...remainingProps } = props;
return (
//通過 Context.Consumer 傳遞 Router 屬性給 Component,達(dá)到增強(qiáng)目的
<RouterContext.Consumer>
{context => {
return (
<Component
{...remainingProps}
{...context}
ref={wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
};
//靜態(tài)屬性拷貝
return hoistStatics(C, Component);
}
通過源碼可以看出 withRouter 的核心功能是 傳遞 Router 屬性給 Component。
3.7 <Link> 源碼分析
<!--Link-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
const Link = forwardRef(
(
{
component = LinkAnchor,
replace,
to,
innerRef, // TODO: deprecate
...rest
},
forwardedRef
) => {
return (
<RouterContext.Consumer>
{context => {
const { history } = context;
//把 to 屬性轉(zhuǎn)換成 href 屬性
const location = normalizeToLocation(
resolveToLocation(to, context.location),
context.location
);
const href = location ? history.createHref(location) : "";
//組裝 props 數(shù)據(jù)
const props = {
...rest,
href,
navigate() {
const location = resolveToLocation(to, context.location);
const method = replace ? history.replace : history.push;
method(location);
}
};
// 設(shè)置forwardedRef
props.ref = forwardedRef
//創(chuàng)建并返回組件
return React.createElement(component, props);
}}
</RouterContext.Consumer>
);
}
);
<!--LinkAnchor-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
const LinkAnchor = forwardRef(
(
{
innerRef, // TODO: deprecate
navigate,
onClick,
...rest
},
forwardedRef
) => {
const { target } = rest;
//組裝 props 數(shù)據(jù)
let props = {
...rest,
onClick: event => {
//處理點(diǎn)擊事件
try {
if (onClick) onClick(event);
} catch (ex) {
event.preventDefault();
throw ex;
}
//執(zhí)行路由跳轉(zhuǎn)事件
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();
}
}
};
// 設(shè)置forwardedRef
props.ref = forwardedRef;
/* eslint-disable-next-line jsx-a11y/anchor-has-content */
return <a {...props} />;
}
);
通過源碼可以看出 Link 的核心功能如下
- 通過 component 自定義 <Link>,否則默認(rèn)使用 <LinkAnchor>
- <Link> 的主要功能就是把 to 屬性轉(zhuǎn)換成 href 屬性
- <LinkAnchor> 主要功能是屏蔽 <a> 默認(rèn)點(diǎn)擊事件,使用 history 進(jìn)行路由跳轉(zhuǎn)
3.8 <NavLink> 源碼分析
<!--NavLink-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
const NavLink = forwardRef(
(
{
"aria-current": ariaCurrent = "page",
activeClassName = "active",
activeStyle,
className: classNameProp,
exact,
isActive: isActiveProp,
location: locationProp,
sensitive,
strict,
style: styleProp,
to,
innerRef, // TODO: deprecate
...rest
},
forwardedRef
) => {
return (
<RouterContext.Consumer>
{context => {
//根據(jù) isActive 屬性判斷是否 Active
const isActive = !!(isActiveProp
? isActiveProp(match, currentLocation)
: match);
//根據(jù) isActive 屬性設(shè)置 className
const className = isActive
? joinClassnames(classNameProp, activeClassName)
: classNameProp;
//根據(jù) isActive 屬性設(shè)置內(nèi)聯(lián)樣式 style
const style = isActive ? { ...styleProp, ...activeStyle } : styleProp;
//組織 props 數(shù)據(jù)
const props = {
"aria-current": (isActive && ariaCurrent) || null,
className,
style,
to: toLocation,
...rest
};
// 設(shè)置forwardedRef
props.ref = forwardedRef;
return <Link {...props} />;
}}
</RouterContext.Consumer>
);
}
);
通過源碼可以看出 <NavLink> 的核心功能如下
- 在 <Link> 基礎(chǔ)上允許自定義默認(rèn)和激活狀態(tài)樣式
- isActive 優(yōu)先級(jí)高于默認(rèn)的路由匹配
4、history 源碼分析
4.1 createBrowserHistory
<!--createBrowserHistory-->
//便于梳理代碼結(jié)構(gòu),已刪除部分代碼
export function createBrowserHistory( options: BrowserHistoryOptions = {} ): BrowserHistory {
//獲取 history
let { window = document.defaultView! } = options;
let globalHistory = window.history;
window.addEventListener(PopStateEventType, handlePop);
let action = Action.Pop;
let [index, location] = getIndexAndLocation();
let listeners = createEvents<Listener>();
//pathname + search + hash
function createHref(to: To) {
return typeof to === 'string' ? to : createPath(to);
}
//用來處理事件訂閱
function applyTx(nextAction: Action) {
action = nextAction;
[index, location] = getIndexAndLocation();
listeners.call({ action, location });
}
//基于 pushState 方法實(shí)現(xiàn)
function push(to: To, state?: State) {
let nextAction = Action.Push;
let nextLocation = getNextLocation(to, state);
function retry() {
push(to, state);
}
if (allowTx(nextAction, nextLocation, retry)) {
let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
// TODO: Support forced reloading
// try...catch because iOS limits us to 100 pushState calls :/
try {
globalHistory.pushState(historyState, '', url);
} catch (error) {
// They are going to lose state here, but there is no real
// way to warn them about it since the page will refresh...
window.location.assign(url);
}
//用來處理事件訂閱
applyTx(nextAction);
}
}
//基于replaceState方法實(shí)現(xiàn)
function replace(to: To, state?: State) {
let nextAction = Action.Replace;
let nextLocation = getNextLocation(to, state);
function retry() {
replace(to, state);
}
if (allowTx(nextAction, nextLocation, retry)) {
let [historyState, url] = getHistoryStateAndUrl(nextLocation, index);
// TODO: Support forced reloading
globalHistory.replaceState(historyState, '', url);
//用來處理事件訂閱
applyTx(nextAction);
}
}
//基于go方法實(shí)現(xiàn)
function go(delta: number) {
globalHistory.go(delta);
}
let history: BrowserHistory = {
get action() {
return action;
},
get location() {
return location;
},
createHref,
push,
replace,
go,
back() {
go(-1);
},
forward() {
go(1);
},
listen(listener) {
return listeners.push(listener);
},
block(blocker) {...}
};
return history;
}
- back、forward 都是基于 go 方法實(shí)現(xiàn)
- push 基于 pushState 方法實(shí)現(xiàn)
- replace 基于 replaceState 方法實(shí)現(xiàn)
- 瀏覽器相關(guān)(url輸入、前進(jìn)、后退)事件通過監(jiān)聽 popstate 事件處理,然后通過事件訂閱的形式傳遞給Route組件
4.2
- createHashHistory 同 createBrowserHistory
- createMemoryHistory 同 createBrowserHistory
參考鏈接