路由进阶 #

一、路由懒加载 #

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