routing-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Routing Patterns

路由模式

Overview

概述

Routing determines how URLs map to content and how navigation between pages works. The approach differs significantly between traditional server-rendered apps and modern SPAs.
路由决定了URL如何映射到内容,以及页面间的导航方式。传统服务端渲染应用与现代SPA在路由实现方式上有显著差异。

Server-Side Routing

服务端路由

Server determines what to render based on the URL.
服务端根据URL决定渲染内容。

How It Works

工作原理

1. User navigates to /about
2. Browser sends request to server
3. Server matches /about to handler
4. Server renders HTML
5. Server sends complete HTML response
6. Browser loads new page (full reload)
1. 用户访问 /about
2. 浏览器向服务端发送请求
3. 服务端将 /about 匹配到对应的处理器
4. 服务端渲染HTML
5. 服务端返回完整的HTML响应
6. 浏览器加载新页面(整页刷新)

Characteristics

特性

Request: GET /products/123

Server:
  Route table:
    /            → HomeController
    /about       → AboutController
    /products/:id → ProductController  ← matches

  ProductController:
    - Fetches product 123
    - Renders HTML
    - Returns response
请求: GET /products/123

服务端:
  路由表:
    /            → HomeController
    /about       → AboutController
    /products/:id → ProductController  ← 匹配成功

  ProductController:
    - 获取产品123的数据
    - 渲染HTML
    - 返回响应

Pros and Cons

优缺点

ProsCons
SEO-friendly (full HTML)Full page reload
Simple mental modelSlower navigation
Works without JavaScriptState lost between pages
Direct URL to content mappingServer must handle every route
优点缺点
对SEO友好(完整HTML)整页刷新
理解成本低导航速度较慢
无需JavaScript即可运行页面间状态丢失
URL与内容直接映射服务端需处理所有路由

Client-Side Routing

客户端路由

JavaScript handles routing in the browser, no server round-trip for navigation.
由浏览器中的JavaScript处理路由,导航无需与服务端交互。

How It Works

工作原理

1. Initial: Browser loads app shell + JS
2. User clicks link
3. JavaScript intercepts click (preventDefault)
4. JS updates URL using History API
5. JS renders new view/component
6. No server request for HTML
1. 初始加载:浏览器加载应用外壳与JS
2. 用户点击链接
3. JavaScript拦截点击事件(preventDefault)
4. JS通过History API更新URL
5. JS渲染新视图/组件
6. 无需向服务端请求HTML

The History API

History API

javascript
// Push new URL to history (no page reload)
history.pushState({ page: 'about' }, '', '/about');

// Replace current URL
history.replaceState({ page: 'home' }, '', '/');

// Listen for back/forward navigation
window.addEventListener('popstate', (event) => {
  // Render based on current URL
  renderRoute(window.location.pathname);
});
javascript
// 向历史记录中添加新URL(不刷新页面)
history.pushState({ page: 'about' }, '', '/about');

// 替换当前URL
history.replaceState({ page: 'home' }, '', '/');

// 监听前进/后退导航
window.addEventListener('popstate', (event) => {
  // 根据当前URL渲染内容
  renderRoute(window.location.pathname);
});

Basic Router Implementation

基础路由实现

javascript
// Simplified client-side router concept
class Router {
  constructor() {
    this.routes = {};
    window.addEventListener('popstate', () => this.handleRoute());
  }

  register(path, component) {
    this.routes[path] = component;
  }

  navigate(path) {
    history.pushState({}, '', path);
    this.handleRoute();
  }

  handleRoute() {
    const path = window.location.pathname;
    const component = this.routes[path] || this.routes['/404'];
    component.render();
  }
}

// Usage
router.register('/about', AboutComponent);
router.register('/products/:id', ProductComponent);
javascript
// 简化的客户端路由概念
class Router {
  constructor() {
    this.routes = {};
    window.addEventListener('popstate', () => this.handleRoute());
  }

  register(path, component) {
    this.routes[path] = component;
  }

  navigate(path) {
    history.pushState({}, '', path);
    this.handleRoute();
  }

  handleRoute() {
    const path = window.location.pathname;
    const component = this.routes[path] || this.routes['/404'];
    component.render();
  }
}

// 使用示例
router.register('/about', AboutComponent);
router.register('/products/:id', ProductComponent);

Link Interception

链接拦截

javascript
// Intercept link clicks for client-side navigation
document.addEventListener('click', (e) => {
  const link = e.target.closest('a');
  if (link && link.href.startsWith(window.location.origin)) {
    e.preventDefault();
    router.navigate(link.pathname);
  }
});
javascript
// 拦截链接点击以实现客户端导航
document.addEventListener('click', (e) => {
  const link = e.target.closest('a');
  if (link && link.href.startsWith(window.location.origin)) {
    e.preventDefault();
    router.navigate(link.pathname);
  }
});

File-Based Routing

基于文件的路由

Route structure derived from file system layout.
路由结构由文件系统布局自动生成。

Common Patterns

常见模式

