🚀 Astro Islands 架构

基于源码的 Islands 架构深度解读

源码深度解读 · withastro/astro
2026-04-01 | Astro Islands Architecture Deep Dive

📑 目录

第一部分:基础概念

  • Astro 框架简介
  • Islands 架构概念
  • 整体架构与包结构

第二部分:核心实现

  • 配置系统 (AstroConfig)
  • Vite 插件体系
  • 构建流程 (Build Pipeline)

第三部分:Islands 深入

  • 客户端指令与水合策略
  • astro-island 组件
  • Props 传递与序列化

第四部分:进阶与生态

  • CSS / HMR / 内容集合
  • 性能优化策略
  • 框架对比与最佳实践

🚀 Astro 框架简介

Astro 是一个专注于内容驱动网站的 Web 框架,核心理念是"默认零 JS",通过 Islands 架构实现局部交互性。

核心特性

  • 默认零 JavaScript 输出
  • Islands 部分水合架构
  • 支持多框架组件 (React/Vue/Svelte…)
  • 内置内容集合 (Content Collections)

渲染模式

  • SSG 静态站点生成
  • SSR 服务端渲染
  • Hybrid 混合渲染
  • Astro View Transitions

💡 核心设计理念

"Bring Your Own Framework" — Astro 不绑定任何 UI 框架,你可以混用 React、Vue、Svelte、Solid 等组件。

"Ship Less JavaScript" — 只发送必要的 JS。静态内容是纯 HTML,只有交互组件才会水合。

"Content First" — Astro 为博客、文档、营销站点等内容密集型场景优化,而非 SPA。

// .astro 文件示例 — HTML 为主,JS 组件为辅
---
import ReactCounter from '../components/ReactCounter.jsx';
import VueGallery from '../components/VueGallery.vue';
---
<h1>我的博客</h1>
<p>这段文字是零 JS 的纯 HTML</p>
<ReactCounter client:load />     {/* 只有这个会水合 */}
<VueGallery client:visible />     {/* 滚动到可见时才水合 */}

🏝️ Islands 架构概念

Islands 架构(2019年由 Preact 作者 Jason Miller 提出):页面大部分是静态 HTML,交互组件像"孤岛"一样散布其中。

┌─────────────────────────────────────────────────┐
│  纯静态 HTML (零 JS)                              │
│  ┌─────────┐    ┌──────────────────────┐          │
│  │ 🏝️ 岛1  │    │ 静态内容...          │          │
│  │ Counter  │    │ 文字、图片、样式     │          │
│  │ (React)  │    │                      │          │
│  └─────────┘    │   ┌─────────┐        │          │
│                  │   │ 🏝️ 岛2  │        │          │
│  静态内容...     │   │ Search  │        │          │
│  更多文字...     │   │ (Vue)   │        │          │
│                  │   └─────────┘        │          │
│                  └──────────────────────┘          │
└─────────────────────────────────────────────────┘
  ↑ 只有 Island 会水合,其余是纯 HTML

❓ 为什么需要 Islands

传统 SPA 问题

  • 整个页面都需要 JS 框架运行时
  • 首屏加载慢 (大量 JS bundle)
  • SEO 不友好
  • 内容密集型页面过度工程化

传统 SSR 问题

  • 虽然首屏快,但 Hydration 成本高
  • 整页水合 = 每个组件都执行 JS
  • Interactive islands 被"非交互"水合拖累

Islands 的解决方案:只水合真正需要交互的组件,其余保持静态 HTML。JS 体积减少 50-90%

🏗️ 整体架构

┌─────────────────────────────────────────────────────────┐
│                    Astro Framework                       │
├─────────────────────────────────────────────────────────┤
│  用户代码: .astro / .mdx / .ts / React / Vue / Svelte  │
├─────────────────────────────────────────────────────────┤
│  编译层: @astrojs/compiler → Vite 插件链                │
│    ├─ astro:build (核心编译)                             │
│    ├─ astro:build:css-hmr (CSS 热更新)                  │
│    ├─ astro:build:normal (虚拟模块解析)                  │
│    └─ 其他框架插件 (React/Vue/Svelte…)                   │
├─────────────────────────────────────────────────────────┤
│  构建层: core/build → 静态页面 / SSR bundle             │
├─────────────────────────────────────────────────────────┤
│  运行时: astro-island (客户端水合) / View Transitions   │
└─────────────────────────────────────────────────────────┘

📦 核心包结构

