React.js 路由

课程目标

  • 掌握 react-router 使⽤⽅法;
  • 能使⽤ react-router 设计开发 react 应⽤;
  • 理解 react-router 关键源码实现。
  • 理解 react-router 和 vue-router 的实现差异,针对⾯试提出的问题能举⼀反三;

课程大纲

  • react-router 使⽤详解;
  • 从0到1搭建⼀个基于 react-router 的应⽤;
  • react-router 关键源码解析;
  • 对⽐ react-router 和 vue-router的差异;

知识要点

react-router 基本使用

路由配置

jsx 用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { render } from "react-dom";
import {
BrowserRouter,
Routes,
Route,
} from "react-router-dom";
// import your route components too
render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route>
</Route>
</Routes>
</BrowserRouter>,
document.getElementById("root")
);

config + hooks 用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { render } from 'react-dom'
import { useRoutes, BrowserRouter } from 'react-router-dom'
function Routes () {
return useRoutes([
{
path: '/',
element: <App>,
children: [
{
path: '/',
element: <Home />,
},
{
path: '/teams',
element: <Teams />,
children: [
{
path: ':tamId',
element: <Team />
}
]
}
]
},
])
}
render(
<BrowserRouter>
<Routes />
</BowserRouter>,
document.getElementById("root")
)

页面跳转

jsx 用法:

1
<Link to="/">To</Link>

hooks 用法:

1
2
const location = useLocation()
location.push("/")

核心源码解析

我们经常使⽤的 BrowserRouter 和 HashRouter 主要依赖三个包:react-router-dom、react-router、 history。

  • react-router 提供 react 路由的核⼼实现,是跨平台的。

  • react-router-dom 提供了路由在 web 端的具体实现,与之同级的还有 react-router-native,提供 react-native 端的路由实现。

  • history 是⼀个对浏览器 history 操作封装,抹平浏览器 history 和 hash 的操作差异,提供统⼀的 location 对象给 react-router-dom 使⽤。

  • 下⾯从最简单的例⼦,进⼊ react-router 源码解析:

1
2
3
4
5
6
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)

BrowserRouter 实现:

react-router-dom/index.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export function BrowserRouter({
basename,
children,
window,
}: BrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
// history ref保存的是history对象,而不是history的实例
if (historyRef.current == null) {
// createBrowserHistory 是 history库中提供的方法,下面的history.listen也是
historyRef.current = createBrowserHistory({ window });
}

let history = historyRef.current;
let [state, setState] = React.useState({
action: history.action,
location: history.location,
});
// history对象变化时,重新监听history的变更
React.useLayoutEffect(() => history.listen(setState), [history]);

return (
<Router
basename={basename}
children={children}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
}

BrowserRouter 包装了 ,并创建了 history 对象,监听 history 变化。

Router实现:

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
export function Router({
basename: basenameProp = "/",
children = null,
location: locationProp,
navigationType = NavigationType.Pop,
navigator, // 导航器,每个平台不一样
static: staticProp = false,
}: RouterProps): React.ReactElement | null {
// 处理basename
let basename = normalizePathname(basenameProp);

let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);

if (typeof locationProp === "string") {
locationProp = parsePath(locationProp);
}

let {
pathname = "/",
search = "",
hash = "",
state = null,
key = "default",
} = locationProp;

let location = React.useMemo(() => {
let trailingPathname = stripBasename(pathname, basename);

if (trailingPathname == null) {
return null;
}

return {
pathname: trailingPathname,
search,
hash,
state,
key,
};
// 当 location 中有如下变化时重新生成 location 对象,触发页面重新渲染
}, [basename, pathname, search, hash, state, key]);

if (location == null) {
return null;
}

// const LocationContext = React.createContext<LocationContextObject>(null!);
/**
* 创建了全局的 context,用于存放 history 对象
*/
return (
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ location, navigationType }}
/>
</NavigationContext.Provider>
);
}

Router 处理了 history 对象,将其⽤ NavigationContext 包裹,使得下层⼦组件都可以访问到这个 history。

Routes实现:

jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
export function Routes({
children,
location,
}: RoutesProps): React.ReactElement | null {
return useRoutes(createRoutesFromChildren(children), location);
}

