Chapter 07

静态生成与动态路由

掌握 getStaticPaths() 静态路由生成、动态路由参数、REST 参数与 paginate() 分页功能

getStaticPaths():静态路由生成

动态路由的基本概念

在 Astro 中,文件名中带 [参数] 的页面是动态路由。对于静态生成(SSG)模式,需要通过 getStaticPaths() 函数告诉 Astro 需要生成哪些具体的页面。

---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content';

// getStaticPaths 在构建时执行,返回所有可能的路由参数
export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({
    params: { slug: post.slug },   // URL 参数
    props: { post },               // 传给页面的 Props
  }));
}

// 从 Props 获取当前页面数据
const { post } = Astro.props;
const { Content } = await post.render();
---

<article>
  <h1>{post.data.title}</h1>
  <Content />
</article>

REST 参数:匹配多级路径

---
// src/pages/docs/[...slug].astro
// 匹配 /docs/getting-started、/docs/api/users、/docs/a/b/c

export async function getStaticPaths() {
  return [
    { params: { slug: 'getting-started' } },    // /docs/getting-started
    { params: { slug: 'api/users' } },           // /docs/api/users
    { params: { slug: undefined } },             // /docs/ (REST 参数可以为空)
  ];
}

// Astro.params.slug 是字符串或 undefined
const { slug } = Astro.params;
---

paginate():内置分页

---
// src/pages/blog/[page].astro
import { getCollection } from 'astro:content';

export async function getStaticPaths({ paginate }) {
  const posts = await getCollection('blog');
  // 按日期排序
  posts.sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime());

  // paginate() 自动生成分页路由
  return paginate(posts, {
    pageSize: 10,  // 每页 10 篇
  });
}

// page 对象包含分页信息
const { page } = Astro.props;
// page.data: 当前页的文章数组
// page.currentPage: 当前页码(1-indexed)
// page.total: 总条数
// page.totalPages: 总页数
// page.url.prev: 上一页 URL
// page.url.next: 下一页 URL
---

<ul>
  {page.data.map(post => (
    <li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>
  ))}
</ul>

<nav>
  {page.url.prev && <a href={page.url.prev}>上一页</a>}
  <span>{page.currentPage} / {page.totalPages}</span>
  {page.url.next && <a href={page.url.next}>下一页</a>}
</nav>

按需渲染(SSR)vs 静态预渲染

export const prerender = true
(混合模式默认值)页面在构建时生成为静态 HTML,不需要服务器动态处理。适合内容不频繁变化的页面。
export const prerender = false
页面在每次请求时服务端动态渲染。适合依赖用户身份、实时数据的页面(如用户个人页、搜索结果)。

构建输出分析

# 运行构建并查看输出统计
npm run build

# 典型静态构建输出(dist/ 目录)
dist/
├── index.html
├── about/
│   └── index.html
├── blog/
│   ├── index.html           # /blog
│   ├── first-post/
│   │   └── index.html       # /blog/first-post
│   └── ...
└── _astro/
    ├── page.Bx3kLpQm.css    # 哈希文件名(缓存破坏)
    └── Counter.B7fK2pXt.js  # 岛屿组件的 JS
Astro 的 JS 输出极其精简

Astro 只会为实际使用了 client: 指令的组件输出 JS 文件。如果你的整个网站没有任何交互岛屿,dist/_astro/ 目录中将没有任何 .js 文件——这是真正的零 JS 输出。

getStaticPaths 的工作时机

构建时执行(Build Time)
getStaticPaths 在 npm run build 时执行,不是运行时。它可以调用 API、读取文件系统、查询数据库——只要能在 Node.js 环境运行即可。执行结果决定了最终生成的 HTML 文件数量。
params 与 props 的区别
params 是路由参数(决定 URL);props 是传给页面的数据。将数据放在 props 而非 params 的原因:props 可以传递任何类型(对象、数组),不需要序列化为字符串;params 只能是字符串,用于构造 URL。
fallback 处理
静态生成模式下,访问未在 getStaticPaths 中声明的路径会得到 404。如需支持动态新增内容(无需重新构建),需切换到 SSR 模式(output: 'server')。

多参数动态路由

---
// src/pages/[lang]/blog/[slug].astro
// 匹配 /zh/blog/my-post、/en/blog/my-post 等

export async function getStaticPaths() {
  const languages = ['zh', 'en', 'ja'];
  const posts = await fetch('/api/posts').then(r => r.json());

  // 笛卡尔积:所有语言 × 所有文章的组合
  return languages.flatMap(lang =>
    posts.map(post => ({
      params: { lang, slug: post.slug },
      props: { post, lang },
    }))
  );
}

const { post, lang } = Astro.props;
---

<h1 lang={lang}>{post.title}</h1>

View Transitions:SPA 风格的页面切换动画

View Transitions API 让 Astro 的多页面应用(MPA)实现类似单页应用(SPA)的页面切换效果——无需客户端路由框架。原理是:Astro 拦截链接点击,用 fetch 获取下一页的 HTML,然后用浏览器原生的 View Transitions API 在新旧 DOM 之间播放过渡动画,最后替换 DOM。