包名职责
packages/astro核心框架:编译、构建、SSR 运行时
packages/compilerAstro 文件编译器 (TS → 严格模式)
packages/cli命令行工具 (dev/build/preview)
packages/integrations/*框架集成 (React/Vue/Svelte/Solid…)
packages/transformers内容转换器 (MDX/Markdown/remark…)
packages/prism代码高亮引擎
packages/markdownMarkdown 解析器
packages/create-astro项目脚手架

📂 Monorepo 组织

withastro/astro/
├── packages/
│   ├── astro/                    # 核心框架
│   │   ├── src/
│   │   │   ├── index.ts          # 公共 API 入口
│   │   │   ├── core/
│   │   │   │   ├── index.ts      # 核心模块 re-export
│   │   │   │   ├── build/        # 构建流程
│   │   │   │   ├── dev/          # 开发服务器
│   │   │   │   ├── config/       # 配置系统
│   │   │   │   └── routing/      # 路由系统
│   │   │   ├── types/            # TypeScript 类型
│   │   │   ├── runtime/          # 客户端/服务端运行时
│   │   │   │   ├── client/       # 客户端 (astro-island)
│   │   │   │   └── server/       # 服务端 (SSR render)
│   │   │   └── vite-plugin-astro/ # Vite 插件
│   │   └── package.json
│   ├── compiler/                 # Astro 编译器
│   ├── integrations/             # 框架集成
│   └── create-astro/             # 脚手架
└── package.json

🚪 入口模块分析

// packages/astro/src/index.ts — 公共 API 入口
export { Astro } from './core/astro-jsx-runtime.js';
export { markHTMLString } from './runtime/server/html/index.js';
export { render } from './runtime/server/render/index.js';

// re-export 核心 API
export { defineConfig } from './core/config/config.js';
export { addVitePlugin } from './core/config/vite-plugin.js';

// 运行时类型
export type {
  AstroClientBlueprint,
  AstroGlobal,
  AstroGlobalPartial,
} from './types/astro.js';

// 元数据
export type { ImageMetadata } from './types/public/images.js';
export type { CollectionEntry, ContentCollectionKey } from './content/index.js';

// 配置类型
export type { AstroConfig, AstroUserConfig } from './types/public/config.js';
入口文件通过分层 re-export 暴露 API,保持模块边界清晰

⚙️ AstroConfig 类型系统

// packages/astro/src/types/public/config.ts (简化)

export interface AstroUserConfig {
  // 基础配置
  site?: string;
  base?: string;
  trailingSlash?: 'never' | 'always' | 'ignore';
  
  // 渲染配置
  output?: 'static' | 'server' | 'hybrid';
  adapter?: AstroAdapter;
  
  // 构建配置
  build?: {
    inlineStylesheets?: 'always' | 'auto' | 'never';
    assets?: string;
    assetsPrefix?: string;
    serverEntry?: string;
  };
  
  // Vite 集成
  vite?: ViteConfigWithUserOptions;
  
  // 模板引擎
  markdown?: MarkdownConfig;
  
  // 集成
  integrations?: AstroIntegration[];
  
  // 实验性功能
  experimental?: ExperimentalConfig;
}

🔧 配置合并与验证

// packages/astro/src/core/config/config.ts

export function mergeConfig(
  userConfig: AstroUserConfig,
  root: string,
  cmd: 'dev' | 'build' | 'preview',
): AstroSettings {
  
  // 1. 设置默认值
  const config: AstroConfig = {
    root,
    srcDir: new URL('./src/', root),
    publicDir: new URL('./public/', root),
    outDir: cmd === 'dev'
      ? new URL('./.astro/', root)
      : new URL('./dist/', root),
    site: userConfig.site ?? '',
    base: userConfig.base ?? '/',
    trailingSlash: userConfig.trailingSlash ?? 'ignore',
    output: userConfig.output ?? 'static',
    // ... 更多默认值
  };
  
  // 2. 验证配置
  validateConfig(config);
  
  // 3. 返回 settings
  return { config, preferences: /* ... */ };
}

📁 配置加载流程

┌──────────────────────────────────────────────────────┐
│                 配置加载流程                             │
├──────────────────────────────────────────────────────┤
│ 1. 读取 astro.config.mjs / astro.config.ts          │
│    └→ 使用 Vite 的 loadConfigFromFile                │
│                                                       │
│ 2. 加载集成 (integrations)                            │
│    └→ 依次调用 integration.setup()                   │
│    └→ 收集 vite 插件 / hooks                         │
│                                                       │
│ 3. 合并配置                                           │
│    └→ mergeConfig(userConfig, root, cmd)             │
│    └→ 填充默认值 + 验证                               │
│                                                       │
│ 4. 创建 Vite 配置                                     │
│    └→ 统合 Astro 内置插件 + 集成插件                  │
│                                                       │
│ 5. 初始化 Astro 实例                                  │
│    └→ 创建 Settings 对象,传递给构建/开发流程         │
└──────────────────────────────────────────────────────┘