export function useRoutes(
routes: RouteObject[],
locationArg?: Partial<Location> | string
): React.ReactElement | null {

let { matches: parentMatches } = React.useContext(RouteContext);

let routeMatch = parentMatches[parentMatches.length - 1];
let parentParams = routeMatch ? routeMatch.params : {};
let parentPathname = routeMatch ? routeMatch.pathname : "/";
let parentPathnameBase = routeMatch ? routeMatch.pathnameBase : "/";
let parentRoute = routeMatch && routeMatch.route;

let locationFromContext = useLocation();

let location;
if (locationArg) {
let parsedLocationArg =
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
location = parsedLocationArg;
} else {
location = locationFromContext;
}

let pathname = location.pathname || "/";
let remainingPathname =
parentPathnameBase === "/"
? pathname
: pathname.slice(parentPathnameBase.length) || "/";
// 匹配路径
let matches = matchRoutes(routes, { pathname: remainingPathname });

/**
* 渲染匹配到的节点
*/
return _renderMatches(
matches &&
matches.map((match) =>
Object.assign({}, match, {
params: Object.assign({}, parentParams, match.params),
pathname: joinPaths([parentPathnameBase, match.pathname]),
pathnameBase:
match.pathnameBase === "/"
? parentPathnameBase
: joinPaths([parentPathnameBase, match.pathnameBase]),
})
),
parentMatches
);
}

export function createRoutesFromChildren(
children: React.ReactNode
): RouteObject[] {
let routes: RouteObject[] = [];

React.Children.forEach(children, (element) => {
if (!React.isValidElement(element)) {
return;
}

if (element.type === React.Fragment) {
// Transparently support React.Fragment and its children.
routes.push.apply(
routes,
createRoutesFromChildren(element.props.children)
);
return;
}

let route: RouteObject = {
caseSensitive: element.props.caseSensitive,
element: element.props.element,
index: element.props.index,
path: element.props.path,
};

if (element.props.children) {
route.children = createRoutesFromChildren(element.props.children);
}

routes.push(route);
});

return routes;
}

Routes 通过 Route ⼦组件⽣成路由列表,通过 location 中的 pathname 匹配组件并渲染。
通过以上代码,我们基本理解了 react-router 如何感知 history 中的 pathname 变化,并渲染对应组件。
但我们具体是如何操作 history 变化的呢?
我们在回到最上⾯的 createBrowserHistory 和 history.listen ⽅法,看看 history 对象是怎么被创建和改变的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
export function createBrowserHistory(
options: BrowserHistoryOptions = {}
): BrowserHistory {
let { window = document.defaultView! } = options;
let globalHistory = window.history;
// 获取浏览器history对象中的location和idx 并转换成标准格式的对象
function getIndexAndLocation(): [number, Location] { }

let blockedPopTx: Transition | null = null;
// 处理history pop(返回上一页)
function handlePop() {
if (blockedPopTx) {
blockers.call(blockedPopTx);
blockedPopTx = null;
} else {
let nextAction = Action.Pop;
let [nextIndex, nextLocation] = getIndexAndLocation();

if (blockers.length) {
if (nextIndex != null) {
let delta = index - nextIndex;
if (delta) {
// Revert the POP
blockedPopTx = {
action: nextAction,
location: nextLocation,
retry() {
go(delta * -1);
},
};

go(delta);
}
} else {
// Trying to POP to a location with no index. We did not create
// this location, so we can't effectively block the navigation.
}
} else {
applyTx(nextAction);
}
}
}

// 监听浏览器的 popState 事件
window.addEventListener(PopStateEventType, handlePop);

let action = Action.Pop;
let [index, location] = getIndexAndLocation();
let listeners = createEvents<Listener>();
let blockers = createEvents<Blocker>();

if (index == null) {
index = 0;
globalHistory.replaceState({ ...globalHistory.state, idx: index }, "");
}

function createHref(to: To) {
return typeof to === "string" ? to : createPath(to);
}

// state defaults to `null` because `window.history.state` does
function getNextLocation(to: To, state: any = null): Location {
return readOnly<Location>({
pathname: location.pathname,
hash: "",
search: "",
...(typeof to === "string" ? parsePath(to) : to),
state,
key: createKey(),
});
}

function getHistoryStateAndUrl(
nextLocation: Location,
index: number
): [HistoryState, string] {
return [
{
usr: nextLocation.state,
key: nextLocation.key,
idx: index,
},
createHref(nextLocation),
];
}

function allowTx(action: Action, location: Location, retry: () => void) {
return (
!blockers.length || (blockers.call({ action, location, retry }), false)
);
}
// 这里触发listener的回调函数
function applyTx(nextAction: Action) {
action = nextAction;
[index, location] = getIndexAndLocation();
listeners.call({ action, location });
}
// push
function push(to: To, state?: any) { }

// replace 操作
function replace(to: To, state?: any) {
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);
}
}

