Astro Islands 架构深入:选择性注水的革命性方案
前言
传统的 SPA 框架有一个根本性的问题:即使页面上 90% 的内容是静态的,也要为 100% 的内容加载和执行 JavaScript。这意味着一篇博客文章的「点赞按钮」需要和整个页面的渲染框架一起下载、解析、执行。
Astro 的 Islands 架构(岛屿架构)彻底翻转了这个范式——默认零 JavaScript,只为需要交互的组件注入最少量的 JS。这不是渐进增强的老调重弹,而是一套基于「选择性注水(Partial Hydration)」的现代化解决方案。
本文将深入 Islands 架构的原理、Astro 的 client:* 指令体系、多框架集成机制,以及它带来的真实性能优势。
一、Islands 架构的核心思想
1.1 从 SPA 到 MPA 再到 Islands
传统 MPA(Multi-Page Application):┌─────────────────────────────┐│ 服务端渲染 HTML │ ← 每个页面都是完整 HTML│ (零或极少 JavaScript) │ ← 交互能力有限└─────────────────────────────┘
SPA(Single-Page Application):┌─────────────────────────────┐│ JavaScript 接管一切 │ ← 客户端渲染│ (加载大量 JS bundle) │ ← 首屏慢,FCP/LCP 差└─────────────────────────────┘
SSR + Hydration(Next.js/Nuxt 模式):┌─────────────────────────────┐│ 服务端渲染 HTML + 全页注水 │ ← 首屏快,但 TTI 慢│ (仍然加载全量 JS) │ ← 注水过程阻塞交互└─────────────────────────────┘
Islands Architecture(Astro 模式):┌─────────────────────────────┐│ ┌──────┐ 静态 ┌────────┐ ││ │Island│ HTML │ Island │ │ ← 静态部分零 JS│ │ (Vue)│ │(Svelte)│ │ ← 每个岛屿独立注水│ └──────┘ └────────┘ │ ← 可以混合多框架│ 静态 HTML 内容 ││ ┌────────────────────┐ ││ │ Island (Vanilla) │ ││ └────────────────────┘ │└─────────────────────────────┘1.2 什么是「岛屿」
在 Islands 架构中,页面被分为两种区域:
- 静态海洋(Static Sea):纯 HTML/CSS,不需要任何 JavaScript。这是页面的主体——导航栏、文章内容、页脚等。
- 交互岛屿(Interactive Islands):需要 JavaScript 来实现交互的组件。如搜索框、评论区、轮播图、点赞按钮等。
关键原则:每个岛屿独立加载、独立注水、互不影响。
1.3 选择性注水 vs 全页注水
// 传统全页注水(Next.js / Nuxt 模式)// 整个页面的组件树都需要在客户端重新激活
// 服务端输出:// <html>// <body>// <header>...</header> ← 需要 JS 注水// <nav>...</nav> ← 需要 JS 注水// <main>// <article>5000字文章</article> ← 需要 JS 注水(虽然是纯静态的!)// <LikeButton /> ← 需要 JS 注水// </main>// <footer>...</footer> ← 需要 JS 注水// </body>// </html>
// 客户端需要下载整个应用的 JS bundle,然后:// hydrate(<App />, document.getElementById('root'))// ↑ 遍历整个组件树,绑定事件、恢复状态
// Astro 选择性注水:// 只有 LikeButton 需要注水,其他都是纯 HTML
// 服务端输出:// <html>// <body>// <header>...</header> ← 纯 HTML,零 JS// <nav>...</nav> ← 纯 HTML,零 JS// <main>// <article>5000字文章</article> ← 纯 HTML,零 JS// <astro-island> ← 独立注水// <LikeButton />// </astro-island>// </main>// <footer>...</footer> ← 纯 HTML,零 JS// </body>// </html>二、Astro 的组件模型
2.1 .astro 组件
Astro 组件(.astro 文件)是服务端组件,它们在构建时执行,输出纯 HTML:
---// 这里是服务端代码(frontmatter),在构建时执行interface Props { title: string; excerpt: string; date: string; slug: string; tags: string[];}
const { title, excerpt, date, slug, tags } = Astro.props;
// 可以调用数据库、API 等(构建时执行)const formattedDate = new Date(date).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric',});---
<!-- 这里是模板,编译为纯 HTML --><article class="card"> <a href={`/posts/${slug}`}> <h2>{title}</h2> <time datetime={date}>{formattedDate}</time> <p>{excerpt}</p> <div class="tags"> {tags.map(tag => ( <span class="tag">{tag}</span> ))} </div> </a></article>
<style> /* Scoped CSS - 自动添加唯一作用域 */ .card { border: 1px solid #e2e8f0; border-radius: 0.5rem; padding: 1.5rem; transition: box-shadow 0.2s; } .card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .tag { display: inline-block; padding: 0.25rem 0.5rem; background: #edf2f7; border-radius: 0.25rem; font-size: 0.875rem; }</style>关键点:.astro 组件永远不会发送 JavaScript 到客户端。 它们是纯粹的模板引擎。
2.2 框架组件作为岛屿
当你需要交互时,可以使用任何前端框架的组件,并通过 client:* 指令将其标记为岛屿:
---import Layout from '@/layouts/Layout.astro';import ArticleContent from '@/components/ArticleContent.astro';// 框架组件import LikeButton from '@/components/LikeButton.vue';import CommentSection from '@/components/CommentSection.svelte';import ShareMenu from '@/components/ShareMenu.vue';import TableOfContents from '@/components/TableOfContents.vue';
const { slug } = Astro.params;const post = await getPost(slug);---
<Layout title={post.title}> <article> <!-- 静态内容:零 JS --> <h1>{post.title}</h1> <time>{post.date}</time> <ArticleContent content={post.content} />
<!-- 交互岛屿:各自独立加载 JS --> <LikeButton client:visible postId={post.id} /> <ShareMenu client:idle url={Astro.url.href} title={post.title} /> <TableOfContents client:media="(min-width: 1024px)" headings={post.headings} /> <CommentSection client:visible postId={post.id} /> </article></Layout>三、client:* 指令深入
3.1 所有指令及其行为
Astro 提供了 5 种 client:* 指令,控制岛屿何时被注水:
<!-- 1. client:load - 页面加载后立即注水 --><!-- 适用于:首屏可见且需要立即交互的组件 --><SearchBar client:load />
<!-- 2. client:idle - 浏览器空闲时注水(requestIdleCallback) --><!-- 适用于:不紧急但最终需要的组件 --><ShareMenu client:idle />
<!-- 3. client:visible - 组件进入视口时注水(IntersectionObserver) --><!-- 适用于:折叠下方的组件,用户滚动到才加载 --><CommentSection client:visible />
<!-- 4. client:media - 满足媒体查询条件时注水 --><!-- 适用于:响应式场景,如只在桌面端加载的侧边栏 --><DesktopSidebar client:media="(min-width: 1024px)" />
<!-- 5. client:only - 跳过 SSR,只在客户端渲染 --><!-- 适用于:依赖浏览器 API 的组件(如 canvas、WebGL) --><ThreeJSScene client:only="vue" />3.2 指令的内部实现原理
Astro 的 client:* 指令在编译时被转换为特定的加载脚本。以下是其内部机制的简化版本:
// Astro 内部:岛屿注水的运行时代码(简化)
// astro-island 自定义元素class AstroIsland extends HTMLElement { connectedCallback() { // 读取指令类型 const directive = this.getAttribute('client'); // 读取组件信息 const componentUrl = this.getAttribute('component-url'); const componentExport = this.getAttribute('component-export') || 'default'; const rendererUrl = this.getAttribute('renderer-url'); const props = JSON.parse(this.getAttribute('props') || '{}');
// 根据指令类型决定注水时机 switch (directive) { case 'load': this.hydrate(componentUrl, rendererUrl, componentExport, props); break;
case 'idle': this.hydrateOnIdle(componentUrl, rendererUrl, componentExport, props); break;
case 'visible': this.hydrateOnVisible(componentUrl, rendererUrl, componentExport, props); break;
case 'media': this.hydrateOnMedia( this.getAttribute('client-value'), componentUrl, rendererUrl, componentExport, props ); break; } }
async hydrate(componentUrl, rendererUrl, exportName, props) { // 1. 动态加载组件代码和框架渲染器 const [componentModule, renderer] = await Promise.all([ import(/* @vite-ignore */ componentUrl), import(/* @vite-ignore */ rendererUrl), ]);
const Component = componentModule[exportName];
// 2. 使用对应框架的渲染器进行注水 // renderer 是框架特定的(如 @astrojs/vue 提供的渲染器) await renderer.default(this, Component, props, this.innerHTML); }
hydrateOnIdle(componentUrl, rendererUrl, exportName, props) { // 使用 requestIdleCallback 延迟注水 const cb = () => this.hydrate(componentUrl, rendererUrl, exportName, props);
if ('requestIdleCallback' in window) { requestIdleCallback(cb); } else { // fallback: 200ms 后执行 setTimeout(cb, 200); } }
hydrateOnVisible(componentUrl, rendererUrl, exportName, props) { // 使用 IntersectionObserver 在组件可见时注水 const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { observer.disconnect(); this.hydrate(componentUrl, rendererUrl, exportName, props); break; } } }, { rootMargin: '200px' } // 提前 200px 开始加载 ); observer.observe(this); }
hydrateOnMedia(query, componentUrl, rendererUrl, exportName, props) { // 使用 matchMedia 在媒体查询匹配时注水 const mql = matchMedia(query);
if (mql.matches) { this.hydrate(componentUrl, rendererUrl, exportName, props); } else { const handler = (e) => { if (e.matches) { mql.removeEventListener('change', handler); this.hydrate(componentUrl, rendererUrl, exportName, props); } }; mql.addEventListener('change', handler); } }}
customElements.define('astro-island', AstroIsland);3.3 编译产物分析
当 Astro 编译一个包含岛屿的页面时,产物结构如下:
<!-- 构建后的 HTML(简化) --><astro-island uid="abc123" component-url="/_astro/LikeButton.vue_vue_type_script_setup_true_lang.BxK2n.js" component-export="default" renderer-url="/_astro/client.Cm9k1.js" props='{"postId":[0,42]}' client="visible"> <!-- 服务端预渲染的 HTML(用户立即可见) --> <button class="like-btn"> <svg>...</svg> <span>42</span> </button></astro-island>注意几个关键点:
- 组件代码按需加载:只有当注水条件满足时,才会下载组件 JS
- 服务端预渲染:岛屿内部有完整的 HTML,用户无需等待 JS 就能看到内容
- Props 序列化:组件 props 被序列化为 JSON,注水时传入
3.4 选择合适的指令
// 决策指南
// ✅ client:load - 首屏关键交互// 场景:搜索框、导航菜单、需要立即可用的表单// 代价:阻塞页面加载,慎用
// ✅ client:idle - 次优先级交互// 场景:分享按钮、主题切换、非紧急工具栏// 原理:requestIdleCallback,在浏览器空闲时执行// 通常在页面加载后 50-200ms 内触发
// ✅ client:visible - 折叠下方内容(最常用)// 场景:评论区、相关文章、无限滚动、图表// 原理:IntersectionObserver,滚动到附近才加载// 设置了 200px 的 rootMargin,提前预加载
// ✅ client:media - 响应式条件// 场景:桌面端侧边栏、移动端底部导航// 原理:matchMedia,只在条件满足时注水
// ✅ client:only - 纯客户端组件// 场景:Canvas/WebGL、使用 window/document 的组件// 注意:不会 SSR,SEO 不友好,仅在必要时使用四、多框架集成
4.1 安装多框架支持
Astro 的一大杀手级特性是同一个项目中可以使用多个框架:
# 安装集成npx astro add vuenpx astro add sveltenpx astro add solid-jsnpx astro add litimport { defineConfig } from 'astro/config';import vue from '@astrojs/vue';import svelte from '@astrojs/svelte';import solidJs from '@astrojs/solid-js';import lit from '@astrojs/lit';
export default defineConfig({ integrations: [ vue(), svelte(), solidJs(), lit(), ],});4.2 混合使用不同框架
---// 根据组件特点选择最合适的框架
// Vue - 复杂的表单和状态管理import DataForm from '@/components/vue/DataForm.vue';
// Svelte - 轻量级动画组件import AnimatedChart from '@/components/svelte/AnimatedChart.svelte';
// Solid - 高频更新的实时数据import LiveTicker from '@/components/solid/LiveTicker.tsx';
// Lit - Web Component,可复用于任何框架import CustomTooltip from '@/components/lit/CustomTooltip.ts';
const dashboardData = await fetchDashboardData();---
<Layout> <h1>Dashboard</h1>
<!-- 每个组件使用最适合它的框架 --> <section class="form-section"> <DataForm client:load initialData={dashboardData.form} /> </section>
<section class="charts"> <AnimatedChart client:visible data={dashboardData.chart} /> </section>
<section class="live-data"> <LiveTicker client:load endpoint="/api/ticker" /> </section>
<!-- Lit Web Component 可以在任何地方使用 --> <CustomTooltip client:idle text="这是一个提示"> <button>Hover me</button> </CustomTooltip></Layout>4.3 框架渲染器的工作原理
每个框架集成都提供了一个「渲染器」,负责服务端渲染和客户端注水:
// @astrojs/vue 的渲染器(简化)// server.js - 服务端渲染import { createSSRApp, h } from 'vue';import { renderToString } from 'vue/server-renderer';
export async function renderToStaticMarkup(Component, props, slotted) { const app = createSSRApp({ render() { return h(Component, props, { default: () => slotted?.default ? h('astro-slot', { innerHTML: slotted.default }) : undefined, }); }, });
const html = await renderToString(app); return { html };}
// client.js - 客户端注水import { createSSRApp, h } from 'vue';
export default async function clientEntrypoint( element, // astro-island DOM 元素 Component, // Vue 组件 props, // 序列化的 props slotHTML // 插槽内容的 HTML) { // 创建 Vue 应用并注水到已有的 DOM 上 const app = createSSRApp({ render() { return h(Component, props, { default: slotHTML ? () => h('astro-slot', { innerHTML: slotHTML }) : undefined, }); }, });
// 注水(hydrate)而不是全新渲染(mount) app.mount(element, true); // true = hydrate mode}4.4 跨框架通信
岛屿之间默认是隔离的,但可以通过多种方式通信:
// 方案一:Nano Stores(Astro 推荐的跨框架状态共享方案)// npm install nanostores @nanostores/vue @nanostores/solid
// stores/cart.ts - 框架无关的 Storeimport { atom, computed } from 'nanostores';
export interface CartItem { id: string; name: string; price: number; quantity: number;}
export const $cartItems = atom<CartItem[]>([]);
export const $cartTotal = computed($cartItems, (items) => items.reduce((sum, item) => sum + item.price * item.quantity, 0));
export function addToCart(item: Omit<CartItem, 'quantity'>) { const items = $cartItems.get(); const existing = items.find(i => i.id === item.id); if (existing) { $cartItems.set( items.map(i => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i) ); } else { $cartItems.set([...items, { ...item, quantity: 1 }]); }}
export function removeFromCart(id: string) { $cartItems.set($cartItems.get().filter(i => i.id !== id));}<!-- Vue 组件中使用 --><script setup>import { useStore } from '@nanostores/vue';import { $cartItems, $cartTotal, addToCart } from '@/stores/cart';
const cartItems = useStore($cartItems);const total = useStore($cartTotal);</script>
<template> <div class="cart"> <div v-for="item in cartItems" :key="item.id"> {{ item.name }} x{{ item.quantity }} - ¥{{ item.price * item.quantity }} </div> <p>总计: ¥{{ total }}</p> </div></template>// Solid 组件中使用同一个 Storeimport { useStore } from '@nanostores/solid';import { $cartTotal, addToCart } from '@/stores/cart';
function AddToCartButton(props: { product: { id: string; name: string; price: number } }) { const total = useStore($cartTotal);
return ( <button onClick={() => addToCart(props.product)}> 加入购物车 (当前总计: ¥{total()}) </button> );}// 方案二:Custom Events(轻量级通信)// 适合简单的事件通知场景
// 发送事件(任意框架组件内)function notifyCartUpdate(item: CartItem) { const event = new CustomEvent('cart:update', { detail: item, bubbles: true, }); document.dispatchEvent(event);}
// 监听事件(任意框架组件内)// Vueimport { onMounted, onUnmounted } from 'vue';
onMounted(() => { const handler = (e: CustomEvent) => { console.log('Cart updated:', e.detail); }; document.addEventListener('cart:update', handler); onUnmounted(() => document.removeEventListener('cart:update', handler));});五、性能优势量化分析
5.1 真实场景对比
以一个典型的技术博客页面为例:
页面组成:- 导航栏(静态)- 文章标题和元信息(静态)- 文章正文 5000 字(静态)- 代码高亮块 x 10(静态,构建时高亮)- 目录导航(交互:滚动高亮)- 点赞按钮(交互:API 调用)- 评论区(交互:表单+列表)- 相关文章推荐(静态)- 页脚(静态)传统 SSR (Nuxt/Next) 的 JS 产物:├── framework-runtime.js ~45KB (gzip) ← 框架运行时├── app.js ~30KB (gzip) ← 应用代码├── vendor.js ~25KB (gzip) ← 第三方依赖├── page-blog-slug.js ~15KB (gzip) ← 页面组件└── Total: ~115KB (gzip) ← 全页注水
Astro Islands 的 JS 产物:├── toc-widget.js ~3KB (gzip) ← 目录组件├── like-button.js ~2KB (gzip) ← 点赞组件├── comment-section.js ~8KB (gzip) ← 评论组件├── vue-runtime (shared) ~15KB (gzip) ← 框架运行时(仅注水组件需要)└── Total: ~28KB (gzip) ← 只加载需要的
JS 减少: 75%+5.2 Core Web Vitals 影响
指标对比(典型博客页面):
传统 SSR Astro Islands 提升FCP (First 0.8s 0.6s 25%Contentful Paint)
LCP (Largest 1.2s 0.8s 33%Contentful Paint)
TBT (Total 350ms 50ms 86%Blocking Time)
TTI (Time to 2.5s 0.9s 64%Interactive)
CLS (Cumulative 0.05 0.02 60%Layout Shift)TBT 的巨大差异是关键:传统 SSR 需要解析和执行大量 JS 来完成注水,这期间主线程被阻塞。Astro 只需注水少量岛屿组件,主线程几乎不被阻塞。
5.3 性能优化技巧
---// 技巧 1: 预加载关键岛屿的 JS// 在 <head> 中预加载即将需要的组件---<head> <!-- 预加载 client:load 组件的 JS --> <link rel="modulepreload" href="/_astro/SearchBar.BxK2n.js" /></head>
<!-- 技巧 2: 使用 transition:persist 保持岛屿状态 --><!-- 在 View Transitions 中,岛屿状态不会丢失 --><nav transition:persist> <ThemeToggle client:load transition:persist /></nav>
<!-- 技巧 3: 服务端数据预取,避免客户端瀑布 -->---// 在服务端获取数据,作为 props 传入const comments = await fetchComments(post.id);---<CommentSection client:visible initialComments={comments} postId={post.id} /><!-- 评论区注水后已有数据,无需额外请求 -->---// 技巧 4: 合理拆分岛屿粒度
// ❌ 不好:整个侧边栏作为一个大岛屿// <Sidebar client:load /> ← 加载了很多不需要交互的内容的 JS
// ✅ 好:只把需要交互的部分作为岛屿---<aside> <!-- 静态内容 --> <h3>作者信息</h3> <p>张三,前端工程师</p>
<!-- 只有搜索框需要 JS --> <SearchWidget client:idle />
<!-- 静态分类列表 --> <h3>分类</h3> <ul> {categories.map(cat => <li><a href={cat.url}>{cat.name}</a></li>)} </ul>
<!-- 只有订阅表单需要 JS --> <NewsletterForm client:visible /></aside>六、实战:构建一个完整的博客
6.1 项目结构
my-blog/├── astro.config.mjs├── src/│ ├── components/│ │ ├── astro/ # 静态 Astro 组件│ │ │ ├── Header.astro│ │ │ ├── Footer.astro│ │ │ ├── PostCard.astro│ │ │ └── Prose.astro│ │ ├── vue/ # Vue 交互组件│ │ │ ├── SearchBar.vue│ │ │ ├── ThemeToggle.vue│ │ │ └── CommentForm.vue│ │ └── svelte/ # Svelte 轻量组件│ │ ├── LikeButton.svelte│ │ └── ReadingProgress.svelte│ ├── content/│ │ └── posts/ # Markdown 文章│ ├── layouts/│ │ └── PostLayout.astro│ ├── pages/│ │ ├── index.astro│ │ └── posts/│ │ └── [...slug].astro│ └── stores/│ └── theme.ts # Nano Stores├── public/└── package.json6.2 文章页面实现
---import { getCollection, type CollectionEntry } from 'astro:content';import PostLayout from '@/layouts/PostLayout.astro';import Prose from '@/components/astro/Prose.astro';
// 交互岛屿import ReadingProgress from '@/components/svelte/ReadingProgress.svelte';import LikeButton from '@/components/svelte/LikeButton.svelte';import SearchBar from '@/components/vue/SearchBar.vue';import CommentForm from '@/components/vue/CommentForm.vue';
export async function getStaticPaths() { const posts = await getCollection('posts'); return posts.map((post) => ({ params: { slug: post.slug }, props: { post }, }));}
type Props = { post: CollectionEntry<'posts'> };const { post } = Astro.props;const { Content, headings } = await post.render();
// 服务端获取点赞数和评论const [likeCount, comments] = await Promise.all([ fetch(`${import.meta.env.API_URL}/likes/${post.slug}`).then(r => r.json()), fetch(`${import.meta.env.API_URL}/comments/${post.slug}`).then(r => r.json()),]);---
<PostLayout title={post.data.title}> <!-- 阅读进度条 - 页面加载即需要 --> <ReadingProgress client:load />
<!-- 搜索 - 空闲时加载 --> <SearchBar client:idle slot="header-actions" />
<article> <header> <h1>{post.data.title}</h1> <time datetime={post.data.published.toISOString()}> {post.data.published.toLocaleDateString('zh-CN')} </time> <div class="tags"> {post.data.tags.map(tag => ( <a href={`/tags/${tag}`} class="tag">{tag}</a> ))} </div> </header>
<!-- 文章内容 - 纯静态 HTML,零 JS --> <Prose> <Content /> </Prose>
<!-- 点赞 - 滚动到可见时加载 --> <LikeButton client:visible slug={post.slug} initialCount={likeCount.count} />
<!-- 评论 - 滚动到可见时加载 --> <CommentForm client:visible slug={post.slug} initialComments={comments} /> </article></PostLayout>6.3 View Transitions 集成
---import { ViewTransitions } from 'astro:transitions';import Header from '@/components/astro/Header.astro';import ThemeToggle from '@/components/vue/ThemeToggle.vue';---
<html lang="zh-CN"><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>{Astro.props.title}</title> <!-- View Transitions API --> <ViewTransitions /></head><body> <Header> <!-- 主题切换在页面导航时保持状态 --> <ThemeToggle client:load transition:persist /> </Header>
<main transition:animate="slide"> <slot /> </main></body></html>七、Islands 架构的局限性
7.1 不适合的场景
❌ 高度交互的 SPA 应用(如 Figma、Google Docs) → 整个页面都是交互式的,Islands 没有优势
❌ 需要复杂客户端路由的应用 → Islands 是 MPA 模式,页面导航是全页刷新(虽然 View Transitions 缓解了这个问题)
❌ 岛屿之间需要大量频繁通信 → 跨岛屿状态同步有额外复杂度
❌ 需要离线支持的 PWA → MPA 模式下 Service Worker 配置更复杂7.2 何时选择 Islands
✅ 内容驱动的网站(博客、文档、营销页面)✅ 电商产品页面(大量静态描述 + 少量交互)✅ 新闻/媒体网站(内容为主)✅ 企业官网/落地页✅ 对 Core Web Vitals 有严格要求的项目✅ 需要极致首屏性能的场景总结
Islands 架构代表了前端性能优化的一次范式转移:
- 默认零 JS:页面主体是纯 HTML,性能天花板极高
- 选择性注水:5 种
client:*指令精确控制何时加载 JS - 框架无关:同一页面可以混用 Vue、Svelte、Solid 等
- 渐进增强:即使 JS 加载失败,用户仍能看到完整内容
- 极致性能:典型场景下 JS 减少 75%+,TBT 降低 80%+
Astro 不是要取代 Vue 或 Svelte——它是让你在正确的地方使用正确的工具。如果你的项目是内容驱动的,Islands 架构几乎是 2026 年的最优选择。
“The best JavaScript is no JavaScript. The second best is the least JavaScript.” —— Islands 架构的哲学
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!