🔌 Vite 插件体系

Astro 基于 Vite 构建,通过插件链处理 .astro 文件的编译、CSS 提取、脚本处理、HMR 等。

插件名职责阶段
astro:build:css-hmrCSS 依赖注册 & HMRpre
astro:build.astro 编译、虚拟模块加载pre
astro:build:normal虚拟模块 fallback 解析normal
@astrojs/reactReact 组件编译 + JSXnormal
@astrojs/vueVue SFC 编译normal
@astrojs/svelteSvelte 组件编译normal

🔧 astro:build 插件详解

// packages/astro/src/vite-plugin-astro/index.ts

export default function astro({ settings, logger }): vite.Plugin[] {
  const { config } = settings;
  let server: vite.ViteDevServer | undefined;
  let compile: (code: string, filename: string) => Promise<CompileAstroResult>;
  
  // 每个文件独立的编译元数据
  let astroFileToCompileMetadata = new Map<string, CompileMetadata>();

  return [
    {
      name: 'astro:build',
      enforce: 'pre',  // 在其他插件之前执行
      
      // 配置 Vite 环境
      async configEnvironment(name, viteConfig) {
        viteConfig.resolve ??= {};
        viteConfig.resolve.conditions.push('astro');  // 添加 astro 条件
      },
      
      // Vite 配置就绪后初始化编译函数
      async configResolved(viteConfig) {
        compile = (code, filename) => {
          return compileAstro({
            astroConfig: config,
            viteConfig,
            filename,
            source: code,
          });
        };
      },
      
      // ... transform / load / handleHotUpdate
    }
  ];
}

📝 Astro 文件编译流程

// packages/astro/src/vite-plugin-astro/index.ts — transform

async handler(source, id) {
  const parsedId = parseAstroRequest(id);
  if (!parsedId.filename.endsWith('.astro')) return;

  // ⚠️ 客户端环境:Astro 组件不可在浏览器中使用
  if (this.environment.name === 'client') {
    return {
      code: `export default import.meta.env.DEV
        ? () => { throw new Error(
          'Astro components cannot be used in the browser.') }
        : {};`,
    };
  }

  // 服务端:编译 .astro 文件
  const transformResult = await compile(source, filename);

  // 生成元数据
  const astroMetadata = {
    hydratedComponents: transformResult.hydratedComponents,
    serverComponents: transformResult.serverComponents,
    scripts: transformResult.scripts,
    containsHead: transformResult.containsHead,
    propagation: transformResult.propagation ? 'self' : 'none',
  };

  return { code: transformResult.code, map: transformResult.map,
    meta: { astro: astroMetadata, vite: { lang: 'ts' } } };
}

📦 虚拟模块系统

Astro 编译 .astro 文件后,CSS 和脚本被拆分为虚拟模块,通过 ?astro&type=style&index=0 等 query string 访问。

// 虚拟模块 load 处理
async handler(id) {
  const parsedId = parseAstroRequest(id);
  const { query, filename } = parsedId;
  const compileMetadata = astroFileToCompileMetadata.get(filename);

  switch (query.type) {
    case 'style': {
      // CSS 虚拟模块
      const result = compileMetadata.css[query.index];
      result.dependencies?.forEach(dep => this.addWatchFile(dep));
      return { code: result.code };
    }
    case 'script': {
      // 脚本虚拟模块 (SSR 环境返回空)
      if (isAstroServerEnvironment(this.environment)) {
        return { code: '/* client script, empty in SSR */' };
      }
      const script = compileMetadata.scripts[query.index];
      return { code: script.type === 'inline' ? script.code
        : `import "${script.src}"` };
    }
  }
}

🏗️ 构建流程总览