File System                          URL
──────────────────────────────       ─────────────
pages/index.tsx                  →   /
pages/about.tsx                  →   /about
pages/blog/index.tsx             →   /blog
pages/blog/[slug].tsx            →   /blog/:slug
pages/docs/[...path].tsx         →   /docs/*
pages/[category]/[id].tsx        →   /:category/:id
文件系统                          URL
──────────────────────────────       ─────────────
pages/index.tsx                  →   /
pages/about.tsx                  →   /about
pages/blog/index.tsx             →   /blog
pages/blog/[slug].tsx            →   /blog/:slug
pages/docs/[...path].tsx         →   /docs/*
pages/[category]/[id].tsx        →   /:category/:id

Framework Implementations

框架实现

Next.js (App Router):
app/
├── page.tsx                    → /
├── about/page.tsx              → /about
├── blog/[slug]/page.tsx        → /blog/:slug
└── (marketing)/                → Route group (no URL segment)
    └── pricing/page.tsx        → /pricing
SvelteKit:
src/routes/
├── +page.svelte                → /
├── about/+page.svelte          → /about
├── blog/[slug]/+page.svelte    → /blog/:slug
└── [[optional]]/+page.svelte   → / or /:optional
Remix:
app/routes/
├── _index.tsx                  → /
├── about.tsx                   → /about
├── blog._index.tsx             → /blog
├── blog.$slug.tsx              → /blog/:slug
└── $.tsx                       → Catch-all (splat)
Next.js(App Router):
app/
├── page.tsx                    → /
├── about/page.tsx              → /about
├── blog/[slug]/page.tsx        → /blog/:slug
└── (marketing)/                → 路由组(无URL分段)
    └── pricing/page.tsx        → /pricing
SvelteKit:
src/routes/
├── +page.svelte                → /
├── about/+page.svelte          → /about
├── blog/[slug]/+page.svelte    → /blog/:slug
└── [[optional]]/+page.svelte   → / 或 /:optional
Remix:
app/routes/
├── _index.tsx                  → /
├── about.tsx                   → /about
├── blog._index.tsx             → /blog
├── blog.$slug.tsx              → /blog/:slug
└── $.tsx                       → 全匹配(通配符)

Dynamic Routes

动态路由

Parameter Types

参数类型

Static:     /about          → Exact match
Dynamic:    /blog/[slug]    → Single parameter
Catch-all:  /docs/[...path] → Multiple segments
Optional:   /[[locale]]/    → With or without parameter
静态路由:     /about          → 精确匹配
动态路由:    /blog/[slug]    → 单个参数
全匹配:  /docs/[...path] → 多段路径
可选参数:   /[[locale]]/    → 带或不带参数均可

Parameter Access

参数获取

Concept (framework-agnostic):
javascript
// URL: /products/shoes/nike-air-max

// Route: /products/[category]/[id]
// Parameters: { category: 'shoes', id: 'nike-air-max' }

// Route: /products/[...path]
// Parameters: { path: ['shoes', 'nike-air-max'] }
通用概念(与框架无关):
javascript
// URL: /products/shoes/nike-air-max

// 路由: /products/[category]/[id]
// 参数: { category: 'shoes', id: 'nike-air-max' }

// 路由: /products/[...path]
// 参数: { path: ['shoes', 'nike-air-max'] }

Route Groups and Layouts

路由组与布局

Nested Layouts

嵌套布局

app/
├── layout.tsx              → Root layout (applies to all)
├── (shop)/
│   ├── layout.tsx          → Shop layout (header, cart)
│   ├── products/page.tsx   → /products (uses shop layout)
│   └── cart/page.tsx       → /cart (uses shop layout)
└── (marketing)/
    ├── layout.tsx          → Marketing layout (different nav)
    ├── about/page.tsx      → /about (uses marketing layout)
    └── pricing/page.tsx    → /pricing (uses marketing layout)
app/
├── layout.tsx              → 根布局(全局生效)
├── (shop)/
│   ├── layout.tsx          → 商城布局(头部、购物车)
│   ├── products/page.tsx   → /products(使用商城布局)
│   └── cart/page.tsx       → /cart(使用商城布局)
└── (marketing)/
    ├── layout.tsx          → 营销布局(不同导航栏)
    ├── about/page.tsx      → /about(使用营销布局)
    └── pricing/page.tsx    → /pricing(使用营销布局)

Parallel Routes

并行路由

Load multiple views simultaneously:
app/
├── @sidebar/
│   └── page.tsx            → Sidebar content
├── @main/
│   └── page.tsx            → Main content
└── layout.tsx              → Renders both in parallel
同时加载多个视图:
app/
├── @sidebar/
│   └── page.tsx            → 侧边栏内容
├── @main/
│   └── page.tsx            → 主内容
└── layout.tsx              → 并行渲染两者

Intercepting Routes

拦截路由

Handle routes differently in different contexts:
app/
├── photos/[id]/page.tsx        → Full page view
├── @modal/(.)photos/[id]/       → Modal overlay (when navigating internally)
│   └── page.tsx
└── feed/page.tsx                → Grid with links to photos
在不同上下文以不同方式处理路由:
app/
├── photos/[id]/page.tsx        → 整页视图
├── @modal/(.)photos/[id]/       → 模态框覆盖层(内部导航时触发)
│   └── page.tsx
└── feed/page.tsx                → 图片网格(带照片链接)

Navigation Patterns

导航模式

Declarative Navigation

声明式导航

jsx
// React Router / Next.js
<Link href="/about">About</Link>

// Vue Router
<router-link to="/about">About</router-link>

// SvelteKit
<a href="/about">About</a>  <!-- Enhanced automatically -->
jsx
// React Router / Next.js
<Link href="/about">关于我们</Link>

// Vue Router
<router-link to="/about">关于我们</router-link>

// SvelteKit
<a href="/about">关于我们</a>  <!-- 自动增强为客户端导航 -->

Programmatic Navigation

编程式导航

javascript
// React Router
const navigate = useNavigate();
navigate('/dashboard');

// Next.js
const router = useRouter();
router.push('/dashboard');

// Vue Router
router.push('/dashboard');

// SvelteKit
import { goto } from '$app/navigation';
goto('/dashboard');
javascript
// React Router
const navigate = useNavigate();
navigate('/dashboard');

// Next.js
const router = useRouter();
router.push('/dashboard');

// Vue Router
router.push('/dashboard');

// SvelteKit
import { goto } from '$app/navigation';
goto('/dashboard');

Navigation Options

导航选项

javascript
// Replace history (no back button)
router.replace('/login');

// With query parameters
router.push('/search?q=term');

// With hash
router.push('/docs#section');

// Scroll to top
router.push('/page', { scroll: true });

// Shallow routing (no data refetch)
router.push('/page', { shallow: true });
javascript
// 替换历史记录(无返回按钮)
router.replace('/login');

// 带查询参数
router.push('/search?q=term');

// 带哈希值
router.push('/docs#section');

// 滚动到顶部
router.push('/page', { scroll: true });

// 浅路由(不重新获取数据)
router.push('/page', { shallow: true });

Route Protection

路由保护

Authentication Guards

认证守卫

javascript
// Middleware pattern (Next.js)
export function middleware(request) {
  const token = request.cookies.get('auth-token');
  
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

// Route-level check
export async function loader({ request }) {
  const user = await getUser(request);
  if (!user) {
    throw redirect('/login');
  }
  return { user };
}
javascript
// 中间件模式(Next.js)
export function middleware(request) {
  const token = request.cookies.get('auth-token');
  
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

// 路由级校验
export async function loader({ request }) {
  const user = await getUser(request);
  if (!user) {
    throw redirect('/login');
  }
  return { user };
}

Authorization Patterns

授权模式

javascript
// Role-based routing
if (!user.roles.includes('admin')) {
  redirect('/unauthorized');
}

// Feature flags
if (!features.newDashboard) {
  redirect('/legacy-dashboard');
}
javascript
// 基于角色的路由
if (!user.roles.includes('admin')) {
  redirect('/unauthorized');
}

// 功能开关
if (!features.newDashboard) {
  redirect('/legacy-dashboard');
}

URL State Management

URL状态管理

Query Parameters

查询参数

javascript
// Read query params
const searchParams = useSearchParams();
const query = searchParams.get('q');

// Update query params
const params = new URLSearchParams(searchParams);
params.set('page', '2');
router.push(`/products?${params.toString()}`);
javascript
// 读取查询参数
const searchParams = useSearchParams();
const query = searchParams.get('q');

// 更新查询参数
const params = new URLSearchParams(searchParams);
params.set('page', '2');
router.push(`/products?${params.toString()}`);

Hash-Based State

哈希值状态

javascript
// URL: /docs#installation
// Scroll to section
useEffect(() => {
  const hash = window.location.hash;
  if (hash) {
    document.querySelector(hash)?.scrollIntoView();
  }
}, []);
javascript
// URL: /docs#installation
// 滚动到对应章节
useEffect(() => {
  const hash = window.location.hash;
  if (hash) {
    document.querySelector(hash)?.scrollIntoView();
  }
}, []);

State in URL vs Component

URL状态与组件状态对比

URL State (shareable, bookmarkable):
  - Current page, filters, search query
  - Tab selection
  - Modal open state (sometimes)

Component State (ephemeral):
  - Form input before submission
  - Hover/focus states
  - Temporary UI state
URL状态(可分享、可收藏):
  - 当前页面、筛选条件、搜索关键词
  - 标签页选择
  - 模态框打开状态(部分场景)

组件状态(临时):
  - 提交前的表单输入内容
  - 悬停/聚焦状态
  - 临时UI状态

Performance Patterns

性能优化模式

Prefetching

预加载

jsx
// Next.js - automatic on hover
<Link href="/about" prefetch={true}>About</Link>

// Manual prefetch
router.prefetch('/dashboard');
jsx
// Next.js - 鼠标悬停时自动预加载
<Link href="/about" prefetch={true}>关于我们</Link>

// 手动预加载
router.prefetch('/dashboard');

Code Splitting by Route

按路由拆分代码

javascript
// Lazy load route components
const Dashboard = lazy(() => import('./Dashboard'));

// Routes load only when accessed
<Route path="/dashboard" element={
  <Suspense fallback={<Loading />}>
    <Dashboard />
  </Suspense>
} />
javascript
// 懒加载路由组件
const Dashboard = lazy(() => import('./Dashboard'));

// 路由仅在访问时加载
<Route path="/dashboard" element={
  <Suspense fallback={<Loading />}>
    <Dashboard />
  </Suspense>
} />

Route Transitions

路由过渡

javascript
// Loading states during navigation
const navigation = useNavigation();

{navigation.state === 'loading' && <LoadingBar />}
javascript
// 导航时的加载状态
const navigation = useNavigation();

{navigation.state === 'loading' && <LoadingBar />}

Common Routing Pitfalls

常见路由陷阱

1. Breaking the Back Button

1. 破坏返回按钮

javascript
// Bad: Replaces every navigation
router.replace('/page');  // Can't go back

// Good: Push for normal navigation
router.push('/page');  // Back button works
javascript
// 错误:替换所有导航记录
router.replace('/page');  // 无法返回上一页

// 正确:正常导航使用push
router.push('/page');  // 返回按钮可正常使用

2. Not Handling 404s

2. 未处理404页面

javascript
// Ensure catch-all route exists
// app/[...not-found]/page.tsx or pages/404.tsx
javascript
// 确保存在全匹配路由
// app/[...not-found]/page.tsx 或 pages/404.tsx

3. Client-Only Routes in SSR

3. SSR中使用客户端专属路由API

javascript
// Bad: useSearchParams in server component
// Causes hydration mismatch

// Good: Use in client component or read in middleware
javascript
// 错误:在服务端组件中使用useSearchParams
// 会导致 hydration 不匹配

// 正确:在客户端组件中使用,或在中间件中读取

4. Hash URLs for SEO Content

4. 对SEO内容使用哈希URL

javascript
// Bad: /#/products/123 (not indexable)
// Good: /products/123 (indexable)

javascript
// 错误:/#/products/123(无法被索引)
// 正确:/products/123(可正常索引)

Deep Dive: Understanding Routing From First Principles

深入理解:从原理出发认识路由

What is a URL? Anatomy of Web Addresses

什么是URL?Web地址的构成

Before understanding routing, you must understand URLs (Uniform Resource Locators):
https://www.example.com:443/products/shoes?color=red&size=10#reviews
│       │              │   │              │                    │
│       │              │   │              │                    └─ Fragment (client-only, not sent to server)
│       │              │   │              │
│       │              │   │              └─ Query String (key-value pairs)
│       │              │   │
│       │              │   └─ Pathname (the route)
│       │              │
│       │              └─ Port (usually implicit: 80 for http, 443 for https)
│       │
│       └─ Hostname (domain)
└─ Protocol (scheme)
Key insight: Only
protocol
,
hostname
,
port
,
pathname
, and
query
are sent to the server. The
fragment
(#hash) stays in the browser.
在理解路由之前,必须先了解URL(统一资源定位符)的构成:
https://www.example.com:443/products/shoes?color=red&size=10#reviews
│       │              │   │              │                    │
│       │              │   │              │                    └─ 片段(仅客户端可见,不发送到服务端)
│       │              │   │              │
│       │              │   │              └─ 查询字符串(键值对)
│       │              │   │
│       │              │   └─ 路径名(路由部分)
│       │              │
│       │              └─ 端口(通常隐含:http为80,https为443)
│       │
│       └─ 主机名(域名)
└─ 协议(方案)
关键知识点: 只有
协议
主机名
端口
路径名
查询参数
会发送到服务端。
片段
(#哈希值)仅在浏览器中生效。

The History of Web Routing

Web路由的发展历程

Era 1: File-Based (1990s)
URL: /products/shoes.html
Server: Find file at /var/www/products/shoes.html, return it
URLs literally mapped to files on disk.
Era 2: Dynamic Routing (2000s)
URL: /products.php?id=123
Server: Execute products.php, which reads $_GET['id'] = 123
Server-side scripts processed parameters.
Era 3: RESTful Routes (2010s)
URL: /products/123
Server: Route pattern /products/:id matches, extract id=123
Clean URLs with pattern matching.
Era 4: Client-Side Routing (2010s-present)
URL: /products/123
Client: JavaScript intercepts, renders Product component with id=123
Server: May never see this request (after initial load)
时代1:基于文件的路由(1990年代)
URL: /products/shoes.html
服务端:找到/var/www/products/shoes.html文件并返回
URL与磁盘上的文件直接对应。
时代2:动态路由(2000年代)
URL: /products.php?id=123
服务端:执行products.php,读取$_GET['id'] = 123
服务端脚本处理参数。
时代3:RESTful路由(2010年代)
URL: /products/123
服务端:路由模式/products/:id匹配成功,提取id=123
使用清晰的URL与模式匹配。
时代4:客户端路由(2010年代至今)
URL: /products/123
客户端:JavaScript拦截请求,渲染id=123的Product组件
服务端:初始加载后可能永远不会收到该路由的请求

How Server-Side Routing Actually Works

服务端路由的实际工作原理

When a server receives a request, it must decide what to do:
javascript
// Express.js example - simplified internal logic
class Router {
  constructor() {
    this.routes = [];
  }
  
  // Register route
  get(pattern, handler) {
    this.routes.push({
      method: 'GET',
      pattern: this.parsePattern(pattern),  // /users/:id → regex
      handler
    });
  }
  
  // Pattern to regex conversion
  parsePattern(pattern) {
    // /users/:id → /^\/users\/([^\/]+)$/
    // /posts/:id/comments → /^\/posts\/([^\/]+)\/comments$/
    const regexStr = pattern
      .replace(/:([^/]+)/g, '([^/]+)')  // :param → capture group
      .replace(/\//g, '\\/');            // Escape slashes
    return new RegExp(`^${regexStr}$`);
  }
  
  // Handle incoming request
  handle(req, res) {
    const { method, url } = req;
    const pathname = new URL(url, 'http://localhost').pathname;
    
    // Find matching route
    for (const route of this.routes) {
      if (route.method !== method) continue;
      
      const match = pathname.match(route.pattern);
      if (match) {
        // Extract params
        req.params = this.extractParams(route.pattern, match);
        return route.handler(req, res);
      }
    }
    
    // No match - 404
    res.status(404).send('Not Found');
  }
}
Route matching order matters:
javascript
// These are checked in order
app.get('/users/new', newUserHandler);     // Must be BEFORE :id
app.get('/users/:id', getUserHandler);     // Would match "new" as id otherwise

// Request: GET /users/new
// Check 1: /users/new matches! → newUserHandler
// Never reaches /users/:id

// Request: GET /users/123
// Check 1: /users/new doesn't match
// Check 2: /users/:id matches! → getUserHandler with id=123
当服务端收到请求时,需要决定如何处理:
javascript
// Express.js示例 - 简化的内部逻辑
class Router {
  constructor() {
    this.routes = [];
  }
  
  // 注册路由
  get(pattern, handler) {
    this.routes.push({
      method: 'GET',
      pattern: this.parsePattern(pattern),  // /users/:id → 正则表达式
      handler
    });
  }
  
  // 模式转正则表达式
  parsePattern(pattern) {
    // /users/:id → /^\/users\/([^\/]+)$/
    // /posts/:id/comments → /^\/posts\/([^\/]+)\/comments$/
    const regexStr = pattern
      .replace(/:([^/]+)/g, '([^/]+)')  // :param → 捕获组
      .replace(/\//g, '\\/');            // 转义斜杠
    return new RegExp(`^${regexStr}$`);
  }
  
  // 处理传入请求
  handle(req, res) {
    const { method, url } = req;
    const pathname = new URL(url, 'http://localhost').pathname;
    
    // 查找匹配的路由
    for (const route of this.routes) {
      if (route.method !== method) continue;
      
      const match = pathname.match(route.pattern);
      if (match) {
        // 提取参数
        req.params = this.extractParams(route.pattern, match);
        return route.handler(req, res);
      }
    }
    
    // 无匹配路由 - 返回404
    res.status(404).send('Not Found');
  }
}
路由匹配顺序很重要:
javascript
// 按注册顺序进行匹配
app.get('/users/new', newUserHandler);     // 必须在:id之前注册
app.get('/users/:id', getUserHandler);     // 否则会将"new"识别为id

// 请求: GET /users/new
// 检查1: /users/new匹配成功! → 执行newUserHandler
// 不会到达/users/:id

// 请求: GET /users/123
// 检查1: /users/new不匹配
// 检查2: /users/:id匹配成功! → 执行getUserHandler,id=123

How Client-Side Routing Intercepts Browser Behavior

客户端路由如何拦截浏览器行为

The browser has default navigation behavior. Client-side routing overrides it:
javascript
// DEFAULT BROWSER BEHAVIOR:
// 1. User clicks <a href="/about">
// 2. Browser sees href attribute
// 3. Browser sends GET request to /about
// 4. Browser receives HTML response
// 5. Browser replaces entire page

// SPA INTERCEPTION:
document.addEventListener('click', (event) => {
  // Find the link element (might be nested: <a><span>Click</span></a>)
  const anchor = event.target.closest('a');
  if (!anchor) return;  // Not a link click
  
  // Check if it's an internal link
  const url = new URL(anchor.href);
  if (url.origin !== window.location.origin) return;  // External link
  
  // Check for special cases
  if (anchor.hasAttribute('download')) return;  // Download link
  if (anchor.target === '_blank') return;       // New tab
  if (event.metaKey || event.ctrlKey) return;   // Cmd/Ctrl+click
  
  // NOW we intercept
  event.preventDefault();  // STOP browser's default behavior
  
  // Handle navigation ourselves
  navigateTo(url.pathname + url.search + url.hash);
});

function navigateTo(path) {
  // Update URL bar without page reload
  window.history.pushState({ path }, '', path);
  
  // Render the appropriate component
  renderRoute(path);
}
浏览器有默认的导航行为,客户端路由会覆盖这一行为:
javascript
// 浏览器默认行为:
// 1. 用户点击<a href="/about">
// 2. 浏览器识别href属性
// 3. 浏览器向/about发送GET请求
// 4. 浏览器接收HTML响应
// 5. 浏览器替换整个页面

// SPA拦截逻辑:
document.addEventListener('click', (event) => {
  // 查找链接元素(可能嵌套:<a><span>点击</span></a>)
  const anchor = event.target.closest('a');
  if (!anchor) return;  // 不是链接点击
  
  // 检查是否为内部链接
  const url = new URL(anchor.href);
  if (url.origin !== window.location.origin) return;  // 外部链接
  
  // 检查特殊情况
  if (anchor.hasAttribute('download')) return;  // 下载链接
  if (anchor.target === '_blank') return;       // 新标签页打开
  if (event.metaKey || event.ctrlKey) return;   // Cmd/Ctrl+点击
  
  // 开始拦截
  event.preventDefault();  // 阻止浏览器默认行为
  
  // 自行处理导航
  navigateTo(url.pathname + url.search + url.hash);
});

function navigateTo(path) {
  // 更新URL栏但不刷新页面
  window.history.pushState({ path }, '', path);
  
  // 渲染对应组件
  renderRoute(path);
}

The History API in Detail

History API详解

The History API is what makes SPAs possible without hash URLs:
javascript
// The history stack is like browser tabs' back/forward buttons
// But for a single tab:

// Initial state
// History: [ {url: '/'} ]
//                  ↑ current

// User navigates to /about
history.pushState({ page: 'about' }, '', '/about');
// History: [ {url: '/'}, {url: '/about'} ]
//                              ↑ current

// User navigates to /contact
history.pushState({ page: 'contact' }, '', '/contact');
// History: [ {url: '/'}, {url: '/about'}, {url: '/contact'} ]
//                                              ↑ current

// User clicks back button
// Browser fires 'popstate' event
// History: [ {url: '/'}, {url: '/about'}, {url: '/contact'} ]
//                              ↑ current (moved back)

// User clicks forward button
// Browser fires 'popstate' event again
// History: [ {url: '/'}, {url: '/about'}, {url: '/contact'} ]
//                                              ↑ current (moved forward)

// IMPORTANT: pushState does NOT fire popstate
// Only back/forward buttons fire popstate
// Your router must handle both!
Handling popstate:
javascript
window.addEventListener('popstate', (event) => {
  // event.state contains the state object from pushState
  // If user used back button, this fires with previous state
  
  // The URL has ALREADY changed when this fires
  const currentPath = window.location.pathname;
  
  renderRoute(currentPath);
});
History API让SPA无需使用哈希URL即可实现导航:
javascript
// 历史记录栈类似于浏览器标签页的前进/后退按钮
// 但针对单个标签页:

// 初始状态
// 历史记录: [ {url: '/'} ]
//                  ↑ 当前页面

// 用户导航到/about
history.pushState({ page: 'about' }, '', '/about');
// 历史记录: [ {url: '/'}, {url: '/about'} ]
//                              ↑ 当前页面

// 用户导航到/contact
history.pushState({ page: 'contact' }, '', '/contact');
// 历史记录: [ {url: '/'}, {url: '/about'}, {url: '/contact'} ]
//                                              ↑ 当前页面

// 用户点击返回按钮
// 浏览器触发'popstate'事件
// 历史记录: [ {url: '/'}, {url: '/about'}, {url: '/contact'} ]
//                              ↑ 当前页面(回退)

// 用户点击前进按钮
// 浏览器再次触发'popstate'事件
// 历史记录: [ {url: '/'}, {url: '/about'}, {url: '/contact'} ]
//                                              ↑ 当前页面(前进)

// 重要提示: pushState不会触发popstate事件
// 只有前进/后退按钮会触发popstate
// 路由必须同时处理两种情况!
处理popstate事件:
javascript
window.addEventListener('popstate', (event) => {
  // event.state包含pushState传入的状态对象
  // 如果用户点击了返回按钮,该事件会触发并携带上一个状态
  
  // 此时URL已经改变
  const currentPath = window.location.pathname;
  
  renderRoute(currentPath);
});

Hash Routing vs History Routing

哈希路由与历史路由对比

Before the History API (HTML5, ~2010), SPAs used hash-based routing:
javascript
// HASH ROUTING:
// URLs look like: /#/about, /#/products/123

// Why hashes? The hash fragment is NEVER sent to server
// So changing it doesn't trigger page reload

// Navigation:
window.location.hash = '#/about';

// Listening for changes:
window.addEventListener('hashchange', () => {
  const route = window.location.hash.slice(1);  // Remove #
  renderRoute(route);
});

// PROBLEMS WITH HASH ROUTING:
// 1. SEO: Crawlers ignore hash fragments
//    /products/shoes and /products/shoes#/details are same URL to Google
//
// 2. Ugly URLs: example.com/#/products/shoes vs example.com/products/shoes
//
// 3. Server can't respond to routes: everything goes to root
History routing advantages:
javascript
// HISTORY ROUTING:
// URLs look like: /about, /products/123 (clean!)

// Navigation:
history.pushState({}, '', '/about');

// Server sees the real URL
// SEO works properly
// Clean, professional URLs

// REQUIREMENT: Server must handle all routes
// Server must return the SPA for ANY route
// Otherwise: /products/123 → 404 if user refreshes
在History API出现之前(HTML5,约2010年),SPA使用基于哈希的路由:
javascript
// 哈希路由:
// URL格式: /#/about, /#/products/123

// 为什么使用哈希? 哈希片段永远不会发送到服务端
// 所以修改哈希不会触发页面刷新

// 导航:
window.location.hash = '#/about';

// 监听哈希变化:
window.addEventListener('hashchange', () => {
  const route = window.location.hash.slice(1);  // 移除#
  renderRoute(route);
});

// 哈希路由的问题:
// 1. SEO问题: 爬虫忽略哈希片段
//    /products/shoes 和 /products/shoes#/details 对Google来说是同一个URL
//
// 2. URL不美观: example.com/#/products/shoes vs example.com/products/shoes
//
// 3. 服务端无法响应路由: 所有请求都指向根路径
历史路由的优势:
javascript
// 历史路由:
// URL格式: /about, /products/123(简洁!)

// 导航:
history.pushState({}, '', '/about');

// 服务端能看到真实URL
// SEO正常工作
// 简洁、专业的URL

// 要求: 服务端必须处理所有路由
// 服务端必须为任意路由返回SPA入口文件
// 否则: 用户刷新页面时/products/123会返回404

File-Based Routing: How It Works Internally

基于文件的路由:内部工作原理

Meta-frameworks use file system as routing configuration:
javascript
// At build time, the framework scans your files:

// File structure:
// app/
// ├── page.tsx          → /
// ├── about/page.tsx    → /about
// └── blog/[slug]/page.tsx → /blog/:slug

// Framework generates route table:
const routes = [
  { pattern: /^\/$/, component: () => import('./app/page.tsx') },
  { pattern: /^\/about$/, component: () => import('./app/about/page.tsx') },
  { pattern: /^\/blog\/([^/]+)$/, component: () => import('./app/blog/[slug]/page.tsx') },
];

// Dynamic segments become route parameters:
// [slug] → :slug → captured as params.slug
// [id] → :id → captured as params.id
// [...path] → * → captured as params.path (array)
Why file-based routing became popular:
EXPLICIT ROUTING (React Router, Express):
- Route definitions separate from components
- Easy to have mismatched routes/files
- Must manually update both

// routes.tsx
<Route path="/products/:id" component={ProductPage} />

// ProductPage.tsx (might be anywhere)
export function ProductPage() { ... }


FILE-BASED ROUTING:
- Location IS the route
- Impossible to have orphan components
- Adding page = adding route automatically

// app/products/[id]/page.tsx (location defines route)
export default function ProductPage() { ... }
元框架将文件系统作为路由配置:
javascript
// 构建时,框架会扫描你的文件:

// 文件结构:
// app/
// ├── page.tsx          → /
// ├── about/page.tsx    → /about
// └── blog/[slug]/page.tsx → /blog/:slug

// 框架生成路由表:
const routes = [
  { pattern: /^\/$/, component: () => import('./app/page.tsx') },
  { pattern: /^\/about$/, component: () => import('./app/about/page.tsx') },
  { pattern: /^\/blog\/([^/]+)$/, component: () => import('./app/blog/[slug]/page.tsx') },
];

// 动态分段会成为路由参数:
// [slug] → :slug → 作为params.slug捕获
// [id] → :id → 作为params.id捕获
// [...path] → * → 作为params.path(数组)捕获
基于文件的路由流行的原因:
显式路由(React Router, Express):
- 路由定义与组件分离
- 容易出现路由与文件不匹配的情况
- 必须手动更新两者

// routes.tsx
<Route path="/products/:id" component={ProductPage} />

// ProductPage.tsx(可能在任意位置)
export function ProductPage() { ... }


基于文件的路由:
- 文件位置即路由
- 不会出现孤立组件
- 添加页面即自动添加路由

// app/products/[id]/page.tsx(位置定义路由)
export default function ProductPage() { ... }

Route Parameters: Extraction and Typing

路由参数:提取与类型

Understanding how parameters flow from URL to code:
javascript
// URL: /products/shoes/nike-air-max/reviews

// Route pattern: /products/[category]/[productId]/reviews
// Regex: /^\/products\/([^/]+)\/([^/]+)\/reviews$/

// Matching process:
const match = url.match(pattern);
// match = [
//   '/products/shoes/nike-air-max/reviews',  // Full match
//   'shoes',                                   // Group 1: category
//   'nike-air-max'                            // Group 2: productId
// ]

// Framework extracts to params object:
const params = {
  category: 'shoes',
  productId: 'nike-air-max'
};

// Your component receives these:
function ProductReviewsPage({ params }) {
  console.log(params.category);   // 'shoes'
  console.log(params.productId);  // 'nike-air-max'
}
Catch-all routes:
javascript
// Route: /docs/[...path]
// URL: /docs/getting-started/installation/windows

// The [...path] captures EVERYTHING after /docs/
const params = {
  path: ['getting-started', 'installation', 'windows']  // Array!
};

// Useful for:
// - Documentation with arbitrary depth
// - File browsers
// - Fallback/404 routes
理解参数如何从URL传递到代码:
javascript
// URL: /products/shoes/nike-air-max/reviews

// 路由模式: /products/[category]/[productId]/reviews
// 正则表达式: /^\/products\/([^/]+)\/([^/]+)\/reviews$/

// 匹配过程:
const match = url.match(pattern);
// match = [
//   '/products/shoes/nike-air-max/reviews',  // 完整匹配
//   'shoes',                                   // 分组1: category
//   'nike-air-max'                            // 分组2: productId
// ]

// 框架将参数提取为对象:
const params = {
  category: 'shoes',
  productId: 'nike-air-max'
};

// 组件接收参数:
function ProductReviewsPage({ params }) {
  console.log(params.category);   // 'shoes'
  console.log(params.productId);  // 'nike-air-max'
}
全匹配路由:
javascript
// 路由: /docs/[...path]
// URL: /docs/getting-started/installation/windows

// [...path]会捕获/docs/之后的所有内容
const params = {
  path: ['getting-started', 'installation', 'windows']  // 数组!
};

// 适用场景:
// - 任意深度的文档
// - 文件浏览器
// - 回退/404路由

Nested Routes and Layouts: The Component Tree

嵌套路由与布局:组件树

Nested routing creates component hierarchies:
URL: /dashboard/settings/profile

Route hierarchy:
app/
├── layout.tsx           ← Root layout (nav, footer)
└── dashboard/
    ├── layout.tsx       ← Dashboard layout (sidebar)
    └── settings/
        ├── layout.tsx   ← Settings layout (tabs)
        └── profile/
            └── page.tsx ← Profile content

Renders as:
<RootLayout>              {/* Always present */}
  <DashboardLayout>       {/* Present for /dashboard/* */}
    <SettingsLayout>      {/* Present for /dashboard/settings/* */}
      <ProfilePage />     {/* The actual content */}
    </SettingsLayout>
  </DashboardLayout>
