YAOTU INSIGHTS

Astro 岛屿架构实战:React/Vue/Svelte 多框架共存

Astro 岛屿架构实战:React/Vue/Svelte 多框架共存
如果你是做内容站、营销页或者文档站点出身大概率经历过这种纠结团队里 React 派和 Vue 派谁也不服谁老板又催着上线最后只能拉一个技术选型会议从下午吵到天黑。我在 2023 年初第一次把 Astro 接入生产项目原本只是为了“少写一点水合脚本”结果发现它真正厉害的地方是“多种 UI 框架支持”——同一个 Astro 项目里React、Vue、Svelte、Solid、Preact、Lit 可以同时存在而且各自作为独立的“岛屿”按需加载 JS。这篇文章就围绕这个能力做一次深度拆解它怎么做到、配置时有哪些坑、混用时的通信方案、性能到底行不行以及 Astro 5 带来的 server:defer 这类新玩法最后给出一份可以直接照抄的选型判断标准。1. 为什么轮到 Astro 来“统一” UI 框架1.1 传统方案的死结一个站点只能忠诚于一个运行时过去十年UI 框架的选型几乎是一次“押注”。选了 Gatsby意味着你的组件只能用 React选了 Nuxt生态里再好的 Svelte 组件也跟你没关系哪怕只是想在某个营销页里嵌一个 Chat Widget而这个 Widget 恰好是 Vue 写的传统静态站点生成器也会让你难受半天——要么把 Widget 改写成 React要么用 iframe 包一层附带一堆跨域和主题同步的问题。Astro 的定位之所以特殊是因为它把“内容优先”和“组件即岛”两件事焊死在一起。Astro 本身不是一个 UI 框架它更像一个“页面编排器”每个页面的静态 HTML 由 Astro 直接渲染项目里可以同时装多个框架的官方集成每个集成只负责一种框架组件的编译和运行时。框架在这里降级为“渲染器”而不是站点的全部命运。这一点在官方文档里写得很清楚但只有真正在一个项目里同时跑三个框架的人才会意识到这个设计有多反常规。1.2 岛屿架构水合的最小单位是组件不是应用要理解多框架支持必须先理解“水合”是什么。所谓水合就是服务端先把 HTML 字符串发到浏览器浏览器再用一份 JS 运行时把静态 DOM“激活”成可以响应事件的交互组件。传统 SPA 的做法是整站水合不管首屏有几个组件需要交互框架运行时都会把整个应用挂在页面上JS 体积和解析成本跟着整个应用走。Astro 的核心判断是绝大多数页面的绝大部分区域其实根本不需要 JS。按钮的 hover 效果可以用 CSS静态内容就是纯 HTML真正需要交互的只有评论区、搜索框、导航抽屉这几个“孤岛”。于是 Astro 默认不水合任何组件只有你显式给组件加上client:load、client:visible这类指令它才会把该组件的 JS 独立打包、独立激活。浏览器里你会看到每个交互区域被一个名为astro-island的自定义元素包裹组件的 props 以 JSON 形式序列化在这个元素的属性里框架运行时只随水合的岛走。这就是“多种 UI 框架支持”能成立的地基框架之间不需要互相认识它们只需要各自对自己的岛负责。1.3 团队协作视角框架支持变成了组织的软能力多框架混用不是炫技它解决的是很现实的组织问题。我接过一个项目母公司维护一套 React 写的用户中心组件子品牌团队整组人都是 Svelte 背景两边完全没法统一技术栈。放在过去只能强行迁移周期按季度算。用 Astro 之后母公司把 React 组件发布成 npm 包子品牌页面里直接import过来加一个client:visible就完事新需求用 Svelte 写互不阻塞。另一个常见场景是渐进式迁移老站有一批 Vue 2 组件想逐步淘汰新页面不想继续背着 Vue 2 的包袱Astro 可以让你“新页面用新框架老组件继续服务”直到最后一个旧组件下线再把集成卸掉。从这个角度看“多种 UI 框架支持”不只是一个技术特性它给了团队一个不站队的缓冲期。2. 初始化一个多框架共存的工程2.1 脚手架与 astro add 的自动配置陷阱多框架工程初始化其实很简单一条命令就能把几个框架集成一起装上npm create astrolatest cd my-site npx astro add react vue svelte solid preactastro add会帮你做三件事安装对应 npm 包、修改astro.config.mjs、在src/env.d.ts里补类型引用。听起来省事但它有一个隐藏风险——它会直接改你的配置文件而且部分包之间有 peer 依赖冲突。我在 npm 下同时装 React 和 Preact 时就遇到过preact/compat的 alias 覆盖问题后面会细说。所以我的建议是在跑astro add之前先提交一次 git commit这样无论集成工具把配置改坏成什么样git checkout都能救回来。另外别一次性把六七个框架全加上先加你真正要用的两三个跑通一个页面再加下一个否则出错时你根本不知道是哪两个集成互相打架。2.2 astro.config.mjs 里集成顺序与同名扩展名冲突装完集成后配置文件长这样// astro.config.mjs import { defineConfig } from astro/config; import react from astrojs/react; import vue from astrojs/vue; import svelte from astrojs/svelte; import solid from astrojs/solid-js; import preact from astrojs/preact; export default defineConfig({ integrations: [react(), vue(), svelte(), solid(), preact()], });集成数组的顺序大多数情况下无所谓因为 Vue 的.vue、Svelte 的.svelte扩展名是唯一的Astro 编译器不会认错。真正会撞车的是.jsx和.tsx——React、Preact、Solid 这三个框架都声称自己能处理这两种扩展名。同一个项目里如果同时装了 React 和 Preact一个.jsx文件导入之后Astro 可能拿它当 React 组件编译你心里想的是 Preact跑出来却是两套运行时都加载了。实践中我的处理原则是同一时期只保留一个会抢.jsx/.tsx的框架。如果实在必须 React 和 Preact 并存给 Preact 组件统一加client:onlypreact强制指定渲染器但代价是这个组件放弃服务端渲染首屏只能看到空白占位。相比之下我更推荐用astrojs/preact的compat选项做整体迁移把源码里的react包名 alias 到preact/compatReact 组件几乎可以原样跑在 Preact 上而不是让两个框架在同一项目里抢同一个扩展名。2.3 tsconfig 与编辑器提示的配置细节多框架工程的类型检查是另一个容易漏的环节。Astro 官方提供了基础 tsconfig{ extends: astro/tsconfigs/strict, compilerOptions: { jsx: preserve, jsxImportSource: react } }注意jsxImportSource只能写一个值。如果项目里同时有 React 和 Solid 的.tsx文件编辑器会用 React 的 JSX 类型去解析 Solid 文件导致一堆匪夷所思的类型报错。Solid 组件文件我建议单独用.tsx且文件头部加// jsxImportSource solid-js注释或者直接在 tsconfig 里为 Solid 目录配置独立的include范围。这块没有银弹本质原因是两个框架的 JSX 运行时本来就不兼容Astro 只是把它们编译到同一份页面并没有义务替你统一类型系统。编辑器方面VS Code 装官方 Astro 扩展后.astro文件里的框架组件导入、props 校验都能正常提示但前提是 tsconfig 正确。跑一下npm run astro check会自动装astrojs/check能一次性把.astro、.tsx、.vue的类型错误都扫出来这个命令建议直接写进 CI比依赖编辑器提示靠谱得多。3. client: 指令的完整行为与选型方法3.1 六个指令的行为拆解Astro 把“什么时候水合”这件事做成了指令每个指令对应一种浏览器调度时机。我整理了一张对照表方便你在做技术方案时直接查指令水合触发时机典型使用场景注意事项无指令永不水合纯展示组件、SEO 内容只有静态 HTML交互功能全部丢失client:load页面加载立即水合首屏折叠区内的关键词搜索、全局导航所有指令里优先级最高多个 load 一起时按依赖顺序client:idle浏览器空闲后水合弹窗、折叠面板、文章目录依赖requestIdleCallback低端设备可能延迟明显client:visible元素进入视口时水合评论区、页脚表单、懒加载区块用 IntersectionObserver 实现超出视口不加载client:media匹配媒体查询时水合移动端抽屉、桌面端专属图表媒体状态变化后也会响应式地启停水合client:only完全不 SSR只在客户端渲染依赖window/localStorage的组件必须指定框架名如client:onlyreact3.2 实战选型三个真实组件的决策过程拿我做过的一个文档站举例页面里同时有导航搜索框、评论区、文章目录和“返回顶部”按钮。我当时的决策是这样的搜索框放在首屏第一区域交互是刚需用户到达页面后随时可能输入我给它client:load。评论区在文章底部用户大概率要滚动一段时间才会看到用client:visible配合loadinglazy的占位骨架屏首屏完全不背评论区的 JS。文章目录虽然也在侧边栏但它只在用户阅读过程中偶尔用到用client:idle浏览器空闲了再水合几乎不影响感知。返回顶部按钮本来想用client:visible但实测发现它在移动端会撑出额外的布局空间干脆用client:media(max-width: 768px)只在手机上出现。这里要提醒一个细节client:visible不是“滚到才加载”它用的是IntersectionObserver如果元素在首屏内就会立即水合client:idle也不是“马上执行”低端 Android 机上requestIdleCallback可能迟迟不触发。所以我的口诀是交互刚需就 load出现即需就 visible可有可无就 idle只有客户端能跑的才用 only。3.3 水合不匹配现象、根因与排查链路多框架项目里最容易遇到的一类问题不是框架冲突而是“水合不匹配”hydration mismatch。症状是浏览器控制台刷红色警告页面上某个组件在服务端渲染出的一种 HTML和客户端重新渲染出来的 HTML 对不上轻则样式闪一下重则整块交互失灵。最典型的根因是组件渲染函数里直接读了浏览器 API// 错误示范渲染期间读取 window/navigator export default function Version() { return span设备类型{navigator.userAgent.includes(Mobile) ? 移动端 : 桌面端}/span; }服务端没有navigatorAstro 的 SSR 环境会返回空值或默认值客户端水合时读到的却是真实值两边 HTML 必然不一致。排查链路我一般按三步走第一步把报错警告里提到的组件找出来检查它的顶层渲染逻辑里有没有window、document、localStorage、matchMedia这些浏览器专用对象。第二步如果确实需要浏览器环境才能渲染改成客户端专用加client:onlyreact让服务端干脆不渲染它也就无从比对。第三步如果组件主体可以 SSR、只是某个局部信息依赖浏览器把那段逻辑挪进useEffect或 Svelte 的onMount里让它在水合完成后用 DOM 操作补上而不是参与首次渲染。这个顺序能覆盖九成以上的 mismatch 问题。4. 多种框架组件在页面里的协作4.1 跨框架通信的三种可行方案多框架同台的最直接问题是React 岛里点了一个按钮怎么让 Vue 岛里的购物车数字跟着变框架各自有响应式系统但彼此不认识。我试过三种方案按推荐程度排序第一种是用浏览器的自定义事件。这是最原生的做法框架无关不会引入额外依赖。React 组件里这么发// React 侧 export default function AddButton({ sku }: { sku: string }) { const handleClick () { window.dispatchEvent(new CustomEvent(cart:add, { detail: { sku } })); }; return button onClick{handleClick}加入购物车/button; }Vue 侧在挂载后监听!-- Vue 侧 -- script setup langts import { onMounted, onBeforeUnmount, ref } from vue; const count ref(0); function handleAdd(e: Event) { const detail (e as CustomEvent).detail; count.value detail.sku ? 1 : 1; } onMounted(() window.addEventListener(cart:add, handleAdd)); onBeforeUnmount(() window.removeEventListener(cart:add, handleAdd)); /script第二种是引一个框架无关的全局存储。Astro 官方社区比较推崇nanostores它本身就是一个极小的响应式 storeReact 用nanostores/react、Vue 用nanostores/vue、Svelte 用nanostores/svelte分别订阅三个框架读到的是同一份状态。如果你产品里有跨框架的“登录态”“购物车”“主题模式”这类全局状态这个方案比自定义事件更省心因为它天然支持响应式推导不需要手动同步。第三种是更低耦合的 DOM 属性协商A 岛把状态写进某个>export default function Card({ children }: { children: React.ReactNode }) { return div{children ?? fallback}/div; }4.3 样式隔离、z-index 与第三方组件库的坑多框架项目里样式问题比通信更隐蔽。Astro 自带 scoped 样式.astro文件里的style默认只作用于当前文件这个没问题。但框架组件的 scoped 机制各不相同Vue 的scoped会给属性加>div classreact-zone AntDTable client:load / /div div classvue-zone ElTable client:load / /div然后在全局样式里针对两个 zone 分别做变量覆盖。z-index 也是重灾区第三方组件库的弹窗、下拉、Tooltip 默认都挂body下Astro 页面里如果自定义了一个高 z-index 的 header弹层被盖住是常有的事。给这类库的弹层统一配置appendTo或指定一个高 z-index 的容器变量通常能解决。5. 多框架真的划算吗体积、加载与性能实测5.1 各框架运行时体积一个岛一份 JS 的现实很多人担心“一个页面塞三个框架JS 不得爆炸”。这个担心部分正确部分多余。正确的地方在于每多一个框架至少多一份该框架的运行时React 岛的运行时不可能给 Vue 岛复用。我用常见构建产物做了个粗略统计框架运行时压缩后估算体积gzip 后估算体积Preact hooks约 10 KB约 4 KBSolidJS约 8 KB约 3 KBSvelte按需编译进组件每组件约 2-5 KB约 1-2 KBVue 3仅运行时约 34 KB约 12 KBReact ReactDOM约 42 KB约 14 KBLit 基础约 9 KB约 5 KB这些是数量级参考不同版本会有浮动。关键在于体积只和你实际水合的岛有关。一个 10 个 React 组件的页面如果只有评论区加了client:visible浏览器实际只下载一份 React 运行时加评论区组件的代码其余 9 个组件留在服务端 HTML 里一个字节的 JS 都不用花。这正是 Astro 相对传统 SPA 最核心的性能优势。5.2 一次真实瘦身把首屏 React 换掉之后去年我优化过一个营销活动页。页面主体是静态内容只有三个模块需要交互倒计时、抽奖转盘、用户反馈弹窗都是 React 写的。按照传统 React 生态的做法首屏至少要把 React 运行时、路由、状态库一起拉下来实测 gzip 后约 18 KB 的 JS再加解析执行时间低端机白屏要 500ms 以上。迁移到 Astro 后我把倒计时改成client:idle抽奖转盘client:visible反馈弹窗client:media(max-width: 1024px)——结果首屏几乎没有任何框架 JS用户看到的是纯静态 HTML。等到滚动到转盘区域浏览器才开始下载 React 运行时倒计时则等浏览器空闲后再水合弹窗只在移动端水合桌面端用户根本不会触发那部分代码。改造后首屏完全加载时间从 3.2s 降到 1.8sJS 总量从 18 KB 降到 7 KB。这个数字不是玄学是因为 Astro 在构建期就把每个岛拆成了独立的 chunk只有真正被水合的 chunk 才会被浏览器请求。5.3 判断标准什么时候混框架值得什么时候纯属炫技我见过一些团队把“多框架支持”当成 KPI一个页面里硬塞 React、Vue、Svelte 三个生态结果维护成本翻倍。这里我给几条判断标准值得混框架的情况包括团队确实分属不同技术栈且没有统一意愿有不可改写的存量组件比如母公司维护的 npm 包页面主体是内容交互只是零散几个岛需要渐进式迁移老框架代码。不值得混框架的情况包括所有代码都是同一个人或同一个小组在写两个框架的岛需要频繁交换复杂状态这时候通信成本会吃掉架构收益团队里没人能同时维护多框架的依赖升级你的核心卖点明明是业务不是“前端架构先进”。个人经验是一个页面里有两个框架的岛属于常态三个以上就要拿出充分的理由。多框架是解决问题的工具不是解决问题的姿势。6. 进阶形态Server Islands、Web Components 与轻量方案6.1 server:defer把动态渲染放回服务器Astro 5 引入了 Server Islands语法上是给组件加一个server:defer指令。它的含义是这个组件不参与构建期的静态渲染而是在用户请求页面时由服务器临时渲染一份 HTML再通过一个子请求流式替换到页面上。这样静态站点也能承担“登录用户专属内容”“实时价格”“个性化推荐”这类必须动态计算的区域而不需要整个页面切到服务器渲染。多框架支持在 Server Islands 下同样生效server:defer的组件可以用 React 写也可以用 Vue 写Astro 在服务端按集成配置把那一段渲染成 HTML再插回静态页的占位符里。部署时要配合支持 SSR 的 adapterNode、Netlify、Vercel 等不是随意一个静态托管都能跑。我在一个电商内容站上用它做过“查询库存状态”的小组件效果是静态页秒开、动态数据区域独立加载两者互不拖累。6.2 Lit 与 Web Components真正的跨框架通用组件如果你要分发的组件不想绑定任何具体框架Web Components 才是终极方案。Astro 官方有astrojs/lit集成Lit 组件加client:load之后就是标准自定义元素React 里onClick可能接不住自定义事件但 Lit 元素在 Vue、Svelte 里都能被当成普通 HTML 标签直接用。适合做设计系统一套 Web Components 组件库页面层可以自由选择用 React 还是 Vue 写业务逻辑。代价是自定义元素的类型提示、属性反射、事件绑定都更麻烦开发体验比纯框架组件粗糙。轻量交互的另一个选择是 Alpine.jsAstro 也有集成但它的定位是“比client:visible更细粒度的局部行为”适合折叠面板、Tab 切换这类组件写起来像是 HTML 内置的语法糖。注意 Alpine 不是传统意义的框架运行时它的体积很小如果你只需要这类交互完全没必要引入 React。6.3 我在多个项目里总结的维护建议多框架项目维护了大半年几个经验值得单独说。第一把框架版本锁死尤其是 React 和 Vue 的 patch 版本两个生态的升级节奏完全不同自动升级工具会把你的构建环境搅乱。第二让每个框架的组件尽量“自包含”组件自己带样式、自己管状态、自己监听事件而不是依赖页面层来回传函数。第三给通信事件命名加前缀我用cart:、auth:、theme:这种命名空间避免和三方组件库的事件撞名。第四定期跑一次astro build看每个岛产出的 chunk 大小发现某个框架的运行时突然变大往往是依赖升级引入的尽早处理比积攒到最后容易得多。写到这里最想分享的其实是另一个体会Astro 的“多种 UI 框架支持”不是让你把复杂变简单而是把“技术栈不统一”这个组织问题变成了可管理的技术方案。它没法消除框架之间的墙但它让你不必在那堵墙面前把整个项目推翻重来。如果你正在做一个内容形态的产品、接手一个多技术栈拼盘的老项目或者只是厌倦了“选错框架就完蛋”的压迫感Astro 这个特性值得你拿一个小页面试水。试之前记住一句话框架是岛内容才是大陆。