┌──────────────────────────────────────────────────────────┐
│                   Astro Build Pipeline                     │
├──────────────────────────────────────────────────────────┤
│ 1. 加载配置 → mergeConfig()                              │
│    └→ 创建 AstroSettings + Vite 配置                     │
│                                                           │
│ 2. 初始化 Vite                                           │
│    └→ 注册所有插件 (astro + 集成)                        │
│                                                           │
│ 3. 扫描页面路由                                          │
│    └→ 发现所有 .astro/.mdx/.md 页面文件                  │
│    └→ 构建路由清单 (RouteManifest)                       │
│                                                           │
│ 4. 构建阶段                                              │
│    ├─ 步骤1: 创建入口点 (生成客户端/服务端入口)           │
│    ├─ 步骤2: Vite 构建 (编译所有模块)                    │
│    ├─ 步骤3: 生成页面 (SSR 渲染每个页面)                 │
│    └─ 步骤4: 生成客户端 bundle                          │
│                                                           │
│ 5. 输出                                                  │
│    └→ 静态 HTML / SSR bundle + 客户端 JS                 │
└──────────────────────────────────────────────────────────┘

🏁 构建入口函数

// packages/astro/src/core/build/index.ts (简化)

export async function build(config: AstroConfig): Promise<BuildResult> {
  const settings = await createSettings(config, 'build');
  const { builder } = settings;

  // 1. 创建 Vite 构建器
  const viteBuild = await createViteBuild(
    settings, { mode: 'build' }
  );
  
  // 2. 构建步骤
  const buildResult = await viteBuild.build({
    buildSteps: async (vite, builder) => {
      // 步骤1: 生成入口文件
      await generateEntryModules(vite, builder);
      
      // 步骤2: Vite 打包
      await vite.build();
      
      // 步骤3: SSR 渲染页面
      const pages = await renderPages(vite, builder);
      
      // 步骤4: 生成客户端 bundle
      await generateClientBundle(vite, builder);
    }
  });

  return buildResult;
}

⚙️ 构建步骤详解

步骤 1: generateEntryModules — 为每个页面生成 SSR 入口和客户端入口文件

步骤 2: Vite Build — 使用 Rollup 打包所有模块,Astro 插件在 pre 阶段编译 .astro

步骤 3: renderPages — 遍历所有路由,使用 SSR 渲染器生成 HTML

步骤 4: generateClientBundle — 为 Islands 组件生成独立的水合 JS bundle

关键点:每个 Island 组件被提取为独立的 chunk,只加载必要的框架运行时

📄 页面构建管道

┌──────────────────────────────────────────────────────┐
│  单页面构建流程                                        │
├──────────────────────────────────────────────────────┤
│                                                       │
│  index.astro                                          │
│       │                                               │
│       ▼                                               │
│  ┌─────────────┐                                     │
│  │ Astro 编译器 │ → 模板 + 脚本 + 样式分离           │
│  └──────┬──────┘                                     │
│         │                                             │
│         ▼                                             │
│  ┌─────────────────┐                                 │
│  │ 识别 Islands     │                                 │
│  │ client:* 指令    │                                 │
│  └──────┬──────────┘                                 │
│         │                                             │
│         ▼                                             │
│  ┌─────────────────────────┐                         │
│  │ SSR Render              │                         │
│  │ → 纯 HTML (静态部分)    │                         │
│  │ → <astro-island> (Island) │                    │
│  │ → <script> (水合代码)     │                       │
│  └─────────────────────────┘                         │
│                                                       │
└──────────────────────────────────────────────────────┘

🌐 SSR 构建模式

output: 'static' (默认)

  • 预渲染所有页面为 HTML
  • 最适合博客/文档
  • 可部署到任何静态托管

output: 'server'

  • 所有页面按需 SSR
  • 需要适配器 (Node/Deno/Cloudflare)
  • 支持 API 路由

output: 'hybrid' — 默认预渲染,但带 export const prerender = false 的页面走 SSR

// astro.config.mjs
export default defineConfig({
  output: 'hybrid',  // 或 'static' / 'server'
  adapter: vercel(),  // SSR 时需要适配器
});

🏷️ Islands 客户端指令

客户端指令是 Astro Islands 的核心语法,通过 client:* 前缀声明组件的水合策略。

<!-- 页面加载时立即水合 -->
<SearchForm  client:load />

<!-- 浏览器空闲时水合 -->
<Sidebar     client:idle />

<!-- 滚动到可视区域时水合 -->
<HeavyChart  client:visible />

<!-- 仅客户端渲染 (无 SSR) -->
<CookieBanner client:only="react" />

<!-- 默认:不水合,仅服务端渲染 -->
<StaticText />

💧 Hydration 策略详解

指令时机适用场景SSR
client:load页面加载完成关键交互 (搜索/导航)
client:idlerequestIdleCallback低优先级交互
client:visibleIntersectionObserver页面底部/折叠区
client:only同 client:load依赖浏览器 API
(无指令)不水合纯静态内容