</RootLayout>
Why nested layouts matter:
jsx
// WITHOUT nested layouts:
// Every page must include all wrappers

function DashboardSettingsProfilePage() {
  return (
    <RootLayout>
      <DashboardSidebar />
      <SettingsTabs />
      <ProfileForm />   {/* Actual unique content */}
    </RootLayout>
  );
}

function DashboardSettingsSecurityPage() {
  return (
    <RootLayout>           {/* Duplicated */}
      <DashboardSidebar /> {/* Duplicated */}
      <SettingsTabs />     {/* Duplicated */}
      <SecurityForm />     {/* Actual unique content */}
    </RootLayout>
  );
}

// WITH nested layouts:
// Layouts persist, only inner content changes

// Navigating from /dashboard/settings/profile
// to /dashboard/settings/security:
// - RootLayout: PRESERVED (no re-render)
// - DashboardLayout: PRESERVED (no re-render)
// - SettingsLayout: PRESERVED (no re-render)
// - Only ProfilePage → SecurityPage changes!
嵌套路由会创建组件层级:
URL: /dashboard/settings/profile

路由层级:
app/
├── layout.tsx           ← 根布局(导航栏、页脚)
└── dashboard/
    ├── layout.tsx       ← 仪表盘布局(侧边栏)
    └── settings/
        ├── layout.tsx   ← 设置布局(标签页)
        └── profile/
            └── page.tsx ← 个人资料内容