// 前进或后退 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) {
let unblock = blockers.push(blocker);

if (blockers.length === 1) {
window.addEventListener(BeforeUnloadEventType, promptBeforeUnload);
}

return function () {
unblock();

// Remove the beforeunload listener so the document may
// still be salvageable in the pagehide event.
// See https://html.spec.whatwg.org/#unloading-documents
if (!blockers.length) {
window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload);
}
};
},
};

return history;
}

createBrowserHistory 创建了⼀个标准的 history 对象,以及对 history 对象操作的各⽅法,且操作变更后,通过 listen ⽅法将变更结果回调给外部。

getIndexAndLocation实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getIndexAndLocation(): [number, Location] {
let { pathname, search, hash } = window.location;
let state = globalHistory.state || {};
return [
state.idx,
readOnly<Location>({
pathname,
search,
hash,
state: state.usr || null,
key: state.key || "default",
}),
];
}

push 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function push(to: To, state?: any) {
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);
// 先pushState改变浏览器history堆栈,后触发react router更新state,重新渲染页面
try {
globalHistory.pushState(historyState, "", url);
} catch (error) {

window.location.assign(url);
}

applyTx(nextAction);
}
}

问: 我们在代码中都是 const location = useLocation(); location.push("/") 这样的⽅式使⽤ push 的,那上⾯这个 push ⽅法到底是怎么跟 useLocation 关联的呢?
还记得 Router 中有这么⼀段代码吗?
const LocationContext = React.createContext<LocationContextObject>(null!);
我们的history对象创建后会被Router注⼊进⼀个LocationContext的全局上下⽂中。 useLocation实际就是包裹了这个上下⽂对象。

1
2
3
export function useLocation(): Location {
return React.useContext(LocationContext).location;
}

总结⼀下,BrowserRouter 核⼼实现包含三部分:

  • 创建 history 对象,提供对浏览器 history 对象的操作。
  • 创建 Router 组件,将创建好的 history 对象注⼊全局上下⽂。
  • Routes 组件,遍历⼦组件⽣成路由表,根据当前全局上下⽂ history 对象中的 pathname 匹配当前激活的组件并渲染。
    HashRouter 和 BrowserRouter 原理类似,只是监听的浏览器原⽣ history 从 pathname 变为 hash。

react-router 和 vue-router 的差异

路由类型

React:

  • browserRouter
  • hashRouter
  • memoryRouter
    Vue:
  • history
  • hash
  • abstract

memoryRouter 和 abstract 作⽤类似,都是在不⽀持浏览器的环境中充当 fallback。

使用方法

  • 路由拦截的实现不同
    • vue router 提供了全局守卫、路由守卫、组件守卫供我们实现路由拦截。
    • react router 没有提供类似 vue router 的守卫供我们使⽤,不过我们可以在组件渲染过程中⾃⼰实现路由拦截。如果是类组件,我们可以在 componentWillMount 或者 getDerivedStateFromProps 中通过 props.history 实现路由拦截;如果是函数式组件,在函数⽅法中通过 props.history 或者 useHistory 返回的 history 对象实现路由拦截。

