Alvin Bryan 和 David Fateh 2024-09-27
Astro 是一个开源的 Web 框架,专注于性能和内容密集型网站,例如落地页、博客、技术文档等。与 Next.js、SvelteKit、Nuxt.js 和 SolidStart 类似,Astro 也有自己的单文件组件(利用了“岛屿架构”,稍后会详细介绍),并为你处理构建过程。
本文将带你全面了解 Astro 及其功能特性。我们将以一个虚构的会议网站为基础,进行一个小型教程。一如既往,你可以在 GitHub 上找到完整的代码。本文内容 100% 兼容 Astro v3。
准备好了吗?让我们开始吧!
为什么选择 Astro 框架?
Astro 自称是一个“一体化”的 Web 框架,提供创建网站所需的一切功能,并且还提供一系列额外集成,供你在需要时进一步自定义项目。它对组件无偏好(component agnostic),适用于静态站点生成(SSG),同时足够灵活,能够支持单页应用(SPA)和多页应用(MPA)。
Astro 官方文档中明确提出了五大核心设计原则,这些原则或许能帮助你判断它是否适合你的项目:
- 以内容为中心(Content-driven):Astro 围绕展示内容而设计,因此非常适合需要快速触达受众的内容密集型网站。
- 服务端优先(Server-first):Astro 优先采用服务端渲染(SSR),而非浏览器中的客户端渲染,以提升网站运行速度。
- 默认快速(Fast by default):通过预渲染为静态 HTML,Astro 实现了快速加载时间和增强的 SEO 效果,在注重用户参与度和转化率的场景中尤为有用。
- 易于使用(Easy-to-use):Astro 力求对所有开发者友好,“无论技能水平或过往 Web 开发经验如何”。它的 UI 语言是 HTML 的超集,但融合了 JSX、React、Svelte 和 Vue 等其他 JavaScript 框架语言的特性。
- 面向开发者(Developer-focused):作为一个开源项目,Astro 投入了大量资源开发开发者工具,并自豪地宣称其社区支持的文档已翻译成 14 种语言。
Astro 的独特之处在于将速度与灵活性集于一身——它是如何做到的呢?接下来,我们将深入探讨 Astro 最引人注目的几项能力。
Astro 默认不输出任何 JavaScript
与其他一些框架不同,Astro 是“HTML 优先”的,默认情况下不输出任何 JavaScript,并同时支持静态站点生成(SSG)和服务端渲染(SSR)。Astro 利用这种轻盈和速度来优化内容加载时间。
Astro 组件与 Svelte 类似:JavaScript、HTML 和 CSS 被清晰地分离开来。它不使用 JSX。例如:
---
// ./index.astro
// 示例 Astro 组件。你可以在 --- 块内编写 JavaScript
// 这里的所有代码都会在服务端运行
export let name = 'Astro';
---
<h1 class="title">Hello {name}</h1>
<style>
.title {
font-family: "Lato", sans-serif;
}
</style>
“万能框架”(The Framework of All Frameworks)
Astro 不仅仅是一个高级的静态站点生成器。我喜欢把它称为“万能框架”——并非指它凌驾于其他框架之上,而是指它的兼容性。Astro 在 JavaScript 框架中独树一帜,因为它支持其他 UI 框架。
没错,你可以直接在 Astro 中导入用 React、Preact、Svelte、Vue、Lit 或 Solid 编写的组件,甚至可以在同一个文件中混合使用它们。
---
// 从不同框架导入组件
import SvelteNavbar from './components/SvelteNavbar.svelte';
import ReactPostList from './components/ReactPostList.jsx';
import VueFooter from './components/VueFooter.vue';
---
<article>
<header>
<SvelteNavbar />
</header>
<main>
<ReactPostList />
</main>
<footer>
<VueFooter />
</footer>
</article>
是的,Astro 支持来自多个框架的组件,这简直不可思议。这一切只需运行以下命令即可实现:
npx astro add @astrojs/react @astrojs/svelte @astrojs/vue
而且,由于 Astro 默认不输出任何 JavaScript,因此每引入一个新框架并不会增加打包体积。每个组件都会被服务端渲染为静态 HTML,因此你无需担心不同语言之间的互操作性问题。
交互性与“岛屿” 🏝️
如果你需要交互性,只需添加 client:load 指令,框架的运行时就会在客户端加载。这是“岛屿架构”(island architecture)的一种实现。
岛屿架构的核心思想是:首先构建以静态 HTML 为主的网站,然后将交互性(及其关联的 JavaScript)限制在特定区域。这些区域是相互隔离的“岛屿”,会在主静态内容加载完成后才加载。通过这种方式,Astro 既能作为静态站点生成器,又能为整个网站提供强大的交互选项。
Astro 组件
如前所述,除了可以复用 React、Svelte 等框架编写的组件外,Astro 还拥有自己的组件系统。下面我们深入了解一下 Astro 组件的工作原理。
以下是一个用于展示会议演讲的基本组件示例:
---
// TalkCard.astro
const { title = 'Learn Astro', time = '2023-09-23T13:30:00' } = Astro.props;
---
<div>
<h2>{title}</h2>
<p>{date}</p>
</div>
组件的前置代码块(front matter)包含服务端将要运行的 JavaScript,然后将结果数据传递给 HTML。
我们的组件期望接收 name 和 time 两个属性,我们通过 Astro.props 设置了默认值("Learn Astro" 和 "2023-09-23T13:30:00")。然后,我们在 HTML 中使用 {} 来引用这些值,就像在 Svelte 或其他模板文件中一样。这只是一个示例。
传递值(Props)
我们可以像这样从父组件向子组件传递值/属性:
---
// ./src/component/TalkGrid
// 使用标准 import 语法导入 TalkCard 组件
import TalkCard from '../components/TalkCard.astro';
// 演讲列表
let talks = [
{ title: 'Learn Astro', date: '2023-09-23T13:30:00' },
{ title: 'Learn Vue.js', date: '2023-09-23T14:30:00' },
];
---
<section>
{talks.map( (talk) => {
return <TalkCard title={talk.title} date={talk.date} />
})}
</section>
基于文件的路由(File-based Routing)
与其他框架一样,src/pages 目录中的文件结构会直接映射为网站的页面。以下是我们 Astro 网站的一个示例目录结构:
.
└── src
├── components
└── TalkCard.astro
├── layout
└── pages
├── about.md
└── index.astro
我们之前提到的 index.astro 文件将成为网站的首页。那么那个 about.md 文件是什么呢?我们将在下一节看到。但在那之前,先聊聊动态路由。
动态路由(Dynamic Routing)
在很多场景下,你并不希望为网站的每个页面都手动创建单独的文件。你的内容可能来自无头 CMS 或内容平台(比如 Contentful)的 API,或者你可能使用了某种动态数据。
这就是所谓的“动态路由”,Astro 通过在文件名中使用 [] 语法来支持这一功能。动态路由参数使 Astro 能够自动创建多个页面——当你需要大量同类型的页面时(例如产品规格页、作者简介页,或者在我们的会议示例中,演讲者简介页),这非常有用。要了解更多关于动态路由的信息,请查阅 Astro 官方文档。
Markdown、MDX 与布局(Layouts)
还记得我们之前看过的目录结构吗?Astro 允许你直接将 Markdown 和 MDX 文件转换为网站页面。你只需在前置元数据(front matter)中指定一个 layout 值即可。
一个关于页面的 Markdown 文件如下所示:
---
title: About
layout: ../layouts/MarkdownPage.astro
---
We're a series of developer conferences that happen all around the world.
那么这个 layout 属性是什么意思呢?
布局(Layouts)
在 Astro 中(与 Svelte 和 Qwik 类似),你可以使用 <slot> 元素来指定子组件应插入的位置。
例如,通常你会有一个包含基础 HTML、导航栏和页脚的组件:
// 示例文件
// ./layout/Main.astro
import Footer from '../components/Footer.astro'
const {title} = Astro.site;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link
rel="icon"
href="https://contentful.com/favicon-32x32.png"
type="image/png"
/>
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
</head>
<body>
<slot /> <!-- 网站其余内容将插入此处 -->
<Footer />
</body>
</html>
布局组件在技术上与其他组件并无区别,但它专为容纳其他组件而设计。
MDX
Astro 通过集成支持 MDX。MDX 被称为“组件时代的 Markdown”,允许你在 Markdown 文档中插入组件。
import Chart from './Chart.astro'
# Hello
This is _markdown_ and **bold text** and everything. Except we can add components.
<Chart />
More text...
在 Astro 中,上面的 <Chart /> 组件将是一个 Astro 组件。
你可以通过以下命令安装 MDX 集成来启用它:
npx astro add mdx
MDX 还有更多功能。同样,你可以查阅 Astro 文档了解更多。
内容集合(Content Collections)
让我们继续以一个假设的会议网站 Astro 项目为例。
你为每场演讲创建了一个 Markdown 文件,内容如下:
title: Learn Astro
date: 2023-09-23T13:30:00
---
Astro is fun!
现在,将此扩展到 20 场演讲。如果其中一个文件中你把 title 错打成了 titlr 会怎样?你的构建过程可能会失败,或者某人的演讲标题可能无法显示。
如果框架能提前警告你这些问题就好了,对吧?这正是 Astro 的“内容集合”(Content Collections)功能所解决的问题。
内容集合为 Markdown 文件增加了类型检查、必填属性等特性,大大降低了出错概率。
下面,我们将示例转换为内容集合。
1. 设置内容文件夹
在 src 目录下创建一个名为 content 的新文件夹,如下所示:
.
└── src
├── content # src/content 文件夹是保留目录
├── components
├── layouts
└── pages
├── about.md
└── index.astro
在该文件夹中添加你的 Markdown 文件。
2. 配置集合
现在,在 ./src/content/config.js 中创建一个配置文件。(也可以是 TypeScript 文件。)
// ./src/content/config.js
// 1. 从 `astro:content` 导入所需库
import { z, defineCollection } from "astro:content";
// 2. 定义你的集合
const talk = defineCollection({
schema: z.object({
title: z.string(),
date: z.date(),
}),
});
// 3. 导出 `collections` 对象
export const collections = {
talk,
};
现在,我们的演讲已被定义为一个集合,接下来需要稍微调整代码以在应用中显示它们。
3. 更新代码
更新你的组件代码,使其如下所示:
---
import { getCollection } from "astro:content";
import TalkCard from "../components/TalkCard.astro";
let talks = await getCollection("talk");
---
<ul role="list" class="link-card-grid">
{
talks.map(({ data }) => { // 信息现在存储在 "data" 属性中
return (
<TalkCard
title={data.title}
date={data.date}
description={data.description}
/>
);
})
}
</ul>
引用(References)
在处理 Markdown 文件时,引用一直是个老大难问题。以我们的会议网站为例,如果我们有一系列演讲,每场都关联一位演讲者的名字,那么 Markdown 前置元数据看起来会是这样:
---
title: Learn Astro
date: 2023-09-23T13:30:00
speaker: Alvin
---
但如果一位演讲者有两场不同的演讲呢?或者如果你想为每位演讲者创建一个包含详细信息的专属页面呢?这与博客中作者信息面临的问题相同。解决方案要么是重复信息(比如两次写演讲者名字),要么是为演讲者创建某种 ID,在一个文件中引用它,并在另一个文件中编码实现关联。
Astro 通过内容集合为你解决了这个问题。
首先,让我们为演讲者创建第二个集合,使配置文件如下所示:
// 1. 从 `astro:content` 导入所需库
import { z, defineCollection, reference } from "astro:content";
// 2. 定义你的集合
const talk = defineCollection({
schema: z.object({
title: z.string(),
date: z.date(),
}),
});
// 添加 `speaker` 集合
const speaker = defineCollection({
schema: z.object({
name: z.string(),
title: z.string(),
twitter: z.string().optional(),
}),
});
// 3. 导出 `collections` 对象
export const collections = {
talk,
speaker,
};
在 ./src/content/speaker 中至少创建一个演讲者的 Markdown 文件。
src/content/
├── config.js
├── speaker
│ ├── alvin.md
│ └── harshil.md
└── talk
├── learn-astro.md
└── learn-vue.md
然后,在配置文件中为演讲集合添加一个引用。
const talk = defineCollection({
schema: z.object({
title: z.string(),
date: z.date(),
// ✨ 这就是神奇之处
// 通过文件名引用 `speaker` 集合中的演讲者
// 别忘了从 "astro:content" 导入 { reference }
speaker: reference("speaker"),
}),
});
现在,如果你忘记在演讲的 Markdown 文件中添加演讲者,Astro 会告诉你,并显示一条(很有帮助的!)错误信息:
引用(References)
Markdoc
内容集合还允许你使用 Markdoc,它有点像 MDX。
Markdoc 是 Astro 的另一个集成,允许你使用简码(shortcode)语法在 Markdown 中添加 Astro 组件。
自定义组件被包裹在类似 {% tags %} 的标签中。
以下是在 Markdoc 中使用 Highlight 组件的示例:
# Welcome to Markdoc 👋
{% highlight color="yellow" %}
This text will be *highlighted* in your docs.
{% /highlight %}
混合渲染(Hybrid Rendering)
最初,我们的会议网站只是一堆零散的 Markdown 文件。接着,我们使用内容集合为其赋予了结构。然后,我们通过处理“引用”问题增加了复杂性。
现在,让我们再增加一层复杂性。如果你需要一个依赖 API 的动态页面怎么办?如果你想显示会议举办地的实时天气预报呢?你不可能每次天气变化就重建整个网站吧?(尤其是在北欧生活的话)。
这时,“混合渲染”(Hybrid Rendering)就派上用场了。
通过混合渲染,你可以指定哪些路由应为静态,哪些应在服务器上动态渲染。
1. 启用混合渲染
你可以在全局 Astro 配置文件 ./astro.config.mjs(注意不是之前创建的 ./content/config.js)中添加 output: "hybrid" 来启用混合渲染。
// ./astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
output: "hybrid",
});
在生产环境中,你需要使用一个 SSR 适配器,可以是 Vercel、Deno,或是 Astro 团队提供的其他适配器。
启用后,一切应照常工作,因为静态站点渲染仍是默认行为。
2. 创建新页面
让我们为会议创建一个新的“场地”(venue)页面,并为其请求一些动态数据。
---
// ./src/pages/venue.astro
let name = "Starry Skies Outdoor Theater";
let location = "123 Park Ave, CityTown";
let capacity = 100;
let facilities = ["Close to public transports", "Wheelchair accessible", "Public toilets", "Free Wifi"];
---
<section>
<div class="theater">
<h2>{name}</h2>
<p>{location}</p>
<p>Capacity: {capacity}</p>
<div class="details" style="display: none;">
<h3>Facilities</h3>
<ul>
{facilities.map((facility) => (
<li>{facility}</li>
))}
</ul>
</div>
</div>
3. 添加动态数据
现在,让我们调用一个 API 来获取假设场地的日出日落时间。这是一个简单且人为构造的例子,只是为了展示这项技术的潜力。
以下是最终文件:
---
// ./src/pages/venue.astro
import Layout from "../layouts/Layout.astro";
// 禁用静态输出
export const prerender = false;
// 调用外部 API 获取动态数据
let res = await fetch(
"https://api.sunrise-sunset.org/json?lat=36.7201600&lng=-4.4203400"
);
let { results } = await res.json();
let { sunrise, sunset } = results;
let name = "Starry Skies Outdoor Theater";
let location = "123 Park Ave, CityTown";
let capacity = 100;
let facilities = [
"Close to public transports",
"Wheelchair accessible",
"Public toilets",
"Free Wifi",
];
---
<Layout title="Venue | Fake Astro Conf">
<section>
<h2>Venue</h2>
<p>This year, our conference takes place at the magnificent {name}</p>
<p>Luckily for us, it's summer, the days are super long!</p>
<p>The sun rises at <b>{sunrise}</b> and sets at <b>{sunset}</b></p>
</section>
<hr />
<div class="theater">
<h2>{name}</h2>
<p>{location}</p>
<p>Capacity: {capacity}</p>
<div class="details">
<h3>Facilities</h3>
<ul>
{facilities.map((facility) => <li>{facility}</li>)}
</ul>
</div>
</div>
</Layout>
理解 Astro 的优势
我们已经看到了 Astro 能为网站构建带来的一些最有趣的功能,以及如何实现它们。显然,这个框架远不止是一个静态站点生成器。那么,如果你选择 Astro JS,一旦项目上线运行,你能为你的项目和整个网站带来哪些好处呢?
- SEO 价值:由于 Astro 以静态 HTML 为主,其页面比重度依赖 JavaScript 的页面更容易被搜索引擎抓取,因此通常能凭借更高的自然流量排名获得优势。
- 开发民主化:只要你能写 HTML,就能用 Astro 开发网站。该框架还支持 TypeScript,配备多功能 CLI,并可通过各种插件和集成进行扩展,让你在需要时逐步增加复杂性。作为一个开源语言,你还可以进一步推动 Astro 以满足定制化需求。
- 功能深度:Astro 开发者开箱即用地访问大量额外功能,包括多语言内容展示和自动图像优化等能力。
- 简洁与灵活并存:Astro 让你能够快速、大规模地发布内容,而不会被细节或复杂的配置难题绊倒。同时,当你需要调整页面或在静态 HTML 上叠加交互性时,它也提供了充足的定制潜力。
- 加载优先级:Astro 能够控制“岛屿架构”的加载顺序。这意味着更重要的页面组件(如页眉和图片标题)可以优先于次要组件(如侧边栏、页脚内容或评论区)加载。
归根结底,Astro JS 适合那些需要兼顾速度与动态性的 Web 开发者,同时又不想牺牲所要展示内容的深度与丰富性。该框架让你能够以简单的资源和专业知识起步,并在需要时逐步为静态站点叠加复杂性,包括交互式组件。
另一方面,如果你是一位经验更丰富的 Web 开发者,对开发静态 HTML 网站不感兴趣,或者你的项目已知具有极高的交互性或复杂的显示需求,那么 Astro 可能不是适合你的 Web 框架。在这种情况下,值得考虑 Next.js、Vue.js 或 React 等替代方案。