⚡ client:load 策略

最激进的水合策略 — 页面加载完成后立即加载组件 JS 并水合。适合首屏可见且需要立即交互的组件。

// SSR 输出示例 (简化)
<astro-island
  uid="abc123"
  component-url="/_astro/SearchForm.js"
  component-export="default"
  renderer-url="/_astro/client.js"
  props="{"query":""}"
  ssr
  client="load"
>
  <!-- 服务端预渲染的 HTML -->
  <form><input placeholder="搜索..." /></form>
  
  <!-- 客户端水合后替换此占位 -->
  <script>initIsland(document.currentScript.parentElement)</script>
</astro-island>

注意:client:load 会在页面加载时阻塞,慎用于大型组件

💤 client:idle 策略

使用 requestIdleCallback 在浏览器空闲时加载组件。平衡了交互性和性能。

// astro-island 内部实现 (简化)
function hydrateIdle(el) {
  const observer = new IntersectionObserver((entries) => {
    // 一旦可见,立即加载(不等待空闲)
    if (entries[0].isIntersecting) {
      observer.disconnect();
      loadAndHydrate(el);
    }
  });
  observer.observe(el);
  
  // 同时注册 idle 回调
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => loadAndHydrate(el));
  } else {
    setTimeout(() => loadAndHydrate(el), 200);
  }
}

适合:侧边栏、非关键的小型交互组件

👁️ client:visible 策略

使用 IntersectionObserver 在组件进入视口时才加载。最适合首屏以下的组件。

// IntersectionObserver 实现 (简化)
function hydrateVisible(el) {
  const observer = new IntersectionObserver(
    (entries, observer) => {
      for (const entry of entries) {
        if (entry.isIntersecting) {
          observer.disconnect();
          loadAndHydrate(entry.target);
        }
      }
    },
    { rootMargin: '200px 0px' }  // 提前 200px 触发
  );
  observer.observe(el);
}

优点

  • 零首屏 JS 开销
  • 自动懒加载
  • rootMargin 预加载

适合

  • 页面底部评论
  • 大型图表/视频
  • 折叠区内容

🎯 client:only 策略

client:only 跳过 SSR,组件只在客户端渲染。适合依赖浏览器 API(window/document)的组件。

<!-- client:only 需要指定框架 -->
<InteractiveMap client:only="react" data-lat="39.9" data-lng="116.4" />

<!-- SSR 输出: 只有占位符,无预渲染 HTML -->
<astro-island
  uid="xyz789"
  component-url="/_astro/InteractiveMap.js"
  component-export="default"
  renderer-url="/_astro/react.js"
  props="{"data-lat":39.9,"data-lng":116.4}"
  client="only"
>
  <!-- 空!没有 SSR HTML -->
</astro-island>

权衡:无 SSR 意味着更长的 FMP (First Meaningful Paint),但避免了 SSR 不兼容问题

📊 Hydration 策略对比

策略JS 时机FMPTTIJS 大小
client:load立即⭐⭐⭐⭐⭐全量
client:idle空闲时⭐⭐⭐⭐⭐⭐全量
client:visible可见时⭐⭐⭐⭐⭐⭐延迟加载
client:only立即⭐⭐⭐⭐全量
(无指令)⭐⭐⭐⭐⭐⭐

推荐策略:默认不加水合指令,只在真正需要交互时使用 client:visibleclient:idle

🏝️ astro-island 组件

<astro-island> 是 Islands 架构的核心 HTML 元素,作为 Island 组件的容器,负责水合调度。

<astro-island
  uid="unique-id"           // 唯一标识符
  component-url="/_astro/Counter.js"  // 组件 JS chunk
  component-export="default"          // 导出名
  renderer-url="/_astro/react.js"     // 框架运行时
  props="{...}"             // 序列化的 props (JSON)
  ssr                       // 是否有 SSR 内容
  client="load"             // 水合策略
>
  <!-- SSR 预渲染的 HTML / 占位符 -->
  <template data-astro-cid-xxx></template>
</astro-island>

⏳ Island 预加载机制

// astro-island 内部 (简化版)

