什么是 Web 路由?
一个比较直观的解释:路由就是 URL 到函数的映射。
/users -> getAllUsers()/users/count -> getUsersCount()/users -> getAllUsers() /users/count -> getUsersCount()/users -> getAllUsers() /users/count -> getUsersCount()
服务端路由(Server-side Routing)
对于服务器来说,当接收到客户端发来的 HTTP 请求,会根据请求的 URL,来找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。
// 当访问 / 的时候,会返回 index 页面app.get('/', (req, res) => {res.sendFile('index');});// 当访问 /users 的时候,会从数据库中读取所有用户数据并返回app.get('/users', (req, res) => {db.queryAllUsers().then(data => res.send(data));});// 当访问 / 的时候,会返回 index 页面 app.get('/', (req, res) => { res.sendFile('index'); }); // 当访问 /users 的时候,会从数据库中读取所有用户数据并返回 app.get('/users', (req, res) => { db.queryAllUsers().then(data => res.send(data)); });// 当访问 / 的时候,会返回 index 页面 app.get('/', (req, res) => { res.sendFile('index'); }); // 当访问 /users 的时候,会从数据库中读取所有用户数据并返回 app.get('/users', (req, res) => { db.queryAllUsers().then(data => res.send(data)); });
客户端路由(Client-side Routing)
对于客户端(如:浏览器)来说,路由的映射函数通常是进行一些 DOM 的显示和隐藏操作。即当访问不同的路径的时候,会显示不同的页面组件。
客户端路由只是控制 DOM 元素的显示和隐藏,即使页面的 URL 发生变化,也不会重新加载整个页面。
为什么需要客户端路由 —— 单页应用 ( SPA )
单页应用(英语:single-page application,缩写 SPA)是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,而非传统的从服务器重新加载整个新页面。这种方法避免了页面之间切换打断用户体验,使应用程序更像一个桌面应用程序。在单页应用中,所有必要的代码(HTML、JavaScript 和 CSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面。
传统 web 网站 vs SPA
- 传统 web 网站
每次请求都需要重新获取并渲染整个页面。
- SPA
面对日益增长的网页需求,网页开始走向模块化、组件化的道路。随之而来的是代码的难以维护、不可控、迭代艰难等问题。为了解决这些问题,不少优秀的现代前端框架如: React 、 Vue 、 Angular 等著名单页面应用框架纷纷问世。这些框架有一个共同的特点 —— “通过 JS 控制页面的渲染”。
传统网站每次请求都会重新获取新的 HTML 页面,单页应用除了第一次请求会加载所有的页面样式,之后请求只是为了获取组件渲染所需要的数据。
SPA 基本实现原理
我们可以用非常简单的 JS 语句来理解一下单页应用的基本实现原理。
<body><div id="root"></div><script>const root = document.getElementById('root');const divNode = document.createElement('div');divNode.innerText = '我是 inner text';root.appendChild(divNode);</script></body><body> <div id="root"></div> <script> const root = document.getElementById('root'); const divNode = document.createElement('div'); divNode.innerText = '我是 inner text'; root.appendChild(divNode); </script> </body><body> <div id="root"></div> <script> const root = document.getElementById('root'); const divNode = document.createElement('div'); divNode.innerText = '我是 inner text'; root.appendChild(divNode); </script> </body>
现在我们知道了 SPA 最基本的实现原理,如果我们有几十个组件需要切换和跳转,应该怎么办?
—— 没错,客户端路由就是为了解决这个问题而存在,即解决如何通过切换浏览器地址,来匹配和渲染对应的页面组件。
怎么实现客户端路由 —— History API
目标: 根据不同 URL 渲染页面组件,但不刷新页面。
浏览器在用户浏览时维护自己的历史堆栈。在传统网站(没有 JavaScript 的 HTML 文档)中,每次用户单击链接、提交表单或单击后退和前进按钮时,浏览器都会向服务器发出请求,而客户端路由的出现,让开发人员能够以编程方式操作浏览器历史堆栈。
History
接口允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。具体细节可参考:History – Web APIs | MDN
- pushState: 创建一个新的 URL,并跳转至该 URL(可通过 back 回到修改之前的 URL)
- replaceState:替换当前 URL 为指定 URL(通过 back 无法再次回到被替换掉的 URL);
- back:返回后一个 URL
- forward:返回前一个 URL
- go:跳转到指定页面的 URL
这些 API 有一个共同的特点:可以修改浏览器地址栏显示的 URL,但不会重新加载页面。
注:在调用 go 方法时,如果没有传参则会与调用 location.reload() 一样,会重新加载页面。
调用 History API 修改 URL
调用 history.pushState
可以实现更新 URL,但不刷新页面
window.history.pushState({page: 1}, '', "/detail");window.history.pushState({page: 1}, '', "/detail");window.history.pushState({page: 1}, '', "/detail");
Demo:
从上面的 Demo 可以看出:
- 我们通过 pushState API 创建了新的 URL ,更新了浏览器地址栏,并且没有引起页面的刷新,但也没有触发 UI 组件的渲染;
- 点击浏览器 back(←)按钮,可以回到上一次的 URL;
- 点击浏览器 forward(→)按钮,再次回到我们创建的 URL,这次浏览器根据路由对 UI 组件进行了重新渲染。
监听 URL(路由) 变化,匹配路由并渲染对应 UI 组件
如何在 URL 更新时,根据新的路由去匹配和渲染对应的 UI 组件?
在同一 HTML 文档的两个历史记录条目之间导航时会触发 popstate 事件,很容易想到我们可以通过监听 popstate 事件来处理 URL (路由)更新后,根据当前路由匹配并渲染相应 UI 组件的事宜。更多细节可以参考下面的 MDN 文档。
从上面的 demo 我们可以知道可以通过 history.pushState()
或者 history.replaceState()
去更新 URL,但是浏览器并没有重新渲染与之对应的 UI 组件。因为调用 history.pushState()
和history.replaceState()
不会触发 popstate 事件,也就是说浏览器并没有监听到 URL 已经发生改变,自然也不会做出后续的动作。
没有条件创建条件,例如可以在每次调用 history.pushState()
和 history.replaceState()
之后主动的去触发 popstate 事件。
history.pushState(state, '', url);let popStateEvent = new PopStateEvent('popstate', { state: state });dispatchEvent(popStateEvent);history.pushState(state, '', url); let popStateEvent = new PopStateEvent('popstate', { state: state }); dispatchEvent(popStateEvent);history.pushState(state, '', url); let popStateEvent = new PopStateEvent('popstate', { state: state }); dispatchEvent(popStateEvent);
之后我们就可以通过监听 popstate 事件去处理 URL 更新后的一系列事情了。
window.onpopstate = function(event) {// URL changed// 根据当前路由匹配相应组件// 渲染相应 UI 组件// ...};window.onpopstate = function(event) { // URL changed // 根据当前路由匹配相应组件 // 渲染相应 UI 组件 // ... };window.onpopstate = function(event) { // URL changed // 根据当前路由匹配相应组件 // 渲染相应 UI 组件 // ... };
上述两个关键步骤就是一个客户端路由实现的核心思想,下文我们将通过阅读 React Router 的源码,来了解 React 的路由机制是怎么实现的。
拓展 —— 微前端架构路由劫持
什么是微前端架构
微前端 ( Micro-Frontends ) 是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用,应用之间相互隔离。
组合式应用路由分发
每个子应用独立构建和部署,运行时由基座应用来进行路由管理,应用加载,启动,卸载,以及通信机制。
暂时无法在飞书文档外展示此内容
基座应用大多数是一个 SPA 项目,然而在 SPA 架构下,路由变化是不会引发页面刷新的,并且子应用之间独立部署,相互隔离。因此在实现微前端路由分发机制的时候,我们需要感知路由的变化,并以此为依据判断是否需要切换子应用。
回顾一下客户端路由实现的两个核心点:
- 通过 History API 更新 URL;
- 监听 URL 更新,渲染与之匹配的 UI 组件。
由此我们可以推广到微前端路由劫持的基本思路:
- 我们可以通过劫持更新 URL 的 API,在切换到具体的子应用之前保存相应的 popstate 事件;
export const hijackRoute = () => {// 劫持 pushState APIwindow.history.pushState = (...args) => {// 调用原有方法originalPush.apply(window.history, args);// URL 更新后处理子应用逻辑// ...};// 劫持 replaceState APIwindow.history.replaceState = (...args) => {originalReplace.apply(window.history, args);// URL 改变逻辑// ...};// popstate 监听事件window.addEventListener("popstate", () => {});// 劫持 popstate 监听事件window.addEventListener = hijackEventListener(window.addEventListener);window.removeEventListener = hijackEventListener(window.removeEventListener);};const capturedListeners: Record<EventType, Function[]> = {hashchange: [],popstate: [],};const hasListeners = (name: EventType, fn: Function) => {return capturedListeners[name].filter((listener) => listener === fn).length;};const hijackEventListener = (func: Function): any => {return function (name: string, fn: Function) {// 为子应用保存 popstate 回调函数if (name === "popstate") {if (!hasListeners(name, fn)) {// addcapturedListeners[name].push(fn);return;} else {// removecapturedListeners[name] = capturedListeners[name].filter((listener) => listener !== fn); }}return func.apply(window, arguments);};};export const hijackRoute = () => { // 劫持 pushState API window.history.pushState = (...args) => { // 调用原有方法 originalPush.apply(window.history, args); // URL 更新后处理子应用逻辑 // ... }; // 劫持 replaceState API window.history.replaceState = (...args) => { originalReplace.apply(window.history, args); // URL 改变逻辑 // ... }; // popstate 监听事件 window.addEventListener("popstate", () => {}); // 劫持 popstate 监听事件 window.addEventListener = hijackEventListener(window.addEventListener); window.removeEventListener = hijackEventListener(window.removeEventListener); }; const capturedListeners: Record<EventType, Function[]> = { hashchange: [], popstate: [], }; const hasListeners = (name: EventType, fn: Function) => { return capturedListeners[name].filter((listener) => listener === fn).length; }; const hijackEventListener = (func: Function): any => { return function (name: string, fn: Function) { // 为子应用保存 popstate 回调函数 if (name === "popstate") { if (!hasListeners(name, fn)) { // add capturedListeners[name].push(fn); return; } else { // remove capturedListeners[name] = capturedListeners[name].filter((listener) => listener !== fn); } } return func.apply(window, arguments); }; };export const hijackRoute = () => { // 劫持 pushState API window.history.pushState = (...args) => { // 调用原有方法 originalPush.apply(window.history, args); // URL 更新后处理子应用逻辑 // ... }; // 劫持 replaceState API window.history.replaceState = (...args) => { originalReplace.apply(window.history, args); // URL 改变逻辑 // ... }; // popstate 监听事件 window.addEventListener("popstate", () => {}); // 劫持 popstate 监听事件 window.addEventListener = hijackEventListener(window.addEventListener); window.removeEventListener = hijackEventListener(window.removeEventListener); }; const capturedListeners: Record<EventType, Function[]> = { hashchange: [], popstate: [], }; const hasListeners = (name: EventType, fn: Function) => { return capturedListeners[name].filter((listener) => listener === fn).length; }; const hijackEventListener = (func: Function): any => { return function (name: string, fn: Function) { // 为子应用保存 popstate 回调函数 if (name === "popstate") { if (!hasListeners(name, fn)) { // add capturedListeners[name].push(fn); return; } else { // remove capturedListeners[name] = capturedListeners[name].filter((listener) => listener !== fn); } } return func.apply(window, arguments); }; };
- 根据 URL 分发到相应的子应用;
- 调用 popstate 的回调函数,处理子应用路由匹配和渲染工作。
// 渲染子应用后,执行当前子应用回调事件export function callCapturedListeners() {if (historyEvent) {Object.keys(capturedListeners).forEach((eventName) => {const listeners = capturedListeners[eventName as EventType]if (listeners.length) {listeners.forEach((listener) => {listener.call(this, historyEvent)});}});historyEvent = null}}// 渲染子应用后,执行当前子应用回调事件 export function callCapturedListeners() { if (historyEvent) { Object.keys(capturedListeners).forEach((eventName) => { const listeners = capturedListeners[eventName as EventType] if (listeners.length) { listeners.forEach((listener) => { listener.call(this, historyEvent) }); } }); historyEvent = null } }// 渲染子应用后,执行当前子应用回调事件 export function callCapturedListeners() { if (historyEvent) { Object.keys(capturedListeners).forEach((eventName) => { const listeners = capturedListeners[eventName as EventType] if (listeners.length) { listeners.forEach((listener) => { listener.call(this, historyEvent) }); } }); historyEvent = null } }
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
history
object 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});let history = createBrowserHistory(); history.listen(({ location, action }) => { // this is called whenever new locations come in // the action is POP, PUSH, 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;// 当前 locationlocation: Partial<Location> | string;// 当前路由跳转的类型,有 POP,PUSH 与 REPLACE 三种navigationType?: NavigationType;// history 中的导航对象,在这里传入统一 historynavigator: 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.Providerchildren={children}value={{ location, navigationType }}/></NavigationContext.Provider>);}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> ); }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,用于在内部获取当前 locationconst LocationContext = React.createContext<LocationContextObject>(null!);// 仅在内部使用,外部不推荐使用/** @internal */export {NavigationContext as UNSAFE_NavigationContext,LocationContext as UNSAFE_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, };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 (<Routerbasename={basename}children={children}location={state.location}navigationType={state.action}navigator={history}/>);}/** * 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} /> ); }/** * 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 thehash
portion 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();});});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(); }); });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>.`);}/** * 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>.` ); }/** * 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;}export interface PathRouteProps { caseSensitive?: boolean; children?: React.ReactNode; // 子路由 element?: React.ReactNode | null; index?: false; path: string; }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><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><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;}export interface LayoutRouteProps { children?: React.ReactNode; element?: React.ReactNode | null; }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><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><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;}export interface IndexRouteProps { element?: React.ReactNode | null; index: true; }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><Routes> <Route path="/teams" element={<Teams />}> <Route path="/teams/:teamId" element={<Team />} /> <Route path="/teams/new" element={<NewTeamForm />} /> {/* 默认路由 */} <Route index element={<LeagueStandings />} /> </Route> </Routes><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);}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); }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;}/** * 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; }/** * 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
useRoutes
hook 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
useRoutes
is either a valid React element you can use to render the route tree, ornull
if 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(...);}/** * 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(...); }/** * 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;}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; }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: [],});interface RouteContextObject { outlet: React.ReactElement | null; matches: RouteMatch[]; } export const RouteContext = React.createContext<RouteContextObject>({ outlet: null, matches: [], });interface RouteContextObject { outlet: React.ReactElement | null; matches: RouteMatch[]; } export const RouteContext = React.createContext<RouteContextObject>({ outlet: null, matches: [], });
路由匹配阶段 —— matchRoutes
matchRoutes
runs the route matching algorithm for a set of routes against a givenlocation
to see which routes (if any) match. If it finds a match, an array ofRouteMatch
objects is returned, one for each route that matched.This is the heart of React Router’s matching algorithm. It is used internally by
useRoutes
and the<Routes> component
to determine which routes match the current location.
function matchRoutes(routes,locationArg,basename){...// 获取当前路由 pathnamelet pathname = stripBasename(location.pathname || "/", basename);...// 扁平化 routes 并计算权重let branches = flattenRoutes(routes);// 根据权重排序 routesrankRouteBranches(branches);let matches = null;for (let i = 0; matches == null && i < branches.length; ++i) {// 路由匹配与合并matches = matchRouteBranch(branches[i], pathname);}return matches;}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; }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; }
- 路由扁平化(方便排序)
routes = [{path: "路由 A",children: [{path: "路由 A-1"}, {path: "路由 A-2"}]},{path: "路由 B",children: [{path: "路由 B-1"}, {path: "路由 B-2"}]}]routes = [{ path: "路由 A", children: [{ path: "路由 A-1" }, { path: "路由 A-2" }] }, { path: "路由 B", children: [{ path: "路由 B-1" }, { path: "路由 B-2" }] }]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" }]routes = [{path: "路由 A-1"}, {path: "路由 A-2"}, { path: "路由 A" }, {path: "路由 B-1"}, { path: "路由 B-2"}, {path: "路由 B" }]routes = [{path: "路由 A-1"}, {path: "路由 A-2"}, { path: "路由 A" }, {path: "路由 B-1"}, { path: "路由 B-2"}, {path: "路由 B" }]
- 路由权值计算与排序
a. 计算权重 —— computeScore
// 动态路由权重,比如 /foo/:idconst 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;// 路由通配符 * :-2if (segments.some(isSplat)) {initialScore += splatPenalty;}// 默认路由:+2if (index) {initialScore += indexRouteValue;}return segments.filter((s) => !isSplat(s)).reduce((score, segment) =>score +(paramRe.test(segment)? dynamicSegmentValue // 动态路由:+3: segment === ""? emptySegmentValue // 空路由:+1: staticSegmentValue), // 静态路由:+10initialScore);}// 动态路由权重,比如 /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 ); }// 动态路由权重,比如 /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)));}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) ) ); }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;}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; }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>;// 当前匹配的 pathnamepathname: string;// 父路由匹配好的 pathnamepathnameBase: 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 ,作为下一项的 outletreturn (<RouteContext.Providerchildren={match.route.element !== undefined ? match.route.element : outlet}value={{outlet,matches: parentMatches.concat(matches.slice(0, index + 1)),}}/>);}, null as React.ReactElement | null);}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); }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 />`** **是包含有所有子组件的聚合组件。****outlet** 是通过不断递归生成的组件,所以我们在外层使用的 **`<Outlet />`** **是包含有所有子组件的聚合组件。****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).> Outlet - A component that renders the next match in a set of [matches](https://reactrouter.com/en/v6.3.0/getting-started/concepts#match).> 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.Providerif (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);}// 在 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); }// 在 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;// 当前导航的 statestate?: 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,加上 basenameif (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;}// 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; }// 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 vs v6
总结
本文首先由三个问题:
- “什么是 Web 路由?” 引出服务端路由和客户端路由的定义;
- “为什么需要客户端路由?”,深入理解了客户端路由存在的意义是:实现 SPA 在不刷新页面的同时更新 URL,并渲染与之匹配的 UI 组件;
- “怎么实现客户端路由?”,了解了客户端路由的基本实现思路是调用 History API 更新 URL,监听 URL 的变化,匹配并渲染对应的 UI 组件。
为后续介绍 React Router 的实现机制做好铺垫。随后深入本文的主题,从 React Router 源码层面,剖析了 React 是怎么实现自己的路由机制:
- 有了前面的铺垫,我们很自然的可引出 React Router 路由导航核心库 —— history。该库基于浏览器 History API 实现了 URL 的更新和浏览器历史堆栈的监听;
- 随后从我们熟悉的 React Router 组件开始,深入源码,从最开始的
Router
上下文讲起,讲到了两种路由配置方式及其实现原理;了解了Route
只是传递参数的工具人,为用户提供命令式的路由配置方式;Routes
是怎么实现路由上下文解析、路由匹配以及路由渲染的;最后拓展了子路由渲染Outlet
和路由跳转Navigate
的工作机制。
最后在了解了 v6 的实现机制的基础上,从组件层面和使用层面对比了 React Router v5 和 v6,更直观的看到 v6 相较于 v5 在实际使用上的差异点。