渲染结构:
<RootLayout>              {/* 始终存在 */}
  <DashboardLayout>       {/* /dashboard/* 路由生效 */}
    <SettingsLayout>      {/* /dashboard/settings/* 路由生效 */}
      <ProfilePage />     {/* 实际内容 */}
    </SettingsLayout>
  </DashboardLayout>
</RootLayout>
嵌套布局的重要性:
jsx
// 无嵌套布局:
// 每个页面必须包含所有外层组件

function DashboardSettingsProfilePage() {
  return (
    <RootLayout>
      <DashboardSidebar />
      <SettingsTabs />
      <ProfileForm />   {/* 唯一的差异化内容 */}
    </RootLayout>
  );
}

function DashboardSettingsSecurityPage() {
  return (
    <RootLayout>           {/* 重复代码 */}
      <DashboardSidebar /> {/* 重复代码 */}
      <SettingsTabs />     {/* 重复代码 */}
      <SecurityForm />     {/* 唯一的差异化内容 */}
    </RootLayout>
  );
}

// 有嵌套布局:
// 布局会保留,仅内部内容变化

// 从/dashboard/settings/profile导航到/dashboard/settings/security:
// - RootLayout: 保留(不重新渲染)
// - DashboardLayout: 保留(不重新渲染)
// - SettingsLayout: 保留(不重新渲染)
// - 仅ProfilePage替换为SecurityPage!