实现差异

  • hash 模式的实现不同 (新版本已相同)
    • react router 的 hash 模式,是基于 window.location.hash(window.location.replace)hashchange 实现的。当通过 push ⽅式跳转⻚⾯时,直接修改 window.location.hash,然后渲染⻚⾯; 当通过 replace ⽅式跳转⻚⾯时,会先构建⼀个修改 hash 以后的临时 url,然后使⽤这个临时 url 通过 window.location.replace 的⽅式替换当前 url,然后渲染⻚⾯;当激活历史记录导致 hash 发⽣变化时,触发 hashchange 事件,重新渲染⻚⾯。
    • vue router 的 hash 模式,是先通过 pushState(replaceState) 在浏览器中新增(修改)历史记录,然后渲染⻚⾯。 当激活某个历史记录时,触发 popstate 事件,重新渲染⻚⾯。 如果浏览器不⽀持 pushState,才会使⽤ window.location.hash(window.location.replace)hashchange 实现。
  • history 模式不⽀持 pushState 的处理⽅式不同
    • 使⽤ react router 时,如果 history 模式下 不⽀持 pushState,会通过重新加载⻚⾯ (window.location.href = href) 的⽅式实现⻚⾯跳转。
    • 使⽤ vue router 时,如果 history 模式下不⽀持 pushState,会根据 fallback 配置项来进⾏下⼀步处理。 如果 fallbacktrue, 回退到 hash 模式;如果 fallbackfalse, 通过重新加载⻚⾯的⽅式实现⻚⾯跳转。
  • 懒加载实现过程不同
    • vue router 在路由懒加载过程中,会先去获取懒加载⻚⾯对应的 js ⽂件。等懒加载⻚⾯对应的 js ⽂件加载并执⾏完毕,才会开始渲染懒加载⻚⾯。
    • react router 在路由懒加载过程中,会先去获取懒加载⻚⾯对应的 js ⽂件,然后渲染 loading ⻚⾯。等懒加载⻚⾯对应的 js⽂ 件加载并执⾏完毕,触发更新,渲染懒加载⻚⾯。

补充知识点

react-router 的登录校验实现思路

  1. 登录信息全局状态
  2. 定义 Auth 组件判断是否登录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
import { Navigate } from 'react-router'

export function Auth ({ children }) {
const auth = useAuth()
if (!auth.userInfo) {
return <Navigate to="/login" replace />
}
return children
}

const AuthContext = createContext(null)

export function AuthProvider ({ children }) {
const [userInfo, setUserInfo] = useState(null)

const login = (name) => {
const user = { name, age: 18 };
localStorage.setItem('user-info', JSON.stringify(user));
setUserInfo(user);
}

const logout = () => {
localStorage.setItem('user-info', '');
setUserInfo(null);
}

const getUserInfo = () => {
let userInfoLocal = localStorage.getItem('user-info');
if (userInfoLocal) {
userInfoLocal = JSON.parse(userInfoLocal)
}
setUserInfo(userInfoLocal);
}

useEffect(() => {
getUserInfo()
}, [])

const value = {
userInfo, login, logout
}

return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}

export function useAuth () {
return useContext(AuthContext)
}

react-router 懒加载的实现思路

  • React.lazy()
  • dynamic import()
  • React.Suspense
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Account = React.lazy(() => import("./pages/Account"));

ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route path="/account" element={
<React.Suspense fallback={(<div>loading...</div>)}>
<Account />
</React.Suspense>
} />
</Route>
</Routes>
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);

在 react-router v6 中如何实现离开页面前确认

基于 history 的 block 实现

usePrompt hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { useCallback, useContext, useEffect, useState } from 'react';
import { UNSAFE_NavigationContext, useLocation, useNavigate } from 'react-router';
import type { History, Transition } from 'history';

export function usePrompt(when: boolean): (boolean | (() => void))[] {
const navigate = useNavigate();
const location = useLocation();
const [showPrompt, setShowPrompt] = useState(false);
const [lastLocation, setLastLocation] = useState<any>(null);
const [confirmedNavigation, setConfirmedNavigation] = useState(false);

const cancelNavigation = useCallback(() => {
setShowPrompt(false);
}, []);

const handleBlockedNavigation = useCallback(
(nextLocation) => {
if (
!confirmedNavigation &&
nextLocation.location.pathname !== location.pathname
) {
setShowPrompt(true);
setLastLocation(nextLocation);
return false;
}
return true;
},
[confirmedNavigation],
);

const confirmNavigation = useCallback(() => {
setShowPrompt(false);
setConfirmedNavigation(true);
}, []);

useEffect(() => {
if (confirmedNavigation && lastLocation) {
navigate(lastLocation.location.pathname);
}
}, [confirmedNavigation, lastLocation]);

const navigator = useContext(UNSAFE_NavigationContext).navigator as History;

useEffect(() => {
if (!when) return;

const unblock = navigator.block((tx: Transition) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
},
};

handleBlockedNavigation(autoUnblockingTx);
});

return unblock;
}, [navigator, handleBlockedNavigation, when]);

return [showPrompt, confirmNavigation, cancelNavigation];
}

如何在服务端处理 react-router

<StaticRouter>

react-router 有哪些路由类型,及其实现原理和差异

为什么要分 react-router、react-router-dom,react-router-native 这样实现的好处是什么

核心实现与平台实现分开,更好的实现跨平台。

react-router 和 vue-router 有哪些异同