路由进阶 #
一、路由懒加载 #
1.1 基本懒加载 #
jsx
import { lazy, Suspense } from 'preact/compat';
import { Router } from 'preact-router';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Router>
<Home path="/" />
<Suspense fallback={<Loading />}>
<About path="/about" />
<Dashboard path="/dashboard" />
</Suspense>
</Router>
);
}
function Loading() {
return (
<div class="loading">
<div class="spinner" />
<p>Loading...</p>
</div>
);
}
1.2 预加载 #
jsx
const Dashboard = lazy(() => import('./pages/Dashboard'));
// 预加载函数
Dashboard.preload = () => import('./pages/Dashboard');
function App() {
return (
<div>
{/* 鼠标悬停时预加载 */}
<Link
href="/dashboard"
onMouseEnter={Dashboard.preload}
>
Dashboard
</Link>
<Router>
<Home path="/" />
<Suspense fallback={<Loading />}>
<Dashboard path="/dashboard" />
</Suspense>
</Router>
</div>
);
}
1.3 按模块分割 #
jsx
// pages/admin/index.js
export { default as Dashboard } from './Dashboard';
export { default as Users } from './Users';
export { default as Settings } from './Settings';
// App.jsx
const AdminModule = lazy(() => import('./pages/admin'));
function App() {
return (
<Router>
<Home path="/" />
<Suspense fallback={<Loading />}>
<AdminModule.Dashboard path="/admin" />
<AdminModule.Users path="/admin/users" />
<AdminModule.Settings path="/admin/settings" />
</Suspense>
</Router>
);
}
二、路由过渡动画 #
2.1 CSS 过渡 #
jsx
import { useState, useEffect } from 'preact/hooks';
import { Router, getCurrentUrl, subscribe } from 'preact-router';
function AnimatedRouter({ children }) {
const [currentPath, setCurrentPath] = useState(getCurrentUrl());
const [isTransitioning, setIsTransitioning] = useState(false);
useEffect(() => {
return subscribe((url) => {
setIsTransitioning(true);
setTimeout(() => {
setCurrentPath(url);
setIsTransitioning(false);
}, 300);
});
}, []);
return (
<div class={`router-container ${isTransitioning ? 'transitioning' : ''}`}>
<Router>
{children}
</Router>
</div>
);
}
// CSS
/*
.router-container {
transition: opacity 0.3s ease;
}
.router-container.transitioning {
opacity: 0;
}
*/
2.2 页面切换动画 #
jsx
import { useState, useEffect, useRef } from 'preact/hooks';
import { subscribe } from 'preact-router';
function PageTransition({ children }) {
const [displayChildren, setDisplayChildren] = useState(children);
const [transitionStage, setTransitionStage] = useState('fade-in');
const prevChildrenRef = useRef(children);
useEffect(() => {
const unsubscribe = subscribe(() => {
setTransitionStage('fade-out');
});
return unsubscribe;
}, []);
useEffect(() => {
if (children !== prevChildrenRef.current) {
setTransitionStage('fade-out');
}
}, [children]);
const handleTransitionEnd = () => {
if (transitionStage === 'fade-out') {
setDisplayChildren(children);
setTransitionStage('fade-in');
prevChildrenRef.current = children;
}
};
return (
<div
class={`page-transition ${transitionStage}`}
onTransitionEnd={handleTransitionEnd}
>
{displayChildren}
</div>
);
}
// CSS
/*
.page-transition {
opacity: 1;
transition: opacity 0.3s ease;
}
.page-transition.fade-out {
opacity: 0;
}
.page-transition.fade-in {
opacity: 1;
}
*/
三、高级路由模式 #
3.1 路由配置对象 #
jsx
const routes = [
{
path: '/',
component: Home,
exact: true
},
{
path: '/about',
component: About
},
{
path: '/users',
component: UserLayout,
children: [
{ path: '/', component: UserList },
{ path: '/:id', component: UserDetail }
]
},
{
path: '/admin',
component: AdminLayout,
protected: true,
children: [
{ path: '/', component: Dashboard },
{ path: '/users', component: AdminUsers }
]
}
];
function App() {
return (
<Router>
{renderRoutes(routes)}
</Router>
);
}
function renderRoutes(routes, parentPath = '') {
return routes.map(route => {
const fullPath = parentPath + route.path;
if (route.children) {
return (
<route.component key={fullPath} path={fullPath}>
{renderRoutes(route.children, fullPath)}
</route.component>
);
}
return (
<route.component key={fullPath} path={fullPath} />
);
});
}
3.2 面包屑导航 #
jsx
const routeConfig = {
'/': { title: 'Home' },
'/users': { title: 'Users' },
'/users/:id': { title: 'User Detail', parent: '/users' },
'/settings': { title: 'Settings' }
};
function Breadcrumbs() {
const [breadcrumbs, setBreadcrumbs] = useState([]);
useEffect(() => {
const updateBreadcrumbs = () => {
const path = getCurrentUrl();
const crumbs = buildBreadcrumbs(path);
setBreadcrumbs(crumbs);
};
updateBreadcrumbs();
return subscribe(updateBreadcrumbs);
}, []);
const buildBreadcrumbs = (path) => {
const crumbs = [];
let currentPath = path;
while (currentPath) {
const config = routeConfig[currentPath];
if (config) {
crumbs.unshift({ path: currentPath, title: config.title });
currentPath = config.parent;
} else {
break;
}
}
return crumbs;
};
return (
<nav class="breadcrumbs">
{breadcrumbs.map((crumb, index) => (
<span key={crumb.path}>
{index < breadcrumbs.length - 1 ? (
<>
<Link href={crumb.path}>{crumb.title}</Link>
<span> / </span>
</>
) : (
<span>{crumb.title}</span>
)}
</span>
))}
</nav>
);
}
3.3 路由元信息 #
jsx
function useRouteMeta() {
const [meta, setMeta] = useState({});
useEffect(() => {
const updateMeta = () => {
const path = getCurrentUrl();
const routeMeta = getRouteMeta(path);
setMeta(routeMeta);
// 更新页面标题
document.title = routeMeta.title || 'My App';
};
updateMeta();
return subscribe(updateMeta);
}, []);
return meta;
}
function getRouteMeta(path) {
const metas = {
'/': { title: 'Home', description: 'Welcome to our site' },
'/about': { title: 'About Us', description: 'Learn more about us' },
'/contact': { title: 'Contact', description: 'Get in touch' }
};
return metas[path] || { title: 'Not Found' };
}
function App() {
const meta = useRouteMeta();
return (
<div>
<Head>
<title>{meta.title}</title>
<meta name="description" content={meta.description} />
</Head>
<Router>
{/* ... */}
</Router>
</div>
);
}
四、路由状态管理 #
4.1 路由状态 Hook #
jsx
import { useState, useEffect } from 'preact/hooks';
import { getCurrentUrl, subscribe } from 'preact-router';
function useRouter() {
const [state, setState] = useState({
path: getCurrentUrl(),
params: {},
query: {}
});
useEffect(() => {
const parseUrl = (url) => {
const [pathname, search] = url.split('?');
const query = {};
if (search) {
new URLSearchParams(search).forEach((value, key) => {
query[key] = value;
});
}
return { path: pathname, query };
};
const unsubscribe = subscribe((url) => {
setState(prev => ({
...prev,
...parseUrl(url)
}));
});
setState(prev => ({
...prev,
...parseUrl(getCurrentUrl())
}));
return unsubscribe;
}, []);
return state;
}
// 使用
function SearchPage() {
const { query } = useRouter();
return (
<div>
<h1>Search: {query.q}</h1>
</div>
);
}
4.2 路由历史管理 #
jsx
function useHistory() {
const [history, setHistory] = useState([]);
const [currentIndex, setCurrentIndex] = useState(-1);
useEffect(() => {
const unsubscribe = subscribe((url) => {
setHistory(prev => {
const newHistory = prev.slice(0, currentIndex + 1);
newHistory.push(url);
setCurrentIndex(newHistory.length - 1);
return newHistory;
});
});
return unsubscribe;
}, [currentIndex]);
const goBack = () => {
if (currentIndex > 0) {
setCurrentIndex(currentIndex - 1);
route(history[currentIndex - 1], true);
}
};
const goForward = () => {
if (currentIndex < history.length - 1) {
setCurrentIndex(currentIndex + 1);
route(history[currentIndex + 1], true);
}
};
return { history, currentIndex, goBack, goForward };
}
五、滚动行为 #
5.1 滚动到顶部 #
jsx
import { useEffect } from 'preact/hooks';
import { subscribe } from 'preact-router';
function useScrollToTop() {
useEffect(() => {
return subscribe(() => {
window.scrollTo(0, 0);
});
}, []);
}
function App() {
useScrollToTop();
return (
<Router>
{/* ... */}
</Router>
);
}
5.2 恢复滚动位置 #
jsx
const scrollPositions = {};
function useScrollRestoration() {
useEffect(() => {
const saveScrollPosition = () => {
scrollPositions[getCurrentUrl()] = window.scrollY;
};
const restoreScrollPosition = (url) => {
const savedPosition = scrollPositions[url];
if (savedPosition !== undefined) {
setTimeout(() => {
window.scrollTo(0, savedPosition);
}, 0);
} else {
window.scrollTo(0, 0);
}
};
window.addEventListener('scroll', saveScrollPosition);
const unsubscribe = subscribe(restoreScrollPosition);
return () => {
window.removeEventListener('scroll', saveScrollPosition);
unsubscribe();
};
}, []);
}
六、路由测试 #
6.1 测试路由组件 #
jsx
import { render, screen } from '@testing-library/preact';
import { Router } from 'preact-router';
function renderWithRouter(ui, { route = '/' } = {}) {
window.history.pushState({}, 'Test page', route);
return render(
<Router>
{ui}
</Router>
);
}
describe('Navigation', () => {
it('renders navigation links', () => {
renderWithRouter(<Navigation />);
expect(screen.getByText('Home')).toBeInTheDocument();
expect(screen.getByText('About')).toBeInTheDocument();
});
});
describe('UserDetail', () => {
it('renders user details', () => {
renderWithRouter(<UserDetail id="123" />, { route: '/users/123' });
expect(screen.getByText('User 123')).toBeInTheDocument();
});
});
七、最佳实践 #
7.1 路由组织 #
text
src/
├── pages/
│ ├── Home.jsx
│ ├── About.jsx
│ └── users/
│ ├── UserList.jsx
│ └── UserDetail.jsx
├── routes/
│ ├── index.js 路由配置
│ └── guards.js 路由守卫
└── App.jsx
7.2 路由常量 #
jsx
// routes/constants.js
export const ROUTES = {
HOME: '/',
ABOUT: '/about',
USERS: '/users',
USER_DETAIL: (id) => `/users/${id}`,
ADMIN: '/admin',
LOGIN: '/login'
};
// 使用
import { ROUTES } from './routes/constants';
<Link href={ROUTES.HOME}>Home</Link>
<Link href={ROUTES.USER_DETAIL(userId)}>User</Link>
八、总结 #
| 要点 | 说明 |
|---|---|
| 懒加载 | Suspense + lazy |
| 过渡动画 | CSS transition |
| 路由配置 | 配置对象模式 |
| 滚动行为 | 滚动恢复 |
| 测试 | 测试路由组件 |
核心原则:
- 懒加载优化性能
- 提供良好的过渡体验
- 合理组织路由结构
- 处理滚动行为
最后更新:2026-03-28