Astro 全栈实战:拆解 RealWorld 项目

这是「同一 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默认"哲学:

  1. .astro 组件默认不输出任何 JS ------如果组件只是展示静态内容,浏览器中没有它的 JS

  2. 只有明确标记的组件才会水合 ------使用 client:load / client:idle / client:visible 指令决定水合时机

  3. 支持多 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 中,"服务端"和"客户端"的边界不是通过目录来区分的,而是通过文件格式(.astro vs .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,有几个原因:

  1. 体积极小:1KB vs Pinia 的 10KB+,符合 Astro 的"最小化 JS 负载"哲学

  2. 框架无关:可以在 Preact 和纯 JS 中共享,也可以换成 Vue/Svelte 组件,不需要换状态库

  3. API 极简:只有 3 个核心概念(atom/computed/effect),学习曲线几乎为零

  4. 够用就行:对于这个项目,只需要共享用户状态,不需要复杂的中间件、持久化等功能

与 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();
}

这个练习串联了哪些知识点:

  1. 文件路由 --- 创建 src/pages/search/index.astro/search

  2. frontmatter 数据获取 --- 在 frontmatter 中读取查询参数,调用 API

  3. Astro.url.searchParams --- 获取查询参数 q

  4. Astro 组件 --- ArticlePreview 是零JS的 Astro 组件

  5. 服务层 --- 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 生态,建议按这个顺序学习:

  1. Astro Content Collections --- Astro 最强大的内容管理功能,用 Markdown/MDX 管理内容,自动生成类型安全的数据层。非常适合博客、文档站

  2. Astro View Transitions --- 使用 astro:transitions 实现页面切换动画,体验类似 SPA,但保持了零JS默认的优势

  3. Server Islands --- Astro 6+ 引入的"服务端岛屿",在静态页面中嵌入动态内容(如最新文章列表、访问计数)

  4. 多框架集成 --- 在同一个 Astro 项目中使用 React + Vue + Svelte,每个交互岛屿用你喜欢的框架

  5. Image Optimization --- Astro 内置的图片优化组件 <Image /><Picture />,自动生成多种尺寸和格式

  6. 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 项目跑起来,动手完成本文的练习,相信你会有更深的理解。

欢迎在评论区交流你的想法和问题。


参考文献

  1. joma1021/realworld-astro --- 本文分析的源码仓库

  2. Astro 官方文档 --- 完整框架文档

  3. Astro Islands 架构文档 --- 岛屿架构官方详解

  4. nanostores 文档 --- 轻量级状态管理库

  5. Preact + Astro 集成指南 --- Preact 官方集成文档

  6. RealWorld 规范 --- 全栈 CRUD 示例规范

  7. Astro Middleware 文档 --- 中间件官方指南

  8. Astro 路由文档 --- 路由官方指南

相关推荐
软件开发JR1 小时前
基于Web的足球青训俱乐部管理后台系统的设计与开发
java·前端·spring boot·毕业设计
凤山老林2 小时前
基于 Spring Batch 的海量数据迁移与批处理架构:分片、容错与断点续跑
java·spring boot·spring·架构·spring batch
那年窗外下的雪.2 小时前
VXLAN EVPN 分层排障:从 VTEP 可达、ARP/MAC 到 MAC Mobility
服务器·前端·网络·数据库·spine
张元清2 小时前
React useTimeout Hook:声明式 setTimeout 与自动清理 (2026)
javascript·react.js
heimeiyingwang3 小时前
【架构实战】Kubernetes网络模型深度解析:从Pod通信到Ingress网关实战
开发语言·架构·php
禁止摆烂_才浅3 小时前
JavaScript 类型判断:instanceof 与 constructor 原理深度解析
前端·javascript·面试
可乐ea3 小时前
Anthropic 的 CI/CD 值班智能体:Claude Tag 当一线响应者的架构拆解与踩坑复盘
ci/cd·架构·claude·devops·ai智能体·mcp
gezg3 小时前
DeepSeek Harness 插件:Excel 拖进输入框,AI 自己去读文件
前端·ai编程
Asize3 小时前
HTTP 明明无状态,登录态怎么就保住了——React + Zustand + JWT 鉴权全流程拆解
前端·javascript