Route Guards and Middleware: The Request Pipeline

路由守卫与中间件:请求流水线

Routes often need protection or preprocessing:
javascript
// THE MIDDLEWARE CHAIN:
// Request flows through layers before reaching handler

// Request: GET /dashboard/admin

// Layer 1: Logging middleware
function loggingMiddleware(request, next) {
  console.log(`${request.method} ${request.url}`);
  return next(request);  // Continue to next middleware
}

// Layer 2: Authentication middleware
function authMiddleware(request, next) {
  const token = request.cookies.get('auth-token');
  if (!token) {
    return redirect('/login');  // Short-circuit: stop here
  }
  request.user = decodeToken(token);
  return next(request);  // Continue to next middleware
}

// Layer 3: Authorization middleware
function adminMiddleware(request, next) {
  if (!request.user.isAdmin) {
    return redirect('/unauthorized');
  }
  return next(request);
}

// Layer 4: Route handler
function adminDashboardHandler(request) {
  return renderPage(<AdminDashboard user={request.user} />);
}

// Request flows:
// loggingMiddleware → authMiddleware → adminMiddleware → adminDashboardHandler
//                           ↓
//                    (if no token)
//                           ↓
//                    redirect('/login')
路由通常需要保护或预处理:
javascript
// 中间件链:
// 请求在到达处理器前会流经多个层级

// 请求: GET /dashboard/admin

// 层级1: 日志中间件
function loggingMiddleware(request, next) {
  console.log(`${request.method} ${request.url}`);
  return next(request);  // 继续到下一个中间件
}

// 层级2: 认证中间件
function authMiddleware(request, next) {
  const token = request.cookies.get('auth-token');
  if (!token) {
    return redirect('/login');  // 短路: 在此处终止
  }
  request.user = decodeToken(token);
  return next(request);  // 继续到下一个中间件
}

// 层级3: 授权中间件
function adminMiddleware(request, next) {
  if (!request.user.isAdmin) {
    return redirect('/unauthorized');
  }
  return next(request);
}

// 层级4: 路由处理器
function adminDashboardHandler(request) {
  return renderPage(<AdminDashboard user={request.user} />);
}

// 请求流程:
// loggingMiddleware → authMiddleware → adminMiddleware → adminDashboardHandler
//                           ↓
//                    (如果没有token)
//                           ↓
//                    redirect('/login')

Prefetching: How Routers Optimize Navigation

预加载:路由如何优化导航

Modern routers predict user navigation:
javascript
// HOVER PREFETCHING:
// When user hovers over a link, likely to click

<Link href="/about" prefetch>About</Link>

// Framework behavior:
link.addEventListener('mouseenter', () => {
  // Start loading the /about route code and data
  router.prefetch('/about');
});

// VIEWPORT PREFETCHING:
// Prefetch links visible on screen

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const href = entry.target.getAttribute('href');
      router.prefetch(href);
    }
  });
});

document.querySelectorAll('a[prefetch]').forEach((link) => {
  observer.observe(link);
});

// WHAT PREFETCH ACTUALLY DOES:
// 1. Loads the route's JavaScript chunk
// 2. May also preload data (framework-dependent)
// 3. Stores in memory for instant navigation
现代路由会预测用户的导航行为:
javascript
// 悬停预加载:
// 当用户悬停在链接上时,很可能会点击

<Link href="/about" prefetch>关于我们</Link>

// 框架行为:
link.addEventListener('mouseenter', () => {
  // 开始加载/about路由的代码和数据
  router.prefetch('/about');
});

// 视口预加载:
// 预加载屏幕中可见的链接

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const href = entry.target.getAttribute('href');
      router.prefetch(href);
    }
  });
});