// 根据 client 指令决定加载时机
function initIsland(el) {
  const strategy = el.getAttribute('client');
  
  switch (strategy) {
    case 'load':
      // 立即加载
      loadAndHydrate(el);
      break;
      
    case 'idle':
      // 空闲时加载,但进入视口后立即加载
      requestIdleCallback(() => loadAndHydrate(el));
      observeVisibility(el, () => loadAndHydrate(el));
      break;
      
    case 'visible':
      // 可见时加载
      observeVisibility(el, () => loadAndHydrate(el));
      break;
      
    case 'only':
      // 立即加载,但跳过 SSR HTML
      loadAndHydrate(el, { replace: true });
      break;
  }
}

💧 Island 水合流程

┌──────────────────────────────────────────────────────────┐
│                 Island 水合流程                            │
├──────────────────────────────────────────────────────────┤
│ 1. 浏览器解析 HTML                                       │
│    └→ 看到静态 HTML + <astro-island> 容器              │
│                                                           │
│ 2. <script> 执行                                         │
│    └→ initIsland(el) 调用                                │
│    └→ 根据 client 策略决定时机                            │
│                                                           │
│ 3. 动态 import 组件 JS                                   │
│    └→ import(componentUrl)                               │
│    └→ import(rendererUrl)  // React/Vue 运行时           │
│                                                           │
│ 4. 框架水合                                              │
│    └→ React: hydrateRoot(container, element)             │
│    └→ Vue: createApp().mount()                           │
│    └→ Svelte: new Component({ target })                  │
│                                                           │
│ 5. 移除 <astro-island> 包装器 (可选)                   │
└──────────────────────────────────────────────────────────┘

🔄 Island 序列化与 SSR

// 服务端渲染 Island 组件 (简化)

async function renderIsland(component, props, clientDirective) {
  // 1. 如果不是 client:only,先做 SSR
  let html = '';
  if (clientDirective !== 'only') {
    html = await renderFrameworkComponent(component, props);
  }
  
  // 2. 序列化 props 为 JSON (放在 data 属性中)
  const serializedProps = JSON.stringify(props);
  
  // 3. 生成 <astro-island> HTML
  return `<astro-island
    uid="${generateUID()}"
    component-url="${component.chunkUrl}"
    component-export="${component.exportName}"
    renderer-url="${component.rendererUrl}"
    props="${escapeHtml(serializedProps)}"
    ssr
    client="${clientDirective}"
  >${html}</astro-island>`;
}

📦 Props 传递机制

Island 组件的 Props 通过 JSON.stringify 序列化后嵌入 HTML 属性。客户端水合时反序列化并传入组件。

// 服务端: 序列化 Props
const props = {
  title: '我的计数器',
  initialValue: 0,
  items: [1, 2, 3],
  // ⚠️ 函数和复杂对象不能序列化!
};

// 嵌入 HTML
<astro-island props="${JSON.stringify(props)}" ...>

// 客户端: 反序列化
const rawProps = JSON.parse(el.getAttribute('props'));

// 传递给框架
hydrate(ReactComponent, el, rawProps);

限制:Props 必须是 JSON 可序列化的(字符串、数字、布尔、数组、简单对象)

🏷️ 组件元数据

// 编译 .astro 文件后生成的元数据
interface CompileMetadata {
  // 需要水合的组件列表
  hydratedComponents: HydratedComponent[];
  
  // 仅服务端组件
  serverComponents: string[];
  
  // 页面脚本
  scripts: ScriptMetadata[];
  
  // CSS 样式
  css: CSSMetadata[];
  
  // 是否包含 <head>
  containsHead: boolean;
  
  // 样式传播模式
  propagation: 'self' | 'none';
  
  // clientOnly 组件
  clientOnlyComponents: HydratedComponent[];
}

interface HydratedComponent {
  resolvedPath: string;    // 组件文件路径
  displayName: string;     // 组件名
  hydrate: string;         // 水合函数名
  componentUrl: string;    // 构建后的 JS chunk URL
  rendererUrl: string;     // 框架运行时 URL
}

🎨 脚本与样式处理

脚本处理

  • <script> 自动提取为虚拟模块
  • 支持 inline 和 external 两种
  • SSR 环境返回空模块
  • 开发模式支持 HMR

样式处理

  • CSS 自动提取 + scoped
  • 虚拟模块: ?astro&type=style&index=0
  • 支持 CSS 预处理 (Sass/Less)
  • 生产构建可内联
// 样式虚拟模块 — 支持 Vite 的 cssScopeTo 做树摇
return {
  code: result.code,
  meta: result.isGlobal ? undefined : {
    vite: {
      cssScopeTo: [filename, 'default'],
    },
  },
};

🎯 CSS Scoped 处理

Astro 组件中的 <style> 默认是 scoped 的。编译时通过添加 data-astro-cid-xxx 属性实现样式隔离。

