React19 路由方案与原理详解
相关资料
- reactrouter.com/
- github.com/molefrog/wo...
- github.com/remix-run/r...
- tanstack.com/router/late...
- developer.mozilla.org/en-US/docs/...
相关面试真题
React Router 路由历史的几种模式及其实现原理
Browser History
Browser History 使用的是 HTML5的 history.pushState 和 history.replaceState APl。这种模式能够创建真实的 URL结构(例如 example.com/about),而不会重新加载页面。
实现原理
- history.pushState:用于向历史堆栈中添加一个新条目,并跳转到对应的 URL。
- history.replaceState:用于替换当前的历史条目,而不会创建新的记录。
- popstate 事件:当用户点击浏览器的后退/前进按钮时触发,React Router 会监听这个事件并相应地更新页面。
优点
- URL 与服务器路由一致,利于 SEO 和用户体验。
- 可以使用浏览器的前进和后退功能。
缺点
- 需要服务器配置以支持所有应用内路由
Hash History
Hash History 使用 URL 的hash 部分来模拟不同的路径(例如 example.com/#/about)。这种模式不需要服务器配置,因为 hash 部分不会被发送到服务器。
实现原理
- window.location.hash:用于获取和设置 URL 的 hash部分。
- hashchange 事件:当 URL 的hash部分变化时触发,React Router 会监听这个事件并相应地更新页面。
优点
- 简单易用,不需要服务器配置。
- 在老旧浏览器上兼容性好。
缺点
- URL结构不美观,不利于 SEO
Memory History
Memory History 将历史记录保存在内存中,而不与浏览器的地址栏同步。通常用于非浏览器环境,例如 React Native 或测试环境。
实现原理
- 维护一个内部堆栈来管理历史记录。
- 无需依赖浏览器的历史 API,完全在内存中操作。
优点
- 适用于非浏览器环境。
- 不依赖于 URL,可以自由操作历史记录。
缺点
- 不能通过地址栏直接访问特定页面。
Static History
Static History 为了支持SSR而引入的一种历史模式,主要运用在服务端渲染应用中,它与前端的 Browser History 不同,它主要用于在服务器端生成页面时模拟浏览器的行为。 访客
实现原理
- createStaticHandler 和 createStaticRouter 提供了静态版本的路由处理和路由器配置,专为SSF 设计。
- 它们允许在服务器端解析路由,并生成对应的路由状态,以便在服务端渲染时使用。
优点
- createstaticHandler:用于创建静态路由处理器,它不依赖于浏览器环境,可以在服务器端解析路径,生成路由匹配结果。
- createstaticRouter:用于创建静态路由器,同样适用于服务器端,它根据提供的初始路径和路由配置来初始化路由器状态。
- StaticRouterProvider:结合 StaticRouter,它负责在服务器端提供路由上下文,使得 useRoutes 和其他路由钩子能够正常工作。
缺点
- 灵活性不足:静态路由无法在运行时动态添加新路由,限制了应用的动态扩展能力。
- 维护困难:随着应用规模增长,静态路由表变得庞大,管理维护成本增加。
- 处理动态路由能力有限:不适合处理复杂的动态路由需求,如RESTful风格的路由,可能需要预先定义大量参数情况。
- SEO挑战:在复杂应用中,静态路由可能影响SEO,尤其是动态生成内容时。
- 测试复杂性:需要为每个路由编写测试用例,路由数量增加导致测试维护困难。
- 性能影响:初始加载时加载所有路由信息,可能增加加载时间,尤其是在路由数量多时。
react-router、react-router-dom、react-router-native 的联系与设计原因
联系
- react-router:核心库,提供了路由的核心功能,不依赖于具体的平台。可以用于任何环境下实现路由功能。
- react-router-dom:基于 react-router,专门为 Web 环境设计,包含 BrowserRouter 和 HashRouter 等适用于浏览器的组件。
- react-router-native: #F react-router 专门为 React Native 环境设计,包含 NativeRouter 于移动应用的组件。
设计原因
- 模块化设计:将核心功能与平台特定实现分离,使得 react-router 能够独立于平台工作,增强了灵活性和可移植性。
- 复用核心逻辑:通过共享 react-router 的核心逻辑, react-router-dom 和 react-routernative 能够复用基本的路由机制,减少了重复代码,提高了维护性。
- 适应不同环境:不同平台有不同的需求,例如浏览器环境需要处理URL,而移动应用则处理组件状态。将平台特定实现分离出来,使得每个模块可以专注于解决特定环境的问题。
动态路由的使用和实现原理
动态路由是指在路径中包含变量部分的路由,例如 /users/:id。React Router 通过路径参数实现动态路由。
实现原理
- 路径匹配:React Router 使用路径模式匹配的方式来解析 URL 中的变量部分。例如,路径 /users/:id 匹配 URL /users/123,并将 123 提取为 id 参数。
- useParams Hook:useParams 是一个 React Hook,用于访问当前匹配的路径参数。它返回一个对象,其中包含所有路径参数的键值对。
- 路由配置:通过定义带有变量部分的路径模式(如 :id),React Router 能够自动解析和提取 URL 中的对应部分,并将其传递给对应的组件。
优点
- 路由定义更加灵活,能够处理复杂的 URL结构。
- 易于实现基于参数的页面,例如用户详情页、文章详情页等。
React Router 基础使用
- 因为在早期,路由使用来定位资源的,url(统一资源定位符/ Uniform Resource Locators)、uri (Uniform Resource Identifier/统一资源标识符),之前的路由是真实跟静态资源映射的。
- 在后来,SPA应用出现后,前端想要自己控制路由的跳转以及路由所对应资源的处理,于是乎出现了框架对应的路由,React 的React-Router, Vue 的Vue-Router
- React Router 是 React 应用程序中用于管理路由的标准库。以下是对 React Router v6.23.1版本的基础使用的详细说明。
详细 API 说明
基础组件
BrowserRouter
- BrowserRouter 使用 HTML5 history API(如 pushState、replacestate)来保持UI 与URL 同步。适用于普通的 web 应用程序。
Routes 和 Route
- Routes: 包含应用中所有路由定义的容器。用于包裹多个 Route 组件。
- Route:用于定义路径和对应的组件。
Link
- Link 组件用于在应用内进行导航。它会渲染一个带有 href 属性的 标签,点击后不会刷新页面。
[###### Outlet
- Outlet 组件用于渲染匹配的子路由组件。它是嵌套路由的占位符。
useParams
- useParams 返回当前路由的参数。例如在路径 /users/:id , id 是一个参数。
useNavigate
- useNavigate 返回一个路由跳转函数,可以使用这个函数轻松实现路由跳转
redirect
- redirect 是一个工具函数,在应用中任何位置都可以调用,用于重定向到给定页面。
redirectDocument
- redirectDocument 是一个工具函数,在应用中任何位置都可以调用,用于重定向到给定页面,需要注意的是, 这个方法是文档级别的重定向,整体表现接近于 window.location。
useBlocker
- 当跳转到其他路由的时候,类似于路由守卫
useBeforeUnload
- 页面离开的时候
进阶功能
动态路由
- 动态路由允许在路径中包含变量。例如,路径 /users/:id,匹配 URL /users/123,并将 123 提取为 id参数
路由保护
- 通过使用自定义组件和逻辑,可以实现路由保护,例如验证用户是否已登录。
其它定义形式的用法
基于 JSON 配置(推荐)
- 新版 React-Router 整体使用相对灵活,我们可以基于json 配置数据使用路由,也可以通过 createRoutesFromELements
基于 createRoutesFromElements
组件式定义
因为在早期,路由使用来定位资源的,url(统一资源定位符/ Uniform Resource Locators)、uri (Uniform Resource Identifier/统一资源标识符),之前的路由是真实跟静态资源映射的。
在后来,SPA应用出现后,前端想要自己控制路由的跳转以及路由所对应资源的处理,于是乎出现了框架对应的路由,React 的React-Router, Vue 的Vue-Router
多类型历史记录栈
react-router 为了满足开发者更多路由历史存储场景,提供了以下几种模式
- 浏览器原生历史记录浏览器
- hash
- 内存型
- 服务端记录
- createBrowserRouter:浏览器提供的历史管理
- createHashRouter(不推荐):基于 hash的路由管理,#hello,但是呢通常 #又可以作为锚链接
- createMemoryRouter:内存型路由,路由的管理存储在内存中
- createStaticRouter(用于 SSR):SSR 服务端的
createBrowserRouter
通过浏览器原生路由进行路由态管理,页面跳转通过 pushState、popState 方法实现
需要注意的是,使用 browserRouter,一般都需要使用类似 Nginx做静态资源代理,另外需要注意404的情况,一般都需要添加 try_files处理
createHashRouter
他的用武之地就在于,我们没有Nginx 作为静态资源代理,我们可能就无法使用浏览器历史作为我们路由状态的存储,这时可以选择 hash router 方案,但是注意,真的非常不推荐。
createMemoryRouter
用于创建一个内存型路田,路由表与历史记录栈存储在内存中,当页面刷新时,路由信息丢失。
其实这种内存型历史记录,我们自己通过状态管理都能够轻松实现,他这就类似于我们定义了集中状态,然后当状态更新时渲染不同页面。而这里只是多了一些关于路由操作方法的实现,比如:push、pop 等。
createStaticRouter
如果我们需要实现服务端渲染,那么在服务端的路由处理则需要使用该 api,因为我们知道客户端的路由是基于浏览器的 history,而服务端是没有浏览器环境的。
React Router 实现原理剖析
React-Router 实现最需要关注的就以下几方面:
- 路由值到视图的映射规则
- 对于路由变更的监听
- 路由操作方法
- 路由记录存储与操作
BrowserRouter 实现流程
History 是整个浏览器路由历史记录大对象,其中包含长度等属性
Location 是单一页面所对应的资源定位对象,我们可以理解为当页面跳转时,先生成 Location,然后将 Location push El History.
浏览器路由实现,最主要的两个概念是变更路由与监听路由变更。
- 历史操作,(注:此操作不会触发 popState)
- history.pushState
- history.replaceState
- 监听变更
- window.addEventListener ("popstate", () => ({})
- 操作,(注:以下操作会触发 popState)
- history.back()
- history.forward()
- history.go()
以下是基于BrowserRouter
实现流程的代码示例,按照您提到的五个步骤逐一实现。每个步骤的代码都清晰地对应了其功能,并附带注释以便理解。
创建 Router
首先,我们需要创建一个 Router
组件。它将负责管理应用的路由状态,并提供上下文供子组件访问路由信息。
javascript
import React, { createContext, useState, useEffect } from 'react';
// 创建路由上下文
export const RouterContext = createContext();
// Router 组件
export function Router({ children }) {
// 状态管理:location 和 history
const [location, setLocation] = useState(window.location.pathname);
// 提供上下文给子组件
return (
<RouterContext.Provider value={{ location, setLocation }}>
{children}
</RouterContext.Provider>
);
}
承载 history
在 BrowserRouter
中,我们使用浏览器的 history
API 来管理路由历史记录。这里我们将封装一个简单的 history
对象。
javascript
// 创建 history 对象
export const createBrowserHistory = () => {
const history = {
// 当前路径
location: window.location.pathname,
// 历史记录栈
stack: [window.location.pathname],
index: 0,
// 推入新路径
push(path) {
this.location = path;
this.stack.push(path);
this.index++;
window.history.pushState({}, '', path);
},
// 替换当前路径
replace(path) {
this.location = path;
this.stack[this.index] = path;
window.history.replaceState({}, '', path);
},
// 返回上一页
goBack() {
if (this.index > 0) {
this.index--;
this.location = this.stack[this.index];
window.history.back();
}
},
// 前进到下一页
goForward() {
if (this.index < this.stack.length - 1) {
this.index++;
this.location = this.stack[this.index];
window.history.forward();
}
},
};
return history;
};
确定 location
location
是当前的 URL 路径。我们需要监听 popstate
事件来同步浏览器的历史记录和应用的状态。
javascript
export function BrowserRouter({ children }) {
// 创建 history 对象
const history = createBrowserHistory();
// 使用 Router 组件管理 location
return (
<Router>
<RouterContext.Consumer>
{({ setLocation }) => {
// 监听 popstate 事件
useEffect(() => {
const handlePopState = () => {
setLocation(window.location.pathname);
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [setLocation]);
return children;
}}
</RouterContext.Consumer>
</Router>
);
}
历史记录栈变更监听
我们需要确保每次路由变化时,history
的状态与 location
同步。这可以通过在 push
和 replace
方法中触发更新来实现。
javascript
export const useHistory = () => {
const { location, setLocation } = React.useContext(RouterContext);
const history = createBrowserHistory();
// 封装 push 方法
const push = (path) => {
history.push(path);
setLocation(path);
};
// 封装 replace 方法
const replace = (path) => {
history.replace(path);
setLocation(path);
};
return { location, push, replace, goBack: history.goBack, goForward: history.goForward };
};
处理 popState 逻辑
popstate
事件会在用户点击浏览器的前进或后退按钮时触发。我们需要在此事件中更新应用的状态。
javascript
export function usePopState() {
const { setLocation } = React.useContext(RouterContext);
useEffect(() => {
const handlePopState = () => {
setLocation(window.location.pathname);
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [setLocation]);
}
完整的代码整合
javascript
import React, { createContext, useState, useEffect } from 'react';
// 创建路由上下文
export const RouterContext = createContext();
// Router 组件
export function Router({ children }) {
const [location, setLocation] = useState(window.location.pathname);
return (
<RouterContext.Provider value={{ location, setLocation }}>
{children}
</RouterContext.Provider>
);
}
// 创建 history 对象
export const createBrowserHistory = () => {
const history = {
location: window.location.pathname,
stack: [window.location.pathname],
index: 0,
push(path) {
this.location = path;
this.stack.push(path);
this.index++;
window.history.pushState({}, '', path);
},
replace(path) {
this.location = path;
this.stack[this.index] = path;
window.history.replaceState({}, '', path);
},
goBack() {
if (this.index > 0) {
this.index--;
this.location = this.stack[this.index];
window.history.back();
}
},
goForward() {
if (this.index < this.stack.length - 1) {
this.index++;
this.location = this.stack[this.index];
window.history.forward();
}
},
};
return history;
};
// BrowserRouter 组件
export function BrowserRouter({ children }) {
const history = createBrowserHistory();
return (
<Router>
<RouterContext.Consumer>
{({ setLocation }) => {
useEffect(() => {
const handlePopState = () => {
setLocation(window.location.pathname);
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [setLocation]);
return children;
}}
</RouterContext.Consumer>
</Router>
);
}
// 自定义 hook:useHistory
export const useHistory = () => {
const { location, setLocation } = React.useContext(RouterContext);
const history = createBrowserHistory();
const push = (path) => {
history.push(path);
setLocation(path);
};
const replace = (path) => {
history.replace(path);
setLocation(path);
};
return { location, push, replace, goBack: history.goBack, goForward: history.goForward };
};
MemoryRouter 的实现流程
- 创建 History 实例
MemoryRouter
使用createMemoryHistory
方法创建一个内存型的history
对象。这个对象负责管理路由状态,包括路径栈、当前路径等。 - 封装 Router
MemoryRouter
是对Router
组件的封装,它将history
对象传递给Router
,从而实现路由功能。 - 统一接口协议
无论是MemoryRouter
还是BrowserRouter
,它们都遵循相同的接口协议,开发者可以无缝切换路由类型,而无需修改业务逻辑代码。
代码示例
以下是一个完整的 MemoryRouter
实现示例:
jsx
import React from "react";
import { Router, createMemoryHistory } from "react-router-dom";
// 自定义 MemoryRouter 组件
function MemoryRouter({ initialEntries = ["/"], initialIndex, children }) {
// 创建内存型 history 对象
const history = createMemoryHistory({ initialEntries, initialIndex });
return (
// 将 history 传递给 Router
<Router history={history}>
{children}
</Router>
);
}
// 示例:使用 MemoryRouter
function App() {
return (
<MemoryRouter initialEntries={["/", "/about", "/contact"]} initialIndex={0}>
<div>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
{/* 路由匹配 */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
</MemoryRouter>
);
}
// 页面组件
function Home() {
return <h2>Home Page</h2>;
}
function About() {
return <h2>About Page</h2>;
}
function Contact() {
return <h2>Contact Page</h2>;
}
export default App;
代码解析
createMemoryHistory
createMemoryHistory
是 React-Router 提供的一个工具函数,用于创建内存型的history
对象。- 参数:
initialEntries
: 初始路径栈,例如["/", "/about", "/contact"]
。initialIndex
: 初始路径索引,默认为0
。
<Router>
** 组件**
Router
是 React-Router 的核心组件,负责接收history
对象并管理路由状态。MemoryRouter
通过将history
传递给Router
,实现了内存型路由的功能。
- 统一接口协议
MemoryRouter
和BrowserRouter
都遵循相同的接口协议,开发者可以通过<Link>
、<Routes>
和<Route>
等组件实现路由跳转和匹配。
适用场景
- 测试环境 : 在单元测试或集成测试中,
MemoryRouter
可以模拟路由行为,而无需依赖浏览器环境。 - React Native : 在移动端开发中,
MemoryRouter
可以替代BrowserRouter
,因为移动端没有浏览器的历史记录 API。 - 嵌套应用 : 在微前端架构中,子应用可能需要独立管理路由状态,
MemoryRouter
是一个理想的选择。
总结
MemoryRouter
的核心在于通过 createMemoryHistory
创建内存型的路由历史记录,并将其封装到 Router
中。它的设计体现了抽象封装的魅力,使得开发者可以在不同环境中灵活使用路由功能,同时保持一致的 API 接口。
更多 React 路由库方案
@tanstack/router 是一个现代化的路由库,旨在为前端应用提供高效、灵活且可扩展的路由解决方案。它以其简洁的设计和强大的功能受到开发者的青睐。以下是对其主要特性的详细扩写:
基础使用示例
@tanstack/router
提供了直观且易于上手的 API,使得开发者可以快速集成路由功能到项目中。以下是一个简单的基础使用示例:
javascript
import { createRouter, createRoute } from '@tanstack/router'
// 创建路由实例
const rootRoute = createRoute({
path: '/',
component: () => <h1>Welcome to the Home Page</h1>,
})
const aboutRoute = createRoute({
path: '/about',
component: () => <h1>About Us</h1>,
})
const router = createRouter({
routes: [rootRoute, aboutRoute],
})
// 在应用中使用路由
function App() {
return <router.Router />
}
通过上述代码可以看出,@tanstack/router
的 API 设计非常直观,开发者只需定义路由路径和对应的组件,即可轻松实现页面跳转和渲染。
约定式路由(基于文件自动生成)
@tanstack/router
支持约定式路由(Convention-based Routing),这是一种基于文件结构自动生成路由配置的方式。这种方式极大地简化了路由管理,尤其适用于中大型项目,能够显著减少手动配置的工作量。
约定式路由的特点:
- 文件即路由:开发者只需按照约定的目录结构组织文件,路由会自动根据文件路径生成。例如:
plain
src/
pages/
index.js -> 路径: '/'
about.js -> 路径: '/about'
users/
index.js -> 路径: '/users'
[id].js -> 动态路径: '/users/:id'
在这种结构下,@tanstack/router
会自动解析 pages
目录中的文件,并生成对应的路由配置。
- 动态路由支持 :通过文件命名约定(如
[id].js
),可以轻松实现动态路由。动态参数会自动解析并传递给组件,方便处理用户详情、文章详情等场景。 - 嵌套路由:约定式路由天然支持嵌套结构,开发者可以通过文件夹层级定义嵌套路由。例如:
plain
src/
pages/
users/
profile.js -> 路径: '/users/profile'
这种方式使得路由的嵌套关系清晰可见,便于维护。
- 无需手动维护路由表:传统的路由配置需要手动编写路由表,而约定式路由完全省去了这一步骤。开发者只需专注于页面逻辑,路由配置由框架自动生成。
示例:约定式路由的实际应用
假设项目中有如下文件结构:
plain
src/
pages/
index.js
about.js
products/
index.js
[id].js
通过 @tanstack/router
的约定式路由功能,以下路由配置会自动生成:
/
-> 渲染index.js
/about
-> 渲染about.js
/products
-> 渲染products/index.js
/products/:id
-> 渲染products/[id].js
开发者无需手动编写路由表,只需专注于页面组件的开发即可。
总结
@tanstack/router
的核心优势在于其简洁的 API 和强大的功能支持。无论是通过基础使用示例快速上手,还是利用约定式路由简化复杂项目的路由管理,它都能为开发者提供高效的解决方案。对于现代前端应用而言,@tanstack/router
是一个值得尝试的路由库,尤其适合追求开发效率和代码可维护性的团队。
原理浅析
wouter 相较于 react-router,同样有他的一些优势,包括但不限于以下几点:
- 轻量化设计与零依赖性
wouter 的核心设计理念是追求极致的轻量化。相较于 react-router 这样功能全面但相对复杂的路由库,wouter 的体积更小(通常只有几 KB),并且没有任何外部依赖。这种特性使得它非常适合用于小型项目、快速原型开发或需要严格控制 bundle size 的场景。开发者无需担心引入庞大的第三方库对项目性能造成额外负担。 - 简单易用的 API
wouter 提供了更加简洁和直观的 API 设计,降低了学习成本。例如,它的<Router>
和<Route>
组件使用方式与 HTML 原生标签非常接近,初学者可以快速上手而无需深入理解复杂的路由概念。此外,wouter 支持基于 Hook 的编程模式,通过useRoute
等自定义 Hook,开发者能够以更灵活的方式管理路由状态,同时保持代码的可读性和维护性。 - 无缝集成现代 React 特性
wouter 充分利用了 React 的最新特性,例如 Hooks 和 Context API,从而实现了高效的路由管理。与 react-router 不同的是,wouter 并未采用复杂的内部架构,而是直接依赖 React 的原生能力来完成路由逻辑。这不仅减少了冗余代码,还提高了运行效率。对于熟悉 React 新特性的开发者来说,这种实现方式显得更加自然且易于调试。 - 动态路由支持与灵活性
尽管 wouter 的体积较小,但它仍然提供了强大的动态路由支持。通过简单的路径匹配规则,开发者可以轻松定义嵌套路由、参数化路由以及通配符路由。与此同时,wouter 允许用户根据需求自由扩展其功能,例如结合自定义中间件或拦截器实现特定的业务逻辑。这种灵活性使其在应对多样化需求时表现出色。 - 默认支持 Suspense 和懒加载
在现代前端开发中,性能优化是一个重要课题。wouter 内置了对 React Suspense 和懒加载的支持,使得开发者可以方便地延迟加载组件,从而进一步提升应用的初始渲染速度。相比之下,react-router 虽然也支持类似的特性,但往往需要额外配置才能达到同样的效果。 - 社区活跃度与生态友好性
虽然 wouter 的生态规模不及 react-router,但其开源社区始终保持较高的活跃度,并且文档详尽、示例丰富。对于大多数常见问题,开发者都可以快速找到解决方案。此外,wouter 设计之初就考虑到了与其他工具链的兼容性,因此无论是 Next.js、Gatsby 还是普通的 CRA 项目,都能够无障碍地集成 wouter。
综上所述,wouter 凭借其轻量级、易用性和高效性,在某些场景下展现出独特的优势。尽管它可能无法完全取代 react-router 在大型复杂项目中的地位,但对于中小型项目或需要快速迭代的团队而言,无疑是一个值得尝试的选择。
wouter
特点
- 最小依赖,仅2.1KB (gzip 压缩后)对比 React Router 的18.7 KB。
- 同时支持 React和 Preact!阅读"Preact 支持"部分以了解更多详情。
- 没有顶级的 组件,它是完全可选的。
- 模仿 React Router 的最佳实践,提供熟悉的 Route、Link、Switch 和 Redirect 组件。
- 拥有基于 hook 的 API,用于更细粒度地控制路由(如动画):useLocation、 useRoute 和 useRouter .