document.querySelectorAll('a[prefetch]').forEach((link) => {
  observer.observe(link);
});

// 预加载实际做的事情:
// 1. 加载路由的JavaScript代码块
// 2. 可能还会预加载数据(取决于框架)
// 3. 存储在内存中以实现即时导航

Scroll Restoration: The Hidden Complexity

滚动恢复:隐藏的复杂性

When navigating, browsers must manage scroll position:
javascript
// BROWSER DEFAULT BEHAVIOR:
// - New navigation: scroll to top
// - Back/forward: restore previous scroll position

// SPA CHALLENGE:
// Browser doesn't know we "navigated" - URL changed via JavaScript
// Must manually handle scroll:

function navigateTo(path) {
  // Save current scroll position
  const scrollPosition = window.scrollY;
  history.replaceState(
    { ...history.state, scrollY: scrollPosition },
    ''
  );
  
  // Navigate
  history.pushState({ path, scrollY: 0 }, '', path);
  
  // Scroll to top for new navigation
  window.scrollTo(0, 0);
}

window.addEventListener('popstate', (event) => {
  renderRoute(window.location.pathname);
  
  // Restore scroll position for back/forward
  if (event.state?.scrollY !== undefined) {
    // Wait for content to render
    requestAnimationFrame(() => {
      window.scrollTo(0, event.state.scrollY);
    });
  }
});
导航时,浏览器必须管理滚动位置:
javascript
// 浏览器默认行为:
// - 新导航: 滚动到顶部
// - 前进/后退: 恢复之前的滚动位置

// SPA的挑战:
// 浏览器不知道我们"导航"了 - URL是通过JavaScript修改的
// 必须手动处理滚动:

function navigateTo(path) {
  // 保存当前滚动位置
  const scrollPosition = window.scrollY;
  history.replaceState(
    { ...history.state, scrollY: scrollPosition },
    ''
  );
  
  // 导航
  history.pushState({ path, scrollY: 0 }, '', path);
  
  // 新导航滚动到顶部
  window.scrollTo(0, 0);
}

window.addEventListener('popstate', (event) => {
  renderRoute(window.location.pathname);
  
  // 前进/后退时恢复滚动位置
  if (event.state?.scrollY !== undefined) {
    // 等待内容渲染完成
    requestAnimationFrame(() => {
      window.scrollTo(0, event.state.scrollY);
    });
  }
});

The Edge Cases Every Router Must Handle

每个路由都必须处理的边缘情况

javascript
// 1. TRAILING SLASHES
// /about vs /about/ - are they the same?
// Your router must decide and be consistent
// Usually: redirect one to the other

// 2. CASE SENSITIVITY
// /About vs /about - same route?
// URLs are technically case-sensitive
// Best practice: lowercase, redirect others

// 3. ENCODED CHARACTERS
// /products/running%20shoes = /products/running shoes
// Router must decode: decodeURIComponent(path)

// 4. DOUBLE SLASHES
// /products//shoes - valid?
// Should probably normalize: /products/shoes

// 5. DOT SEGMENTS
// /products/../about = /about
// May need to resolve relative paths

// 6. QUERY STRING HANDLING
// /search?q=foo vs /search?q=bar
// Same route, different parameters
// Router matches path, your code handles query

// 7. HASH FRAGMENTS
// /products#reviews
// Hash not sent to server
// Client must scroll to #reviews element

javascript
// 1. 尾部斜杠
// /about vs /about/ - 是否视为同一路由?
// 路由必须做出决定并保持一致
// 通常: 将其中一个重定向到另一个

// 2. 大小写敏感性
// /About vs /about - 同一路由?
// URL在技术上是区分大小写的
// 最佳实践: 使用小写,将其他情况重定向

// 3. 编码字符
// /products/running%20shoes = /products/running shoes
// 路由必须解码: decodeURIComponent(path)

// 4. 双斜杠
// /products//shoes - 是否有效?
// 通常应该规范化为: /products/shoes

// 5. 点分段
// /products/../about = /about
// 可能需要解析相对路径

// 6. 查询字符串处理
// /search?q=foo vs /search?q=bar
// 同一路由,不同参数
// 路由匹配路径,代码处理查询参数

// 7. 哈希片段
// /products#reviews
// 哈希不会发送到服务端
// 客户端必须滚动到#reviews元素

For Framework Authors: Building Routing Systems

面向框架开发者:构建路由系统

Implementation Note: The patterns and code examples below represent one proven approach to building routing systems. Different frameworks take different approaches—React Router uses a declarative model, Next.js uses file-based routing, and SvelteKit combines both. The direction shown here provides core concepts that most routers share. Adapt these patterns based on your framework's component model, build pipeline, and developer experience goals.
实现说明: 以下模式和代码示例代表了一种经过验证的路由系统构建方法。不同框架采用不同的方式——React Router使用声明式模型,Next.js使用基于文件的路由,SvelteKit则结合了两者。此处展示的方向提供了大多数路由系统共有的核心概念。请根据你的框架的组件模型、构建流程和开发者体验目标调整这些模式。

Implementing a Route Matcher

实现路由匹配器

javascript
// ROUTE MATCHING IMPLEMENTATION

class RouteMatcher {
  constructor() {
    this.routes = [];
  }
  
  // Add route with pattern
  add(pattern, handler, meta = {}) {
    const { regex, paramNames, score } = this.compilePattern(pattern);
    this.routes.push({ pattern, regex, paramNames, handler, meta, score });
    // Keep sorted by specificity
    this.routes.sort((a, b) => b.score - a.score);
  }
  
  // Compile pattern to regex
  compilePattern(pattern) {
    const paramNames = [];
    let score = 0;
    
    const regexStr = pattern
      .split('/')
      .filter(Boolean)
      .map(segment => {
        // Catch-all: [...param]
        if (segment.startsWith('[...') && segment.endsWith(']')) {
          paramNames.push(segment.slice(4, -1));
          score += 1; // Lowest priority
          return '(.+)';
        }
        
        // Optional: [[param]]
        if (segment.startsWith('[[') && segment.endsWith(']]')) {
          paramNames.push(segment.slice(2, -2));
          score += 5;
          return '([^/]*)';
        }
        
        // Dynamic: [param]
        if (segment.startsWith('[') && segment.endsWith(']')) {
          paramNames.push(segment.slice(1, -1));
          score += 10;
          return '([^/]+)';
        }
        
        // Static segment
        score += 100;
        return escapeRegex(segment);
      })
      .join('/');
    
    return {
      regex: new RegExp(`^/${regexStr}/?$`),
      paramNames,
      score,
    };
  }
  
  // Match URL to route
  match(pathname) {
    for (const route of this.routes) {
      const match = pathname.match(route.regex);
      if (match) {
        const params = {};
        route.paramNames.forEach((name, i) => {
          const value = match[i + 1];
          // Handle catch-all as array
          if (route.pattern.includes(`[...${name}]`)) {
            params[name] = value ? value.split('/') : [];
          } else {
            params[name] = value;
          }
        });
        
        return { route, params };
      }
    }
    return null;
  }
}

