跳转至

Astro 静态站点框架完全指南

为什么要学 Astro

  1. 零 JavaScript 默认,性能极致:Astro 默认不向客户端发送任何 JavaScript。页面被预渲染为纯 HTML 和 CSS,Core Web Vitals 天然优秀。只在需要交互的地方才加载 JS(Islands 架构),比传统 SPA 框架快 40% 以上。

  2. 框架无关,自由组合:在同一个 Astro 项目中,你可以同时使用 React、Vue、Svelte、Solid、Preact、Lit 甚至 Alpine.js 组件。不被任何前端框架锁定,团队成员可以用各自熟悉的技术。

  3. 内容驱动的最佳选择:Astro 为博客、文档站、营销页、电商列表等内容密集型网站量身设计。内置 Markdown/MDX 支持、内容集合(Content Collections)、自动生成 RSS/sitemap 等。

  4. 开发体验优秀:文件系统路由、热模块替换(HMR)、TypeScript 开箱即用、图片优化(<Image />组件)、View Transitions API 支持,开发者体验不输 Next.js。

  5. 灵活的渲染模式:支持 SSG(静态生成)、SSR(服务端渲染)和混合模式。可以按页面粒度选择渲染方式,静态页面和动态页面共存于一个项目。


核心概念详解

Islands 架构(白话解释)

想象一座岛屿:大部分是静态的陆地(HTML),只有几个小岛是有生命活动的(交互组件)。Astro 的 Islands 架构就是这个概念——整个页面是静态 HTML,只有你标记了需要交互的组件才会加载 JavaScript 并变得可交互。

传统 SPA 的做法是整个页面都是 JavaScript 渲染的——像是整片海洋都在翻涌。即使用户只想看一篇文章,也要加载整个应用的 JS 代码。

技术定义

Astro 是一个以内容为中心的 Web 框架,使用组件岛屿(Component Islands)架构实现选择性水合(Partial Hydration)。它在构建时将页面预渲染为静态 HTML,通过 client:* 指令控制哪些组件需要在客户端激活。

客户端指令(Client Directives)

指令加载时机适用场景
client:load页面加载时立即加载立即可见且需交互的组件
client:idle浏览器空闲时加载非关键交互组件
client:visible组件滚动进视口时加载页面下方的组件
client:media="(query)"满足媒体查询时加载仅在特定屏幕尺寸加载
client:only="react"仅客户端渲染,跳过SSR依赖浏览器API的组件
不加任何 client 指令不加载 JS纯展示组件
---
import ReactCounter from './Counter.jsx';
import VueCarousel from './Carousel.vue';
import SvelteChat from './Chat.svelte';
---

<!-- 立即加载(关键交互) -->
<ReactCounter client:load />

<!-- 滚动到可见时加载 -->
<VueCarousel client:visible />

<!-- 浏览器空闲时加载 -->
<SvelteChat client:idle />

<!-- 不加载 JS,纯静态渲染 -->
<ReactCounter />

Astro 组件 (.astro 文件)

---
// --- 之间是服务端代码(frontmatter)
// 这里的代码只在构建时/服务端运行,不会发送到客户端

import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';

// 可以导入数据
const response = await fetch('https://api.example.com/posts');
const posts = await response.json();

// Props
interface Props {
  title: string;
  description?: string;
}
const { title, description = '默认描述' } = Astro.props;

// 可以使用 Node.js API
import fs from 'node:fs';
---

<!-- 下面是模板部分 -->
<Layout title={title}>
  <h1>{title}</h1>
  <p>{description}</p>

  <ul>
    {posts.map(post => (
      <li>
        <Card title={post.title} href={`/posts/${post.slug}`} />
      </li>
    ))}
  </ul>
</Layout>

<style>
  /* 默认作用域样式 */
  h1 { color: navy; }

  /* 全局样式 */
  :global(body) { margin: 0; }
</style>

<script>
  // 客户端 JavaScript(会发送到浏览器)
  console.log('这段代码在浏览器运行');
</script>

Astro vs Next.js vs Gatsby 对比

