Learn React Router v6 In 45 Minutes
对 React Router v6 不太熟悉的同学可以看下这个视频,主要介绍了 React Router v6 的新特性和基本使用。
React Router 机制解析
History 库
在了解 React Router 的实现机制之前,我们需要了解一下 history 库。history 库也是 Remix 团队开发的,内部封装了一些系列监听操作浏览器历史 堆栈 的功能,是 react-router 内部路由导航的核心库。
react-router 目前依赖 history 5.x 版本,提供了三种不同性质的 history 导航创建方法:
-
createBrowserHistory:基于浏览器 History 对象最新 AP; -
createHashHistory:基于浏览器 URL 的 hash 参数; -
createMemoryHistory:基于内存栈,不依赖任何平台。
上面三种方法创建的 history 对象在 react-router 中作为三种主要路由的导航器使用:
BrowserRouter对应createBrowserHistory,由 react-router-dom 提供;HashRouter对应createHashHistory,由 react-router-dom 提供;MemoryRouter对应createMemoryHistory,由 react-router 提供,主要用于 react-native 等基于内存的路由系统。
History 库基于 History API ****封装了监听和操作浏览器历史 堆栈等核心功能,而 react-router 只是围绕 History 库做了一层基于 React 的封装。
更多细节请参考:github.com/remix-run/h…
如何监听 URL 变化?
上文我们提到了,实现客户端路由的关键是监听 URL(路由)变化,进一步匹配和渲染路由组件,但是 History API 提供的 history.pushState() 和history.replaceState() API 并不会触发popstate 事件因此我们无法监听到 URL 的变化。React Router 团队在 History 库中提供了解决方案。
That's where a React Router specific
historyobject comes into play. It provides a way to "listen for URL" changes whether the history action is push, pop, or replace.
let history = createBrowserHistory();
history.listen(({ location, action }) => {
// this is called whenever new locations come in
// the action is POP, PUSH, or REPLACE
});
应用程序不需要自己设置历史对象,这是 的工作(下文中会介绍到):订阅历史堆栈中的更改,并在 URL 更改时更新 history 状态,随后应用会重新渲染正确的 UI。
"The router routes you to a route"
"router routes via a route"
Router
Router 的定义
Router 的主要作用是提供全局的路由导航对象( 一般由 history 库提供)以及当前的路由导航状态,使用时一般是必须并且唯一的。
interface Location {
pathname: string;
search: string;
hash: string;
state: any;
key: string;
}
export interface RouterProps {
// 路由前缀
basename?: string;
children?: React.ReactNode;
// 当前 location
location: Partial<Location> | string;
// 当前路由跳转的类型,有 POP,PUSH 与 REPLACE 三种
navigationType?: NavigationType;
// history 中的导航对象,在这里传入统一 history
navigator: Navigator;
// 是否为静态路由(ssr)
static?: boolean;
}
interface Path {
pathname: string;
search: string;
hash: string;
}
export function Router({
basename: basenameProp = "/",
children = null,
location: locationProp,
navigationType = NavigationType.Pop,
navigator,
static: staticProp = false
}: RouterProps): React.ReactElement | null {
...
// 全局的导航上下文信息,包括路由前缀,导航对象等
let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
// 转换 location,将传入 string 转换为 path 对象
...
let location = React.useMemo(() => {
// stripBasename 用于去除 pathname 前面 basename 部分
let trailingPathname = stripBasename(pathname, basename);
if (trailingPathname == null) {
return null;
}
return { pathname: trailingPathname, search, hash, state, key };
}, [basename, pathname, search, hash, state, key]);
if (location == null) {
return null;
}
return (
// 唯一传入 location 的地方
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider
children={children}
value={{ location, navigationType }}
/>
</NavigationContext.Provider>
);
}
NavigationContext & LocationContext
import React from 'react'
import type { History, Location } from "history";
import { Action as NavigationType } from "history";
// 用于在 react-router 中进行路由跳转
export type Navigator = Pick<History, "go" | "push" | "replace" | "createHref">;
interface NavigationContextObject {
basename: string;
navigator: Navigator;
static: boolean;
}
const NavigationContext = React.createContext<NavigationContextObject>(null!);
interface LocationContextObject {
location: Location;
navigationType: NavigationType;
}
// 包含当前 location 与 action 的 type,用于在内部获取当前 location
const LocationContext = React.createContext<LocationContextObject>(null!);
// 仅在内部使用,外部不推荐使用
/** @internal */
export {
NavigationContext as UNSAFE_NavigationContext,
LocationContext as UNSAFE_LocationContext,
};
Router 只是提供 Context 与格式化外部传入的location对象。
BrowserRouter
<BrowserRouter>is the recommended interface for running React Router in a web browser. A<BrowserRouter>stores the current location in the browser's address bar using clean URLs and navigates using the browser's built-in history stack.
/**
* A `<Router>` for use in web browsers. Provides the cleanest URLs.
*
* @see https://reactrouter.com/docs/en/v6/routers/browser-router
*/
export function BrowserRouter({
basename,
children,
window,
}: BrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
// 返回一个 BrowserHistory 对象(继承自 History 对象)
historyRef.current = createBrowserHistory({ window });
}
let history = historyRef.current;
let [state, setState] = React.useState({
action: history.action,
location: history.location,
});
React.useLayoutEffect(() => history.listen(setState), [history]);
return (
<Router
basename={basename}
children={children}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
}
监听浏览器 history 堆栈,提供当前 location 和 navigator 上下文。
HashRouter
<HashRouter>is for use in web browsers when the URL should not (or cannot) be sent to the server for some reason. This may happen in some shared hosting scenarios where you do not have full control over the server. In these situations,<HashRouter>makes it possible to store the current location in thehashportion of the current URL, so it is never sent to the server.
和 BrowserRouter 的定义一样,只是创建的 History 对象有一些差别,通过 hash 来存储在不同状态下的 history 信息。
MemoryRouter
<MemoryRouter>stores its locations internally in an array. Unlike<BrowserHistory>and<HashHistory>, it isn't tied to an external source, like the history stack in a browser. This makes it ideal for scenarios where you need complete control over the history stack, like testing.
示例
import * as React from "react";
import { create } from "react-test-renderer";
import {
MemoryRouter,
Routes,
Route,
} from "react-router-dom";
describe("My app", () => {
it("renders correctly", () => {
let renderer = create(
<MemoryRouter initialEntries={["/users/mjackson"]}>
<Routes>
<Route path="users" element={<Users />}>
<Route path=":id" element={<UserProfile />} />
</Route>
</Routes>
</MemoryRouter>
);
expect(renderer.toJSON()).toMatchSnapshot();
});
});
Route
定义
/**
* Declares an element that should be rendered at a certain URL path.
*
* @see https://reactrouter.com/docs/en/v6/components/route
*/
export function Route(
_props: PathRouteProps | LayoutRouteProps | IndexRouteProps
): React.ReactElement | null {
invariant(
false,
`A <Route> is only ever to be used as the child of <Routes> element, ` +
`never rendered directly. Please wrap your <Route> in a <Routes>.`
);
}
路径路由
最普遍的路由定义方式,可以定义要匹配的 path 以及是否允许大小写不同等配置。
export interface PathRouteProps {
caseSensitive?: boolean;
children?: React.ReactNode; // 子路由
element?: React.ReactNode | null;
index?: false;
path: string;
}
示例
<Routes>
<Route path="/" element={<App />} />
<Route path="/teams" element={<Teams />} caseSensitive>
<Route path="/teams/:teamId" element={<Team />} />
<Route path="/teams/new" element={<NewTeamForm />} />
</Route>
</Routes>
布局路由
用于处理有共同布局时的路由定义方式,使用这种方式可以减少重复性的组件渲染。
export interface LayoutRouteProps {
children?: React.ReactNode;
element?: React.ReactNode | null;
}
示例
<Routes>
<Route path="/" element={<App />} />
{/* 布局路由 */}
<Route element={<PageLayout />}>
<Route path="/privacy" element={<Privacy />} />
<Route path="/tos" element={<Tos />} />
</Route>
<Route path="/contact-us" element={<Contact />} />
</Routes>
默认路由
最特殊的路由定义方式,当设置 index 为 true 时会启用该路由,该路由内部不能有子路由,并且它能匹配到的 path 永远与父路由(非*)路径一致。相当于目录里面的 index.js 文件,当引入目录时,默认会引用到它。
export interface IndexRouteProps {
element?: React.ReactNode | null;
index: true;
}
示例
<Routes>
<Route path="/teams" element={<Teams />}>
<Route path="/teams/:teamId" element={<Team />} />
<Route path="/teams/new" element={<NewTeamForm />} />
{/* 默认路由 */}
<Route index element={<LeagueStandings />} />
</Route>
</Routes>
Route 组件只是用来传递参数的工具人,为用户提供 命令式 的路由配置方式。
Routes
定义
export interface RoutesProps {
children?: React.ReactNode;
location?: Partial<Location> | string;
}
/**
* A container for a nested tree of <Route> elements that renders the branch
* that best matches the current location.
*
* @see https://reactrouter.com/docs/en/v6/components/routes
*/
export function Routes({
children,
location,
}: RoutesProps): React.ReactElement | null {
return useRoutes(createRoutesFromChildren(children), location);
}
使用 <Routes /> 的时候,本质上是通过 useRoutes 返回的 react element 对象。
createRoutesFromChildren
/**
* Creates a route config from a React "children" object, which is usually
* either a `<Route>` element or an array of them. Used internally by
* `<Routes>` to create a route config from its children.
*
* @see https://reactrouter.com/docs/en/v6/utils/create-routes-from-children
*/
export function createRoutesFromChildren(
children: React.ReactNode
): RouteObject[] {
let routes: RouteObject[] = [];
React.Children.forEach(children, (element) => {
// 元素/类型检查等
...
// Route 组件解析为 route 对象
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;
}
将 Route 组件解析为 route 对象,提供给 useRoutes 使用。
useRoutes
The
useRouteshook is the functional equivalent of<Routes>, but it uses JavaScript objects instead of<Route>elements to define your routes. These objects have the same properties as normal<Route> elements, but they don't require JSX.The return value of
useRoutesis either a valid React element you can use to render the route tree, ornullif nothing matched.
useRoutes 提供一种声明式的路由生成方式。
/**
* A route object represents a logical route, with (optionally) its child
* routes organized in a tree-like structure.
*/
export interface RouteObject {
caseSensitive?: boolean;
children?: RouteObject[];
element?: React.ReactNode;
index?: boolean;
path?: string;
}
/**
* Returns the element of the route that matched the current location, prepared
* with the correct context to render the remainder of the route tree. Route
* elements in the tree must render an <Outlet> to render their child route's
* element.
*
* @see https://reactrouter.com/docs/en/v6/hooks/use-routes
*/
export function useRoutes(
routes: RouteObject[],
locationArg?: Partial<Location> | string
): React.ReactElement | null {
...
return _renderMatches(...);
}
示例
import * as React from "react";
import { useRoutes } from "react-router-dom";
function App() {
let element = useRoutes([
{
path: "/",
element: <Dashboard />,
children: [
{
path: "messages",
element: <DashboardMessages />,
},
{ path: "tasks", element: <DashboardTasks /> },
],
},
{ path: "team", element: <AboutPage /> },
]);
return element;
}
路由上下文解析阶段
RouterContext
用于解决多次调用 useRoutes 时内置的 route 上下文问题,继承外层的匹配结果。
interface RouteContextObject {
outlet: React.ReactElement | null;
matches: RouteMatch[];
}
export const RouteContext = React.createContext<RouteContextObject>({
outlet: null,
matches: [],
});
路由匹配阶段 —— matchRoutes
matchRoutesruns the route matching algorithm for a set of routes against a givenlocationto see which routes (if any) match. If it finds a match, an array ofRouteMatchobjects is returned, one for each route that matched.This is the heart of React Router's matching algorithm. It is used internally by
useRoutesand the<Routes> componentto determine which routes match the current location.
function matchRoutes(routes,locationArg,basename){
...
// 获取当前路由 pathname
let pathname = stripBasename(location.pathname || "/", basename);
...
// 扁平化 routes 并计算权重
let branches = flattenRoutes(routes);
// 根据权重排序 routes
rankRouteBranches(branches);
let matches = null;
for (let i = 0; matches == null && i < branches.length; ++i) {
// 路由匹配与合并
matches = matchRouteBranch(branches[i], pathname);
}
return matches;
}
1、路由扁平化(方便排序)
routes = [{
path: "路由 A",
children: [{
path: "路由 A-1"
}, {
path: "路由 A-2"
}]
},
{
path: "路由 B",
children: [{
path: "路由 B-1"
}, {
path: "路由 B-2"
}]
}]
routes = [{path: "路由 A-1"},
{path: "路由 A-2"}, { path: "路由 A" },
{path: "路由 B-1"}, { path: "路由 B-2"},
{path: "路由 B" }]
2、路由权值计算与排序
a. 计算权重 —— computeScore
// 动态路由权重,比如 /foo/:id
const dynamicSegmentValue = 3;
// 默认路由权重(index 为 true 属性的路由)
const indexRouteValue = 2;
// 空路由权重,当一段路径值为空时匹配,只有最后的路径以 / 结尾才会用到它
const emptySegmentValue = 1;
// 静态路由权重
const staticSegmentValue = 10;
// 路由通配符权重
const splatPenalty = -2;
// 判断是否是动态参数,比如 :id 等
const paramRe = /^:\w+$/;
// 判断是否为 *
const isSplat = (s: string) => s === "*";
function computeScore(path: string, index: boolean | undefined): number {
let segments = path.split("/");
// 初始权重为路径长度
let initialScore = segments.length;
// 路由通配符 * :-2
if (segments.some(isSplat)) {
initialScore += splatPenalty;
}
// 默认路由:+2
if (index) {
initialScore += indexRouteValue;
}
return segments
.filter((s) => !isSplat(s))
.reduce(
(score, segment) =>
score +
(paramRe.test(segment)
? dynamicSegmentValue // 动态路由:+3
: segment === ""
? emptySegmentValue // 空路由:+1
: staticSegmentValue), // 静态路由:+10
initialScore
);
}
b. 排序 —— rankRouteBranches
function rankRouteBranches(branches: RouteBranch[]): void {
branches.sort((a, b) =>
a.score !== b.score
? b.score - a.score // Higher score first
: compareIndexes(
a.routesMeta.map((meta) => meta.childrenIndex),
b.routesMeta.map((meta) => meta.childrenIndex)
)
);
}
3、路由匹配与合并 —— matchRouteBranch
function matchRouteBranch<ParamKey extends string = string>(
branch: RouteBranch,
pathname: string
): RouteMatch<ParamKey>[] | null {
let { routesMeta } = branch;
let matchedParams = {};
let matchedPathname = "/";
let matches: RouteMatch[] = [];
for (let i = 0; i < routesMeta.length; ++i) {
...
// 匹配路由
let match = matchPath(
{ path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
remainingPathname
);
if (!match) return null;
Object.assign(matchedParams, match.params);
let route = meta.route;
matches.push({
params: matchedParams,
pathname: joinPaths([matchedPathname, match.pathname]),
pathnameBase: normalizePathname(
joinPaths([matchedPathname, match.pathnameBase])
),
route,
});
if (match.pathnameBase !== "/") {
// 合并路由
matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
}
}
return matches;
}
路由渲染阶段 —— _renderMatches
export interface RouteMatch<ParamKey extends string = string> {
// 动态参数
params: Params<ParamKey>;
// 当前匹配的 pathname
pathname: string;
// 父路由匹配好的 pathname
pathnameBase: string;
// The route object that was used to match.
route: RouteObject;
}
export function _renderMatches(
matches: RouteMatch[] | null,
parentMatches: RouteMatch[] = []
): React.ReactElement | null {
if (matches == null) return null;
// 从右往左遍历
return matches.reduceRight((outlet, match, index) => {
// 把前一项的 element ,作为下一项的 outlet
return (
<RouteContext.Provider
children={
match.route.element !== undefined ? match.route.element : outlet
}
value={{
outlet,
matches: parentMatches.concat(matches.slice(0, index + 1)),
}}
/>
);
}, null as React.ReactElement | null);
}
**outlet** 是通过不断递归生成的组件,所以我们在外层使用的 **`<Outlet />`** **是包含有所有子组件的聚合组件。**
整体概括一下 useRoutes 做的事情:
- 获取上下文中调用
useRoutes后的信息,如果有信息证明此次调用时作为子路由使用的,需要合并父路由的匹配信息。 - 移除父路由已经匹配完毕的
pathname前缀后,调用matchRoutes与当前传入的routes配置相匹配,返回匹配到的matches数组。 - 调用
_renderMatches方法,渲染上一步得到的matches数组。
子路由渲染 —— Outlet & useOutlet
> Outlet - A component that renders the next match in a set of [matches](https://reactrouter.com/en/v6.3.0/getting-started/concepts#match).
从上一小节的内容很容易就能猜到,子路由的渲染就是使用的 useContext 获取 RouteContext.Provider 中的 outlet 属性。同样,react-router 也提供了两种调用方式:<Outlet />与useOutlet。
// 在 outlet 中传入的上下文信息
const OutletContext = React.createContext<unknown>(null);
/**
* 可以在嵌套的 routes 中使用,
* 这里的上下文信息是用户在使用 <Outlet /> 或者 useOutlet 时传入的
*/
export function useOutletContext<Context = unknown>(): Context {
return React.useContext(OutletContext) as Context;
}
/**
* 拿到当前的 outlet,这里可以直接传入 outlet 的上下文信息
*/
export function useOutlet(context?: unknown): React.ReactElement | null {
let outlet = React.useContext(RouteContext).outlet;
// 当 context 有值时才使用 OutletContext.Provider
if (outlet) {
return (
<OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
);
}
// 如果没有值会继续沿用父路由的 OutletContext.Provider 中的值
return outlet;
}
export interface OutletProps {
// 可以传入要提供给 outlet 内部元素的上下文信息
context?: unknown;
}
/**
* Outlet 组件
*/
export function Outlet(props: OutletProps): React.ReactElement | null {
return useOutlet(props.context);
}
- react-router 中使用
<Outlet />或useOutlet渲染子路由,实际就是渲染RouteContext中的outlet属性。 <Outlet />和useOutlet中可以传入上下文信息,在子路由中使用useOutletContext获取。传入该参数会覆盖掉父路由的上下文信息,如果不传,则会由内向外获取上下文信息。
路由跳转 —— Navigate & useNavigate
和子路由的渲染一样,react-router 同样提供了两种路由跳转的方式:<Navigate />与useNavigate。
// useNavigate 返回的 navigate 函数定义,可以传入 to 或者传入数字控制浏览器页面栈的显示
export interface NavigateFunction {
(to: To, options?: NavigateOptions): void;
(delta: number): void;
}
export interface NavigateOptions {
// 是否替换当前栈
replace?: boolean;
// 当前导航的 state
state?: any;
}
/**
* 返回的 navigate 函数可以传和文件夹相同的路径规则
*/
export function useNavigate(): NavigateFunction {
...
// Router 提供的 navigator,本质是 history 对象
let { basename, navigator } = React.useContext(NavigationContext);
// 当前路由层级的 matches 对象(我们在前面说了,不同的 RouteContext.Provider 层级不同该值不同)
let { matches } = React.useContext(RouteContext);
let { pathname: locationPathname } = useLocation();
// 依次匹配到的子路由之前的路径(/* 之前)
let routePathnamesJson = JSON.stringify(
matches.map(match => match.pathnameBase)
);
...
// 返回的跳转函数
let navigate: NavigateFunction = React.useCallback(
(to: To | number, options: NavigateOptions = {}) => {
if (!activeRef.current) return;
// 如果是数字
if (typeof to === "number") {
navigator.go(to);
return;
}
// 实际路径的获取
let path = resolveTo(
to,
JSON.parse(routePathnamesJson),
locationPathname
);
// 有 basename,加上 basename
if (basename !== "/") {
path.pathname = joinPaths([basename, path.pathname]);
}
(!!options.replace ? navigator.replace : navigator.push)(
path,
options.state
);
},
[basename, navigator, routePathnamesJson, locationPathname]
);
return navigate;
}
export interface NavigateProps {
to: To;
replace?: boolean;
state?: any;
}
/**
* 组件式导航,当页面渲染后立刻调用 navigate 方法,很简单的封装
*/
export function Navigate({ to, replace, state }: NavigateProps): null {
// 必须在 Router 上下文中
...
let navigate = useNavigate();
React.useEffect(() => {
navigate(to, { replace, state });
});
return null;
}
react-router 中使用 <Navigate /> 或 useNavigate 跳转路由,但实际内部是使用的 NavigationContext提 供的 navigator 对象(也就是 history 库提供的路由跳转对象)。
React Router v5 、 v6、v7
React 19 + React-Router v7 超级详细、实用、好理解的优雅动态路由懒加载
总结
React 是怎么实现自己的路由机制:
- React Router 路由导航核心库 —— history。该库基于浏览器 History API 实现了 URL 的更新和浏览器历史堆栈的监听;
- 随后从我们熟悉的 React Router 组件开始,深入源码,从最开始的
Router上下文讲起,讲到了两种路由配置方式及其实现原理;了解了Route只是传递参数的工具人,为用户提供命令式的路由配置方式;Routes是怎么实现路由上下文解析、路由匹配以及路由渲染的;最后拓展了子路由渲染Outlet和路由跳转Navigate的工作机制。
最后在了解了 v6 的实现机制的基础上,从组件层面和使用层面对比了 React Router v5 和 v6,更直观的看到 v6 相较于 v5 在实际使用上的差异点。
参考:
图解 history api 和 React Router 实现原理
react-router browserHistory刷新页面404问题解决