<!-- 源码 -->
<style>
  h1 { color: orange; }
</style>
<h1>Hello Astro</h1>

<!-- 编译后 -->
<style>
  h1[data-astro-cid-a1b2c3] { color: orange; }
</style>
<h1 data-astro-cid-a1b2c3>Hello Astro</h1>

全局样式:使用 <style is:global> 声明不 scoped 的样式

🔥 CSS HMR 热更新

// astro:build:css-hmr 插件
{
  name: 'astro:build:css-hmr',
  enforce: 'pre',
  applyToEnvironment(env) {
    return env.name === 'client';
  },
  transform: {
    filter: {
      id: { include: [/(?:\\?|&)astro(?:&|=|$)/] },
    },
    async handler(_source, id) {
      const parsedId = parseAstroRequest(id);
      const compileMetadata = astroFileToCompileMetadata
        .get(parsedId.filename);
      
      if (compileMetadata && parsedId.query.type === 'style') {
        const result = compileMetadata.css[parsedId.query.index];
        // 注册 CSS 依赖,文件变化时触发 HMR
        result.dependencies?.forEach((dep) => {
          this.addWatchFile(dep);
        });
      }
    },
  },
}

🛡️ 客户端环境检测

Astro 在构建时通过 applyToEnvironmentenvironment.name 区分客户端/服务端,确保组件不会在错误的环境中执行。

// 客户端环境名常量
const ASTRO_VITE_ENVIRONMENT_NAMES = {
  client: 'client',
  server: 'server',
};

// 在 astro:build 插件中检测环境
applyToEnvironment(environment) {
  return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client;
}

// 客户端环境 → Astro 组件返回空模块
if (this.environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client) {
  return {
    code: `export default import.meta.env.DEV
      ? () => { throw new Error(
        'Astro components cannot be used in the browser.') }
      : {};`,
  };
}

🖥️ 开发服务器

Astro 开发服务器基于 Vite Dev Server,增加了 Astro 文件编译、HMR、和路由处理。

// 开发服务器初始化 (简化)
async function startDevServer(settings: AstroSettings) {
  const viteConfig = createViteConfig(settings, 'dev');
  
  const server = await vite.createServer({
    ...viteConfig,
    server: {
      host: settings.config.server?.host,
      port: settings.config.server?.port ?? 4321,
    },
  });
  
  await server.listen();
  return server;
}

// 文件变更监听
server.watcher.on('unlink', (filename) => {
  // 清理编译缓存
  astroFileToCompileMetadata.delete(filename);
});

🔄 HMR 热模块替换

// packages/astro/src/vite-plugin-astro/hmr.ts (简化)

async function handleHotUpdate(ctx, { logger, compile, astroFileToCompileMetadata }) {
  const { file, read, server } = ctx;
  
  // 如果变更的是 .astro 文件
  if (file.endsWith('.astro')) {
    // 重新编译
    const source = await read();
    await compile(source, file);
    
    // 通知客户端更新
    const timestamp = Date.now();
    server.ws.send({
      type: 'update',
      updates: [{
        type: 'js-update',
        path: file,
        acceptedPath: file,
        timestamp,
      }],
    });
    
    logger.info(`[HMR] ${file} updated`);
  }
}

📚 内容集合系统

Content Collections 是 Astro 的内容管理方案,提供类型安全的 Markdown/MDX 内容查询。

// src/content/config.ts — 定义集合 schema
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    date: z.date(),
    tags: z.array(z.string()),
    draft: z.boolean().default(false),
  }),
});

export const collections = { blog };

// 在 .astro 中查询
import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => !data.draft);

🎬 视图过渡 API

Astro 内置了基于浏览器 View Transitions API 的页面过渡效果,无需额外 JS 框架。

// src/layouts/Base.astro
---
import { ViewTransitions } from 'astro:transitions';
---
<html>
  <head>
    <title>{title}</title>
    <ViewTransitions />  {/* 一行启用! */}
  </head>
  <body>
    <slot />
  </body>
</html>

// 自定义过渡
<ViewTransitions
  transition:animate="morph"
  transition:fallback="fade"
/>

特性:自动处理前进/后退动画、保持滚动位置、支持生命周期钩子

🚀 性能优化策略

构建时优化

  • Tree Shaking: 框架运行时按需加载
  • Code Splitting: 每个 Island 独立 chunk
  • CSS 内联: 可选 inlineStylesheets
  • 图片优化: 内置 <Image> 组件