特性AstroNext.js 15Gatsby 5
默认JS零JS大量JS(React运行时)大量JS(React运行时)
架构IslandsApp Router (RSC)静态站点生成
框架锁定无(多框架)ReactReact
渲染模式SSG + SSR + 混合SSG + SSR + ISRSSG + DSG
内容处理内容集合 + MD/MDX自行配置GraphQL 数据层
图片优化内置 <Image>内置 next/imagegatsby-plugin-image
构建速度中等
首屏性能极快快(RSC帮助)
学习曲线中高
适用场景内容站/博客/文档复杂Web应用内容站(维护模式)
社区规模快速增长极大缩小中
View Transitions原生支持需自行实现不支持

内容集合(Content Collections)

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: z.string().optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

安装与配置

创建新项目

# 使用官方 CLI
npm create astro@latest

# 交互式选择:
# - 项目名称
# - 模板(空白/博客/文档/最小)
# - TypeScript(推荐 strict)
# - 安装依赖
# - 初始化 Git

# 或一行命令
npm create astro@latest my-blog -- --template blog --typescript strict

项目结构

my-blog/
├── public/              # 静态资源(不经过处理)
│   ├── favicon.svg
│   └── robots.txt
├── src/
│   ├── assets/          # 需要处理的资源(图片优化等)
│   ├── components/      # 组件(.astro, .jsx, .vue, .svelte...)
│   ├── content/         # 内容集合
│   │   └── blog/
│   │       ├── first-post.md
│   │       └── second-post.mdx
│   ├── layouts/         # 布局组件
│   │   └── Layout.astro
│   ├── pages/           # 文件系统路由
│   │   ├── index.astro
│   │   ├── about.astro
│   │   └── blog/
│   │       ├── index.astro
│   │       └── [...slug].astro
│   ├── styles/          # 全局样式
│   └── content.config.ts  # 内容集合配置
├── astro.config.mjs
├── tsconfig.json
└── package.json

添加框架集成

# React
npx astro add react

# Vue
npx astro add vue

# Svelte
npx astro add svelte

# Tailwind CSS
npx astro add tailwind

# MDX
npx astro add mdx

# Sitemap
npx astro add sitemap

# SSR 适配器
npx astro add node        # Node.js
npx astro add vercel      # Vercel
npx astro add cloudflare  # Cloudflare
npx astro add netlify     # Netlify

astro.config.mjs

import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import vue from '@astrojs/vue';
import tailwind from '@astrojs/tailwind';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import node from '@astrojs/node';

export default defineConfig({
  site: 'https://example.com',
  output: 'hybrid', // 'static' | 'server' | 'hybrid'
  adapter: node({ mode: 'standalone' }),
  integrations: [
    react(),
    vue(),
    tailwind(),
    mdx(),
    sitemap(),
  ],
  image: {
    domains: ['images.unsplash.com'],
    remotePatterns: [{ protocol: 'https' }],
  },
  vite: {
    css: {
      preprocessorOptions: {
        scss: { additionalData: `@import "src/styles/variables.scss";` }
      }
    }
  },
  markdown: {
    shikiConfig: {
      theme: 'github-dark',
      wrap: true,
    },
    remarkPlugins: [],
    rehypePlugins: [],
  },
});

快速上手:5 分钟最小示例

npm create astro@latest my-site -- --template minimal --typescript strict
cd my-site
npm install

src/pages/index.astro

---
const features = [
  { title: '零JS默认', desc: '页面不包含JavaScript,加载极快' },
  { title: '多框架支持', desc: '同时使用React、Vue、Svelte' },
  { title: '内容驱动', desc: 'Markdown/MDX一等公民' },
];
---

<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width" />
  <title>我的Astro站点</title>
</head>
<body>
  <main>
    <h1>欢迎来到 <span class="gradient">Astro</span></h1>
    <p>构建更快的网站</p>

    <div class="features">
      {features.map(f => (
        <div class="card">
          <h2>{f.title}</h2>
          <p>{f.desc}</p>
        </div>
      ))}
    </div>
  </main>
</body>
</html>