function escapeRegex(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
javascript
// 路由匹配实现

class RouteMatcher {
  constructor() {
    this.routes = [];
  }
  
  // 添加带模式的路由
  add(pattern, handler, meta = {}) {
    const { regex, paramNames, score } = this.compilePattern(pattern);
    this.routes.push({ pattern, regex, paramNames, handler, meta, score });
    // 按特异性排序
    this.routes.sort((a, b) => b.score - a.score);
  }
  
  // 将模式编译为正则表达式
  compilePattern(pattern) {
    const paramNames = [];
    let score = 0;
    
    const regexStr = pattern
      .split('/')
      .filter(Boolean)
      .map(segment => {
        // 全匹配: [...param]
        if (segment.startsWith('[...') && segment.endsWith(']')) {
          paramNames.push(segment.slice(4, -1));
          score += 1; // 最低优先级
          return '(.+)';
        }
        
        // 可选参数: [[param]]
        if (segment.startsWith('[[') && segment.endsWith(']]')) {
          paramNames.push(segment.slice(2, -2));
          score += 5;
          return '([^/]*)';
        }
        
        // 动态参数: [param]
        if (segment.startsWith('[') && segment.endsWith(']')) {
          paramNames.push(segment.slice(1, -1));
          score += 10;
          return '([^/]+)';
        }
        
        // 静态分段
        score += 100;
        return escapeRegex(segment);
      })
      .join('/');
    
    return {
      regex: new RegExp(`^/${regexStr}/?$`),
      paramNames,
      score,
    };
  }
  
  // 匹配URL到路由
  match(pathname) {
    for (const route of this.routes) {
      const match = pathname.match(route.regex);
      if (match) {
        const params = {};
        route.paramNames.forEach((name, i) => {
          const value = match[i + 1];
          // 全匹配参数处理为数组
          if (route.pattern.includes(`[...${name}]`)) {
            params[name] = value ? value.split('/') : [];
          } else {
            params[name] = value;
          }
        });
        
        return { route, params };
      }
    }
    return null;
  }
}

function escapeRegex(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

Building a File-Based Router Generator

构建基于文件的路由生成器

javascript
// FILE-BASED ROUTING GENERATOR (Build Tool)

import { glob } from 'glob';
import path from 'path';
import { parse } from '@babel/parser';

async function generateRouteManifest(config) {
  const { pagesDir, extensions = ['.tsx', '.jsx', '.ts', '.js'] } = config;
  
  // Find all route files
  const pattern = `**/*{${extensions.join(',')}}`;
  const files = await glob(pattern, { cwd: pagesDir });
  
  const routes = [];
  
  for (const file of files) {
    // Skip special files
    if (file.startsWith('_') || file.includes('/_')) continue;
    
    const route = await processRouteFile(pagesDir, file);
    if (route) routes.push(route);
  }
  
  // Sort by specificity
  routes.sort((a, b) => b.score - a.score);
  
  return routes;
}

async function processRouteFile(pagesDir, file) {
  const fullPath = path.join(pagesDir, file);
  const content = await fs.readFile(fullPath, 'utf-8');
  const ast = parse(content, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
  
  // Extract route metadata from exports
  const meta = extractRouteMeta(ast);
  
  // Convert file path to route pattern
  let route = file
    .replace(/\.(tsx?|jsx?)$/, '')    // Remove extension
    .replace(/\/index$/, '')           // /index -> /
    .replace(/\[\.\.\.(\w+)\]/g, '*')  // [...slug] -> *
    .replace(/\[\[(\w+)\]\]/g, ':$1?') // [[id]] -> :id?
    .replace(/\[(\w+)\]/g, ':$1');     // [id] -> :id
  
  if (!route) route = '/';
  else if (!route.startsWith('/')) route = '/' + route;
  
  return {
    path: route,
    file: fullPath,
    ...meta,
    score: calculateScore(route),
  };
}

function extractRouteMeta(ast) {
  const meta = {};
  
  // Look for exported config
  for (const node of ast.program.body) {
    if (node.type === 'ExportNamedDeclaration') {
      if (node.declaration?.declarations?.[0]?.id?.name === 'config') {
        // Extract static config
        meta.config = evaluateStaticObject(node.declaration.declarations[0].init);
      }
    }
  }
  
  return meta;
}

// Generate route manifest code
function emitRouteManifest(routes) {
  return `
// Auto-generated route manifest
export const routes = [
${routes.map(r => `  {
    path: ${JSON.stringify(r.path)},
    component: () => import(${JSON.stringify(r.file)}),
    meta: ${JSON.stringify(r.meta || {})},
  }`).join(',\n')}
];
`;
}
javascript
// 基于文件的路由生成器(构建工具)

import { glob } from 'glob';
import path from 'path';
import { parse } from '@babel/parser';

async function generateRouteManifest(config) {
  const { pagesDir, extensions = ['.tsx', '.jsx', '.ts', '.js'] } = config;
  
  // 查找所有路由文件
  const pattern = `**/*{${extensions.join(',')}}`;
  const files = await glob(pattern, { cwd: pagesDir });
  
  const routes = [];
  
  for (const file of files) {
    // 跳过特殊文件
    if (file.startsWith('_') || file.includes('/_')) continue;
    
    const route = await processRouteFile(pagesDir, file);
    if (route) routes.push(route);
  }
  
  // 按特异性排序
  routes.sort((a, b) => b.score - a.score);
  
  return routes;
}

async function processRouteFile(pagesDir, file) {
  const fullPath = path.join(pagesDir, file);
  const content = await fs.readFile(fullPath, 'utf-8');
  const ast = parse(content, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
  
  // 从导出中提取路由元数据
  const meta = extractRouteMeta(ast);
  
  // 将文件路径转换为路由模式
  let route = file
    .replace(/\.(tsx?|jsx?)$/, '')    // 移除扩展名
    .replace(/\/index$/, '')           // /index -> /
    .replace(/\[\.\.\.(\w+)\]/g, '*')  // [...slug] -> *
    .replace(/\[\[(\w+)\]\]/g, ':$1?') // [[id]] -> :id?
    .replace(/\[(\w+)\]/g, ':$1');     // [id] -> :id
  
  if (!route) route = '/';
  else if (!route.startsWith('/')) route = '/' + route;
  
  return {
    path: route,
    file: fullPath,
    ...meta,
    score: calculateScore(route),
  };
}

function extractRouteMeta(ast) {
  const meta = {};
  
  // 查找导出的配置
  for (const node of ast.program.body) {
    if (node.type === 'ExportNamedDeclaration') {
      if (node.declaration?.declarations?.[0]?.id?.name === 'config') {
        // 提取静态配置
        meta.config = evaluateStaticObject(node.declaration.declarations[0].init);
      }
    }
  }
  
  return meta;
}

// 生成路由清单代码
function emitRouteManifest(routes) {
  return `
// 自动生成的路由清单
export const routes = [
${routes.map(r => `  {
    path: ${JSON.stringify(r.path)},
    component: () => import(${JSON.stringify(r.file)}),
    meta: ${JSON.stringify(r.meta || {})},
  }`).join(',\n')}
];
`;
}

Implementing Nested Routes and Layouts

实现嵌套路由与布局

javascript
// NESTED ROUTING IMPLEMENTATION

class NestedRouter {
  constructor() {
    this.root = { children: [], layout: null, page: null };
  }
  
  // Build route tree from flat routes
  buildTree(routes) {
    for (const route of routes) {
      this.insertRoute(route);
    }
  }
  
  insertRoute(route) {
    const segments = route.path.split('/').filter(Boolean);
    let node = this.root;
    
    for (let i = 0; i < segments.length; i++) {
      const segment = segments[i];
      let child = node.children.find(c => c.segment === segment);
      
      if (!child) {
        child = { segment, children: [], layout: null, page: null };
        node.children.push(child);
      }
      
      node = child;
    }
    
    // Assign component based on file type
    if (route.file.includes('layout')) {
      node.layout = route.component;
    } else if (route.file.includes('page')) {
      node.page = route.component;
    }
  }
  
  // Match and collect all layouts + page
  match(pathname) {
    const segments = pathname.split('/').filter(Boolean);
    const matched = [];
    let node = this.root;
    const params = {};
    
    // Always include root layout
    if (node.layout) matched.push({ component: node.layout, params: {} });
    
    for (const segment of segments) {
      // Find matching child
      let child = node.children.find(c => c.segment === segment);
      
      // Try dynamic match
      if (!child) {
        child = node.children.find(c => c.segment.startsWith(':'));
        if (child) {
          const paramName = child.segment.slice(1);
          params[paramName] = segment;
        }
      }
      
      if (!child) return null; // No match
      
      if (child.layout) {
        matched.push({ component: child.layout, params: { ...params } });
      }
      
      node = child;
    }
    
    // Add final page
    if (node.page) {
      matched.push({ component: node.page, params: { ...params }, isPage: true });
    }
    
    return { matched, params };
  }
}

// Render nested routes with outlet pattern
async function renderNestedRoute(matched) {
  let content = null;
  
  // Render inside-out (page first, then wrap with layouts)
  for (let i = matched.length - 1; i >= 0; i--) {
    const { component, params, isPage } = matched[i];
    const Component = await component();
    
    if (isPage) {
      content = createElement(Component.default, { params });
    } else {
      // Layout receives children as outlet
      content = createElement(Component.default, { 
        params, 
        children: content,
      });
    }
  }
  
  return content;
}
javascript
// 嵌套路由实现

class NestedRouter {
  constructor() {
    this.root = { children: [], layout: null, page: null };
  }
  
  // 从扁平路由构建路由树
  buildTree(routes) {
    for (const route of routes) {
      this.insertRoute(route);
    }
  }
  
  insertRoute(route) {
    const segments = route.path.split('/').filter(Boolean);
    let node = this.root;
    
    for (let i = 0; i < segments.length; i++) {
      const segment = segments[i];
      let child = node.children.find(c => c.segment === segment);
      
      if (!child) {
        child = { segment, children: [], layout: null, page: null };
        node.children.push(child);
      }
      
      node = child;
    }
    
    // 根据文件类型分配组件
    if (route.file.includes('layout')) {
      node.layout = route.component;
    } else if (route.file.includes('page')) {
      node.page = route.component;
    }
  }
  
  // 匹配并收集所有布局 + 页面
  match(pathname) {
    const segments = pathname.split('/').filter(Boolean);
    const matched = [];
    let node = this.root;
    const params = {};
    
    // 始终包含根布局
    if (node.layout) matched.push({ component: node.layout, params: {} });
    
    for (const segment of segments) {
      // 查找匹配的子节点
      let child = node.children.find(c => c.segment === segment);
      
      // 尝试动态匹配
      if (!child) {
        child = node.children.find(c => c.segment.startsWith(':'));
        if (child) {
          const paramName = child.segment.slice(1);
          params[paramName] = segment;
        }
      }
      
      if (!child) return null; // 无匹配
      
      if (child.layout) {
        matched.push({ component: child.layout, params: { ...params } });
      }
      
      node = child;
    }
    
    // 添加最终页面
    if (node.page) {
      matched.push({ component: node.page, params: { ...params }, isPage: true });
    }
    
    return { matched, params };
  }
}

// 使用出口模式渲染嵌套路由
async function renderNestedRoute(matched) {
  let content = null;
  
  // 从内到外渲染(先页面,再用布局包裹)
  for (let i = matched.length - 1; i >= 0; i--) {
    const { component, params, isPage } = matched[i];
    const Component = await component();
    
    if (isPage) {
      content = createElement(Component.default, { params });
    } else {
      // 布局接收children作为出口
      content = createElement(Component.default, { 
        params, 
        children: content,
      });
    }
  }
  
  return content;
}

Route Preloading System

路由预加载系统

javascript
// ROUTE PRELOADING IMPLEMENTATION

class RoutePreloader {
  constructor(router) {
    this.router = router;
    this.preloaded = new Set();
    this.preloading = new Map();
  }
  
  // Preload on hover
  setupHoverPreload() {
    document.addEventListener('mouseenter', (e) => {
      const link = e.target.closest('a[href]');
      if (!link) return;
      
      const href = link.getAttribute('href');
      if (href.startsWith('/') && !this.preloaded.has(href)) {
        this.preload(href);
      }
    }, true);
  }
  
  // Preload visible links
  setupViewportPreload() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const href = entry.target.getAttribute('href');
          if (!this.preloaded.has(href)) {
            // Use requestIdleCallback for low-priority preload
            requestIdleCallback(() => this.preload(href));
          }
        }
      });
    }, { rootMargin: '200px' });
    
    document.querySelectorAll('a[href^="/"]').forEach(link => {
      observer.observe(link);
    });
  }
  
  async preload(pathname) {
    if (this.preloading.has(pathname)) {
      return this.preloading.get(pathname);
    }
    
    const match = this.router.match(pathname);
    if (!match) return;
    
    const preloadPromise = (async () => {
      // Preload component code
      const componentPromises = match.matched.map(m => m.component());
      
      // Preload data if route has loader
      const route = match.matched.find(m => m.isPage);
      const dataPromise = route?.loader?.({ 
        params: match.params, 
        preload: true 
      });
      
      await Promise.all([...componentPromises, dataPromise]);
      this.preloaded.add(pathname);
    })();
    
    this.preloading.set(pathname, preloadPromise);
    
    try {
      await preloadPromise;
    } finally {
      this.preloading.delete(pathname);
    }
  }
}
javascript
// 路由预加载实现

