Appearance
动态路由与参数获取
概述
动态路由是现代Web应用的核心特性,允许根据URL参数动态渲染不同的内容。React Router v6提供了强大的参数获取和处理机制,包括路径参数、查询参数、状态传递等多种方式。
路径参数详解
基础路径参数
jsx
import { useParams, Link } from 'react-router-dom';
// 路由定义
<Routes>
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/products/:category/:productId" element={<ProductDetail />} />
<Route path="/blog/:year/:month/:day/:slug" element={<BlogPost />} />
</Routes>
// 获取单个参数
function UserProfile() {
const { userId } = useParams();
// userId 总是字符串类型,需要转换
const numericUserId = parseInt(userId, 10);
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser(numericUserId)
.then(setUser)
.catch(console.error)
.finally(() => setLoading(false));
}, [numericUserId]);
if (loading) return <div>Loading user...</div>;
if (!user) return <div>User not found</div>;
return (
<div className="user-profile">
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
<p>User ID: {userId}</p>
{/* 相关链接 */}
<nav>
<Link to={`/users/${userId}/posts`}>Posts</Link>
<Link to={`/users/${userId}/followers`}>Followers</Link>
<Link to={`/users/${userId}/settings`}>Settings</Link>
</nav>
</div>
);
}
// 获取多个参数
function ProductDetail() {
const { category, productId } = useParams();
const [product, setProduct] = useState(null);
useEffect(() => {
fetchProductByCategory(category, productId)
.then(setProduct);
}, [category, productId]);
return (
<div className="product-detail">
<nav className="breadcrumb">
<Link to="/products">Products</Link>
<span> / </span>
<Link to={`/products/category/${category}`}>{category}</Link>
<span> / </span>
<span>{product?.name}</span>
</nav>
{product && (
<div className="product-info">
<h1>{product.name}</h1>
<p>Category: {category}</p>
<p>Product ID: {productId}</p>
<p>Price: ${product.price}</p>
</div>
)}
</div>
);
}
// 复杂路径参数
function BlogPost() {
const { year, month, day, slug } = useParams();
const postDate = new Date(`${year}-${month}-${day}`);
const formattedDate = postDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
const [post, setPost] = useState(null);
useEffect(() => {
fetchPostByDate(year, month, day, slug)
.then(setPost);
}, [year, month, day, slug]);
return (
<article className="blog-post">
<header>
<h1>{post?.title}</h1>
<time dateTime={`${year}-${month}-${day}`}>
Published on {formattedDate}
</time>
<div className="post-meta">
<Link to={`/blog/${year}`}>Posts from {year}</Link>
<Link to={`/blog/${year}/${month}`}>Posts from {formattedDate.split(' ')[0]} {year}</Link>
</div>
</header>
{post && (
<div className="post-content">
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</div>
)}
</article>
);
}可选参数
jsx
// 路由定义 - 使用 ? 表示可选参数
<Routes>
<Route path="/blog/:year?/:month?/:day?" element={<BlogArchive />} />
<Route path="/shop/:category?/:subcategory?" element={<Shop />} />
</Routes>
function BlogArchive() {
const { year, month, day } = useParams();
const [posts, setPosts] = useState([]);
// 根据参数构建过滤条件
const buildDateFilter = useCallback(() => {
const filter = {};
if (year) {
filter.year = parseInt(year, 10);
if (month) {
filter.month = parseInt(month, 10);
if (day) {
filter.day = parseInt(day, 10);
}
}
}
return filter;
}, [year, month, day]);
useEffect(() => {
const dateFilter = buildDateFilter();
fetchPosts(dateFilter).then(setPosts);
}, [buildDateFilter]);
const getArchiveTitle = () => {
if (day && month && year) {
return `Posts from ${month}/${day}/${year}`;
} else if (month && year) {
return `Posts from ${month}/${year}`;
} else if (year) {
return `Posts from ${year}`;
} else {
return 'All Posts';
}
};
return (
<div className="blog-archive">
<header>
<h1>{getArchiveTitle()}</h1>
{/* 导航链接 */}
<nav className="archive-nav">
<Link to="/blog">All Years</Link>
{year && (
<>
<span> / </span>
<Link to={`/blog/${year}`}>{year}</Link>
</>
)}
{month && year && (
<>
<span> / </span>
<Link to={`/blog/${year}/${month}`}>{month}</Link>
</>
)}
{day && month && year && (
<>
<span> / </span>
<span>{day}</span>
</>
)}
</nav>
</header>
<div className="posts-list">
{posts.map(post => (
<article key={post.id} className="post-preview">
<h2>
<Link to={`/blog/${post.year}/${post.month}/${post.day}/${post.slug}`}>
{post.title}
</Link>
</h2>
<time>{post.publishedAt}</time>
<p>{post.excerpt}</p>
</article>
))}
</div>
</div>
);
}
// 商店页面with可选参数
function Shop() {
const { category, subcategory } = useParams();
const [products, setProducts] = useState([]);
const [categories, setCategories] = useState([]);
useEffect(() => {
// 根据参数获取产品
const filters = {};
if (category) filters.category = category;
if (subcategory) filters.subcategory = subcategory;
fetchProducts(filters).then(setProducts);
}, [category, subcategory]);
useEffect(() => {
// 获取分类列表
fetchCategories().then(setCategories);
}, []);
return (
<div className="shop">
<aside className="categories-sidebar">
<h3>Categories</h3>
<ul>
<li>
<Link to="/shop" className={!category ? 'active' : ''}>
All Products
</Link>
</li>
{categories.map(cat => (
<li key={cat.id}>
<Link
to={`/shop/${cat.slug}`}
className={category === cat.slug ? 'active' : ''}
>
{cat.name}
</Link>
{/* 子分类 */}
{category === cat.slug && cat.subcategories && (
<ul className="subcategories">
{cat.subcategories.map(subcat => (
<li key={subcat.id}>
<Link
to={`/shop/${cat.slug}/${subcat.slug}`}
className={subcategory === subcat.slug ? 'active' : ''}
>
{subcat.name}
</Link>
</li>
))}
</ul>
)}
</li>
))}
</ul>
</aside>
<main className="products-main">
<header>
<h1>
{subcategory ? `${subcategory} in ${category}` :
category ? category :
'All Products'}
</h1>
<p>{products.length} products found</p>
</header>
<ProductGrid products={products} />
</main>
</div>
);
}参数验证和转换
jsx
// 自定义Hook进行参数验证
function useValidatedParams(schema) {
const params = useParams();
const navigate = useNavigate();
const validatedParams = useMemo(() => {
const result = {};
const errors = [];
Object.entries(schema).forEach(([key, validator]) => {
const value = params[key];
try {
result[key] = validator(value);
} catch (error) {
errors.push({ key, error: error.message });
}
});
if (errors.length > 0) {
console.error('Parameter validation errors:', errors);
navigate('/404', { replace: true });
return null;
}
return result;
}, [params, schema, navigate]);
return validatedParams;
}
// 验证器函数
const validators = {
positiveInteger: (value) => {
const num = parseInt(value, 10);
if (isNaN(num) || num <= 0) {
throw new Error('Must be a positive integer');
}
return num;
},
slug: (value) => {
if (!/^[a-z0-9-]+$/.test(value)) {
throw new Error('Invalid slug format');
}
return value;
},
date: (value) => {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new Error('Invalid date format');
}
return date;
}
};
// 使用参数验证
function ValidatedUserProfile() {
const params = useValidatedParams({
userId: validators.positiveInteger
});
if (!params) return null; // 验证失败,会重定向
const { userId } = params;
return (
<div>
<h1>User #{userId}</h1>
</div>
);
}
function ValidatedBlogPost() {
const params = useValidatedParams({
year: (value) => {
const year = parseInt(value, 10);
const currentYear = new Date().getFullYear();
if (year < 2000 || year > currentYear) {
throw new Error('Year must be between 2000 and current year');
}
return year;
},
month: (value) => {
const month = parseInt(value, 10);
if (month < 1 || month > 12) {
throw new Error('Month must be between 1 and 12');
}
return month;
},
slug: validators.slug
});
// 使用验证后的参数...
}查询参数管理
useSearchParams详解
jsx
import { useSearchParams, useNavigate } from 'react-router-dom';
function ProductsPage() {
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
// 获取查询参数
const currentFilters = {
category: searchParams.get('category') || '',
minPrice: parseFloat(searchParams.get('minPrice')) || 0,
maxPrice: parseFloat(searchParams.get('maxPrice')) || 1000,
sortBy: searchParams.get('sortBy') || 'name',
order: searchParams.get('order') || 'asc',
page: parseInt(searchParams.get('page')) || 1,
limit: parseInt(searchParams.get('limit')) || 12,
inStock: searchParams.get('inStock') === 'true',
onSale: searchParams.get('onSale') === 'true',
brand: searchParams.getAll('brand') // 获取多个同名参数
};
// 更新单个参数
const updateFilter = (key, value) => {
const newParams = new URLSearchParams(searchParams);
if (value === '' || value === null || value === undefined) {
newParams.delete(key);
} else {
newParams.set(key, value.toString());
}
// 重置页码
if (key !== 'page') {
newParams.set('page', '1');
}
setSearchParams(newParams);
};
// 批量更新参数
const updateFilters = (updates) => {
setSearchParams(prev => {
const newParams = new URLSearchParams(prev);
Object.entries(updates).forEach(([key, value]) => {
if (value === '' || value === null || value === undefined) {
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;
});
};
// 清除特定过滤器
const clearFilter = (key) => {
setSearchParams(prev => {
const newParams = new URLSearchParams(prev);
newParams.delete(key);
return newParams;
});
};
// 清除所有过滤器
const clearAllFilters = () => {
setSearchParams({});
};
// 添加到收藏夹(保持当前过滤器)
const addToFavorites = () => {
const currentUrl = `${location.pathname}?${searchParams.toString()}`;
addFavoriteUrl(currentUrl, 'Filtered Products');
};
return (
<div className="products-page">
{/* 过滤器区域 */}
<aside className="filters-sidebar">
<h3>Filters</h3>
{/* 分类过滤器 */}
<div className="filter-group">
<label>Category</label>
<select
value={currentFilters.category}
onChange={(e) => updateFilter('category', e.target.value)}
>
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
<option value="books">Books</option>
</select>
</div>
{/* 价格范围 */}
<div className="filter-group">
<label>Price Range</label>
<input
type="number"
placeholder="Min Price"
value={currentFilters.minPrice}
onChange={(e) => updateFilter('minPrice', e.target.value)}
/>
<input
type="number"
placeholder="Max Price"
value={currentFilters.maxPrice}
onChange={(e) => updateFilter('maxPrice', e.target.value)}
/>
</div>
{/* 复选框过滤器 */}
<div className="filter-group">
<label>
<input
type="checkbox"
checked={currentFilters.inStock}
onChange={(e) => updateFilter('inStock', e.target.checked)}
/>
In Stock Only
</label>
<label>
<input
type="checkbox"
checked={currentFilters.onSale}
onChange={(e) => updateFilter('onSale', e.target.checked)}
/>
On Sale
</label>
</div>
{/* 多选过滤器 */}
<div className="filter-group">
<label>Brands</label>
<div className="checkbox-group">
{['Apple', 'Samsung', 'Google', 'Microsoft'].map(brand => (
<label key={brand}>
<input
type="checkbox"
checked={currentFilters.brand.includes(brand)}
onChange={(e) => {
const newBrands = e.target.checked
? [...currentFilters.brand, brand]
: currentFilters.brand.filter(b => b !== brand);
updateFilter('brand', newBrands);
}}
/>
{brand}
</label>
))}
</div>
</div>
<div className="filter-actions">
<button onClick={clearAllFilters}>Clear All</button>
<button onClick={addToFavorites}>Save Filters</button>
</div>
</aside>
{/* 产品列表区域 */}
<main className="products-main">
<header className="products-header">
<h1>Products</h1>
{/* 排序控件 */}
<div className="sort-controls">
<select
value={`${currentFilters.sortBy}-${currentFilters.order}`}
onChange={(e) => {
const [sortBy, order] = e.target.value.split('-');
updateFilters({ sortBy, order });
}}
>
<option value="name-asc">Name (A-Z)</option>
<option value="name-desc">Name (Z-A)</option>
<option value="price-asc">Price (Low to High)</option>
<option value="price-desc">Price (High to Low)</option>
<option value="date-desc">Newest First</option>
<option value="rating-desc">Highest Rated</option>
</select>
</div>
{/* 显示数量控件 */}
<div className="limit-controls">
<label>Show: </label>
<select
value={currentFilters.limit}
onChange={(e) => updateFilter('limit', e.target.value)}
>
<option value="12">12 per page</option>
<option value="24">24 per page</option>
<option value="48">48 per page</option>
</select>
</div>
</header>
{/* 活跃过滤器显示 */}
<ActiveFilters
filters={currentFilters}
onRemoveFilter={clearFilter}
/>
{/* 产品网格 */}
<ProductGrid
products={products}
loading={loading}
/>
{/* 分页 */}
<Pagination
currentPage={currentFilters.page}
totalPages={totalPages}
onPageChange={(page) => updateFilter('page', page)}
/>
</main>
</div>
);
}
// 活跃过滤器组件
function ActiveFilters({ filters, onRemoveFilter }) {
const activeFilters = Object.entries(filters)
.filter(([key, value]) => {
if (key === 'page' || key === 'limit') return false;
if (Array.isArray(value)) return value.length > 0;
return value !== '' && value !== 0 && value !== false;
});
if (activeFilters.length === 0) return null;
return (
<div className="active-filters">
<span>Active filters:</span>
{activeFilters.map(([key, value]) => (
<div key={key} className="filter-tag">
<span>{key}: {Array.isArray(value) ? value.join(', ') : value.toString()}</span>
<button onClick={() => onRemoveFilter(key)}>×</button>
</div>
))}
</div>
);
}查询参数持久化
jsx
// 自定义Hook:持久化查询参数
function usePersistentSearchParams(storageKey, defaultParams = {}) {
const [searchParams, setSearchParams] = useSearchParams();
// 从localStorage恢复参数
useEffect(() => {
const saved = localStorage.getItem(storageKey);
if (saved) {
try {
const savedParams = JSON.parse(saved);
const urlParams = new URLSearchParams();
Object.entries({...defaultParams, ...savedParams}).forEach(([key, value]) => {
if (value !== '' && value !== null && value !== undefined) {
urlParams.set(key, value.toString());
}
});
setSearchParams(urlParams, { replace: true });
} catch (error) {
console.error('Failed to restore search params:', error);
}
}
}, [storageKey, setSearchParams]);
// 保存参数到localStorage
useEffect(() => {
const paramsObject = Object.fromEntries(searchParams);
localStorage.setItem(storageKey, JSON.stringify(paramsObject));
}, [searchParams, storageKey]);
return [searchParams, setSearchParams];
}
// 使用持久化参数
function ProductsWithPersistentFilters() {
const [searchParams, setSearchParams] = usePersistentSearchParams(
'products-filters',
{ sortBy: 'name', order: 'asc', limit: '12' }
);
// 其余逻辑...
}动态路由生成
基于数据的路由
jsx
// 动态生成路由配置
function createDynamicRoutes(categories, userRole) {
const routes = [
{
path: '/',
element: <Layout />,
children: [
{ index: true, element: <Home /> }
]
}
];
// 根据分类数据生成路由
const categoryRoutes = categories.map(category => ({
path: `category/${category.slug}`,
element: <CategoryPage />,
loader: ({ params }) => loadCategoryData(params.slug),
children: category.subcategories?.map(sub => ({
path: sub.slug,
element: <SubcategoryPage />,
loader: ({ params }) => loadSubcategoryData(params)
})) || []
}));
routes[0].children.push(...categoryRoutes);
// 根据用户角色添加路由
if (userRole === 'admin') {
routes[0].children.push({
path: 'admin',
element: <AdminPanel />,
children: [
{ index: true, element: <AdminDashboard /> },
{ path: 'categories', element: <ManageCategories /> }
]
});
}
return routes;
}
// 动态路由Provider
function DynamicRoutesProvider({ children }) {
const [categories, setCategories] = useState([]);
const [userRole, setUserRole] = useState(null);
const [router, setRouter] = useState(null);
useEffect(() => {
Promise.all([
fetchCategories(),
fetchUserRole()
]).then(([cats, role]) => {
setCategories(cats);
setUserRole(role);
const routes = createDynamicRoutes(cats, role);
setRouter(createBrowserRouter(routes));
});
}, []);
if (!router) return <div>Loading application...</div>;
return <RouterProvider router={router} />;
}条件路由渲染
jsx
// 基于功能标志的路由
function FeatureFlagRoutes() {
const { features } = useFeatureFlags();
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
{/* 基础功能路由 */}
<Route path="products" element={<Products />} />
<Route path="about" element={<About />} />
{/* 条件路由 - 只在功能启用时显示 */}
{features.analytics && (
<Route path="analytics" element={<Analytics />} />
)}
{features.advanced_search && (
<Route path="search" element={<AdvancedSearch />} />
)}
{features.social_features && (
<Route path="social" element={<SocialLayout />}>
<Route index element={<SocialFeed />} />
<Route path="friends" element={<Friends />} />
<Route path="messages" element={<Messages />} />
</Route>
)}
{/* 基于用户类型的路由 */}
<Route path="dashboard" element={<DashboardSelector />} />
</Route>
</Routes>
);
}
function DashboardSelector() {
const { user } = useAuth();
// 根据用户类型渲染不同的仪表板
switch (user.type) {
case 'admin':
return <AdminDashboard />;
case 'manager':
return <ManagerDashboard />;
case 'customer':
return <CustomerDashboard />;
default:
return <Navigate to="/login" replace />;
}
}
// 实验性路由
function ExperimentalRoutes() {
const { experiments } = useExperiments();
return (
<Routes>
<Route path="/experiment" element={<ExperimentLayout />}>
{experiments.includes('new-ui') && (
<Route path="new-ui" element={<NewUIExperiment />} />
)}
{experiments.includes('beta-features') && (
<Route path="beta" element={<BetaFeatures />} />
)}
</Route>
</Routes>
);
}高级参数模式
参数解析和验证
jsx
// 复合参数解析
function useCompoundParams() {
const { compoundParam } = useParams();
const parseCompoundParam = useCallback((param) => {
if (!param) return null;
// 解析复合参数:userId-postId-commentId
const parts = param.split('-');
if (parts.length !== 3) {
throw new Error('Invalid compound parameter format');
}
return {
userId: parseInt(parts[0], 10),
postId: parseInt(parts[1], 10),
commentId: parseInt(parts[2], 10)
};
}, []);
const params = useMemo(() => {
try {
return parseCompoundParam(compoundParam);
} catch (error) {
console.error('Parameter parsing error:', error);
return null;
}
}, [compoundParam, parseCompoundParam]);
return params;
}
// 路由定义
<Route path="/thread/:compoundParam" element={<ThreadDetail />} />
// 使用
function ThreadDetail() {
const params = useCompoundParams();
const navigate = useNavigate();
if (!params) {
navigate('/404', { replace: true });
return null;
}
const { userId, postId, commentId } = params;
return (
<div>
<h1>Comment Detail</h1>
<p>User ID: {userId}</p>
<p>Post ID: {postId}</p>
<p>Comment ID: {commentId}</p>
</div>
);
}参数变换和映射
jsx
// 参数变换Hook
function useTransformedParams(transformers) {
const rawParams = useParams();
return useMemo(() => {
const transformed = {};
Object.entries(rawParams).forEach(([key, value]) => {
const transformer = transformers[key];
transformed[key] = transformer ? transformer(value) : value;
});
return transformed;
}, [rawParams, transformers]);
}
// 参数变换器
const paramTransformers = {
userId: (value) => parseInt(value, 10),
slug: (value) => decodeURIComponent(value),
date: (value) => new Date(value),
coordinates: (value) => {
const [lat, lng] = value.split(',');
return {
latitude: parseFloat(lat),
longitude: parseFloat(lng)
};
},
tags: (value) => value.split('+').map(decodeURIComponent)
};
// 使用变换后的参数
function TransformedParamsComponent() {
const params = useTransformedParams(paramTransformers);
// params.userId 现在是数字类型
// params.date 现在是Date对象
// params.coordinates 现在是对象 {latitude, longitude}
return (
<div>
<p>User ID: {params.userId}</p>
<p>Date: {params.date?.toLocaleDateString()}</p>
<p>Location: {params.coordinates?.latitude}, {params.coordinates?.longitude}</p>
</div>
);
}参数同步到状态
jsx
// 将URL参数同步到组件状态
function useParamsSync(paramKey, defaultValue, parser = (v) => v) {
const [searchParams, setSearchParams] = useSearchParams();
const paramValue = searchParams.get(paramKey);
const [localState, setLocalState] = useState(() => {
return paramValue ? parser(paramValue) : defaultValue;
});
// 参数变化时更新本地状态
useEffect(() => {
if (paramValue !== null) {
setLocalState(parser(paramValue));
}
}, [paramValue, parser]);
// 更新函数同时更新URL和本地状态
const updateValue = useCallback((newValue) => {
setLocalState(newValue);
if (newValue === defaultValue) {
// 如果是默认值,从URL中移除参数
setSearchParams(prev => {
const newParams = new URLSearchParams(prev);
newParams.delete(paramKey);
return newParams;
});
} else {
setSearchParams(prev => {
const newParams = new URLSearchParams(prev);
newParams.set(paramKey, newValue.toString());
return newParams;
});
}
}, [paramKey, defaultValue, setSearchParams]);
return [localState, updateValue];
}
// 使用参数同步
function SearchPage() {
const [query, setQuery] = useParamsSync('q', '');
const [sortBy, setSortBy] = useParamsSync('sort', 'relevance');
const [page, setPage] = useParamsSync('page', 1, parseInt);
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
// 参数变化时执行搜索
useEffect(() => {
if (query) {
setLoading(true);
searchAPI(query, { sortBy, page })
.then(setResults)
.finally(() => setLoading(false));
}
}, [query, sortBy, page]);
return (
<div className="search-page">
<header className="search-header">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="relevance">Most Relevant</option>
<option value="date">Newest</option>
<option value="popularity">Most Popular</option>
</select>
</header>
{loading ? (
<div>Searching...</div>
) : (
<SearchResults results={results} />
)}
<Pagination
current={page}
onChange={setPage}
/>
</div>
);
}实战案例
案例1:文件管理器
jsx
function FileManager() {
return (
<Routes>
<Route path="/files" element={<FileManagerLayout />}>
{/* 根目录 */}
<Route index element={<FilesRoot />} />
{/* 文件夹路径 - 支持任意深度 */}
<Route path="*" element={<FolderView />} />
</Route>
</Routes>
);
}
function FolderView() {
const location = useLocation();
const navigate = useNavigate();
// 从路径解析文件夹层级
const folderPath = location.pathname.replace('/files/', '') || '';
const pathSegments = folderPath ? folderPath.split('/') : [];
const [files, setFiles] = useState([]);
const [folders, setFolders] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchFolderContents(folderPath)
.then(({ files, folders }) => {
setFiles(files);
setFolders(folders);
})
.finally(() => setLoading(false));
}, [folderPath]);
const navigateToFolder = (folderName) => {
const newPath = folderPath ? `${folderPath}/${folderName}` : folderName;
navigate(`/files/${newPath}`);
};
const navigateUp = () => {
if (pathSegments.length > 0) {
const parentPath = pathSegments.slice(0, -1).join('/');
navigate(`/files/${parentPath}`);
}
};
if (loading) return <div>Loading folder contents...</div>;
return (
<div className="folder-view">
{/* 路径导航 */}
<nav className="folder-breadcrumb">
<Link to="/files">Files</Link>
{pathSegments.map((segment, index) => {
const segmentPath = pathSegments.slice(0, index + 1).join('/');
return (
<span key={index}>
<span className="separator"> / </span>
<Link to={`/files/${segmentPath}`}>
{decodeURIComponent(segment)}
</Link>
</span>
);
})}
</nav>
{/* 操作栏 */}
<div className="folder-actions">
{pathSegments.length > 0 && (
<button onClick={navigateUp}>
← Back to Parent Folder
</button>
)}
<button onClick={() => setShowUploadModal(true)}>
Upload Files
</button>
<button onClick={() => setShowCreateFolderModal(true)}>
New Folder
</button>
</div>
{/* 文件夹列表 */}
{folders.length > 0 && (
<div className="folders-grid">
<h3>Folders</h3>
{folders.map(folder => (
<div
key={folder.id}
className="folder-item"
onClick={() => navigateToFolder(folder.name)}
>
<div className="folder-icon">📁</div>
<span className="folder-name">{folder.name}</span>
<span className="folder-size">{folder.itemCount} items</span>
</div>
))}
</div>
)}
{/* 文件列表 */}
{files.length > 0 && (
<div className="files-grid">
<h3>Files</h3>
{files.map(file => (
<div key={file.id} className="file-item">
<div className="file-icon">
{getFileIcon(file.type)}
</div>
<span className="file-name">{file.name}</span>
<span className="file-size">{formatFileSize(file.size)}</span>
<time className="file-modified">
{new Date(file.modifiedAt).toLocaleDateString()}
</time>
</div>
))}
</div>
)}
{folders.length === 0 && files.length === 0 && (
<div className="empty-folder">
<p>This folder is empty</p>
</div>
)}
</div>
);
}案例2:多语言路由
jsx
// 支持多语言的路由系统
const SUPPORTED_LOCALES = ['en', 'zh', 'ja', 'ko'];
const DEFAULT_LOCALE = 'en';
function LocalizedApp() {
return (
<Routes>
{/* 默认语言路由(不带语言前缀) */}
<Route path="/*" element={<LocalizedRoutes locale={DEFAULT_LOCALE} />} />
{/* 带语言前缀的路由 */}
{SUPPORTED_LOCALES.map(locale => (
<Route
key={locale}
path={`/${locale}/*`}
element={<LocalizedRoutes locale={locale} />}
/>
))}
</Routes>
);
}
function LocalizedRoutes({ locale }) {
const { t } = useTranslation(locale);
return (
<LocaleContext.Provider value={locale}>
<Routes>
<Route path="/" element={<LocalizedLayout />}>
<Route index element={<Home />} />
{/* 产品路由 */}
<Route path="products" element={<ProductsLayout />}>
<Route index element={<ProductsList />} />
<Route path="category/:category" element={<CategoryProducts />} />
<Route path=":productId" element={<ProductDetail />}>
<Route index element={<ProductOverview />} />
<Route path="reviews" element={<ProductReviews />} />
<Route path="specifications" element={<ProductSpecs />} />
</Route>
</Route>
{/* 用户相关路由 */}
<Route path="account" element={<AccountLayout />}>
<Route index element={<AccountDashboard />} />
<Route path="profile" element={<Profile />} />
<Route path="orders" element={<Orders />} />
<Route path="wishlist" element={<Wishlist />} />
</Route>
{/* 支持页面 */}
<Route path="support" element={<SupportLayout />}>
<Route index element={<SupportHome />} />
<Route path="faq" element={<FAQ />} />
<Route path="contact" element={<Contact />} />
<Route path="tickets" element={<SupportTickets />} />
<Route path="tickets/:ticketId" element={<TicketDetail />} />
</Route>
</Route>
</Routes>
</LocaleContext.Provider>
);
}
function LocalizedLayout() {
const locale = useContext(LocaleContext);
const location = useLocation();
// 语言切换器
const switchLanguage = (newLocale) => {
const currentPath = location.pathname;
// 移除当前语言前缀
const pathWithoutLocale = currentPath.startsWith(`/${locale}`)
? currentPath.slice(`/${locale}`.length)
: currentPath;
// 添加新语言前缀
const newPath = newLocale === DEFAULT_LOCALE
? pathWithoutLocale || '/'
: `/${newLocale}${pathWithoutLocale}`;
navigate(newPath + location.search);
};
return (
<div className="localized-layout" data-locale={locale}>
<header className="layout-header">
<nav className="main-nav">
<Link to={locale === DEFAULT_LOCALE ? '/' : `/${locale}`}>
Home
</Link>
<Link to={locale === DEFAULT_LOCALE ? '/products' : `/${locale}/products`}>
Products
</Link>
<Link to={locale === DEFAULT_LOCALE ? '/account' : `/${locale}/account`}>
Account
</Link>
</nav>
<div className="language-switcher">
<select value={locale} onChange={(e) => switchLanguage(e.target.value)}>
<option value="en">English</option>
<option value="zh">中文</option>
<option value="ja">日本語</option>
<option value="ko">한국어</option>
</select>
</div>
</header>
<main className="layout-main">
<Outlet />
</main>
</div>
);
}
// 本地化的链接组件
function LocalizedLink({ to, ...props }) {
const locale = useContext(LocaleContext);
const localizedTo = useMemo(() => {
if (locale === DEFAULT_LOCALE) {
return to;
}
// 确保不重复添加语言前缀
if (to.startsWith(`/${locale}`)) {
return to;
}
return `/${locale}${to}`;
}, [to, locale]);
return <Link to={localizedTo} {...props} />;
}最佳实践总结
1. 参数处理
jsx
// 参数处理最佳实践
const parameterBestPractices = {
// 1. 类型转换
typeConversion: {
good: `
const { id } = useParams();
const numericId = parseInt(id, 10);
if (isNaN(numericId)) {
navigate('/404', { replace: true });
return;
}
`,
explanation: '始终验证和转换参数类型'
},
// 2. 参数验证
validation: {
good: `
const { slug } = useParams();
if (!/^[a-z0-9-]+$/.test(slug)) {
navigate('/404', { replace: true });
return;
}
`,
explanation: '验证参数格式防止注入攻击'
},
// 3. 默认值处理
defaults: {
good: `
const page = parseInt(searchParams.get('page')) || 1;
const limit = parseInt(searchParams.get('limit')) || 10;
`,
explanation: '为参数提供合理的默认值'
},
// 4. 错误处理
errorHandling: {
good: `
useEffect(() => {
fetchUser(userId)
.then(setUser)
.catch(() => navigate('/users', { replace: true }));
}, [userId]);
`,
explanation: '参数无效时优雅地处理错误'
}
};2. 性能优化
jsx
// 参数变化优化
function OptimizedParamsComponent() {
const { id } = useParams();
const [searchParams] = useSearchParams();
// 使用useMemo避免不必要的重新计算
const filters = useMemo(() => {
return {
category: searchParams.get('category'),
minPrice: parseFloat(searchParams.get('minPrice')) || 0,
maxPrice: parseFloat(searchParams.get('maxPrice')) || Infinity
};
}, [searchParams]);
// 防抖的参数处理
const debouncedFilters = useDebounce(filters, 300);
useEffect(() => {
fetchData(id, debouncedFilters).then(setData);
}, [id, debouncedFilters]);
return (
<div>
{/* 组件内容 */}
</div>
);
}
// 防抖Hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}总结
动态路由和参数获取是构建现代Web应用的基础:
- 路径参数:通过useParams获取URL中的动态部分
- 查询参数:通过useSearchParams管理URL查询字符串
- 参数验证:确保参数格式正确和类型安全
- 状态同步:将URL参数与组件状态同步
- 性能优化:合理使用缓存和防抖
- 错误处理:优雅处理无效参数
掌握这些技术可以构建出用户友好、SEO友好的动态路由系统。