<style>
  main { max-width: 800px; margin: 0 auto; padding: 2rem; font-family: system-ui; }
  .gradient { background: linear-gradient(90deg, #f97316, #ec4899);
              -webkit-background-clip: text; color: transparent; }
  .features { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 2rem; }
  .card { padding: 1.5rem; border: 1px solid #e5e7eb; border-radius: 8px; }
  .card h2 { margin: 0 0 0.5rem; }
</style>

运行:

npm run dev
# 打开 http://localhost:4321

构建:

npm run build    # 输出到 dist/
npm run preview  # 预览构建结果


进阶用法

场景一:博客系统(内容集合 + 动态路由)

// src/content.config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()).default([]),
  }),
});

export const collections = { blog };
---
// src/pages/blog/[...slug].astro
import { getCollection, render } from 'astro:content';
import Layout from '../../layouts/Layout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({
    params: { slug: post.id },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await render(post);
---

<Layout title={post.data.title}>
  <article>
    <h1>{post.data.title}</h1>
    <time>{post.data.pubDate.toLocaleDateString('zh-CN')}</time>
    <div class="tags">
      {post.data.tags.map(tag => <span class="tag">{tag}</span>)}
    </div>
    <Content />
  </article>
</Layout>

场景二:多框架组件混用

---
// src/pages/demo.astro
import Layout from '../layouts/Layout.astro';
import ReactCounter from '../components/ReactCounter.jsx';
import VueTimer from '../components/VueTimer.vue';
import SvelteToggle from '../components/SvelteToggle.svelte';
---

<Layout title="多框架演示">
  <h1>在同一页面使用多个框架</h1>

  <section>
    <h2>React 计数器</h2>
    <ReactCounter client:visible initialCount={5} />
  </section>

  <section>
    <h2>Vue 计时器</h2>
    <VueTimer client:idle />
  </section>

  <section>
    <h2>Svelte 开关</h2>
    <SvelteToggle client:load />
  </section>
</Layout>

场景三:View Transitions(页面过渡动画)

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

<html lang="zh-CN">
<head>
  <ViewTransitions />
</head>
<body>
  <nav transition:persist>
    <a href="/">首页</a>
    <a href="/about">关于</a>
    <a href="/blog">博客</a>
  </nav>

  <main transition:animate="slide">
    <slot />
  </main>
</body>
</html>
---
// 为特定元素添加过渡
---
<img
  src="/hero.jpg"
  transition:name="hero"
  transition:animate="fade"
/>

场景四:SSR + API 端点

// astro.config.mjs
export default defineConfig({
  output: 'hybrid', // 默认静态,按需 SSR
});
---
// src/pages/dashboard.astro
export const prerender = false; // 此页面使用 SSR

const session = Astro.cookies.get('session')?.value;
if (!session) {
  return Astro.redirect('/login');
}

const user = await getUser(session);
---

<h1>欢迎, {user.name}</h1>
// src/pages/api/submit.ts
import type { APIRoute } from 'astro';

export const POST: APIRoute = async ({ request, cookies }) => {
  const data = await request.json();

  // 处理数据...
  const result = await processData(data);

  cookies.set('lastAction', new Date().toISOString(), {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
  });

  return new Response(JSON.stringify({ success: true, result }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  });
};

场景五:图片优化

---
import { Image, Picture } from 'astro:assets';
import heroImage from '../assets/hero.jpg';
---

<!-- 本地图片(自动优化) -->
<Image
  src={heroImage}
  alt="Hero image"
  width={800}
  height={400}
  format="avif"
  quality={80}
/>

<!-- 响应式图片 -->
<Picture
  src={heroImage}
  formats={['avif', 'webp']}
  widths={[400, 800, 1200]}
  sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
  alt="Responsive hero"
/>

<!-- 远程图片 -->
<Image
  src="https://images.unsplash.com/photo-xxx"
  alt="Remote image"
  width={600}
  height={400}
  inferSize
/>

场景六:国际化 (i18n)

// astro.config.mjs
export default defineConfig({
  i18n: {
    defaultLocale: 'zh',
    locales: ['zh', 'en', 'ja'],
    routing: {
      prefixDefaultLocale: false,
    },
  },
});
src/pages/
├── index.astro          # /(中文,默认)
├── about.astro          # /about
├── en/
│   ├── index.astro      # /en
│   └── about.astro      # /en/about
└── ja/
    ├── index.astro      # /ja
    └── about.astro      # /ja/about