class RoutePreloader {
  constructor(router) {
    this.router = router;
    this.preloaded = new Set();
    this.preloading = new Map();
  }
  
  // 设置悬停预加载
  setupHoverPreload() {
    document.addEventListener('mouseenter', (e) => {
      const link = e.target.closest('a[href]');
      if (!link) return;
      
      const href = link.getAttribute('href');
      if (href.startsWith('/') && !this.preloaded.has(href)) {
        this.preload(href);
      }
    }, true);
  }
  
  // 设置视口预加载
  setupViewportPreload() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const href = entry.target.getAttribute('href');
          if (!this.preloaded.has(href)) {
            // 使用requestIdleCallback进行低优先级预加载
            requestIdleCallback(() => this.preload(href));
          }
        }
      });
    }, { rootMargin: '200px' });
    
    document.querySelectorAll('a[href^="/"]').forEach(link => {
      observer.observe(link);
    });
  }
  
  async preload(pathname) {
    if (this.preloading.has(pathname)) {
      return this.preloading.get(pathname);
    }
    
    const match = this.router.match(pathname);
    if (!match) return;
    
    const preloadPromise = (async () => {
      // 预加载组件代码
      const componentPromises = match.matched.map(m => m.component());
      
      // 如果路由有加载器,预加载数据
      const route = match.matched.find(m => m.isPage);
      const dataPromise = route?.loader?.({ 
        params: match.params, 
        preload: true 
      });
      
      await Promise.all([...componentPromises, dataPromise]);
      this.preloaded.add(pathname);
    })();
    
    this.preloading.set(pathname, preloadPromise);
    
    try {
      await preloadPromise;
    } finally {
      this.preloading.delete(pathname);
    }
  }
}

Navigation State Management

导航状态管理

javascript
// NAVIGATION STATE IMPLEMENTATION

class NavigationManager {
  constructor() {
    this.state = 'idle'; // idle | loading | submitting
    this.location = window.location.pathname;
    this.listeners = new Set();
  }
  
  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
  
  notify() {
    this.listeners.forEach(l => l({
      state: this.state,
      location: this.location,
    }));
  }
  
  async navigate(to, options = {}) {
    const { replace = false, state = {} } = options;
    
    this.state = 'loading';
    this.notify();
    
    try {
      // Run before navigation hooks
      for (const hook of this.beforeHooks) {
        const result = await hook(this.location, to);
        if (result === false) {
          this.state = 'idle';
          this.notify();
          return;
        }
        if (typeof result === 'string') {
          to = result; // Redirect
        }
      }
      
      // Update URL
      if (replace) {
        history.replaceState(state, '', to);
      } else {
        history.pushState(state, '', to);
      }
      
      this.location = to;
      
      // Load and render route
      await this.renderRoute(to);
      
      // Scroll to top or hash
      if (to.includes('#')) {
        document.querySelector(to.split('#')[1])?.scrollIntoView();
      } else {
        window.scrollTo(0, 0);
      }
      
    } catch (error) {
      console.error('Navigation failed:', error);
      throw error;
    } finally {
      this.state = 'idle';
      this.notify();
    }
  }
}

// React hook for navigation state
function useNavigation() {
  const [state, setState] = useState({ state: 'idle', location: '' });
  
  useEffect(() => {
    return navigationManager.subscribe(setState);
  }, []);
  
  return state;
}

// Usage in components
function LoadingBar() {
  const { state } = useNavigation();
  
  return state === 'loading' ? <ProgressBar /> : null;
}
javascript
// 导航状态实现

class NavigationManager {
  constructor() {
    this.state = 'idle'; // idle | loading | submitting
    this.location = window.location.pathname;
    this.listeners = new Set();
  }
  
  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
  
  notify() {
    this.listeners.forEach(l => l({
      state: this.state,
      location: this.location,
    }));
  }
  
  async navigate(to, options = {}) {
    const { replace = false, state = {} } = options;
    
    this.state = 'loading';
    this.notify();
    
    try {
      // 运行导航前钩子
      for (const hook of this.beforeHooks) {
        const result = await hook(this.location, to);
        if (result === false) {
          this.state = 'idle';
          this.notify();
          return;
        }
        if (typeof result === 'string') {
          to = result; // 重定向
        }
      }
      
      // 更新URL
      if (replace) {
        history.replaceState(state, '', to);
      } else {
        history.pushState(state, '', to);
      }
      
      this.location = to;
      
      // 加载并渲染路由
      await this.renderRoute(to);
      
      // 滚动到顶部或哈希位置
      if (to.includes('#')) {
        document.querySelector(to.split('#')[1])?.scrollIntoView();
      } else {
        window.scrollTo(0, 0);
      }
      
    } catch (error) {
      console.error('Navigation failed:', error);
      throw error;
    } finally {
      this.state = 'idle';
      this.notify();
    }
  }
}

// React导航状态钩子
function useNavigation() {
  const [state, setState] = useState({ state: 'idle', location: '' });
  
  useEffect(() => {
    return navigationManager.subscribe(setState);
  }, []);
  
  return state;
}

// 在组件中使用
function LoadingBar() {
  const { state } = useNavigation();
  
  return state === 'loading' ? <ProgressBar /> : null;
}

Related Skills

相关技能

  • See web-app-architectures for SPA vs MPA routing
  • See meta-frameworks-overview for framework-specific routing
  • See seo-fundamentals for SEO-friendly URLs
  • 查看 web-app-architectures 了解SPA与MPA的路由差异
  • 查看 meta-frameworks-overview 了解框架专属路由实现
  • 查看 seo-fundamentals 了解SEO友好的URL设计