View Transitions API(浏览器原生)
Chrome 111+ / Safari 18+ 支持的浏览器原生动画 API。通过 document.startViewTransition() 在 DOM 变更前后捕获快照并插值动画。Astro 封装了这个 API,使其在多页面应用中无缝工作。
ViewTransitions 组件
Astro 内置组件,放入 Layout 的 <head> 中即可为整站启用 View Transitions。自动注入客户端路由逻辑,拦截导航、管理 <head> 更新、触发过渡动画。
transition:name
为特定元素指定命名过渡。相同 transition:name 的元素在页面间切换时,浏览器会自动对它们播放位置/大小变换动画(即"共享元素过渡"),产生流畅的视觉连续性效果。
transition:persist
标记此元素在页面切换时保持状态,不被替换(如音乐播放器、视频)。适合需要跨页持续播放的媒体元素,或需要在页面间保留滚动位置的侧边栏。

启用 View Transitions

---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from 'astro:transitions';
---

<html>
<head>
  <title>{Astro.props.title}</title>
  <!-- 放入 head 中即可为整站启用 View Transitions -->
  <ViewTransitions />
</head>
<body>
  <slot />
</body>
</html>

共享元素过渡(Shared Element Transition)

最常见的用途是列表页 → 详情页的平滑过渡:列表中的图片"飞"到详情页的位置。实现方式是给两个页面的对应元素设置相同的 transition:name

---
// src/pages/blog/index.astro(列表页)
const posts = await getCollection('blog');
---

{posts.map(post => (
  <a href={`/blog/${post.id}/`}>
    <!-- transition:name 相同的元素,在页面切换时自动动画 -->
    <img
      src={post.data.cover}
      alt={post.data.title}
      transition:name={`post-cover-${post.id}`}
    />
    <h2 transition:name={`post-title-${post.id}`}>
      {post.data.title}
    </h2>
  </a>
))}
---
// src/pages/blog/[id].astro(详情页)
const { id } = Astro.params;
const post = await getEntry('blog', id);
---

<article>
  <!-- 与列表页相同的 transition:name,浏览器自动播放元素过渡动画 -->
  <img
    src={post.data.cover}
    alt={post.data.title}
    transition:name={`post-cover-${post.id}`}
  />
  <h1 transition:name={`post-title-${post.id}`}>
    {post.data.title}
  </h1>
  <div set:html={post.body}></div>
</article>

自定义过渡动画

---
import { fade, slide, fly } from 'astro:transitions';
---

<!-- 内置动画:fade(淡入淡出)、slide(滑动)、none(无动画)-->
<main transition:animate={fade()}>内容</main>
<aside transition:animate={slide({ duration: '0.3s' })}>侧栏</aside>

<!-- 自定义 CSS 动画 -->
<header transition:animate="my-header-anim">导航栏</header>

<style>
  /* ::view-transition-old(root) 是旧页面消失时的动画 */
  @keyframes fade-in {
    from { opacity: 0; transform: translateY(8px); }
  }
  @keyframes fade-out {
    to { opacity: 0; transform: translateY(-8px); }
  }
  ::view-transition-old(root) { animation: fade-out 0.15s ease-out; }
  ::view-transition-new(root) { animation: fade-in 0.2s ease-out; }
</style>

View Transitions 生命周期事件

// 在 script 标签中监听导航生命周期事件
document.addEventListener('astro:before-preparation', (e) => {
  // 即将开始加载新页面(可调用 e.cancel() 取消导航)
});

document.addEventListener('astro:after-preparation', () => {
  // 新页面 HTML 已获取,即将开始过渡动画
});

document.addEventListener('astro:page-load', () => {
  // 新页面加载完成(等同于 DOMContentLoaded,每次导航都触发)
  // 重要:初始化客户端库/事件监听器要在这里,不能只在首次加载时初始化
  document.querySelectorAll('[data-tooltip]').forEach(initTooltip);
});
View Transitions 的常见陷阱
  • script 重复执行:View Transitions 不刷新页面,普通 <script> 在首次加载时执行一次后不再执行。如果需要在每次导航后重新初始化(如第三方 Widget、分析代码),必须监听 astro:page-load 事件。
  • transition:name 必须全局唯一:同一时刻页面上不能有两个相同 transition:name 的元素,否则浏览器不知道对哪个元素应用过渡。列表页的动态 transition:name 要包含唯一 ID(如文章 id)。
  • 浏览器兼容性:View Transitions API 在 Safari 18 之前不支持。Astro 会自动降级——不支持的浏览器会进行普通的全页面刷新,不影响功能,只是没有动画效果。

本章小结

本章核心要点
  • getStaticPaths 必须性:静态模式下,任何带 [参数] 的页面都必须导出 getStaticPaths(),否则构建失败——Astro 必须在构建时知道所有需要生成的 URL。
  • params vs props:params 决定 URL(只能是字符串);props 传递页面数据(可以是任意 JS 对象)。将数据放在 props 避免了在 URL 参数中携带复杂数据的需要。
  • REST 参数 [...slug]:匹配任意深度的路径段;slug 值为 undefined 时匹配根路径(如 /docs/);适合多级文档导航、多语言路径等场景。
  • paginate() 内置分页:传入完整数据集 + pageSize,Astro 自动生成 /page/1、/page/2 等路由;page.url.prev/next 用于分页导航链接;page.data 是当前页的数据子集。
  • View Transitions API:在 Layout 的 <head> 中加入 <ViewTransitions />,即可为整站启用 SPA 风格页面切换;用 transition:name 为对应元素设置共享元素动画(列表图到详情图的"飞入"效果);用 astro:page-load 事件代替 DOMContentLoaded 初始化脚本。
  • 构建输出规律:每个路由生成一个 index.html 文件(如 /blog/post-1/ → dist/blog/post-1/index.html);岛屿 JS 文件带内容哈希指纹用于长期缓存。