场景七:中间件

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware(async (context, next) => {
  const start = Date.now();

  // 认证检查
  const token = context.cookies.get('token')?.value;
  if (context.url.pathname.startsWith('/admin') && !token) {
    return context.redirect('/login');
  }

  // 注入数据
  context.locals.user = token ? await verifyToken(token) : null;

  const response = await next();

  // 添加响应头
  response.headers.set('X-Response-Time', `${Date.now() - start}ms`);

  return response;
});

场景八:Markdown/MDX 增强

---
// src/content/blog/interactive-post.mdx
title: '交互式文章'
pubDate: 2024-12-01
---

import Chart from '../../components/Chart.jsx';
import Callout from '../../components/Callout.astro';

# 交互式数据可视化

这篇文章包含交互组件。

<Callout type="info">
  这是一个信息提示框(纯 Astro 组件,零 JS)
</Callout>

下面是一个交互式图表:

<Chart
  client:visible
  data={[10, 20, 30, 40, 50]}
  title="月度数据"
/>

## 代码示例

普通的 Markdown 内容会被静态渲染。

常见问题与排错

问题一:组件不响应交互

原因:忘了添加 client:* 指令。Astro 默认不发送 JS。

<!-- 错误:组件只是静态 HTML,点击无响应 -->
<ReactCounter />

<!-- 正确:添加客户端指令 -->
<ReactCounter client:load />

问题二:样式在生产环境丢失

排查: 1. 检查是否使用了 :global() 影响到了其他组件 2. 确认动态类名是否被 Tailwind 的 PurgeCSS 删除 3. 检查 CSS 文件是否正确导入

---
// 确保全局样式被导入
import '../styles/global.css';
---

问题三:内容集合 schema 验证失败

[ERROR] Invalid frontmatter in "blog/my-post.md"

检查 Markdown frontmatter 是否匹配 schema 定义。使用 z.coerce.date() 而不是 z.date() 来处理字符串日期。

问题四:环境变量不生效

# .env
PUBLIC_API_URL=https://api.example.com   # 客户端可用
SECRET_KEY=abc123                         # 仅服务端
---
// 服务端可以使用所有变量
const secret = import.meta.env.SECRET_KEY;
const apiUrl = import.meta.env.PUBLIC_API_URL;
---

<!-- 客户端只能使用 PUBLIC_ 前缀的变量 -->
<script>
  console.log(import.meta.env.PUBLIC_API_URL);
</script>

问题五:不同框架组件之间如何通信

使用 nanostores(官方推荐的跨框架状态管理):

npm install nanostores @nanostores/react @nanostores/vue
// src/stores/cart.ts
import { atom, map } from 'nanostores';

export const $cartItems = map<Record<string, number>>({});

export function addToCart(id: string) {
  const current = $cartItems.get()[id] || 0;
  $cartItems.setKey(id, current + 1);
}
// React 组件
import { useStore } from '@nanostores/react';
import { $cartItems } from '../stores/cart';

export function CartCount() {
  const items = useStore($cartItems);
  const total = Object.values(items).reduce((a, b) => a + b, 0);
  return <span>购物车: {total}</span>;
}

问题六:如何添加 RSS 订阅

npm install @astrojs/rss
// src/pages/rss.xml.ts
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';

export async function GET(context) {
  const posts = await getCollection('blog');
  return rss({
    title: '我的博客',
    description: '技术分享',
    site: context.site,
    items: posts.map(post => ({
      title: post.data.title,
      pubDate: post.data.pubDate,
      description: post.data.description,
      link: `/blog/${post.id}/`,
    })),
  });
}

参考资源

  • 官方文档:https://docs.astro.build/
  • 官方教程:https://docs.astro.build/en/tutorial/0-introduction/
  • Astro GitHub:https://github.com/withastro/astro
  • 主题市场:https://astro.build/themes/
  • 集成目录:https://astro.build/integrations/
  • Astro Showcase:https://astro.build/showcase/
  • Astro Discord:https://astro.build/chat
  • nanostores(跨框架状态管理):https://github.com/nanostores/nanostores