react-router 的模式核心分为了三种吧:
1. 框架使用层面的完成和实现
2. 数据使用层面的完成和实现(推荐吧)
3. 声明式的使用层面的完成和实现
React-Router 发展史
声明式路由
在这个阶段我们的路由的管理模式实现的是进行对应的将路由看作的是组件树的一部分吧,以及核心和我们的react 的声明式编程范式是相互结合的讷
该阶段的编码形式为:
路由的配置是直接嵌套在组件树中的,和组件的生命周期是紧密联系的讷
路由的切换会导致组件树的重新渲染吧,和 React 的更新机制是一致的讷
但是这个时候我们的数据的获取和路由是完全分离的讷,一般是在组件挂载后 componentDisMount 或者 useEffect 中进行获取数据实现吧,这就导致了加载状态的分散和数据依赖的不明确的问题吧
import { BrowserRouter as Router, Route, Switch } from "react-router-dom"
// 开始在我们的 APP 跟组件中进行对应的结构化设置我们的路由信息吧
function App() {
return (
<React.Fragment>
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/" component={HomePage} />
<Route path="/" component={HomePage} />
</Switch>
</React.Fragment>
)
}
function Users() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
})
}, [])
return (<></>)
}框架式路由
在我们的这个阶段的化,react-router 实现提供了很多的框架层面的钩子函数和API,以便于更加精准的控制路由行为吧,同时也实现了支持代码分割和懒加载吧
表现特征
提供了 useHistory useLocation useParams 等 hooks API,让我们可以快速的访问我们的路由信息吧
引入了 Suspense 和 懒加载实现,支持了代码分割的实现吧
但是数据的获取任然存在一定的问题,依旧分散在组件中进行的获取数据吧,导致状态的管理和错误的处理变得十分的复杂吧
但是开发最好的点就是可以不用在组件内部进行编写对应的懒加载的反馈组件了,直接在 Suspense 中进行统一反馈给用户吧
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
const Users = lazy(() => import('./Users'));
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/users" component={Users} />
</Switch>
</Suspense>
</Router>
);
}数据驱动路由
这个阶段的路由模式的话,实现了数据获取和路由信息进行绑定的实现吧,让路由可以预先加载数据,并管理加载状态和错误的状态吧,核心的设计理念来源于 Remix 的开发框架吧
表现特性是
路由配置集中化的管理实现吧,并且可以定义多个路由的数据加载函数(loader)和动作函数(action)吧
数据的加载和路由切换实现同步执行,避免了加载状态的闪烁和请求瀑布流吧
提供了错误边界处理,可以实现统一的处理加载错误和提交错误吧
支持表单提交和数据突变吧,并自动实现处理以及重新验证吧
import {
createBrowserRouter,
RouterProvider,
Outlet,
useLoaderData,
Form,
redirect
} from 'react-router-dom';
// 定义路由配置
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <Home />,
loader: () => fetch('/api/home-data').then(res => res.json())
},
{
path: 'users',
element: <Users />,
loader: () => fetch('/api/users').then(res => res.json())
},
{
path: 'users/:id',
element: <UserDetail />,
loader: ({ params }) =>
fetch(`/api/users/${params.id}`).then(res => {
if (!res.ok) throw new Response('Not Found', { status: 404 });
return res.json();
}),
errorElement: <ErrorBoundary />,
action: async ({ request, params }) => {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
await fetch(`/api/users/${params.id}`, {
method: 'PATCH',
body: JSON.stringify(updates),
headers: { 'Content-Type': 'application/json' }
});
return redirect(`/users/${params.id}`);
}
}
]
}
]);
function Layout() {
return (
<div>
<nav>
<Link to="/">Home</Link>
<Link to="/users">Users</Link>
</nav>
<Outlet />
</div>
);
}
function Users() {
const users = useLoaderData(); // 获取loader返回的数据
return (
<div>
{users.map(user => (
<div key={user.id}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</div>
))}
</div>
);
}
function UserDetail() {
const user = useLoaderData();
return (
<div>
<h1>{user.name}</h1>
<Form method="post">
<input name="name" defaultValue={user.name} />
<button type="submit">Update</button>
</Form>
</div>
);
}
function App() {
return <RouterProvider router={router} />;
}声明式路由:简单易用,但数据获取分散,难以管理加载状态和错误。
框架化路由:提供了更多控制,但数据获取仍然由组件处理,加载状态和错误处理仍然复杂。
数据驱动路由:将数据获取与路由绑定,集中管理加载状态和错误,提供了更优的用户体验和开发体验。
Demo 案例吧(慢慢分析即可吧)
import {
createBrowserRouter,
RouterProvider,
useLoaderData,
useActionData,
Form,
Link,
Outlet,
useNavigation,
useFetcher
} from 'react-router-dom'
// 1. 集中式路由配置
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <GlobalErrorBoundary />,
children: [
{
index: true,
element: <Dashboard />,
loader: dashboardLoader,
// 数据预取:用户悬停时预加载
shouldPreload: ({ href }) => {
return document.querySelector(`a[href="${href}"]`)?.matches(':hover')
}
},
{
path: 'projects',
element: <ProjectsLayout />,
// 并行加载:同时获取项目和团队数据
loader: async ({ request }) => {
const [projects, team] = await Promise.all([
fetch('/api/projects').then(r => r.json()),
fetch('/api/team').then(r => r.json())
])
return { projects, team }
},
children: [
{
index: true,
element: <ProjectList />,
// 骨架屏 + 渐进式加载
lazy: () => import('./components/ProjectList')
},
{
path: ':projectId',
element: <ProjectDetail />,
loader: projectLoader,
// 依赖数据:依赖父路由数据
shouldRevalidate: ({ currentParams, nextParams }) => {
return currentParams.teamId !== nextParams.teamId
}
}
]
},
{
path: 'users',
element: <UserManagement />,
loader: usersLoader,
// 表单操作
action: async ({ request }) => {
const formData = await request.formData()
const intent = formData.get('intent')
switch (intent) {
case 'create':
return createUser(formData)
case 'update':
return updateUser(formData)
case 'delete':
return deleteUser(formData.get('id'))
default:
throw new Error('Unknown intent')
}
}
}
]
}
])
// 2. 智能错误边界
function GlobalErrorBoundary() {
const error = useRouteError()
const navigation = useNavigation()
// 自动重试机制
const [retryCount, setRetryCount] = useState(0)
const handleRetry = () => {
setRetryCount(prev => prev + 1)
// 清除错误缓存,重新加载
router.navigate(navigation.location.pathname, { replace: true })
}
if (retryCount > 3) {
return <FallbackUI error={error} />
}
return (
<div className="error-boundary">
<h2>Oops! Something went wrong</h2>
<button onClick={handleRetry}>
Retry ({retryCount}/3)
</button>
<details>
<summary>Error Details</summary>
<pre>{error.stack}</pre>
</details>
</div>
)
}
// 3. 数据依赖管理器
function ProjectDetail() {
const { project, team } = useLoaderData()
const navigation = useNavigation()
const fetcher = useFetcher()
// 乐观更新
const [optimisticProject, setOptimisticProject] = useState(project)
const handleUpdate = async (updates) => {
setOptimisticProject(prev => ({ ...prev, ...updates }))
fetcher.submit(
{ ...updates, intent: 'update' },
{ method: 'post', action: `/projects/${project.id}` }
)
}
// 骨架屏过渡
if (navigation.state === 'loading') {
return <ProjectSkeleton />
}
return (
<div>
<h1>{optimisticProject.name}</h1>
<TeamSelect
team={team}
currentTeamId={project.teamId}
onChange={handleUpdate}
/>
</div>
)
}
// 4. 实时数据同步
function Dashboard() {
const initialData = useLoaderData()
const [data, setData] = useState(initialData)
// WebSocket 实时更新
useEffect(() => {
const ws = new WebSocket('/api/dashboard/ws')
ws.onmessage = (event) => {
const update = JSON.parse(event.data)
setData(prev => mergeUpdates(prev, update))
}
return () => ws.close()
}, [])
// 离线支持
const [isOnline, setIsOnline] = useState(navigator.onLine)
useEffect(() => {
const handleOnline = () => setIsOnline(true)
const handleOffline = () => setIsOnline(false)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
return (
<div>
{!isOnline && <OfflineBanner />}
<RealTimeMetrics data={data} />
</div>
)
}// 权限路由守卫系统
function createProtectedRouter(user) {
return createBrowserRouter([
{
path: '/',
element: <AuthLayout user={user} />,
// 全局数据加载
loader: async () => {
const [notifications, preferences] = await Promise.all([
fetchNotifications(user.id),
fetchUserPreferences(user.id)
])
return { notifications, preferences }
},
children: [
{
path: 'admin',
element: <AdminRoute />,
// 路由级权限检查
loader: async () => {
if (!user.isAdmin) {
throw new Response('Forbidden', { status: 403 })
}
return fetchAdminData()
},
// 路由分割点
lazy: () => import('./admin/Routes')
},
{
path: 'settings',
element: <Settings />,
// 表单操作链
action: async ({ request }) => {
const formData = await request.formData()
// 验证
const validation = validateSettings(formData)
if (!validation.valid) {
return validation.errors
}
// 保存
const result = await saveSettings(formData, user.id)
// 重新验证相关路由
router.revalidate('/profile')
router.revalidate('/dashboard')
return result
}
}
]
}
])
}
// 智能预取系统
function SmartLink({ to, children }) {
const [shouldPrefetch, setShouldPrefetch] = useState(false)
useEffect(() => {
const link = document.querySelector(`a[href="${to}"]`)
if (!link) return
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setShouldPrefetch(true)
}
})
},
{ threshold: 0.1 }
)
observer.observe(link)
return () => observer.disconnect()
}, [to])
useEffect(() => {
if (shouldPrefetch) {
// 预取路由数据
router.prefetch(to)
// 预取依赖的组件
import(`./pages${to}`).then(module => {
// 缓存组件
componentCache.set(to, module.default)
})
}
}, [shouldPrefetch, to])
return <Link to={to}>{children}</Link>
}// 1. 零加载状态路由
const router = createBrowserRouter([
{
path: '/products',
element: <ProductCatalog />,
// 使用流式数据
loader: async ({ request }) => {
const response = fetch('/api/products/stream')
const reader = response.body.getReader()
const decoder = new TextDecoder()
return {
// 返回一个可迭代的流
stream: async function* () {
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
const products = JSON.parse(chunk)
yield products
}
}
}
}
}
])
function ProductCatalog() {
const { stream } = useLoaderData()
const [products, setProducts] = useState([])
useEffect(() => {
// 流式渲染产品列表
const processStream = async () => {
for await (const chunk of stream()) {
setProducts(prev => [...prev, ...chunk])
}
}
processStream()
}, [stream])
return (
<div>
{/* 立即渲染已加载的部分 */}
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
{/* 流式加载时没有明显的加载状态 */}
</div>
)
}
// 2. 智能缓存 + 后台同步
function createOptimisticRouter() {
const cache = new Map()
return createBrowserRouter([
{
path: '/todos',
element: <TodoApp />,
loader: async ({ request }) => {
const url = new URL(request.url)
const cacheKey = `todos-${url.searchParams}`
// 检查缓存
if (cache.has(cacheKey)) {
const cached = cache.get(cacheKey)
// 后台刷新
fetch('/api/todos')
.then(res => res.json())
.then(freshData => {
if (!deepEqual(cached, freshData)) {
cache.set(cacheKey, freshData)
// 通知 UI 更新
router.emit('data:updated', { key: cacheKey, data: freshData })
}
})
return cached
}
const data = await fetch('/api/todos').then(r => r.json())
cache.set(cacheKey, data)
return data
},
action: async ({ request }) => {
const formData = await request.formData()
const todo = Object.fromEntries(formData)
// 乐观更新
const cacheKey = 'todos'
const current = cache.get(cacheKey) || []
cache.set(cacheKey, [...current, { ...todo, id: Date.now(), status: 'optimistic' }])
try {
const result = await saveTodo(todo)
// 更新缓存
cache.set(cacheKey, current.map(t =>
t.status === 'optimistic' ? result : t
))
return result
} catch (error) {
// 回滚
cache.set(cacheKey, current.filter(t => t.status !== 'optimistic'))
throw error
}
}
}
])
}React-Router 生态结合
和Redux结合
// ==================================================================================
// 先实现定义我们的 routerSlice 吧
// @reduxjs/toolkit 就是redux的工具库实现的是进行简化redux的实现吧
import { createSlice } from "@reduxjs/toolkit";
// 定义我们的 routerSlice 吧
const routerSlice = createSlice({
name: 'router',
initialState: {
currentPage: '/',
params: {},
search: '',
},
reducers: {
setCurrentPage: (state, action) => {
state.currentPage = action.payload;
state.params = action.payload.params;
state.search = action.payload.search;
}
},
extraReducers: (builder) => {
builder.addCase(setCurrentPage, (state, action) => {
state.currentPage = action.payload.path;
state.params = action.payload.params;
state.search = action.payload.search;
})
}
});
// 导出我们的 routerSlice 吧
export const { setCurrentPage } = routerSlice.actions;
export default routerSlice.reducer;
// ==================================================================================
// 由于redux 的设计理念是我们的单一数据源,所以说单独的 storeSlice 需要进行统一的注册一下吧
/**
* redux 的核心步骤是
* 1. 定义我们的 storeSlice 吧
* 2. 配置我们的 store 吧
* 3. 导出我们的 store 吧
* 数据的流程是
* 1. 组件 dispatch 一个 action 实现的是进行触发状态更新的实现吧
* 2. reducer 接收到 action 实现的是进行状态更新的实现吧
* 3. store 中的状态发生变化实现的是进行状态更新的实现吧
* 4. 组件通过 useSelector 订阅状态变化实现的是进行状态更新的实现吧
* redux 核心特点
* 1. 单一数据源
* 2. 状态是只读的
* 3. 使用纯函数进行状态更新
* 4. middleware 中间件支持
* 5. 热模块替换支持
* 6. 可预测的状态更新
*/
import { configureStore } from "@reduxjs/toolkit";
// 配置我们的 store 吧
const store = configureStore({
// reducer 是我们的状态管理库实现的是进行状态管理的实现吧
reducer: {
router: routerSlice.reducer
},
// 配置 middleware 的中间件支持吧
middleware: (getDefaultMiddleware) => getDefaultMiddleware(),
});
// 导出我们的 store 吧
export {
store
};
// ==================================================================================
// react-redux 作用是:连接 React 组件与 Redux 状态管理系统,
// 使得组件可以订阅 Redux 状态的变化,并且可以 dispatch actions 来更新同步状态的实现吧
// useSelector 作用:就类似于我们的 getter 实现的是进行获取得到 redux 中的数据并且实现数据二次处理吧
// useDispatch 作用:就是进行 dispatch 实现的是进行触发 action 实现的是进行更新状态的实现吧,通知状态管理库的数据同步更新实现吧
import { useSelector, useDispatch } from "react-redux";
// react-router-dom 就是react应用 csr 模式下的路由实现吧
// useNavigate 作用:就是进行路由跳转实现的是进行页面跳转的实现吧
// useParams 作用:就是进行获取路由参数实现的是进行获取路由参数的实现吧
// useLocation 作用:就是进行获取当前路由信息实现的是进行获取当前路由信息的实现吧
import { useNavigate, useParams, useLocation } from "react-router-dom";
// 就是自定义的路由状态管理工具的实现吧
import { setCurrentPage } from './store/slices/routerSlice';
function UserPage() {
const dispatch = useDispatch(); // 触发 action 实现的是进行状态更新的实现吧
const params = useParams(); // 获取路由参数实现的是进行获取路由参数的实现吧
const location = useLocation(); // 获取当前路由信息实现的是进行获取当前路由信息的实现吧
const navigate = useNavigate(); // 路由跳转实现的是进行页面跳转的实现吧
React.useEffect(() => {
dispatch(setCurrentPage({ path: location.pathname, params, search: location.search }));
// 并且实现路由的跳转
navigate(location.pathname + location.search);
}, [dispatch, location, params]);
const currentPage = useSelector((state) => state.router.currentPage);
const currentParams = useSelector((state) => state.router.params);
const currentSearch = useSelector((state) => state.router.search);
return (
<div>
<h1>当前路由信息</h1>
<p>当前路由路径:{currentPage}</p>
<p>当前路由参数:{JSON.stringify(currentParams)}</p>
<p>当前路由查询参数:{currentSearch}</p>
</div>
)
}和zustand的结合
首先我们的 zustand 是一个轻量的状态管理库吧
// zustand 是一个轻量级的状态库吧
import create from "zustand";
import { useLocation, useNavigate } from "react-router-dom";
const useRouterStore = create((set) => ({
currentPage: '/',
params: {},
search: '',
setCurrentPage: (page) => set({
currentPage: page.path,
params: page.params,
search: page.search
}),
}));
function RouteSync() {
const location = useLocation();
const navigate = useNavigate(); // 路由跳转实现的是进行页面跳转的实现吧
const setCurrentPage = useRouterStore(state => state.setCurrentPage);
React.useEffect(() => {
setCurrentPage({ path: location.pathname, params, search: location.search });
// 并且实现路由的跳转
navigate(location.pathname + location.search);
}, [dispatch, location, params]);
return null;
}
路由元数据
import { createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <HomePage />,
// 元数据
meta: {
title: '首页',
requiresAuth: false,
breadcrumb: 'Home',
},
},
{
path: 'dashboard',
element: <Dashboard />,
meta: {
title: '仪表板',
requiresAuth: true,
breadcrumb: 'Dashboard',
},
loader: dashboardLoader,
},
{
path: 'profile',
element: <Profile />,
meta: {
title: '个人资料',
requiresAuth: true,
breadcrumb: 'Profile',
},
},
],
},
]);
// types.ts - 类型定义
interface RouteMeta {
// 基础信息
title: string | ((params: Record<string, string>) => string)
description?: string
keywords?: string[]
// SEO 相关
seo?: {
canonical?: string
robots?: 'index' | 'noindex' | 'follow' | 'nofollow'
og?: {
type: 'website' | 'article' | 'product'
image: string
}
}
// 权限控制
security: {
requiresAuth: boolean
roles?: UserRole[]
permissions?: string[]
// 数据权限
dataScopes?: {
entity: string
access: 'read' | 'write' | 'admin'
}[]
}
// 性能优化
performance?: {
prefetch?: boolean
preload?: 'none' | 'data' | 'component' | 'both'
cache?: {
strategy: 'network-first' | 'cache-first' | 'stale-while-revalidate'
ttl: number
}
}
// UI/UX
ui?: {
layout: 'default' | 'auth' | 'admin' | 'dashboard'
hideHeader?: boolean
hideFooter?: boolean
theme?: 'light' | 'dark' | 'auto'
// 面包屑
breadcrumb?: {
title: string
parent?: string
showInNav?: boolean
}
// 过渡动画
transition?: {
enter: 'fade' | 'slide' | 'zoom'
exit: 'fade' | 'slide' | 'zoom'
duration: number
}
}
// 数据依赖
dataDependencies?: {
[key: string]: {
endpoint: string
method: 'GET' | 'POST'
params?: string[] // 从路由参数映射
transform?: (data: any) => any
}
}
// 分析追踪
analytics?: {
pageView: boolean
event?: string
customDimensions?: Record<string, string>
}
}网络请求思考
也是可以和我们的 ReactQuery 和 useSWR 进行相互结合的讷
用户交互层 → 路由协调层 → 状态管理层 → 数据服务层
↑ ↑ ↑ ↑
路由组件 ← 路由元数据 ← 状态原子 ← API适配器React-Router 的常用 hooks
useNavigate
是我们的编程式路由导航的核心吧,实现的是我们的进行对应的实现一定的编程式路由导航的跳转吧
// 开始实现我们 useNavigator 的简单使用吧
import { useNavigate } from "react-router-dom";
const NavigatorManager = {
navigate: useNavigate(),
navigateTo: (path) => NavigatorManager.navigate(
path + search,
),
navigateToWithParams: (path, params = {}) => NavigatorManager.navigateTo(
path,
'',
params
),
goBack: () => NavigatorManager.navigate(-1),
goForward: () => NavigatorManager.navigate(1),
goHome: () => NavigatorManager.navigate('/'),
goTo: (path) => NavigatorManager.navigateTo(path),
goToWithParams: (path, params = {}) => NavigatorManager.navigateToWithParams(
path,
params
),
}
const useNavigator = () => NavigatorManager;
useLocation
实现的是获取得到我们的本地连接的一些信息吧
import { useLocation, Location } from 'react-router-dom'
import { useEffect, useMemo, useState } from 'react'
function LocationTracker() {
const location = useLocation<{
from?: string
modal?: boolean
timestamp?: number
}>()
// 监听位置变化
useEffect(() => {
console.log('位置变化:', {
pathname: location.pathname,
search: location.search,
hash: location.hash,
state: location.state,
key: location.key // 唯一标识每次导航
})
// 发送分析事件
trackPageView(location.pathname)
// 恢复滚动位置
if (location.state?.preserveScroll) {
window.scrollTo(0, savedScrollPosition)
} else {
window.scrollTo(0, 0)
}
// 清理函数
return () => {
saveScrollPosition(window.scrollY)
}
}, [location])
// 解析查询参数
const queryParams = useMemo(() => {
const params = new URLSearchParams(location.search)
return Object.fromEntries(params.entries())
}, [location.search])
// 检查是否是模态框
const isModal = useMemo(() => {
return location.state?.modal === true
}, [location.state])
// 获取路由来源
const from = useMemo(() => {
return location.state?.from || '/'
}, [location.state])
// 路径匹配检查
const isActivePath = (path: string) => {
return location.pathname.startsWith(path)
}
// 哈希变化监听
useEffect(() => {
if (location.hash) {
const element = document.getElementById(location.hash.slice(1))
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}
}
}, [location.hash])
// 深度监听状态变化
const [previousState, setPreviousState] = useState<Location['state']>(null)
useEffect(() => {
if (previousState !== location.state) {
console.log('状态变化:', {
from: previousState,
to: location.state
})
setPreviousState(location.state)
}
}, [location.state, previousState])
return (
<div>
<p>当前路径: {location.pathname}</p>
<p>查询参数: {JSON.stringify(queryParams)}</p>
<p>路由来源: {from}</p>
{isModal && <div className="modal-backdrop" />}
</div>
)
}useParmas
实现的是获取得到一些相关的动态参数的信息吧
import { useParams, Params } from 'react-router-dom'
import { useEffect, useMemo } from 'react'
interface RouteParams {
id: string
slug?: string
tab?: 'info' | 'settings' | 'analytics'
}
function ParamHandler() {
const params = useParams<RouteParams>()
// 类型安全的参数访问
const productId = params.id ? parseInt(params.id, 10) : null
const slug = params.slug || 'default'
const activeTab = (params.tab as RouteParams['tab']) || 'info'
// 参数验证
useEffect(() => {
if (!productId || isNaN(productId)) {
console.error('无效的产品ID:', params.id)
// 重定向到错误页面或显示404
}
// 检查slug格式
if (slug && !/^[a-z0-9-]+$/.test(slug)) {
console.warn('非法的slug格式:', slug)
}
}, [productId, slug, params.id])
// 参数依赖的数据获取
useEffect(() => {
if (productId) {
fetchProduct(productId)
}
}, [productId])
// 生成参数相关的链接
const generateTabUrl = useMemo(() => {
return (tab: RouteParams['tab']) => {
const base = `/products/${productId}/${slug}`
return tab === 'info' ? base : `${base}/${tab}`
}
}, [productId, slug])
// 多个参数的组合逻辑
const isDetailView = useMemo(() => {
return !!productId && !!slug
}, [productId, slug])
// 处理可选参数
const hasOptionalParam = useMemo(() => {
return !!params.tab // 检查可选参数是否存在
}, [params.tab])
// 参数变化时清理副作用
useEffect(() => {
let isSubscribed = true
const loadData = async () => {
if (productId && isSubscribed) {
const data = await fetchProduct(productId)
if (isSubscribed) {
// 处理数据
}
}
}
loadData()
return () => {
isSubscribed = false
// 清理操作
cancelPendingRequests(productId)
}
}, [productId])
return (
<div>
<h1>产品ID: {productId}</h1>
<h2>Slug: {slug}</h2>
<div className="tabs">
{(['info', 'settings', 'analytics'] as const).map(tab => (
<a
key={tab}
href={generateTabUrl(tab)}
className={activeTab === tab ? 'active' : ''}
>
{tab}
</a>
))}
</div>
</div>
)
}useSearchParams
查询参数的处理吧
import { useSearchParams, URLSearchParamsInit } from 'react-router-dom'
import { useCallback, useMemo, useEffect } from 'react'
type QueryParams = {
page?: string
sort?: 'asc' | 'desc'
filter?: string[]
search?: string
view?: 'list' | 'grid'
}
function QueryParamManager() {
const [searchParams, setSearchParams] = useSearchParams()
// 获取单个参数
const page = searchParams.get('page') || '1'
const sort = (searchParams.get('sort') as 'asc' | 'desc') || 'asc'
const search = searchParams.get('search') || ''
const view = (searchParams.get('view') as 'list' | 'grid') || 'list'
// 获取数组参数
const filters = useMemo(() => {
return searchParams.getAll('filter')
}, [searchParams])
// 设置参数 - 替换模式
const setPage = useCallback((newPage: number) => {
setSearchParams(prev => {
const newParams = new URLSearchParams(prev)
newParams.set('page', newPage.toString())
return newParams
})
}, [setSearchParams])
// 设置参数 - 合并模式
const updateParams = useCallback((updates: Partial<QueryParams>) => {
setSearchParams(prev => {
const newParams = new URLSearchParams(prev)
Object.entries(updates).forEach(([key, value]) => {
if (value === undefined || value === null) {
newParams.delete(key)
} else if (Array.isArray(value)) {
newParams.delete(key)
value.forEach(v => newParams.append(key, v))
} else {
newParams.set(key, value.toString())
}
})
return newParams
})
}, [setSearchParams])
// 批量更新参数
const applyFilters = useCallback((newFilters: string[]) => {
setSearchParams(prev => {
const newParams = new URLSearchParams(prev)
// 删除所有现有的filter参数
newParams.delete('filter')
// 添加新的filter参数
newFilters.forEach(filter => {
newParams.append('filter', filter)
})
return newParams
})
}, [setSearchParams])
// 重置参数
const resetParams = useCallback(() => {
setSearchParams({}, { replace: true })
}, [setSearchParams])
// 监听参数变化
useEffect(() => {
const params = Object.fromEntries(searchParams.entries())
console.log('查询参数变化:', params)
// 触发搜索
if (params.search) {
debouncedSearch(params.search)
}
}, [searchParams])
// 同步参数到状态
const currentPage = useMemo(() => {
return parseInt(page, 10) || 1
}, [page])
// 参数持久化
useEffect(() => {
// 保存到sessionStorage
sessionStorage.setItem('queryParams', searchParams.toString())
}, [searchParams])
// 从URL解析复杂对象
const parsedParams = useMemo(() => {
const params: QueryParams = {}
for (const [key, value] of searchParams.entries()) {
if (key === 'page') {
params.page = value
} else if (key === 'sort') {
params.sort = value as 'asc' | 'desc'
} else if (key === 'filter') {
if (!params.filter) params.filter = []
params.filter.push(value)
} else if (key === 'search') {
params.search = value
} else if (key === 'view') {
params.view = value as 'list' | 'grid'
}
}
return params
}, [searchParams])
return (
<div>
<div className="controls">
<button onClick={() => setPage(currentPage + 1)}>
下一页
</button>
<select
value={sort}
onChange={(e) => updateParams({ sort: e.target.value as 'asc' | 'desc' })}
>
<option value="asc">升序</option>
<option value="desc">降序</option>
</select>
<input
type="text"
value={search}
onChange={(e) => updateParams({ search: e.target.value })}
placeholder="搜索..."
/>
<button onClick={resetParams}>
重置
</button>
</div>
<div className="filters">
{['featured', 'onsale', 'new'].map(filter => (
<label key={filter}>
<input
type="checkbox"
checked={filters.includes(filter)}
onChange={(e) => {
const newFilters = e.target.checked
? [...filters, filter]
: filters.filter(f => f !== filter)
applyFilters(newFilters)
}}
/>
{filter}
</label>
))}
</div>
<pre>
当前参数: {JSON.stringify(parsedParams, null, 2)}
</pre>
</div>
)
}useLoaderData
路由数据的处理吧
import {
useLoaderData,
LoaderFunctionArgs,
json,
defer,
Await
} from 'react-router-dom'
import { Suspense } from 'react'
// 1. 基本数据加载
export async function productLoader({ params, request }: LoaderFunctionArgs) {
const productId = params.id
// 验证参数
if (!productId) {
throw new Response('Product ID is required', { status: 400 })
}
// 获取数据
const product = await fetchProduct(productId)
// 缓存控制
return json(product, {
status: 200,
headers: {
'Cache-Control': 'public, max-age=300',
'ETag': generateETag(product)
}
})
}
function ProductPage() {
const product = useLoaderData<typeof productLoader>()
// 类型安全的数据访问
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>价格: ${product.price}</p>
</div>
)
}
// 2. 延迟数据加载
export async function dashboardLoader({ request }: LoaderFunctionArgs) {
// 立即获取用户信息
const userPromise = fetchUser()
// 延迟加载大文件数据
const reportsPromise = fetchReports().then(data => {
// 数据处理
return processReports(data)
})
// 并行加载多个数据源
const [notifications, settings] = await Promise.all([
fetchNotifications(),
fetchUserSettings()
])
return defer({
user: userPromise,
reports: reportsPromise, // 延迟加载
notifications, // 立即加载
settings
})
}
function DashboardPage() {
const { user, reports, notifications, settings } =
useLoaderData<typeof dashboardLoader>()
return (
<div>
{/* 立即显示的部分 */}
<UserProfile user={user} />
<NotificationsList notifications={notifications} />
{/* 延迟加载的部分 */}
<Suspense fallback={<ReportsLoading />}>
<Await resolve={reports}>
{(resolvedReports) => (
<ReportsDashboard reports={resolvedReports} />
)}
</Await>
</Suspense>
</div>
)
}
// 3. 错误处理
function ProductDetail() {
const product = useLoaderData<typeof productLoader>()
// 数据验证
if (!product) {
return <div>产品不存在</div>
}
if (product.status === 'archived') {
return (
<div className="archived-product">
<h2>该产品已下架</h2>
<p>您可以查看类似产品</p>
</div>
)
}
return <ProductView product={product} />
}useActionData
表单提交响应
import {
useActionData,
ActionFunctionArgs,
json,
Form
} from 'react-router-dom'
import { useEffect, useState } from 'react'
export async function loginAction({ request }: ActionFunctionArgs) {
const formData = await request.formData()
const email = formData.get('email')
const password = formData.get('password')
const remember = formData.get('remember') === 'on'
// 验证输入
const errors: Record<string, string> = {}
if (!email) {
errors.email = '邮箱不能为空'
} else if (!isValidEmail(email.toString())) {
errors.email = '邮箱格式不正确'
}
if (!password) {
errors.password = '密码不能为空'
} else if (password.toString().length < 6) {
errors.password = '密码至少6位'
}
if (Object.keys(errors).length > 0) {
return json({
success: false,
errors,
timestamp: new Date().toISOString()
}, { status: 400 })
}
try {
// 执行登录逻辑
const user = await authenticate(email.toString(), password.toString())
// 设置cookie或session
if (remember) {
setRememberMeCookie(user.id)
}
return json({
success: true,
user,
message: '登录成功',
redirect: '/dashboard'
})
} catch (error) {
return json({
success: false,
error: error instanceof Error ? error.message : '登录失败',
timestamp: new Date().toISOString()
}, { status: 401 })
}
}
function LoginForm() {
const actionData = useActionData<typeof loginAction>()
const [isSubmitting, setIsSubmitting] = useState(false)
// 处理action响应
useEffect(() => {
if (actionData) {
if (actionData.success) {
// 登录成功,跳转
navigate(actionData.redirect || '/')
// 显示成功消息
showToast(actionData.message)
} else {
// 登录失败,显示错误
setIsSubmitting(false)
if (actionData.errors) {
// 表单验证错误
Object.values(actionData.errors).forEach(error => {
showError(error)
})
} else if (actionData.error) {
// 服务器错误
showError(actionData.error)
}
}
}
}, [actionData])
const handleSubmit = (event: React.FormEvent) => {
setIsSubmitting(true)
// Form组件会自动处理提交
}
return (
<Form method="post" onSubmit={handleSubmit}>
<div>
<label htmlFor="email">邮箱:</label>
<input
type="email"
id="email"
name="email"
defaultValue=""
aria-invalid={actionData?.errors?.email ? 'true' : 'false'}
/>
{actionData?.errors?.email && (
<div className="error">{actionData.errors.email}</div>
)}
</div>
<div>
<label htmlFor="password">密码:</label>
<input
type="password"
id="password"
name="password"
defaultValue=""
aria-invalid={actionData?.errors?.password ? 'true' : 'false'}
/>
{actionData?.errors?.password && (
<div className="error">{actionData.errors.password}</div>
)}
</div>
<div>
<label>
<input type="checkbox" name="remember" />
记住我
</label>
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '登录中...' : '登录'}
</button>
{/* 显示通用错误 */}
{actionData?.error && !actionData.errors && (
<div className="alert error">
{actionData.error}
</div>
)}
{/* 显示时间戳用于调试 */}
{actionData?.timestamp && (
<div className="timestamp">
请求时间: {new Date(actionData.timestamp).toLocaleString()}
</div>
)}
</Form>
)
}useFetcher
后台数据处理吧
import { useFetcher, FetcherWithComponents } from 'react-router-dom'
import { useEffect, useState } from 'react'
function LikeButton({ productId }: { productId: string }) {
const fetcher = useFetcher<{ success: boolean; likes: number }>()
const [optimisticLikes, setOptimisticLikes] = useState(0)
// 获取当前喜欢状态
useEffect(() => {
fetcher.load(`/api/products/${productId}/likes`)
}, [productId])
// 乐观更新
const handleLike = () => {
// 立即更新UI
setOptimisticLikes(prev => prev + 1)
// 提交表单
fetcher.submit(
{ action: 'like' },
{ method: 'post', action: `/api/products/${productId}/like` }
)
}
// 处理响应
useEffect(() => {
if (fetcher.data) {
if (fetcher.data.success) {
// 成功,更新状态
setOptimisticLikes(fetcher.data.likes)
} else {
// 失败,回滚乐观更新
setOptimisticLikes(prev => prev - 1)
showError('操作失败,请重试')
}
}
}, [fetcher.data])
// 显示当前状态
const currentLikes = fetcher.data?.likes || optimisticLikes
const isLoading = fetcher.state === 'loading' || fetcher.state === 'submitting'
return (
<button
onClick={handleLike}
disabled={isLoading}
className={fetcher.state === 'submitting' ? 'loading' : ''}
>
❤️ {currentLikes}
{isLoading && <span className="spinner" />}
</button>
)
}
// 高级用法:批量操作
function BulkActions() {
const fetcher = useFetcher()
const [selectedItems, setSelectedItems] = useState<string[]>([])
const performBulkAction = (action: 'delete' | 'archive' | 'publish') => {
fetcher.submit(
{
action,
items: JSON.stringify(selectedItems)
},
{
method: 'post',
action: '/api/bulk-actions'
}
)
}
// 监听状态变化
useEffect(() => {
if (fetcher.state === 'loading') {
// 开始加载
showLoading('正在处理...')
} else if (fetcher.state === 'idle' && fetcher.data) {
// 处理完成
hideLoading()
if (fetcher.data.success) {
showSuccess('操作成功')
setSelectedItems([])
} else {
showError(fetcher.data.message || '操作失败')
}
}
}, [fetcher.state, fetcher.data])
return (
<div>
<button onClick={() => performBulkAction('delete')}>
批量删除
</button>
<button onClick={() => performBulkAction('archive')}>
批量归档
</button>
</div>
)
}
// 轮询数据
function LiveDataWidget({ endpoint }: { endpoint: string }) {
const fetcher = useFetcher()
const [data, setData] = useState<any>(null)
useEffect(() => {
// 初始加载
fetcher.load(endpoint)
// 设置轮询
const interval = setInterval(() => {
fetcher.load(endpoint)
}, 5000) // 每5秒轮询一次
return () => clearInterval(interval)
}, [endpoint])
useEffect(() => {
if (fetcher.data) {
setData(fetcher.data)
}
}, [fetcher.data])
return (
<div className="live-widget">
{data ? (
<pre>{JSON.stringify(data, null, 2)}</pre>
) : (
<div>加载中...</div>
)}
{fetcher.state === 'loading' && (
<div className="updating">更新中...</div>
)}
</div>
)
}useNavigation
实现的是进行路由状态的管理吧
import { useNavigation, Navigation } from 'react-router-dom'
import { useEffect, useMemo } from 'react'
function NavigationIndicator() {
const navigation = useNavigation()
// 导航状态
const isNavigating = navigation.state !== 'idle'
const isSubmitting = navigation.state === 'submitting'
const isLoading = navigation.state === 'loading'
// 获取表单数据(如果是表单提交)
const formData = useMemo(() => {
if (isSubmitting && navigation.formData) {
return navigation.formData
}
return null
}, [isSubmitting, navigation.formData])
// 获取目标位置
const targetLocation = navigation.location
// 显示加载指示器
useEffect(() => {
if (isNavigating) {
showLoadingOverlay()
} else {
hideLoadingOverlay()
}
}, [isNavigating])
// 表单提交时的特殊处理
useEffect(() => {
if (isSubmitting && formData) {
// 显示提交中的状态
const action = formData.get('action')
console.log(`正在提交表单,操作: ${action}`)
// 可以在这里添加提交分析
trackFormSubmission({
action: action?.toString(),
formData: Object.fromEntries(formData.entries())
})
}
}, [isSubmitting, formData])
// 防止重复提交
const preventMultipleSubmissions = useMemo(() => {
return isSubmitting || isLoading
}, [isSubmitting, isLoading])
// 导航取消功能
const cancelNavigation = () => {
// React Router 没有直接的取消API
// 但可以通过其他方式实现,如:
window.addEventListener('beforeunload', () => {
// 取消导航
})
}
// 计算导航耗时
useEffect(() => {
let startTime: number
if (isNavigating) {
startTime = Date.now()
}
return () => {
if (startTime) {
const duration = Date.now() - startTime
console.log(`导航耗时: ${duration}ms`)
}
}
}, [isNavigating])
return (
<div className="navigation-indicator">
{isSubmitting && (
<div className="submitting">
正在提交...
{formData && (
<div className="form-data">
提交数据: {formData.get('action')}
</div>
)}
</div>
)}
{isLoading && (
<div className="loading">
正在加载: {targetLocation?.pathname}
</div>
)}
{/* 全局加载指示器 */}
{isNavigating && (
<div className="global-loading-bar">
<div className="loading-bar" />
</div>
)}
</div>
)
}useMatches
实现的是路由匹配信息的管理
import { useMatches, UIMatch } from 'react-router-dom'
import { useMemo } from 'react'
interface RouteHandle {
title?: string
breadcrumb?: string
requiresAuth?: boolean
permissions?: string[]
}
function Breadcrumbs() {
const matches = useMatches() as UIMatch<unknown, RouteHandle>[]
// 生成面包屑导航
const breadcrumbs = useMemo(() => {
return matches
.filter(match => match.handle?.breadcrumb)
.map(match => ({
pathname: match.pathname,
breadcrumb: match.handle?.breadcrumb,
params: match.params
}))
}, [matches])
return (
<nav className="breadcrumbs">
{breadcrumbs.map((crumb, index) => (
<span key={crumb.pathname}>
{index > 0 && ' / '}
<a href={crumb.pathname}>
{typeof crumb.breadcrumb === 'function'
? crumb.breadcrumb(crumb.params)
: crumb.breadcrumb}
</a>
</span>
))}
</nav>
)
}
// 获取当前页面标题
function PageTitle() {
const matches = useMatches()
const pageTitle = useMemo(() => {
const matchWithTitle = matches
.slice()
.reverse()
.find(match => match.handle?.title)
return matchWithTitle?.handle?.title || '默认标题'
}, [matches])
useEffect(() => {
document.title = pageTitle
}, [pageTitle])
return null
}
// 检查权限
function useRoutePermissions() {
const matches = useMatches()
return useMemo(() => {
return matches.reduce((permissions, match) => {
if (match.handle?.permissions) {
return [...permissions, ...match.handle.permissions]
}
return permissions
}, [] as string[])
}, [matches])
}
// 获取路由元数据
function useRouteMeta() {
const matches = useMatches()
return useMemo(() => {
return matches.reduce((meta, match) => {
return {
...meta,
...(match.handle || {})
}
}, {} as RouteHandle)
}, [matches])
}
// 调试路由信息
function RouteDebugger() {
const matches = useMatches()
return (
<details className="route-debugger">
<summary>路由调试信息</summary>
<pre>{JSON.stringify(matches, null, 2)}</pre>
</details>
)
}具体的案例
// 1. 路由权限检查Hook
export function useRouteAuth() {
const matches = useMatches()
const location = useLocation()
const navigate = useNavigate()
const { isAuthenticated, user } = useAuth()
// 检查当前路由是否需要认证
const requiresAuth = useMemo(() => {
return matches.some(match =>
match.handle?.requiresAuth === true
)
}, [matches])
// 检查用户是否有权限
const hasPermission = useMemo(() => {
const routePermissions = matches.flatMap(match =>
match.handle?.permissions || []
)
if (routePermissions.length === 0) return true
return routePermissions.every(permission =>
user.permissions?.includes(permission)
)
}, [matches, user])
// 自动重定向
useEffect(() => {
if (requiresAuth && !isAuthenticated) {
navigate('/login', {
state: { from: location.pathname },
replace: true
})
} else if (requiresAuth && !hasPermission) {
navigate('/forbidden', { replace: true })
}
}, [requiresAuth, isAuthenticated, hasPermission, location, navigate])
return {
requiresAuth,
hasPermission,
isAuthorized: !requiresAuth || (isAuthenticated && hasPermission)
}
}
// 2. 路由数据预取Hook
export function useRoutePrefetch() {
const location = useLocation()
const matches = useMatches()
const navigate = useNavigate()
// 预取相关路由
const prefetchRelated = useCallback((routePath: string) => {
// 获取路由配置
const routeConfig = getRouteConfig(routePath)
if (routeConfig?.prefetch) {
// 预取数据
routeConfig.prefetch.forEach(endpoint => {
fetch(endpoint, {
method: 'HEAD' // 只需要检查可用性
})
})
// 预取组件
if (routeConfig.component) {
import(`../pages${routePath}`)
.then(module => {
// 缓存组件
componentCache.set(routePath, module.default)
})
}
}
}, [])
// 监听悬停进行预取
const useHoverPrefetch = (to: string) => {
const ref = useRef<HTMLAnchorElement>(null)
useEffect(() => {
const element = ref.current
if (!element) return
const handleMouseEnter = () => {
prefetchRelated(to)
}
const handleTouchStart = () => {
prefetchRelated(to)
}
element.addEventListener('mouseenter', handleMouseEnter)
element.addEventListener('touchstart', handleTouchStart, { passive: true })
return () => {
element.removeEventListener('mouseenter', handleMouseEnter)
element.removeEventListener('touchstart', handleTouchStart)
}
}, [to, prefetchRelated])
return ref
}
// 智能预取:基于用户行为
useEffect(() => {
// 预取当前路由的相关路由
const relatedRoutes = getRelatedRoutes(location.pathname)
relatedRoutes.forEach(prefetchRelated)
}, [location.pathname, prefetchRelated])
return {
prefetchRelated,
useHoverPrefetch
}
}
// 3. 路由状态持久化Hook
export function useRoutePersist<T>(key: string, initialValue: T) {
const location = useLocation()
const [state, setState] = useState<T>(() => {
// 尝试从sessionStorage恢复
const saved = sessionStorage.getItem(
`route:${location.pathname}:${key}`
)
return saved ? JSON.parse(saved) : initialValue
})
// 自动保存
useEffect(() => {
sessionStorage.setItem(
`route:${location.pathname}:${key}`,
JSON.stringify(state)
)
}, [location.pathname, key, state])
// 路由离开时清理
useEffect(() => {
const handleBeforeUnload = () => {
// 如果需要长期保存,可以转移到localStorage
if (shouldPersistAcrossSessions(key)) {
localStorage.setItem(
`route:${location.pathname}:${key}`,
JSON.stringify(state)
)
}
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload)
}
}, [location.pathname, key, state])
return [state, setState] as const
}
// 使用示例
function SearchPage() {
const [query, setQuery] = useRoutePersist('searchQuery', '')
const [filters, setFilters] = useRoutePersist('filters', {})
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
</div>
)
}// hooks/useEnterpriseRouter.ts
import {
useNavigate,
useLocation,
useParams,
useSearchParams,
useLoaderData,
useActionData,
useNavigation,
useMatches,
useOutletContext
} from 'react-router-dom'
import {
useCallback,
useMemo,
useEffect,
useState
} from 'react'
// 企业级导航Hook
export function useEnterpriseNavigate() {
const navigate = useNavigate()
const navigateWithAnalytics = useCallback((
to: string,
options?: {
trackAs?: string
category?: string
label?: string
[key: string]: any
}
) => {
// 发送分析事件
trackNavigation({
from: window.location.pathname,
to,
...options
})
// 执行导航
navigate(to, options)
}, [navigate])
const navigateWithConfirmation = useCallback((
to: string,
message: string = '确定要离开当前页面吗?'
) => {
return new Promise<boolean>((resolve) => {
if (window.confirm(message)) {
navigate(to)
resolve(true)
} else {
resolve(false)
}
})
}, [navigate])
const navigateBack = useCallback((fallback = '/') => {
if (window.history.length > 1) {
navigate(-1)
} else {
navigate(fallback)
}
}, [navigate])
return {
navigate,
navigateWithAnalytics,
navigateWithConfirmation,
navigateBack,
// 快捷方法
goHome: () => navigate('/'),
goBack: () => navigate(-1),
goForward: () => navigate(1)
}
}
// 企业级参数管理Hook
export function useEnterpriseParams<T extends Record<string, string>>() {
const params = useParams<T>()
const location = useLocation()
// 参数验证
const validateParams = useCallback(<K extends keyof T>(keys: K[]) => {
const missing = keys.filter(key => !params[key])
return {
isValid: missing.length === 0,
missingParams: missing
}
}, [params])
// 参数变化监听
const [previousParams, setPreviousParams] = useState(params)
useEffect(() => {
if (JSON.stringify(previousParams) !== JSON.stringify(params)) {
console.log('参数变化:', {
from: previousParams,
to: params
})
setPreviousParams(params)
}
}, [params, previousParams])
// 生成带参数的URL
const generateUrlWithParams = useCallback((
path: string,
additionalParams?: Record<string, string>
) => {
let url = path
// 替换路径参数
Object.entries(params).forEach(([key, value]) => {
url = url.replace(`:${key}`, value)
})
// 添加查询参数
if (additionalParams) {
const searchParams = new URLSearchParams(location.search)
Object.entries(additionalParams).forEach(([key, value]) => {
searchParams.set(key, value)
})
url += `?${searchParams.toString()}`
}
return url
}, [params, location.search])
return {
params,
validateParams,
generateUrlWithParams,
hasParams: Object.keys(params).length > 0
}
}
// 企业级数据加载Hook
export function useEnterpriseLoaderData<T>() {
const data = useLoaderData() as T
const navigation = useNavigation()
const location = useLocation()
// 数据缓存键
const cacheKey = useMemo(() => {
return `loader:${location.pathname}:${location.search}`
}, [location.pathname, location.search])
// 缓存数据
useEffect(() => {
if (data && navigation.state === 'idle') {
sessionStorage.setItem(cacheKey, JSON.stringify(data))
}
}, [data, navigation.state, cacheKey])
// 获取缓存数据
const getCachedData = useCallback(() => {
const cached = sessionStorage.getItem(cacheKey)
return cached ? JSON.parse(cached) : null
}, [cacheKey])
// 数据验证
const isValidData = useMemo(() => {
if (!data) return false
// 添加业务逻辑验证
return true
}, [data])
return {
data,
isLoading: navigation.state === 'loading',
isValidData,
getCachedData,
// 数据转换
transformData: <R>(transformer: (data: T) => R) => {
return transformer(data)
}
}
}
// 企业级路由状态管理
export function useEnterpriseRouteState() {
const matches = useMatches()
const location = useLocation()
const navigation = useNavigation()
// 当前路由信息
const currentRoute = useMemo(() => {
return matches[matches.length - 1]
}, [matches])
// 路由元数据
const routeMeta = useMemo(() => {
return matches.reduce((meta, match) => ({
...meta,
...(match.handle || {})
}), {})
}, [matches])
// 面包屑导航
const breadcrumbs = useMemo(() => {
return matches
.filter(match => match.handle?.breadcrumb)
.map((match, index) => ({
path: match.pathname,
title: match.handle.breadcrumb,
isCurrent: index === matches.length - 1
}))
}, [matches])
// 页面过渡状态
const transitionState = useMemo(() => {
if (navigation.state === 'loading') {
return 'entering'
} else if (navigation.state === 'submitting') {
return 'submitting'
}
return 'idle'
}, [navigation.state])
// 页面标题
const pageTitle = useMemo(() => {
const titleMatch = matches
.slice()
.reverse()
.find(match => match.handle?.title)
const title = titleMatch?.handle?.title || '默认标题'
// 动态标题
if (typeof title === 'function') {
return title(currentRoute.params)
}
return title
}, [matches, currentRoute.params])
return {
currentRoute,
routeMeta,
breadcrumbs,
transitionState,
pageTitle,
isModal: location.state?.modal === true,
// 工具方法
hasPermission: (permission: string) => {
const permissions = routeMeta.permissions || []
return permissions.includes(permission)
},
shouldShowHeader: routeMeta.showHeader !== false,
shouldShowFooter: routeMeta.showFooter !== false
}
}
// 使用示例
function EnterpriseComponent() {
const {
navigateWithAnalytics,
navigateWithConfirmation
} = useEnterpriseNavigate()
const { params, validateParams } = useEnterpriseParams()
const { data, isLoading, transformData } = useEnterpriseLoaderData()
const {
breadcrumbs,
pageTitle,
hasPermission
} = useEnterpriseRouteState()
// 验证必需参数
useEffect(() => {
const validation = validateParams(['id', 'slug'])
if (!validation.isValid) {
console.error('缺少参数:', validation.missingParams)
}
}, [validateParams])
// 安全导航
const handleNavigate = async () => {
const confirmed = await navigateWithConfirmation(
'/dashboard',
'确定要离开当前页面吗?未保存的更改将会丢失。'
)
if (confirmed) {
navigateWithAnalytics('/dashboard', {
trackAs: 'dashboard_navigation',
category: 'navigation'
})
}
}
// 数据处理
const processedData = useMemo(() => {
return transformData((rawData) => ({
...rawData,
formattedDate: new Date(rawData.timestamp).toLocaleDateString(),
canEdit: hasPermission('edit')
}))
}, [transformData, hasPermission])
if (isLoading) {
return <LoadingSkeleton />
}
return (
<div>
<Breadcrumbs items={breadcrumbs} />
<h1>{pageTitle}</h1>
<DataDisplay data={processedData} />
<button onClick={handleNavigate}>
前往仪表盘
</button>
</div>
)
}前端路由底层原理
windows 对象监听对象
前端路由的核心机制是进行对应的监听 URL 发生的变化来触发页面内容的更新,在传统的开发中,核心使用的机制是:监听 load 事件来反向的执行脚本,但是在现在的SPA应用中我们实现的机制是使用前端路由来进行管理吧
1. popstate事件 当我们的活动历史页面发生了条目更改后,触发 popstate 事件实现路由的更新
调用的 history.pushState 或者 history.replaceState 不会触发 popstate 事件的执行吧
只有在触发我们的浏览器的动作事件的时候才会触发该事件的执行,也就是调用 history.back 或者 history.forward 吧
2. hashchange事件 当URL的片段标识符 hash 发生变化的时候,触发 hashchange 事件吧
前端路由的形式
Hash模式 利用的是URL的hash值部分,当hash变化的收,触发hashchange 事件,然后根据hash渲染不同的资源
History模式 利用的是html5的history API(popstate、replacestate)来进行修改URL,但是该种模式是不会触发页面更新的讷,所以说需要自己进行 trigger 事件进行手动触发更新机制吧
另外,还有一种抽象模式,比如MemoryRouter,它将路由状态保存在内存中,而不改变URL,常用于非浏览器环境(如React Native)或者测试。
数据携带形式
URL 参数形式携带参数
路径参数,也就是我们的动态路由吧
/user/:id,实际的路由就是/user/id查询参数,也就是?后的数据吧
/user?id=xxx
状态 state:
在History模式下,可以通过pushState传递一个state对象,该对象会保存在历史记录中,通过popstate事件的事件对象的state属性获取
hash:在Hash模式下,可以将数据放在hash中,但通常hash用于表示路由路径,不太适合携带复杂数据
本地存储(LocalStorage/SessionStorage):对于大量数据或者需要持久化的数据,可以先将数据存储在本地,然后在路由跳转时读取
路由变换后的执行流程
1. 路由变化触发:用户操作某个按钮实现触发了 hashchange 或者 popstate 的事件,触发页面的更新和重新渲染实现吧
2. 监听路由变化:通过监听对应的事件的触发,来实现捕获路由的变化吧
3. 解析 URL:从数据拦截的 URL 中解析出路、查询参数等
4. 匹配路由:将解析出来的路径和定义的路由规则进行匹配,找到对应的组件吧
5. 获取数据:根据匹配的路由,可能需要从服务器获取数据(例如,通过API请求),或者从本地存储中读取数据
6. 渲染组件:使用获取的数据和匹配到的组件,通过React的createElement或Vue的h函数创建虚拟DOM,然后更新到真实DOM
7. 更新TDK吧:根据路由配置,可能还需要更新页面标题等其他元信息
React/Vue 结合
在React或Vue中,路由库(如React Router, Vue Router)会提供更声明式的方式。它们内部也是基于类似原理,但利用了React和Vue的响应式系统。
在React中,路由变化会触发组件的重新渲染,因为React Router使用了Context来传递路由状态,当路由变化时,Context的值发生变化,消费该Context的组件就会重新渲染。
在Vue中,Vue Router通过Vue的响应式系统,将当前路由作为响应式数据,当路由变化时,依赖该数据的组件会自动更新。
路由思考
动态路由匹配:支持参数,如
/user/:id。嵌套路由:路由可以有子路由,形成嵌套的组件树。
路由守卫:在路由跳转前进行验证(如登录检查)。
懒加载:将组件按需加载,提高初始加载速度。
路由变化 → 解析URL → 匹配路由 → 获取数据 → 渲染组件核心实现
事件管理器
// 路由事件监听管理器
class RouterEventManager {
constructor() {
this.listeners = new Map()
this.init()
}
init() {
// 1. popstate - 历史记录变化(最主要的事件)
window.addEventListener('popstate', (event) => {
console.log('popstate 触发:', {
state: event.state, // pushState/replaceState 设置的状态
location: window.location,
timestamp: Date.now()
})
this.emit('popstate', event)
})
// 2. hashchange - Hash 路由变化
window.addEventListener('hashchange', (event) => {
console.log('hashchange 触发:', {
oldURL: event.oldURL,
newURL: event.newURL,
hash: window.location.hash
})
this.emit('hashchange', event)
})
// 3. beforeunload - 页面卸载前(可用于路由守卫)
window.addEventListener('beforeunload', (event) => {
const message = '确定要离开吗?未保存的数据可能会丢失。'
event.returnValue = message // 标准方式
return message // 旧浏览器兼容
})
// 4. pagehide/pageshow - 页面隐藏/显示(配合bfcache)
window.addEventListener('pagehide', (event) => {
console.log('页面隐藏,persisted:', event.persisted)
if (event.persisted) {
// 页面被缓存(bfcache)
this.saveStateToCache()
}
})
window.addEventListener('pageshow', (event) => {
console.log('页面显示,persisted:', event.persisted)
if (event.persisted) {
// 从bfcache恢复
this.restoreStateFromCache()
}
})
}
// 自定义事件系统
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set())
}
this.listeners.get(event).add(callback)
return () => this.off(event, callback)
}
off(event, callback) {
if (this.listeners.has(event)) {
this.listeners.get(event).delete(callback)
}
}
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(cb => cb(data))
}
}
// 拦截 pushState/replaceState
hijackHistory() {
const originalPushState = window.history.pushState
const originalReplaceState = window.history.replaceState
window.history.pushState = (state, title, url) => {
console.log('pushState 被调用:', { state, title, url })
// 触发自定义事件
const event = new CustomEvent('pushstate', {
detail: { state, title, url }
})
window.dispatchEvent(event)
// 调用原始方法
const result = originalPushState.call(window.history, state, title, url)
// 触发状态变化事件(pushState不会触发popstate)
this.emit('historychange', {
type: 'push',
state,
url,
location: window.location
})
return result
}
window.history.replaceState = (state, title, url) => {
console.log('replaceState 被调用:', { state, title, url })
const event = new CustomEvent('replacestate', {
detail: { state, title, url }
})
window.dispatchEvent(event)
const result = originalReplaceState.call(window.history, state, title, url)
this.emit('historychange', {
type: 'replace',
state,
url,
location: window.location
})
return result
}
}
// 拦截链接点击
interceptLinks() {
document.addEventListener('click', (event) => {
const link = event.target.closest('a[href]')
if (!link) return
const href = link.getAttribute('href')
// 排除外部链接和特殊协议
if (
href.startsWith('http') ||
href.startsWith('mailto:') ||
href.startsWith('tel:') ||
href.startsWith('#') ||
link.target === '_blank' ||
link.hasAttribute('download') ||
event.ctrlKey ||
event.metaKey ||
event.shiftKey
) {
return // 让浏览器处理
}
// 阻止默认行为,使用前端路由
event.preventDefault()
const url = new URL(href, window.location.origin)
// 触发自定义导航事件
this.emit('linkclick', {
href,
url,
link,
event
})
// 执行路由跳转
this.navigate(url.pathname + url.search + url.hash)
}, true) // 使用捕获阶段
}
navigate(to, options = {}) {
const { state = {}, replace = false } = options
if (replace) {
window.history.replaceState(state, '', to)
} else {
window.history.pushState(state, '', to)
}
// 手动触发路由变化(因为pushState不会触发popstate)
this.handleRouteChange()
}
handleRouteChange() {
// 触发路由变化事件
const routeChangeEvent = new CustomEvent('routechange', {
detail: {
location: window.location,
state: window.history.state,
timestamp: Date.now()
}
})
window.dispatchEvent(routeChangeEvent)
}
}
// 使用示例
const routerEvents = new RouterEventManager()
routerEvents.hijackHistory()
routerEvents.interceptLinks()
// 监听各种事件
routerEvents.on('popstate', (event) => {
console.log('路由变化(浏览器前进后退)')
})
routerEvents.on('linkclick', ({ href, url }) => {
console.log('链接被点击:', href)
})
routerEvents.on('historychange', ({ type, url }) => {
console.log(`历史记录${type}: ${url}`)
})
window.addEventListener('routechange', (event) => {
console.log('路由变化事件:', event.detail)
})
不同模式路由
class HashRouter {
constructor() {
this.routes = new Map()
this.currentHash = ''
this.init()
}
init() {
// 监听 hashchange
window.addEventListener('hashchange', this.handleHashChange.bind(this))
// 初始加载
this.handleHashChange()
}
handleHashChange() {
const hash = window.location.hash.slice(1) || '/'
// 解析路径和查询参数
const [path, queryString] = hash.split('?')
const queryParams = new URLSearchParams(queryString || '')
// 解析 hash 中的数据
const hashData = this.parseHashData(hash)
console.log('📍 Hash 路由变化:', {
raw: window.location.hash,
path,
queryParams: Object.fromEntries(queryParams),
hashData
})
// 执行路由匹配
this.matchRoute(path, {
queryParams,
hashData,
mode: 'hash'
})
}
parseHashData(hash) {
// 支持 hash 中的额外数据格式: #/path?query#extraData
const parts = hash.split('#')
if (parts.length > 2) {
try {
return JSON.parse(decodeURIComponent(parts[2]))
} catch {
return parts[2]
}
}
return null
}
// 数据携带方式
navigate(path, data = {}) {
// 方式1: 查询参数
const queryParams = new URLSearchParams(data.query || {})
// 方式2: 存储在 hash 片段中(不推荐,有长度限制)
let hash = `#${path}`
if (queryParams.toString()) {
hash += `?${queryParams.toString()}`
}
// 方式3: 使用 sessionStorage 传递大数据
if (data.large) {
const key = `route:${Date.now()}`
sessionStorage.setItem(key, JSON.stringify(data.large))
queryParams.set('_data', key)
}
window.location.hash = hash
}
}
// 使用场景:简单应用、兼容性要求高、不需要SEO
// 数据携带:查询参数、sessionStorage、hash片段class HashRouter {
constructor() {
this.routes = new Map()
this.currentHash = ''
this.init()
}
init() {
// 监听 hashchange
window.addEventListener('hashchange', this.handleHashChange.bind(this))
// 初始加载
this.handleHashChange()
}
handleHashChange() {
const hash = window.location.hash.slice(1) || '/'
// 解析路径和查询参数
const [path, queryString] = hash.split('?')
const queryParams = new URLSearchParams(queryString || '')
// 解析 hash 中的数据
const hashData = this.parseHashData(hash)
console.log('📍 Hash 路由变化:', {
raw: window.location.hash,
path,
queryParams: Object.fromEntries(queryParams),
hashData
})
// 执行路由匹配
this.matchRoute(path, {
queryParams,
hashData,
mode: 'hash'
})
}
parseHashData(hash) {
// 支持 hash 中的额外数据格式: #/path?query#extraData
const parts = hash.split('#')
if (parts.length > 2) {
try {
return JSON.parse(decodeURIComponent(parts[2]))
} catch {
return parts[2]
}
}
return null
}
// 数据携带方式
navigate(path, data = {}) {
// 方式1: 查询参数
const queryParams = new URLSearchParams(data.query || {})
// 方式2: 存储在 hash 片段中(不推荐,有长度限制)
let hash = `#${path}`
if (queryParams.toString()) {
hash += `?${queryParams.toString()}`
}
// 方式3: 使用 sessionStorage 传递大数据
if (data.large) {
const key = `route:${Date.now()}`
sessionStorage.setItem(key, JSON.stringify(data.large))
queryParams.set('_data', key)
}
window.location.hash = hash
}
}
// 使用场景:简单应用、兼容性要求高、不需要SEO
// 数据携带:查询参数、sessionStorage、hash片段// 完整路由渲染引擎
class RouterRenderEngine {
constructor() {
this.components = new Map() // 组件缓存
this.templates = new Map() // 模板缓存
this.currentRender = null
this.initRenderPipeline()
}
initRenderPipeline() {
// 监听路由变化事件
window.addEventListener('routechange', async (event) => {
await this.handleRouteChange(event.detail)
})
}
async handleRouteChange(routeInfo) {
const startTime = performance.now()
try {
// 🎯 阶段1: 路由守卫和验证
const canProceed = await this.runRouteGuards(routeInfo)
if (!canProceed) return
// 🎯 阶段2: 解析路由配置
const routeConfig = await this.resolveRouteConfig(routeInfo.pathname)
if (!routeConfig) {
await this.renderError(404)
return
}
// 🎯 阶段3: 数据预加载
const data = await this.preloadData(routeConfig, routeInfo)
// 🎯 阶段4: 组件加载(代码分割)
const Component = await this.loadComponent(routeConfig.component)
// 🎯 阶段5: 创建渲染上下文
const context = this.createRenderContext({
route: routeInfo,
config: routeConfig,
data,
Component
})
// 🎯 阶段6: 执行渲染
await this.renderComponent(Component, context)
// 🎯 阶段7: 后处理
await this.postRender(context)
const endTime = performance.now()
console.log(`🚀 完整渲染耗时: ${(endTime - startTime).toFixed(2)}ms`)
} catch (error) {
console.error('❌ 路由渲染失败:', error)
await this.renderError(500, error)
}
}
// 阶段1: 路由守卫
async runRouteGuards(routeInfo) {
const guards = this.getRouteGuards(routeInfo.pathname)
for (const guard of guards) {
try {
const result = await guard(routeInfo)
if (result === false || result?.redirect) {
if (result?.redirect) {
this.navigate(result.redirect)
}
return false
}
} catch (error) {
console.error('路由守卫错误:', error)
return false
}
}
return true
}
// 阶段2: 解析路由配置
async resolveRouteConfig(pathname) {
// 动态导入路由配置
const { routes } = await import('./routes-config.js')
// 匹配路由
for (const route of routes) {
if (this.matchPath(pathname, route.path)) {
const params = this.extractParams(pathname, route.path)
return { ...route, params }
}
}
return null
}
// 阶段3: 数据预加载
async preloadData(routeConfig, routeInfo) {
const data = {}
if (routeConfig.loaders) {
// 并行加载所有数据
const loaderPromises = Object.entries(routeConfig.loaders).map(
async ([key, loader]) => {
try {
return [key, await loader(routeInfo)]
} catch (error) {
console.error(`数据加载失败 ${key}:`, error)
return [key, null]
}
}
)
const results = await Promise.allSettled(loaderPromises)
results.forEach(result => {
if (result.status === 'fulfilled') {
const [key, value] = result.value
data[key] = value
}
})
}
return data
}
// 阶段4: 组件加载
async loadComponent(componentPath) {
// 检查缓存
if (this.components.has(componentPath)) {
return this.components.get(componentPath)
}
// 动态导入
const module = await import(/* webpackChunkName: "[request]" */ `./${componentPath}`)
const Component = module.default
// 缓存
this.components.set(componentPath, Component)
return Component
}
// 阶段5: 创建渲染上下文
createRenderContext({ route, config, data, Component }) {
return {
route,
config,
data,
Component,
container: document.getElementById('app'),
// 添加渲染元数据
metadata: {
startTime: performance.now(),
renderId: Math.random().toString(36).slice(2),
routeDepth: route.pathname.split('/').length - 1
}
}
}
// 阶段6: 执行渲染(核心)
async renderComponent(Component, context) {
const { container, data } = context
// 取消之前的渲染
if (this.currentRender?.abort) {
this.currentRender.abort()
}
// 创建新的 AbortController 用于可中断渲染
const abortController = new AbortController()
this.currentRender = { abort: () => abortController.abort() }
// 🎨 React-like 渲染
if (this.isReactComponent(Component)) {
await this.renderReactComponent(Component, container, data, abortController)
}
// 🖼️ Vue-like 渲染
else if (this.isVueComponent(Component)) {
await this.renderVueComponent(Component, container, data, abortController)
}
// 📦 普通函数组件
else {
await this.renderFunctionComponent(Component, container, data, abortController)
}
}
// React-like 渲染实现
async renderReactComponent(Component, container, props, abortController) {
return new Promise((resolve, reject) => {
// 检查是否中断
if (abortController.signal.aborted) {
reject(new DOMException('渲染被中断', 'AbortError'))
return
}
// 模拟 React.createElement
const createElement = (type, props, ...children) => {
if (typeof type === 'function') {
// 函数组件
return type(props)
}
// DOM 元素
const element = document.createElement(type)
// 设置属性
if (props) {
Object.entries(props).forEach(([key, value]) => {
if (key === 'className') {
element.className = value
} else if (key === 'style' && typeof value === 'object') {
Object.assign(element.style, value)
} else if (key.startsWith('on') && typeof value === 'function') {
const eventName = key.slice(2).toLowerCase()
element.addEventListener(eventName, value)
} else if (value !== undefined && value !== null) {
element.setAttribute(key, value)
}
})
}
// 处理子元素
children.forEach(child => {
if (Array.isArray(child)) {
child.forEach(c => this.appendChild(element, c))
} else {
this.appendChild(element, child)
}
})
return element
}
// 递归处理子节点
const appendChild = (parent, child) => {
if (child == null || child === false || child === true) {
return
}
if (typeof child === 'string' || typeof child === 'number') {
parent.appendChild(document.createTextNode(String(child)))
} else if (child instanceof Node) {
parent.appendChild(child)
} else if (typeof child === 'object' && child.type) {
// 递归渲染子组件
const childElement = createElement(child.type, child.props, ...(child.children || []))
parent.appendChild(childElement)
}
}
this.appendChild = appendChild
try {
// 执行组件函数,获取虚拟DOM
const vnode = Component(props)
// 转换为真实DOM
const domTree = createElement(
vnode.type,
vnode.props,
...(vnode.children || [])
)
// 清空容器
while (container.firstChild) {
container.removeChild(container.firstChild)
}
// 添加新DOM
container.appendChild(domTree)
// 触发生命周期
setTimeout(() => {
if (Component.componentDidMount) {
Component.componentDidMount()
}
}, 0)
resolve()
} catch (error) {
reject(error)
}
})
}
// 阶段7: 后处理
async postRender(context) {
// 设置页面标题
if (context.config.title) {
document.title = context.config.title
}
// 滚动恢复
this.restoreScrollPosition(context.route.pathname)
// 发送分析事件
this.trackPageView(context)
// 预取下一页
this.prefetchNextRoute(context.route.pathname)
// 触发自定义事件
window.dispatchEvent(new CustomEvent('routerendered', {
detail: context
}))
}
// 错误处理渲染
async renderError(status, error = null) {
const ErrorComponent = await this.loadComponent(`./errors/${status}.js`)
const container = document.getElementById('app')
container.innerHTML = ''
await this.renderFunctionComponent(
ErrorComponent,
container,
{ error },
new AbortController()
)
}
}路由渲染引擎
// 完整路由渲染引擎
class RouterRenderEngine {
constructor() {
this.components = new Map() // 组件缓存
this.templates = new Map() // 模板缓存
this.currentRender = null
this.initRenderPipeline()
}
initRenderPipeline() {
// 监听路由变化事件
window.addEventListener('routechange', async (event) => {
await this.handleRouteChange(event.detail)
})
}
async handleRouteChange(routeInfo) {
const startTime = performance.now()
try {
// 🎯 阶段1: 路由守卫和验证
const canProceed = await this.runRouteGuards(routeInfo)
if (!canProceed) return
// 🎯 阶段2: 解析路由配置
const routeConfig = await this.resolveRouteConfig(routeInfo.pathname)
if (!routeConfig) {
await this.renderError(404)
return
}
// 🎯 阶段3: 数据预加载
const data = await this.preloadData(routeConfig, routeInfo)
// 🎯 阶段4: 组件加载(代码分割)
const Component = await this.loadComponent(routeConfig.component)
// 🎯 阶段5: 创建渲染上下文
const context = this.createRenderContext({
route: routeInfo,
config: routeConfig,
data,
Component
})
// 🎯 阶段6: 执行渲染
await this.renderComponent(Component, context)
// 🎯 阶段7: 后处理
await this.postRender(context)
const endTime = performance.now()
console.log(`🚀 完整渲染耗时: ${(endTime - startTime).toFixed(2)}ms`)
} catch (error) {
console.error('❌ 路由渲染失败:', error)
await this.renderError(500, error)
}
}
// 阶段1: 路由守卫
async runRouteGuards(routeInfo) {
const guards = this.getRouteGuards(routeInfo.pathname)
for (const guard of guards) {
try {
const result = await guard(routeInfo)
if (result === false || result?.redirect) {
if (result?.redirect) {
this.navigate(result.redirect)
}
return false
}
} catch (error) {
console.error('路由守卫错误:', error)
return false
}
}
return true
}
// 阶段2: 解析路由配置
async resolveRouteConfig(pathname) {
// 动态导入路由配置
const { routes } = await import('./routes-config.js')
// 匹配路由
for (const route of routes) {
if (this.matchPath(pathname, route.path)) {
const params = this.extractParams(pathname, route.path)
return { ...route, params }
}
}
return null
}
// 阶段3: 数据预加载
async preloadData(routeConfig, routeInfo) {
const data = {}
if (routeConfig.loaders) {
// 并行加载所有数据
const loaderPromises = Object.entries(routeConfig.loaders).map(
async ([key, loader]) => {
try {
return [key, await loader(routeInfo)]
} catch (error) {
console.error(`数据加载失败 ${key}:`, error)
return [key, null]
}
}
)
const results = await Promise.allSettled(loaderPromises)
results.forEach(result => {
if (result.status === 'fulfilled') {
const [key, value] = result.value
data[key] = value
}
})
}
return data
}
// 阶段4: 组件加载
async loadComponent(componentPath) {
// 检查缓存
if (this.components.has(componentPath)) {
return this.components.get(componentPath)
}
// 动态导入
const module = await import(/* webpackChunkName: "[request]" */ `./${componentPath}`)
const Component = module.default
// 缓存
this.components.set(componentPath, Component)
return Component
}
// 阶段5: 创建渲染上下文
createRenderContext({ route, config, data, Component }) {
return {
route,
config,
data,
Component,
container: document.getElementById('app'),
// 添加渲染元数据
metadata: {
startTime: performance.now(),
renderId: Math.random().toString(36).slice(2),
routeDepth: route.pathname.split('/').length - 1
}
}
}
// 阶段6: 执行渲染(核心)
async renderComponent(Component, context) {
const { container, data } = context
// 取消之前的渲染
if (this.currentRender?.abort) {
this.currentRender.abort()
}
// 创建新的 AbortController 用于可中断渲染
const abortController = new AbortController()
this.currentRender = { abort: () => abortController.abort() }
// 🎨 React-like 渲染
if (this.isReactComponent(Component)) {
await this.renderReactComponent(Component, container, data, abortController)
}
// 🖼️ Vue-like 渲染
else if (this.isVueComponent(Component)) {
await this.renderVueComponent(Component, container, data, abortController)
}
// 📦 普通函数组件
else {
await this.renderFunctionComponent(Component, container, data, abortController)
}
}
// React-like 渲染实现
async renderReactComponent(Component, container, props, abortController) {
return new Promise((resolve, reject) => {
// 检查是否中断
if (abortController.signal.aborted) {
reject(new DOMException('渲染被中断', 'AbortError'))
return
}
// 模拟 React.createElement
const createElement = (type, props, ...children) => {
if (typeof type === 'function') {
// 函数组件
return type(props)
}
// DOM 元素
const element = document.createElement(type)
// 设置属性
if (props) {
Object.entries(props).forEach(([key, value]) => {
if (key === 'className') {
element.className = value
} else if (key === 'style' && typeof value === 'object') {
Object.assign(element.style, value)
} else if (key.startsWith('on') && typeof value === 'function') {
const eventName = key.slice(2).toLowerCase()
element.addEventListener(eventName, value)
} else if (value !== undefined && value !== null) {
element.setAttribute(key, value)
}
})
}
// 处理子元素
children.forEach(child => {
if (Array.isArray(child)) {
child.forEach(c => this.appendChild(element, c))
} else {
this.appendChild(element, child)
}
})
return element
}
// 递归处理子节点
const appendChild = (parent, child) => {
if (child == null || child === false || child === true) {
return
}
if (typeof child === 'string' || typeof child === 'number') {
parent.appendChild(document.createTextNode(String(child)))
} else if (child instanceof Node) {
parent.appendChild(child)
} else if (typeof child === 'object' && child.type) {
// 递归渲染子组件
const childElement = createElement(child.type, child.props, ...(child.children || []))
parent.appendChild(childElement)
}
}
this.appendChild = appendChild
try {
// 执行组件函数,获取虚拟DOM
const vnode = Component(props)
// 转换为真实DOM
const domTree = createElement(
vnode.type,
vnode.props,
...(vnode.children || [])
)
// 清空容器
while (container.firstChild) {
container.removeChild(container.firstChild)
}
// 添加新DOM
container.appendChild(domTree)
// 触发生命周期
setTimeout(() => {
if (Component.componentDidMount) {
Component.componentDidMount()
}
}, 0)
resolve()
} catch (error) {
reject(error)
}
})
}
// 阶段7: 后处理
async postRender(context) {
// 设置页面标题
if (context.config.title) {
document.title = context.config.title
}
// 滚动恢复
this.restoreScrollPosition(context.route.pathname)
// 发送分析事件
this.trackPageView(context)
// 预取下一页
this.prefetchNextRoute(context.route.pathname)
// 触发自定义事件
window.dispatchEvent(new CustomEvent('routerendered', {
detail: context
}))
}
// 错误处理渲染
async renderError(status, error = null) {
const ErrorComponent = await this.loadComponent(`./errors/${status}.js`)
const container = document.getElementById('app')
container.innerHTML = ''
await this.renderFunctionComponent(
ErrorComponent,
container,
{ error },
new AbortController()
)
}
}异步渲染和并发
// 完整的虚拟DOM渲染引擎
class VDomRenderer {
constructor() {
this.patchQueue = []
this.isPatching = false
this.workInProgress = null
}
// 创建虚拟DOM
createElement(type, props, ...children) {
return {
$$typeof: Symbol.for('react.element'),
type,
props: props || {},
children: children.flat(),
key: props?.key || null,
ref: props?.ref || null
}
}
// 渲染到真实DOM
render(vnode, container) {
// 开始渲染工作循环
this.workLoop(container, vnode)
}
// 工作循环(类似React的Fiber架构)
workLoop(container, vnode) {
this.workInProgress = {
container,
vnode,
dom: null,
child: null,
sibling: null,
return: null,
alternate: null, // 上次渲染的fiber
effectTag: 'PLACEMENT',
stateNode: null
}
// 开始渲染
this.performUnitOfWork(this.workInProgress)
// 提交阶段
this.commitRoot()
}
performUnitOfWork(fiber) {
// 阶段1: 开始工作(类似componentWillMount)
this.beginWork(fiber)
// 如果有子节点,处理子节点
if (fiber.vnode.children && fiber.vnode.children.length > 0) {
let prevChild = null
fiber.vnode.children.forEach((child, index) => {
const newFiber = {
container: fiber.container,
vnode: child,
dom: null,
child: null,
sibling: null,
return: fiber,
effectTag: 'PLACEMENT',
stateNode: null
}
if (index === 0) {
fiber.child = newFiber
} else {
prevChild.sibling = newFiber
}
prevChild = newFiber
})
}
// 如果有子fiber,返回子fiber继续处理
if (fiber.child) {
return fiber.child
}
// 否则,向上回溯,处理兄弟节点
let nextFiber = fiber
while (nextFiber) {
// 阶段2: 完成工作(类似componentDidMount)
this.completeWork(nextFiber)
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.return
}
return null
}
beginWork(fiber) {
const { type, props } = fiber.vnode
if (typeof type === 'function') {
// 函数组件
fiber.stateNode = null
fiber.vnode = type(props) // 执行组件函数
} else if (typeof type === 'string') {
// DOM元素
if (!fiber.dom) {
fiber.dom = this.createDOMElement(type, props)
}
// 更新DOM属性
this.updateDOMProperties(fiber.dom, {}, props)
}
}
completeWork(fiber) {
if (fiber.dom) {
// 建立DOM树关系
if (fiber.return && fiber.return.dom) {
fiber.return.dom.appendChild(fiber.dom)
} else if (fiber.container) {
fiber.container.appendChild(fiber.dom)
}
}
}
createDOMElement(type, props) {
const element = document.createElement(type)
// 设置属性
if (props) {
Object.keys(props).forEach(name => {
if (name === 'children') return
if (name === 'className') {
element.className = props[name]
} else if (name === 'style') {
Object.assign(element.style, props[name])
} else if (name.startsWith('on')) {
const eventType = name.toLowerCase().substring(2)
element.addEventListener(eventType, props[name])
} else {
element.setAttribute(name, props[name])
}
})
}
return element
}
updateDOMProperties(dom, prevProps, nextProps) {
// 处理属性变化
const isEvent = key => key.startsWith('on')
const isAttribute = key => !isEvent(key) && key !== 'children'
// 移除旧的属性
Object.keys(prevProps)
.filter(isAttribute)
.forEach(name => {
dom[name] = null
})
// 移除旧的事件
Object.keys(prevProps)
.filter(isEvent)
.forEach(name => {
const eventType = name.toLowerCase().substring(2)
dom.removeEventListener(eventType, prevProps[name])
})
// 设置新的属性
Object.keys(nextProps)
.filter(isAttribute)
.forEach(name => {
dom[name] = nextProps[name]
})
// 添加新的事件
Object.keys(nextProps)
.filter(isEvent)
.forEach(name => {
const eventType = name.toLowerCase().substring(2)
dom.addEventListener(eventType, nextProps[name])
})
}
commitRoot() {
// 执行副作用(真实的DOM操作)
this.commitWork(this.workInProgress)
this.workInProgress = null
}
commitWork(fiber) {
if (!fiber) return
// 提交当前fiber
if (fiber.effectTag === 'PLACEMENT' && fiber.dom) {
const parentDom = this.getParentDom(fiber)
if (parentDom) {
parentDom.appendChild(fiber.dom)
}
} else if (fiber.effectTag === 'UPDATE' && fiber.dom) {
this.updateDOMProperties(
fiber.dom,
fiber.alternate?.props || {},
fiber.vnode.props
)
}
// 递归提交子节点和兄弟节点
this.commitWork(fiber.child)
this.commitWork(fiber.sibling)
}
getParentDom(fiber) {
let parentFiber = fiber.return
while (parentFiber) {
if (parentFiber.dom) {
return parentFiber.dom
}
parentFiber = parentFiber.return
}
return fiber.container
}
}
// 使用示例
const renderer = new VDomRenderer()
const React = { createElement: renderer.createElement.bind(renderer) }
function App(props) {
return React.createElement('div', { className: 'app' },
React.createElement('h1', null, 'Hello ', props.name),
React.createElement('button', {
onClick: () => console.log('Clicked!')
}, 'Click me')
)
}
const vdom = React.createElement(App, { name: 'World' })
renderer.render(vdom, document.getElementById('app'))class ConcurrentRenderer { constructor() { this.taskQueue = [] this.isRendering = false this.deadline = 0 this.frameTime = 1000 / 60 // 16.6ms per frame this.currentTask = null } // 启动渲染循环 startRenderLoop(container, vnode) { const task = { container, vnode, priority: 'normal', status: 'pending' } this.taskQueue.push(task) if (!this.isRendering) { this.scheduleRender() } } scheduleRender() { if ('requestIdleCallback' in window) { requestIdleCallback(this.workLoop.bind(this)) } else { setTimeout(this.workLoop.bind(this), 0) } } workLoop(deadline) { this.deadline = deadline ? deadline.timeRemaining() : Infinity while (this.taskQueue.length > 0 && this.deadline > 0) { this.currentTask = this.taskQueue.shift() this.performTask(this.currentTask) } if (this.taskQueue.length > 0) { this.scheduleRender() } else { this.isRendering = false } } performTask(task) { task.status = 'running' // 分片渲染 const renderSlice = this.createRenderSlice(task.vnode) // 使用时间分片 const startTime = performance.now() let nodesProcessed = 0 while (renderSlice.hasNext() && performance.now() - startTime < 5) { const node = renderSlice.next() this.renderNode(node, task.container) nodesProcessed++ } console.log(`⏱️ 本次渲染处理了 ${nodesProcessed} 个节点`) if (renderSlice.hasNext()) { // 还有工作,重新入队 task.vnode = renderSlice.getRemaining() this.taskQueue.unshift(task) } else { task.status = 'completed' } } createRenderSlice(vnode) { // 实现可中断的渲染切片 let currentNode = vnode let childIndex = 0 return { hasNext() { return currentNode !== null }, next() { const node = currentNode // 深度优先遍历 if (node.children && node.children.length > childIndex) { currentNode = node.children[childIndex] childIndex = 0 } else { currentNode = node.parent childIndex = node.index + 1 } return node }, getRemaining() { return vnode // 简化版本,实际应该返回剩余节点 } } } }
代码分割:按路由加载组件
数据预取:提前加载可能需要的资源
缓存策略:合理使用 HTTP 缓存和内存缓存
渲染优化:避免强制同步布局
资源优先级:合理设置 preload/prefetch