Astro 静态站点框架完全指南¶
为什么要学 Astro¶
零 JavaScript 默认,性能极致:Astro 默认不向客户端发送任何 JavaScript。页面被预渲染为纯 HTML 和 CSS,Core Web Vitals 天然优秀。只在需要交互的地方才加载 JS(Islands 架构),比传统 SPA 框架快 40% 以上。
框架无关,自由组合:在同一个 Astro 项目中,你可以同时使用 React、Vue、Svelte、Solid、Preact、Lit 甚至 Alpine.js 组件。不被任何前端框架锁定,团队成员可以用各自熟悉的技术。
内容驱动的最佳选择:Astro 为博客、文档站、营销页、电商列表等内容密集型网站量身设计。内置 Markdown/MDX 支持、内容集合(Content Collections)、自动生成 RSS/sitemap 等。
开发体验优秀:文件系统路由、热模块替换(HMR)、TypeScript 开箱即用、图片优化(
<Image />组件)、View Transitions API 支持,开发者体验不输 Next.js。灵活的渲染模式:支持 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 对比¶
| 特性 | Astro | Next.js 15 | Gatsby 5 |
|---|---|---|---|
| 默认JS | 零JS | 大量JS(React运行时) | 大量JS(React运行时) |
| 架构 | Islands | App Router (RSC) | 静态站点生成 |
| 框架锁定 | 无(多框架) | React | React |
| 渲染模式 | SSG + SSR + 混合 | SSG + SSR + ISR | SSG + DSG |
| 内容处理 | 内容集合 + MD/MDX | 自行配置 | GraphQL 数据层 |
| 图片优化 | 内置 <Image> | 内置 next/image | gatsby-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 分钟最小示例¶
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>
运行:
构建:
进阶用法¶
场景一:博客系统(内容集合 + 动态路由)¶
// 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>
场景四:SSR + API 端点¶
---
// 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。
问题二:样式在生产环境丢失¶
排查: 1. 检查是否使用了 :global() 影响到了其他组件 2. 确认动态类名是否被 Tailwind 的 PurgeCSS 删除 3. 检查 CSS 文件是否正确导入
问题三:内容集合 schema 验证失败¶
检查 Markdown frontmatter 是否匹配 schema 定义。使用 z.coerce.date() 而不是 z.date() 来处理字符串日期。
问题四:环境变量不生效¶
---
// 服务端可以使用所有变量
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(官方推荐的跨框架状态管理):
// 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 订阅¶
// 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