运行时优化

  • 延迟水合: client:idle/visible
  • 预加载: <link rel="modulepreload">
  • Shared Runtime: 同框架共享运行时
  • Prefetch: 内置页面预取

✨ 零 JS 默认原则

Astro 最强大的特性:不添加任何 client 指令的组件完全零 JS。这是与传统框架的根本区别。

// 这个页面完全零 JS!
---
const posts = await getCollection('blog');
---
<html>
  <body>
    <h1>博客列表</h1>
    {posts.map(post => (
      <article>
        <h2>{post.data.title}</h2>
        <p>{post.data.date}</p>
      </article>
    ))}
    {/* 只有 Counter 需要 JS */}
    <Counter client:visible />
  </body>
</html>

// 输出: 纯 HTML + Counter.js (仅一个 chunk)

⚖️ 部分水合 vs 完整水合

┌─────────────────────────────────────────────────────┐
│  完整水合 (传统 SSR 框架)                             │
│  ┌─────┬─────┬─────┬─────┬─────┬─────┐              │
│  │ H1  │ Nav │Post │Form │Side │Foot │ ← 全部水合    │
│  │ JS  │ JS  │ JS  │ JS  │ JS  │ JS  │              │
│  └─────┴─────┴─────┴─────┴─────┴─────┘              │
│  总 JS: ~150KB                                       │
├─────────────────────────────────────────────────────┤
│  部分水合 (Astro Islands)                            │
│  ┌─────┬─────┬─────┬─────┬─────┬─────┐              │
│  │ H1  │ Nav │Post │Form │Side │Foot │              │
│  │ 0KB │ 0KB │ 0KB │ 12KB│ 0KB │ 0KB │ ← 只有 Form │
│  └─────┴─────┴─────┴─────┴─────┴─────┘              │
│  总 JS: ~12KB (减少 92%)                             │
└─────────────────────────────────────────────────────┘

⚔️ 与其他框架对比

特性AstroNext.jsNuxtRemix
默认 JS
水合方式Islands整页/部分整页整页
多框架❌ (React)❌ (Vue)❌ (React)
SSG
SSR
适用场景内容站通用通用Web 应用

🌍 集成生态

UI 框架

  • @astrojs/react — React 18/19
  • @astrojs/vue — Vue 3
  • @astrojs/svelte — Svelte 4/5
  • @astrojs/solid — SolidJS
  • @astrojs/preact — Preact
  • @astrojs/lit — Lit

部署适配器

  • @astrojs/node — Node.js
  • @astrojs/vercel — Vercel
  • @astrojs/netlify — Netlify
  • @astrojs/cloudflare — CF Workers
  • @astrojs/deno — Deno

其他

  • Tailwind CSS / MDX / sitemap
  • Partytown / Analytics

✅ 最佳实践

推荐做法

  • 默认不加 client 指令
  • 优先使用 client:visible
  • Content Collections 管理内容
  • <Image> 优化图片
  • <ViewTransitions> 做过渡

性能建议

  • 减少 Island 数量
  • 大型 Island 拆分为多个小 Island
  • 避免在 Island 间传递复杂 props
  • 利用 hybrid 模式混合渲染
  • 用 prefetch 提升导航体验

⚠️ 常见陷阱

1. 在 Astro 组件中添加事件监听器

// ❌ Astro 组件不支持客户端事件
<button onClick={() => count++}>Click</button>

// ✅ 使用 client 指令
<Button client:idle />  // Button 是 React/Vue 组件

2. 传递不可序列化的 Props

// ❌ 函数不能通过 props 传递给 Island
<Chart client:load onData={handleData} />

// ✅ 使用事件或自定义属性
<Chart client:load data={jsonArray} />

🎯 总结

架构核心

  • Islands — 部分水合,零 JS 默认
  • 多框架 — React/Vue/Svelte 混用
  • Vite — 编译和构建基础
  • Content Collections — 类型安全内容

设计亮点

  • 虚拟模块拆分 CSS/Script
  • 编译时静态分析
  • 4 种水合策略灵活选择
  • SSG/SSR/Hybrid 自由切换
  • View Transitions 内置

Astro Islands 架构重新定义了内容驱动网站的开发方式:用最少的 JS 实现最大的交互性。源码体现了"编译时做尽可能多的工作,运行时只做必要的"这一哲学。

🚀 感谢阅读

Astro Islands Architecture Deep Dive

源码地址
https://github.com/withastro/astro

访问链接: https://atcfu.com/ai-articles/astro-islands/