这是「同一 RealWorld 规范,多种技术架构」系列第七篇。前六期我们依次拆了 React SPA、Vue 3 SPA、Nuxt 3 SSR、Next.js SSR、SvelteKit SSR、Remix。这次轮到 Astro------一个"岛屿架构"框架,和前面六篇的"全量水合"框架,思维模型完全不同。
引言:内容驱动框架的新范式
如果你是从前六篇一路跟过来的,应该已经熟悉了 SSR 的基本思路------"服务端渲染,解决内容可见性问题"。但 Astro 的 SSR 和前面所有框架的 SSR,有一个本质区别。
全量水合 vs 按需水合
前面我们拆过的 SSR 框架(Next.js、Remix、SvelteKit、Nuxt 3),都遵循一个共同的假设:
整个页面需要水合。所有组件都在客户端重新渲染一遍。
这意味着,即使你的页面 90% 内容是静态的(博客文章、新闻、产品介绍),浏览器也需要下载整个框架的 JS,然后把整个页面重新渲染一遍。这个过程叫"全量水合"。
Astro 推翻了这个假设:
页面中只有需要交互的组件才需要水合。静态组件不需要输出任何 JS。
这就是岛屿架构的核心思想:静态内容是海洋,交互组件是岛屿。浏览器只需要下载并水合那些交互岛屿,静态海洋保持零 JS。
零JS默认的哲学
Astro 把这个思想贯彻到底,形成了"零JS默认"哲学:
-
.astro组件默认不输出任何 JS ------如果组件只是展示静态内容,浏览器中没有它的 JS -
只有明确标记的组件才会水合 ------使用
client:load/client:idle/client:visible指令决定水合时机 -
支持多 UI 框架混用 ------你可以在同一个 Astro 项目中使用 Preact、React、Vue、Svelte 等多种框架,每个交互岛屿用你喜欢的框架
对比一下 JS 负载:
| 框架 | 一个典型博客首页的 JS 体积 |
|---|---|
| Astro(岛屿架构) | ~15-30KB |
| SvelteKit | ~30-50KB |
| Next.js | ~80-120KB |
| Remix | ~70-110KB |
这个差异不是一点点------这是数量级的差异。
🤔 思考:你在前面的六篇文章中看到了各种 SSR 框架:Next.js 的全量水合、Remix 的 Web 标准优先、SvelteKit 的编译时优化。但所有这些框架都遵循一个假设------整个页面需要水合。Astro 问了一个不同的问题:如果一个页面 90% 的内容是静态的,只有 10% 是交互的,为什么不让那 90% 保持零 JS?这就是"岛屿架构"的核心思想。但问题是:当你的页面交互越来越多,"岛屿"越来越多,这个模式的边界在哪里?
铺垫完背景,我们看看这个 Astro 版 RealWorld 项目到底长什么样------它和前面六期的项目,架构上有本质区别吗?
项目全景:joma1021/realworld-astro
什么是 RealWorld
RealWorld 是一套统一的 Medium 克隆规范,由 Thinkster 社区维护。它定义了一个完整的博客平台需要哪些功能,并提供了统一的 API 和设计规范。前六期我们拆解了不同技术栈的 RealWorld 实现,这次是同一规范,Astro 实现。
技术栈
先看 package.json 里的依赖------项目 2026 年 7 月更新,49 commits,技术栈非常现代:
| 技术 | 版本 | 用途 |
|---|---|---|
| Astro | 7.1.5 | 全栈框架(岛屿架构) |
| Preact | 10.29.7 | UI 交互组件 |
| TypeScript | 5.9.3 | 类型系统 |
| nanostores | 1.4.2 | 轻量级响应式状态管理 |
| @astrojs/preact | 最新 | Preact 集成 |
| @astrojs/vercel | 最新 | Vercel SSR 适配器 |
| npm | - | 包管理器 |
几个关键发现:
-
Astro 7.1.5,最新版本,编译器重写为 Rust,构建速度提升显著
-
使用 Preact 做交互组件,而非 React。开启
compat: true模式兼容 React 生态 -
使用 nanostores 做状态管理,替代 Pinia/Redux/Zustand,体积仅 1KB
-
使用 Cookie-based JWT Session 做认证,而非 localStorage
-
使用
astro:middleware做路由保护 -
使用
astro:transitions的 ClientRouter 做客户端导航 -
样式通过 CDN 引入 Ionicons 字体图标 + 全局 CSS,没有 TailwindCSS
目录结构
项目的目录结构比前面任何一个框架都"简单":
src/
├── Layout.astro # 根布局(HTML骨架 + Header + Footer + ClientRouter)
├── middleware.ts # 中间件(路由保护 + 用户认证)
├── env.d.ts # 类型声明
├── models/ # 数据模型
│ ├── article.ts # 文章模型
│ ├── comment.ts # 评论模型
│ ├── profile.ts # 用户资料模型
│ └── user.ts # 用户模型
├── pages/ # 页面路由(文件路由)
│ ├── index.astro # 首页(文章列表 + 标签 + 分页 + 你的Feed/全局Feed)
│ ├── article/ # 文章详情
│ │ └── [slug].astro # 动态路由
│ ├── editor/ # 编辑/创建文章
│ │ └── [slug].astro # 编辑已有文章
│ ├── login/ # 登录
│ │ └── index.astro # 登录表单
│ ├── profile/ # 用户主页
│ │ └── index.astro # 用户资料
│ ├── register/ # 注册
│ │ └── index.astro # 注册表单
│ └── settings/ # 设置
│ └── index.astro # 用户设置
├── components/ # UI组件(Astro + Preact islands)
│ ├── article/ # 文章相关组件
│ ├── buttons/ # 按钮组件(含Preact交互)
│ ├── comments/ # 评论组件
│ ├── editor/ # 编辑器组件
│ ├── errors/ # 错误展示组件
│ ├── home/ # 首页组件
│ ├── layout/ # 布局组件(Header.astro, Footer.astro)
│ └── tags/ # 标签组件
├── services/ # 服务层(API调用)
│ ├── article-service.ts # 文章API
│ ├── auth-service.ts # 认证API
│ ├── comment-service.ts # 评论API
│ ├── profile-service.ts # 用户资料API
│ └── session-service.ts # Session管理(Cookie读写)
├── common/ # 通用工具
└── styles/ # 样式
└── global.css # 全局样式
对比一下前面几期的目录结构:
-
Next.js:需要
pages/api/目录放 API 端点 -
Remix:需要
app/routes/扁平路由,用后缀做分组 -
Nuxt 3:需要
server/目录放服务端代码 -
Astro :不需要这些。
pages/就是路由,frontmatter 就是服务端代码。
Clone + 跑起来
git clone https://github.com/joma1021/realworld-astro.git
cd realworld-astro
npm install
npm run dev
项目连接 RealWorld 官方 API,不需要本地数据库,直接就能跑。
🤔 思考 :注意到
src/pages/目录了吗?Astro 没有 Next.js 的pages/api/,没有 Nuxt 的server/,也没有 Remix 的app/routes/。它的目录结构比前面任何一个框架都"简单"------就是pages/+components/+services/。但"简单"不等于"功能少"。Astro 的"简单"是因为它把很多复杂性交给了"岛屿架构"来处理------你不需要在路由层做数据加载的配置,因为数据加载就是 frontmatter 中的服务端代码。你觉得这种"简单"是真正的简化,还是把复杂性转移到了其他地方?
项目跑起来后,我们看看 Astro 最基础的部分------它的文件路由。
Astro 文件路由 + 服务端渲染
Astro 路由系统其实非常简单------就是文件系统路由,类似 Next.js Pages Router。理解起来几乎没有学习成本。
文件路由:src/pages/ = 路由
Astro 的路由约定是:
src/pages/目录中的.astro文件,自动映射为路由。
不需要任何配置文件,没有 next.config.js,没有 routes.ts,文件名就是路由。
举几个例子:
| 路由文件 | 实际路由 |
|---|---|
src/pages/index.astro |
/ |
src/pages/article/[slug].astro |
/article/:slug |
src/pages/editor/[slug].astro |
/editor/:slug |
src/pages/login/index.astro |
/login |
src/pages/register/index.astro |
/register |
[slug].astro 动态路由
动态路由使用方括号 [slug] 语法:
src/pages/article/[slug].astro → /article/:slug
在页面中通过 Astro.params.slug 获取参数:
---
const { slug } = Astro.params;
const article = await getArticleBySlug(slug);
---
这种语法和 Next.js Pages Router 的 [pid].tsx 几乎一模一样,如果你用过 Next.js,直接就能上手。
SSR 模式配置
项目使用 SSR 模式部署到 Vercel,配置在 astro.config.mjs:
import { defineConfig } from "astro/config";
import preact from "@astrojs/preact";
import vercel from "@astrojs/vercel";
export default defineConfig({
integrations: [preact({ compat: true })],
output: "server",
adapter: vercel(),
});
-
output: "server"--- 启用 SSR 模式,每次请求都在服务端渲染 -
默认是
static模式,生成纯静态站点 -
适配器决定部署平台(Vercel/Netlify/Cloudflare)
-
preact({ compat: true })--- 开启 React 兼容模式,可使用 React 生态库
Root 布局(Layout.astro)
根布局定义了所有页面共享的 HTML 骨架:
---
import Footer from "./components/layout/Footer.astro";
import Header from "./components/layout/Header.astro";
import "./styles/global.css";
import { ClientRouter } from "astro:transitions";
const { pageTitle } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{pageTitle}</title>
<link href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css" rel="stylesheet" />
<ClientRouter />
</head>
<body>
<Header />
<slot />
<Footer />
</body>
</html>
关键特点:
-
使用
Astro.props接收页面标题 -
使用
<slot />插槽渲染子页面内容(类似 Vue) -
使用
ClientRouter开启客户端导航(类似 SPA 路由切换) -
所有全局资源在这里引入(CSS、CDN 图标)
与 Next.js/Remix 对比
| 维度 | Astro | Next.js (Pages Router) | Remix |
|---|---|---|---|
| 路由目录 | src/pages/ |
pages/ |
app/routes/ |
| 动态路由 | [slug].astro |
[pid].tsx |
$slug.tsx |
| 路由配置 | 零配置 | 零配置 | 扁平路由 + 后缀 |
| 布局系统 | Layout.astro + slot | _app.tsx 手动 | root.tsx + Outlet |
| SSR 模式 | output: "server" | 默认 SSR | 默认 SSR |
| 客户端导航 | ClientRouter | 内置 Link | 内置 Link |
🤔 思考 :Astro 的文件路由和 Next.js 的 Pages Router 非常相似------都用
[slug]做动态路由,都用目录结构映射路由。但 Astro 有一个关键区别:它没有pages/api/目录。因为 Astro 的"服务端代码"不在 API 路由中,而是在 frontmatter 中直接写。这意味着在 Astro 中,"服务端"和"客户端"的边界不是通过目录来区分的,而是通过文件格式(.astrovs.tsx)和 frontmatter 来区分的。你觉得哪种方式更清晰?
理解了路由,下一个问题是:页面用什么组件来构建?Astro 的组件模型和前面所有框架都不同------它有两种组件,服务于不同的目的。
岛屿架构:Astro 组件模型
Astro 最独特的地方不是路由,也不是数据获取------而是它的组件模型:页面由两种组件构成,分别服务于两种不同的目的。
两种组件,两种角色
| 组件类型 | 后缀 | 默认 JS 输出 | 用途 | 适合 |
|---|---|---|---|---|
| Astro 组件 | .astro |
零 JS | 静态内容展示 | 页面骨架、文章预览、页眉页脚 |
| Preact Islands | .tsx |
按需水合 | 交互逻辑 | 按钮、表单、编辑器 |
这就是"岛屿架构":
-
静态海洋 --- 所有
.astro组件都在服务端渲染,输出纯 HTML,不携带任何 JS -
交互岛屿 --- 只有需要交互的 Preact 组件才会被水合,只下载水合那些必要的 JS
Astro 组件:零JS默认
一个典型的 Astro 组件:
---
// 这段代码全部在服务端执行
// 浏览器中看不到任何 JS
import type { Article } from "../models/article";
const { article } = Astro.props as { article: Article };
---
<li class="article-preview">
<div class="article-meta">
<a href="/profile/{{article.author.username}}">
<img src="{{article.author.image}}" alt="{{article.author.username}}" />
</a>
<div class="info">
<a href="/profile/{{article.author.username}}" class="author">
{article.author.username}
</a>
<span class="date">{article.createdAt}</span>
</div>
</div>
<a href="/article/{{article.slug}}">
<h1>{article.title}</h1>
<p>{article.description}</p>
</a>
<ul class="tag-list">
{article.tagList.map(tag => (
<li class="tag-default tag-pill tag-outline">{tag}</li>
))}
</ul>
</li>
这个组件输出一个文章预览卡片。它的所有代码都在服务端执行,浏览器中没有任何 JS。整个组件就是纯 HTML + CSS。
这就是 Astro 的"零JS默认"哲学:
如果一个组件不需要交互,就不要输出 JS。
Preact Islands:按需水合
当你需要交互时,使用 Preact 组件,并通过 client:* 指令控制水合时机:
// components/buttons/FavoriteButton.tsx
import { useStore } from '@nanostores/preact';
import { $user } from '../../stores/user';
import { favoriteArticle } from '../../services/article-service';
export default function FavoriteButton({ article }) {
const user = useStore($user);
const [favorite, setFavorite] = useState(article.favorited);
const handleClick = async () => {
await favoriteArticle(article.slug, favorite);
setFavorite(!favorite);
};
return (
<button onClick={handleClick} className="btn btn-sm">
<i className="ion-heart"></i>
<span> {favorite ? 'Unfavorite' : 'Favorite'} ({article.favoritesCount})</span>
</button>
);
}
然后在 Astro 页面中使用,添加 client:load 指令:
<FavoriteButton
article={article}
client:load
/>
水合指令有几种:
| 指令 | 水合时机 | 适用场景 |
|---|---|---|
client:load |
页面加载时立即水合 | 需要立即交互的组件(按钮、导航) |
client:idle |
浏览器空闲时水合 | 非关键交互组件 |
client:visible |
元素进入视口时水合 | 下方的评论区、编辑器 |
client:media |
满足媒体查询时水合 | 移动端菜单 |
<slot /> 插槽机制
Astro 的 <slot /> 和 Vue 的插槽完全一样------父组件定义骨架,子组件填充内容:
<!-- Layout.astro -->
<body>
<Header />
<slot /> <!-- 子页面内容注入到这里 -->
<Footer />
</body>
然后每个页面这样使用:
<Layout pageTitle="Conduit - Home">
<!-- 这里的内容会被渲染到 <slot /> 的位置 -->
<div class="home-page">
...
</div>
</Layout>
---
与 SvelteKit/React 对比
| 维度 | Astro(岛屿架构) | SvelteKit | React(Next.js) |
|---|---|---|---|
| 默认JS输出 | 零JS | 零JS(近似) | 全量JS |
| 交互组件 | Preact/Vue/Svelte Islands | Svelte 组件 | React 组件 |
| 水合策略 | client:load/idle/visible | 全量水合 | 全量水合 |
| 多UI框架 | 支持混用 | 仅 Svelte | 仅 React |
| 组件后缀 | .astro / .tsx | .svelte | .tsx / .jsx |
🤔 思考:Astro 的"岛屿架构"有一个很有意思的推论:一个页面上的交互组件越多,"岛屿"就越多,"默认"的零JS优势就越小。如果你的页面 80% 都是交互组件(比如一个在线编辑器),那 Astro 的优势可能就不明显了。但对于一个"内容型"网站(博客、文档、新闻、电商产品页),岛屿架构的优势是巨大的------因为这类网站通常 90% 以上的内容是静态的,只有少数交互点(收藏按钮、评论表单、搜索框)。你觉得你的项目属于哪种类型?适合用 Astro 吗?
岛屿架构解决了"组件如何渲染"的问题,那"数据从哪来"呢?Astro 的数据获取方式------在 frontmatter 中写服务端代码------可能是它最被低估的设计。
服务端数据获取:Astro frontmatter 代码
Astro 的数据获取方式非常直接------所有需要在服务端获取的数据,直接在 --- frontmatter 中写代码。没有"loader",没有"getInitialProps",就是直接写。
frontmatter 中的服务端代码
我们看看首页 index.astro 的 frontmatter:
---
import Layout from "../Layout.astro";
import ArticlePreview from "../components/article/ArticlePreview.astro";
import TagSidebar from "../components/tags/TagSidebar.astro";
import type { ArticlesDTO } from "../models/article";
import { getGlobalArticles, getYourArticles } from "../services/article-service";
import { getUserSessionData } from "../services/session-service";
const pageTitle = "Conduit - Home";
const filter = Astro.url.searchParams.get("filter") as string;
const page = Number(Astro.url.searchParams.get("page") ?? 1);
const userSession = getUserSessionData(Astro.cookies);
const feedFilter = filter ?? (userSession.isLoggedIn ? "your" : "global");
let articles: ArticlesDTO | null;
if (feedFilter === "global") {
articles = await getGlobalArticles(userSession.token, page);
} else if (feedFilter === "your") {
articles = await getYourArticles(userSession.token, page);
} else {
articles = await getGlobalArticles(userSession.token, page, feedFilter);
}
---
这些代码全部在服务端执行,不会出现在浏览器中。执行完成后,把结果传递给模板渲染。
Astro 全局对象
在 frontmatter 中,你可以通过 Astro 全局对象访问各种信息:
| 属性 | 用途 |
|---|---|
Astro.url |
当前请求的 URL 对象,可获取查询参数 searchParams |
Astro.params |
动态路由参数(如 [slug]) |
Astro.props |
组件接收的 props |
Astro.cookies |
读取/写入 Cookie |
Astro.request |
标准 Web Request 对象 |
这就是全部------不需要学框架特定的 API,就是对象属性。
首页数据获取实战
我们完整分析一下首页的数据获取逻辑:
// 1. 获取 URL 查询参数中的 filter 和 page
const filter = Astro.url.searchParams.get("filter") as string;
const page = Number(Astro.url.searchParams.get("page") ?? 1);
// 2. 从 Cookie 中读取用户 Session
const userSession = getUserSessionData(Astro.cookies);
// 3. 根据用户登录状态决定默认 Feed
const feedFilter = filter ?? (userSession.isLoggedIn ? "your" : "global");
// 4. 根据过滤器获取文章数据
let articles: ArticlesDTO | null;
if (feedFilter === "global") {
articles = await getGlobalArticles(userSession.token, page);
} else if (feedFilter === "your") {
articles = await getYourArticles(userSession.token, page);
} else {
articles = await getGlobalArticles(userSession.token, page, feedFilter);
}
整个过程就是普通的 TypeScript 代码:
-
读取查询参数 → 读取 Cookie → 条件分支 → await API → 得到数据
-
没有框架约定,没有特殊 API,就是你熟悉的代码
然后模板直接使用数据:
{articles ? (
<ul>{articles.articles.map((article) => <ArticlePreview article={article} />)}</ul>
) : (<div>Error loading articles</div>)}
这里有一个关键点:分页计算也在服务端完成,直接生成 HTML:
<ul class="pagination">
{articles && Array(Math.ceil((articles.articlesCount ?? 0) / 10)).fill(null).map((_, i) => (
<li class={`page-item ${i == page - 1 ? "active" : ""}`}>
<a href={`/?filter=${feedFilter}&page=${i + 1}`}>{i + 1}</a>
</li>
))}
</ul>
分页链接直接输出在 HTML 中,不需要 JS 动态生成。
与 Next.js loader/Remix loader 对比
| 维度 | Astro(frontmatter) | Next.js(getInitialProps) | Remix(loader) |
|---|---|---|---|
| 代码位置 | --- frontmatter 中 |
组件静态方法 | 独立 export 函数 |
| 获取参数 | Astro.url / Astro.params |
context.query |
{ request, params } |
| Cookie 读取 | Astro.cookies |
context.req.cookies |
request.headers |
| 异步支持 | 直接 await | 直接 await | 直接 await |
| 类型安全 | 手动类型声明 | 泛型参数 | 泛型参数 |
| 框架 API | 无 | 有 | 有 |
🤔 思考 :Astro 的 frontmatter 数据获取方式,和前面六期框架最大的不同是:它没有"loader"或"getInitialProps"这样的框架特定 API。你只是在
---中写普通的 JavaScript/TypeScript,用await调用服务层函数。这种方式的好处是"零学习成本"------如果你会写 Node.js,你就会写 Astro 的数据获取。但问题是:没有框架 API 意味着没有框架提供的"约定"------比如数据缓存、请求去重、错误边界。这些需要你自己实现。你觉得"零 API"和"约定 API"哪个更适合你的项目?
数据获取之后,下一个关键问题是用户认证。Astro 的认证方案和前面几期的框架有什么不同?
Session 认证:Cookie-based JWT + Middleware
Astro 的认证方案非常"手动"------没有框架级的认证库,没有封装好的 OAuth 集成。就是手动读取 Cookie,手动验证 JWT,中间件做路由保护。但这种"手动"的方式,反而更清晰。
Session 服务(session-service.ts)
所有 Session 相关逻辑都封装在 session-service.ts 中:
import type { User } from "../models/user";
export interface UserSession {
isLoggedIn: boolean;
token: string;
user: User | null;
}
// 从 Cookie 中读取并验证 Session
export function getUserSessionData(cookies: AstroCookies): UserSession {
const token = cookies.get("token")?.value;
if (!token) {
return { isLoggedIn: false, token: "", user: null };
}
// 验证 JWT token 有效性
// 解析用户信息
try {
const user = verifyJWT(token);
return { isLoggedIn: true, token, user };
} catch {
return { isLoggedIn: false, token: "", user: null };
}
}
在任何需要用户信息的地方,直接调用:
---
const userSession = getUserSessionData(Astro.cookies);
---
中间件路由保护(middleware.ts)
Astro 内置了中间件支持,所有请求都会经过中间件。我们可以在这里做路由保护:
import { defineMiddleware } from "astro:middleware";
import { getUserSessionData } from "./services/session-service";
export const onRequest = defineMiddleware(async ({ cookies, url, redirect }, next) => {
const userSession = getUserSessionData(cookies);
const pathname = url.pathname;
if (!userSession.isLoggedIn) {
if (pathname === "/settings") return redirect("/register");
if (pathname === "/editor") return redirect("/register");
if (pathname.includes("/editor/")) return redirect("/register");
}
return next();
});
这段代码逻辑非常清晰:
-
每个请求进来,先从 Cookie 读取用户 Session
-
如果用户未登录,访问
/settings//editor等保护路由,直接重定向到注册页 -
否则继续处理请求
中间件在服务端执行,不会影响客户端 JS 体积。
与 Next.js/Remix 认证方案对比
| 维度 | Astro | Next.js | Remix |
|---|---|---|---|
| 认证方案 | Cookie JWT + 中间件 | localStorage JWT | remix-auth + Session Cookie |
| 中间件 | astro:middleware 内置 | 无内置中间件 | 无内置中间件 |
| 路由保护 | middleware.ts 中统一处理 | 组件内手动判断 | Loader 中判断 |
| Cookie 读写 | Astro.cookies 内置 | 需额外库 | createCookieSessionStorage |
| SSR 兼容 | 天然兼容 | 需手动处理(typeof window) | 天然兼容 |
| 复杂度 | 简单(手动) | 简单(手动) | 中等(框架化) |
🤔 思考:Astro 的认证方案很有意思------它没有像 remix-auth 这样"框架化"的认证库,也没有像 NextAuth.js 这样成熟的生态方案。认证就是"手动读 Cookie → 验证 JWT → 判断登录状态"。这种"手动"的方式,好处是"你完全控制"------没有黑盒,没有框架约定。坏处是"你需要自己实现"------没有开箱即用的 OAuth 集成。你觉得对于 Astro 这种"内容驱动"框架来说,认证不应该太复杂,还是说 Astro 生态需要一个标准化的认证方案?
认证之外,还有一个"非核心但很重要"的问题:状态管理。Astro 选择了一个非常轻量的方案------nanostores。
nanostores 状态管理
Astro 的岛屿架构需要一个"轻量级、框架无关"的状态管理方案,因为你的状态可能需要在多个 Preact 岛屿之间共享。项目选择了 nanostores------这个选择非常符合 Astro 的整体哲学。
nanostores 是什么
nanostores 是一个轻量级响应式状态管理库:
-
体积仅 1KB(gzip 后)
-
框架无关------可与 React/Vue/Svelte/Solid/Preact 配合使用
-
原子存储------基于"原子"的简单 API
-
TypeScript 原生支持
基本用法
声明一个 store:
// stores/user.ts
import { atom } from 'nanostores';
import type { User } from '../models/user';
export const $user = atom<User | null>(null);
export function setUser(user: User | null) {
$user.set(user);
}
export function clearUser() {
$user.set(null);
}
在 Preact 组件中使用:
import { useStore } from '@nanostores/preact';
import { $user } from '../stores/user';
export function UserMenu() {
const user = useStore($user);
return (
<div>
{user ? (
<span>Welcome, {user.username}</span>
) : (
<a href="/login">Login</a>
)}
</div>
);
}
在 Astro 组件中,因为是服务端渲染,不需要读取状态------状态只在客户端的 Preact 岛屿之间共享。
为什么选择 nanostores
项目选择 nanostores,而不是 Pinia 或 Redux 或 Zustand,有几个原因:
-
体积极小:1KB vs Pinia 的 10KB+,符合 Astro 的"最小化 JS 负载"哲学
-
框架无关:可以在 Preact 和纯 JS 中共享,也可以换成 Vue/Svelte 组件,不需要换状态库
-
API 极简:只有 3 个核心概念(atom/computed/effect),学习曲线几乎为零
-
够用就行:对于这个项目,只需要共享用户状态,不需要复杂的中间件、持久化等功能
与 Pinia/React Context 对比
| 维度 | nanostores | Pinia | React Context |
|---|---|---|---|
| 体积 | 1KB | 10KB+ | 内置 |
| 框架依赖 | 无 | Vue | React |
| 学习曲线 | 低(3个API) | 中 | 低 |
| TypeScript | 原生支持 | 优秀 | 手动 |
| 异步 | 支持 | 支持 | 需手动 |
🤔 思考:nanostores 的选择反映了 Astro 生态的一个特点:追求"最小化"。在 Next.js 项目中,你可能会用 Redux Toolkit 或 Zustand;在 Vue 项目中,你会用 Pinia。但在 Astro 项目中,连 1KB 的 nanostores 都被认为是"够用就行"。这种"够用就行"的哲学,和 Astro 的"零JS默认"是一脉相承的------能少用就少用,能不用就不用。但问题是:当你的项目状态管理变得复杂(比如需要中间件、持久化、跨标签页同步),nanostores 够用吗?还是说,Astro 的定位决定了它不适合复杂状态管理的场景?
拆解完 Astro 的六大核心概念(文件路由、岛屿架构、frontmatter 数据获取、中间件认证、nanostores),我们做一个全景对比------把 Astro、Next.js、Remix、SvelteKit 四个全栈框架放在一起,看看它们在架构层面的差异。
架构对比:Astro vs Next.js vs Remix vs SvelteKit
我们把四个框架的关键设计决策放在一张表格里对比,你就能清楚看到它们之间的本质差异。
路由对比
| 维度 | Astro | Next.js (Pages Router) | Remix | SvelteKit |
|---|---|---|---|---|
| 路由约定 | pages/ 文件路由 | pages/ 文件路由 | 扁平路由 + + 后缀 | + 前缀约定 |
| 动态路由 | [slug].astro |
[pid].tsx |
$slug.tsx |
[slug]/ |
| 布局系统 | Layout.astro + slot | _app.tsx 手动 | root.tsx + Outlet | +layout.svelte |
| API 路由 | 无内置 | pages/api/ | 无内置(Resource Routes) | +server.js |
组件模型对比
| 维度 | Astro | Next.js | Remix | SvelteKit |
|---|---|---|---|---|
| 默认 JS 输出 | 零JS | 全量JS | 全量JS | 零JS(近似) |
| 组件类型 | .astro + Islands | .tsx / .jsx | .tsx / .jsx | .svelte |
| 多UI框架 | 支持混用 | 仅 React | 仅 React | 仅 Svelte |
| 水合策略 | 按需(client:*) | 全量 | 全量 | 全量 |
数据获取对比
| 维度 | Astro | Next.js | Remix | SvelteKit |
|---|---|---|---|---|
| 数据获取方式 | frontmatter 代码 | getInitialProps | loader 函数 | load 函数 |
| 框架 API | 无(直接写代码) | getServerSideProps | loader export | export load |
| Cookie 读取 | Astro.cookies | req.cookies | request.headers | cookies |
| URL 参数 | Astro.url.searchParams | context.query | url.searchParams | url.searchParams |
架构对比图
核心差异总结
| 维度 | Astro | Next.js | Remix | SvelteKit |
|---|---|---|---|---|
| 核心定位 | 内容驱动框架 | React 全栈框架 | Web 标准优先框架 | 编译时全栈框架 |
| 哲学 | 零JS默认,按需交互 | React 优先,全栈一站式 | Web 标准优先,渐进增强 | 编译时优化,零运行时 |
| JS 负载 | 最小(只有交互需要) | 最大(全量水合) | 大(全量水合) | 较小(编译时优化) |
| 最适合 | 内容型网站(博客、文档、新闻) | 全栈应用 | 全栈应用 | 内容/应用均可 |
| 学习曲线 | 平缓(几乎没新 API) | 陡峭(App Router 概念多) | 中等(Loader/Action 新概念) | 平缓(Svelte 语法) |
一句话总结:
Next.js:最"重"------从 Pages Router 到 App Router,概念多,学习曲线陡峭
Remix:最"标准"------Web 标准优先,你学到的知识在 Web 平台上通用
SvelteKit:最"轻"------编译时优化,运行时开销最小
Astro:最"克制"------零JS默认,只有需要交互的地方才输出 JS
🤔 思考:四个框架的对比,本质上反映了四个生态对"前端架构"的不同理解:Next.js 认为"前端 = React 全栈";Remix 认为"前端 = Web 标准";SvelteKit 认为"前端 = 编译时优化";Astro 认为"前端 = 内容优先 + 按需交互"。没有绝对的对错,但 Astro 的"零JS默认"哲学确实最接近"性能最佳实践"------你的页面只加载它需要的 JS。但问题是:如果你的项目是一个"交互密集型"应用(如在线文档编辑器、仪表盘),Astro 的优势还明显吗?
理论说完了,我们来做点实际的事情------练习、部署,以及未来的进阶方向。
练习 + 部署 + 进阶
练习:添加文章搜索功能
这是一个串联全文知识点的练习。建议的步骤:
第一步:创建搜索页面路由
在 src/pages/ 下创建 search/index.astro,路由自动映射为 /search。
第二步:在 frontmatter 中处理搜索逻辑
---
import Layout from "../../Layout.astro";
import ArticlePreview from "../../components/article/ArticlePreview.astro";
import { searchArticles } from "../../services/article-service";
const q = Astro.url.searchParams.get("q") || "";
let articles = null;
if (q) {
articles = await searchArticles(q);
}
---
<Layout pageTitle={`Search: ${q}`}>
<div class="search-page">
<form method="get" action="/search">
<input
type="text"
name="q"
defaultValue={q}
placeholder="Search articles by title or tag..."
/>
<button type="submit" class="btn btn-primary">Search</button>
</form>
{q && articles && (
<div class="search-results">
<p>Found {articles.length} results for "{q}":</p>
<ul>
{articles.map(article => (
<ArticlePreview article={article} />
))}
</ul>
</div>
)}
</div>
</Layout>
第三步:服务层封装搜索 API
在 services/article-service.ts 中添加搜索方法:
export async function searchArticles(query: string): Promise<Article[]> {
const response = await fetch(`${API_URL}/articles/search?q=${query}`);
return response.json();
}
这个练习串联了哪些知识点:
-
文件路由 --- 创建
src/pages/search/index.astro→/search -
frontmatter 数据获取 --- 在 frontmatter 中读取查询参数,调用 API
-
Astro.url.searchParams --- 获取查询参数
q -
Astro 组件 ---
ArticlePreview是零JS的 Astro 组件 -
服务层 ---
article-service.ts封装 API 调用
提示:试着用两种方式实现搜索 URL:
-
方式一:查询参数方式 →
/search?q=keyword(上面就是这种) -
方式二:动态路由方式 →
/search/[keyword].astro→/search/keyword
在 Astro 中,这两种方式分别怎么实现?你更喜欢哪种?
部署方式
Astro 的部署非常灵活,取决于你选择的适配器:
# 构建
npm run build
# 部署到 Vercel(项目已配置 @astrojs/vercel)
# 直接推到 GitHub,Vercel 自动部署
vercel --prod
# 部署到 Netlify
# 需要安装 @astrojs/netlify
netlify deploy --prod
# 部署到 Cloudflare Pages
# 需要安装 @astrojs/cloudflare
项目已经配置好了 output: "server" 和 adapter: vercel(),直接推到 GitHub 连接 Vercel 就能用。
进阶路径
拆完这个项目后,如果想继续深入 Astro 生态,建议按这个顺序学习:
-
Astro Content Collections --- Astro 最强大的内容管理功能,用 Markdown/MDX 管理内容,自动生成类型安全的数据层。非常适合博客、文档站
-
Astro View Transitions --- 使用
astro:transitions实现页面切换动画,体验类似 SPA,但保持了零JS默认的优势 -
Server Islands --- Astro 6+ 引入的"服务端岛屿",在静态页面中嵌入动态内容(如最新文章列表、访问计数)
-
多框架集成 --- 在同一个 Astro 项目中使用 React + Vue + Svelte,每个交互岛屿用你喜欢的框架
-
Image Optimization --- Astro 内置的图片优化组件
<Image />和<Picture />,自动生成多种尺寸和格式 -
RSS Feed 生成 --- Astro 内置的 RSS 支持,一行代码生成 RSS Feed
🤔 思考 :练习的关键不是"写出来",而是"改装"------在理解项目架构的基础上,新增一个功能模块。搜索功能看似简单,但它涉及了本期所有知识点:新路由(文件路由)、数据获取(frontmatter)、查询参数(Astro.url)、服务层调用(article-service)。一个练习,串联全文。试着想想:在 Astro 中,搜索结果的 URL 应该怎么设计?
/search?q=keyword还是/search/[keyword]?这两种方式在 Astro 中分别怎么实现?
从 Next.js 到 Astro,你学到的不仅是一个新框架,更是一种"内容优先"的思考方式。
结语
回顾全文,我们从"岛屿架构 + 零JS默认"的哲学出发,拆解了 joma1021/realworld-astro 项目:
-
理解了文件路由系统,
[slug].astro动态路由和 SSR 配置 -
深入了岛屿架构------两种组件(Astro 零JS vs Preact Islands),按需水合策略
-
看到了最"直接"的数据获取方式------frontmatter 中的服务端代码
-
分析了认证方案------Cookie-based JWT + 中间件路由保护
-
理解了为什么选择 nanostores------轻量级状态管理符合 Astro 的哲学
-
最后做了全景对比------Astro vs Next.js vs Remix vs SvelteKit
核心落点:从"全量水合"到"按需交互"的思维跃迁
传统 SSR 框架的思维:全量水合,JS 加载是默认行为。不管页面多少内容是静态的,都需要下载整个框架的 JS,然后水合整个页面。
Astro 的思维:零JS默认,交互是例外而非默认。90% 的静态内容保持零 JS,只有 10% 的交互组件需要水合。
这种思维跃迁,是前端工程师从"框架驱动"到"内容驱动"的关键一步。
系列收束
这个系列走到第七篇,我们已经有了完整的图谱:
| 篇数 | 框架 | 模式 | 定位 |
|---|---|---|---|
| 1 | React (CRA) | CSR | 客户端渲染 |
| 2 | Vue 3 (Vite) | CSR | 客户端渲染 |
| 3 | Nuxt 3 | SSR | Vue 生态全栈框架 |
| 4 | Next.js | SSR | React 生态一站式框架 |
| 5 | SvelteKit | SSR | 编译时全栈框架 |
| 6 | Remix | SSR | Web 标准优先框架 |
| 7 | Astro | 岛屿架构 | 内容驱动框架 |
七篇文章,七种架构,覆盖了当前前端生态最主流的全栈方案。技术是工具,架构思维才是核心------理解不同框架的设计决策,才能在你的项目中做出正确的选择。
如果你对 Astro 的岛屿架构感兴趣,可以 clone 项目跑起来,动手完成本文的练习,相信你会有更深的理解。
欢迎在评论区交流你的想法和问题。
参考文献
-
joma1021/realworld-astro --- 本文分析的源码仓库
-
Astro 官方文档 --- 完整框架文档
-
Astro Islands 架构文档 --- 岛屿架构官方详解
-
nanostores 文档 --- 轻量级状态管理库
-
Preact + Astro 集成指南 --- Preact 官方集成文档
-
RealWorld 规范 --- 全栈 CRUD 示例规范
-
Astro Middleware 文档 --- 中间件官方指南
-
Astro 路由文档 --- 路由官方指南
