高级前端进阶
虽然以前也尝试过了解孤岛架构,以及该架构在前端的实践,但是总觉得不得要领。今天刚好看到了patterns.dev上的一篇文章《Islands Architecture》,觉得解释的非常好,所以特地拿来分享给大家。话不多说,直接开始!
孤岛架构(Islands Architecture)鼓励在服务器渲染的网页中使用小而集中的交互块。孤岛架构输出的是逐步增强的 HTML,同时具体地说明了增强是如何发生的。 孤岛架构不是由单个应用程序控制整页渲染,而是有多个入口点。 这些交互“孤岛”的脚本可以独立交付和混合,同时允许页面的其余部分只是静态 HTML。

开发者都知道,加载和处理过多的 JavaScript 会损害页面性能。 但是,页面通常需要一定程度的交互性和 JavaScript,即使在静态网站中也是如此。 静态渲染的变体使开发者能够构建尝试在以下两者之间找到平衡的应用程序:
可与客户端渲染 (CSR) 应用程序相媲美的交互性与 SSR 应用程序相媲美的 SEO 优势。SSR 的核心原则是 HTML 在服务器上渲染,同时附加必要的 JavaScript 以在客户端补水(Rehydration)。 Rehydration 是在服务端渲染后在客户端重新生成 UI 组件状态的过程。 由于补水是有代价的,SSR 的每个变体都试图优化补水过程。 主要是通过关键组件的部分水合(Partial Hydration)或渲染时组件的流化(Streaming of Component)来实现的。 然而,在这些架构下,最终的 JavaScript 体积问题始终没有解决。
Islands architecture 一词由 Katie Sylor-Miller 和 Jason Miller 推广开来,描述了一种范式,旨在通过交互“孤岛”减少传送的 JavaScript 数量,这些交互可以在其他静态 HTML 之上独立交付。 孤岛架构 是一种基于组件的架构,它建议使用静态和动态岛来划分页面视图。 页面的静态区域是纯非交互式 HTML,不需要水合作用。 动态区域是 HTML 和脚本的组合,能够在渲染后自行恢复交互性。
如上图展示了SSR、Progressive Hydration、Islands architecture在解决水合问题的异同。
SSR:一次性渲染所有组件,然后水合Progressive Hydration:渲染所有组件,首次水合部分关键组件,然后渐进式补水Islands architecture :静态 HTML 由服务端渲染,可交互性组件动态添加脚本2.动态组件岛(Islands of dynamic components)大多数页面都是静态和动态内容的组合。 通常,页面由静态内容组成,其中散布着可以隔离的交互区域。 例如:
博客、、站点主页包含文本和图像以及社交媒体嵌入和聊天等交互式组件。电子商务网站上的产品页面包含静态产品描述和指向应用程序其他页面的链接。 图像轮播和搜索等交互式组件在页面的不同区域可用。一个典型的银行账户详细信息页面包含一个静态交易列表,过滤器提供一些交互性。静态内容是无状态的,不会触发事件,渲染后也不需要再水合。 然而,渲染的动态内容(按钮、过滤器、搜索栏)必须重新连接到事件, DOM 必须在客户端重新生成(虚拟 DOM)。 这种重新生成(Regeneration)、再水合和事件处理功能有助于将 JavaScript 发送到客户端。
Islands 体系结构有助于在服务器端渲染页面及其所有静态内容。 但是,在这种情况下,渲染的 HTML 将包含动态内容的占位符, 内含独立的组件小部件。 每个小部件都类似于一个应用程序,并结合了服务器渲染的输出和用于在客户端上激活应用程序的 JavaScript。
在渐进式水合中,页面的水合架构是自上而下的, 页面控制各个组件的调度和水合作用。 每个组件在 Islands 架构中都有自己的水合脚本,该脚本异步执行,独立于页面上的任何其他脚本,一个组件中的性能问题不应影响另一个组件。
比如上图中的评论组件,社交按钮组件都是需要动态水合的组件,但是两者互不干扰。
3.如何实现 Islands ArchitectureIsland 架构借鉴了不同源的概念,旨在将它们最佳地组合起来。 Jekyll 和 Hugo 等基于模板的静态站点生成器支持将静态组件渲染到页面。 大多数现代 JavaScript 框架还支持同构渲染,它允许开发者使用相同的代码在服务器和客户端上渲染元素。
也有人建议使用 requestIdleCallback() 来实施一种用于水合组件的调度方法。 组件级部分水合的静态同构渲染和调度可以构建到框架中以支持 Islands 架构。 因此,框架应该具有以下特点:
支持零 JavaScript 在服务器上静态呈现页面。支持通过静态内容中的占位符嵌入独立的动态组件。 每个动态组件都包含它的脚本,并且可以在主线程空闲时使用 requestIdleCallback() 来自动补水允许在服务器上同构渲染组件,在客户端上进行水合作用,以在两端识别相同的组件。下面将介绍几个基于孤岛架构的最佳实践框架。
4.孤岛架构的最佳框架4.1 Marko什么是 MarkoMarko 是由 eBay 开发和维护的开源框架,用于提高服务端渲染性能。 它通过将流式渲染与自动部分水合相结合来支持 Islands 架构。下面是官方对 Marko 的描述:
HTML 和其他静态资产一旦准备就绪,Marko 就会将内容流式传输到客户端,然后由客户端交互式组件自行水合。 注意,Hydration 代码仅适用于交互式组件,它可以更改浏览器上的状态,而且支持同构,Marko 编译器根据运行位置(客户端或服务器)生成优化代码。Marko 有以下典型特点:
熟悉:如果开发者知道 HTML、CSS 和 Javascript,就知道 Marko高性能:支持流式处理(Streaming)、部分水合作用、优化编译器和小型运行时可扩展:从简单的 HTML 模板开始,根据需要添加强大的组件高可用:Marko 为 ebay.com 等高流量网站提供支持快速上手 Marko下面以一个简单的 hello world 示例开始,Marko 使用类似于 HTML 的语法可以轻松地表示 UI:
//hello.marko<h1>Hello World</h1>
事实上,Marko 非常像 HTML,开发者可以用它来替代 handlebars、mustache 或 pug 等模板语言:
// template.marko<!doctype html><html><head> <title>Hello World</title></head><body> <h1>Hello World</h1></body></html>
然而,Marko 不仅仅是一种模板语言,它允许开发者通过描述应用程序视图如何随时间变化以及如何响应用户操作来以声明方式构建应用程序。在浏览器中,当表示 UI 的数据发生变化时,Marko 将自动高效地更新 DOM 以反映变化。
下面使用 Marko 来开发一个组件,假设想要在单击 <button> 时执行一个操作:
// button.marko<button>Click me!</button>
Marko 使一切变得非常简单,允许开发者在 .marko 视图中为组件定义一个类,并使用 on- 属性调用该类的方法:
class { sayHi() { alert("Hi!"); }}<button on-click("sayHi")>Click me!</button>
单击按钮时将弹窗 alert,但是如何更新 UI 以响应操作呢? 这时候就需要 Marko 的有状态组件。 开发者需要做的就是在组件类中设置 this.state, 这是一个新的状态变量可用于视图。 当 this.state 中的值更改时,视图将自动重新渲染并仅更新更改的 DOM 部分。比如下面的 counter.marko 组件示例:
class { onCreate() { this.state = { count: 0 }; } increment() { this.state.count++; }}<div>The current count is ${state.count}</div><button on-click("increment")>Click me!</button>
关于 Marko 的更多用法可以参考文末资料。
4.2 Astro什么是 AstroAstro 是一个静态网站构建器,可以从其他框架(如 React、Preact、Svelte、Vue 等)中构建的 UI 组件生成轻量级静态 HTML 页面,而需要客户端 JavaScript 的组件与其依赖项一起单独加载。 因此,Astro 提供了内置的部分水合作用。 Astro 还可以根据组件何时可见来延迟加载组件。Astro 有以下明显特点:
以内容为中心:Astro 专为内容丰富的网站而设计服务器优先:网站在服务器上渲染 HTML 时运行速度更快速度快:通过 Astro 构建的网站具有很高的站点性能易于使用:无需大量学习即可轻松使用 Astro 构建功能齐全&灵活:超过 100 多种 Astro 集成可供选择使用 Astro以下是使用 Astro 实现的示例博客页面, SamplePost 页面导入了一个交互式组件 SocialButtons。下面是 SocialButtons 的组件代码:
import { useState } from 'preact/hooks';// preact组件,同时有事件处理机制export function SocialButtons() { const [count, setCount] = useState(0); const add = () => setCount((i) => i + 1); const subtract = () => setCount((i) => i - 1); return ( <> <div>{count} people liked this post</div> <div align="right"> <Image src="/like.png" width="32" height="32" onclick={add}></img> <Image src="/unlike.png" width="32" height="32" onclick={subtract} ></img> </div> </> );}
主页面导入代码如下:
---// SocialButtons组件导入import { SocialButtons } from '../../components/SocialButtons.js';---<html lang="en"> <head> <link rel="stylesheet" href="/blog.css" /> </head> <body> <div class="layout"> <article class="content"> <section class="intro"> <h1 class="title">博客标题(静态)</h1> <br/> <p>博客子标题(静态)</p> </section> <section class="intro"> <p>这是带有服务器渲染的图像的帖子内容</p> <Image src="https://source.unsplash.com/user/c_v_r/200x200" /> <p>下一部分包含交互式社交按钮组件,其中包含其脚本。</p> </section> <section class="social"> <div> <SocialButtons client:visible></SocialButtons> </div> </section> </article> </div> </body></html>
SocialButtons 组件是一个带有 HTML 的 Preact 组件,并包含相应的事件处理程序。 该组件在运行时嵌入到页面中,并在客户端进行水合,以便点击事件按需运行。
Astro 允许 HTML、CSS 和脚本之间的清晰分离,并鼓励基于组件的设计。使用此框架很容易安装和开始构建网站。下面是组件运行示例的截图:
4.3 Eleventy + Preact什么是 Eleventy
Markus Oberlehner 演示了 Eleventy 的使用,Eleventy 是一种静态站点生成器,具有可以部分水合的同构 Preact 组件,它还支持
请注意,Eleventy 早于孤岛架构,但包含支持它所需的一些功能。 然而,Astro 是基于定义构建的,并且本质上支持 Islands 架构。
// 其中+表示为了达到部分水合效果需要添加的代码const { html, render } = require('htm/preact');+const whenVisible = require('./utils/when-visible'); const LikeForm = require('./components/LikeForm'); const componentMap = { LikeForm, }; const $componentMarkers = document.querySelectorAll(`[data-cmp-id]`); Array.from($componentMarkers).forEach(($marker) => { const $component = $marker.nextElementSibling;+ whenVisible($component, () => { const { name, props } = window.__STATE__.components[$marker.dataset.cmpId]; const Component = componentMap[name]; render(html`<${Component} ...${props}/>`, $component.parentNode, $component);+ }); });
4.4 Qwik什么是 Qwik
Qwik 的作者是 builder.io 的 CTO 「miško hevery」,同时也是 Angular/AngularJS 的发明者。Qwik 框架的特点是:超细粒度的孤岛架构 ,且粒度是开发者可控的。
Qwik 通过 RESUMABILITY 带来了一个全新的渲染模式,即可恢复性。Resumability 消除了许多现代框架的范式,如 Angular、NextJS 和 NUXTJS。 这些框架使用了 Hydration(水合),帮助开发者以各种方式建立 SSR(服务器端渲染)网络应用的交互。
但是,水合也会带来诸多问题,典型的就是水合作用很昂贵。主要因为以下两点:
框架必须下载与当前页面相关的所有组件代码。框架必须执行与页面上的组件关联的模板,以重建侦听器位置和内部组件树。Qwik 提出 Resumable(可恢复)的概念,启动时不需要补水过程,也就大大缩减了启动时间。Qwik 通过将事件侦听序列化到 DOM 中,然后通过 Qwikloader 来解决上述问题。比如下面的示例:
<button on:click="./chunk.js#handler_symbol">click me</button>大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
Qwik 组件
组件是 Qwik 应用程序的基本构建块, 许多 UI 开发人员应该对 Qwik 组件很熟悉。
import { component$, useSignal } from '@builder.io/qwik';export const MyCmp = component$((props: MyCmpProps) => { // 声明state const count = useSignal(0); // 返回 JSX return ( <> <h3>Hello, {props.name}</h3> <p>You have clicked {count.value} times</p> <button onClick$={() => { // 更新state并导致重新渲染。 // 反应性是 Qwik 内置实现!
count.value++; }} > Increment </button> </> );});
Qwik 组件的独特之处在于:
Qwik 组件会被优化器自动分解为延迟加载的块。它们是可恢复的(组件可以在服务器上创建并在客户端上继续执行)。它们是反应式的,并且独立于页面上的其他组件呈现。关于 Qwik 的更多用法可以参考文末资料,这里不再展开。
5.Fresh什么是FreshFresh 是下一代 Web 框架,专为速度、可靠性和简单性而构建。Fresh有一些突出的特点:
边缘上的即时渲染。基于孤岛架构的客户端水合作用,以实现最大的交互性。零运行时开销:默认情况下不会将 JS 发送到客户端。没有构建步骤、0配置。开箱即用的 TypeScript 支持、文件系统路由Next.js提供如何使用FreshFresh 将构成页面的各种组件,分为 route 和 island 两类,约定存放在 routes/ 和 islands/ 两个目录。Fresh 处理这两类组件的方式完全不同:
Route Component:仅在服务端执行,直接响应 SSR 渲染出的 HTML 给客户端,在客户端不会加载和执行任何 JS 代码,更不用说 hydrate 了。Island Component:不仅在服务端执行,它的 JS 也会在客户端加载,并且 hydrate,所以 Island 可以响应用户的交互。在Fresh这个架构中,Islands 之间是相互独立的,一个崩溃不会影响另一个,同时它们的 hydrate 过程也是独立的,hydrate 完成后即可立即响应用户的交互。
Route 和 Island 这两种组件都是 Preact 组件。Island 比 Route 更 “正常” 一些,和常规的参与 SSR 的组件没太大区别。而 Route,更像是被 Fresh 当做模板引擎使用。任何需要在客户端执行 JS 的区块,都必须抽成一个 Island Component 独立出去。
这是官方的一个很简单的示例:
// routes/index.tsximport Counter from '../islands/Counter.tsx'export default function Home() { return ( <div> <p> Welcome to Fresh. Try to update this message in the ./routes/index.tsx file, and refresh. </p> <Counter start={3} /> </div> )}
// islands/Counter.tsximport { useState } from 'preact/hooks'import { IS_BROWSER } from '$fresh/runtime.ts'interface CounterProps { start: number}export default function Counter(props: CounterProps) { const [count, setCount] = useState(props.start) return ( <div> <p>{count}</p> {/ 这个 disabled 可以说很细节了 /} <button onClick={() => setCount(count - 1)} disabled={!IS_BROWSER}> -1 </button> <button onClick={() => setCount(count + 1)} disabled={!IS_BROWSER}> +1 </button> </div> )}
运行效果:
可以看到,客户端加载的 JS 代码很少:
main.js 和 chunk-TDJO6WAF.js 主要是 Fresh 的 runtime 代码和 preactisland-counter.js 就是的 Island 组件Fresh 内部强依赖 Preact,通过 Preact 将所有组件渲染为 HTML,给 Islands 打好标记。同时 JS 的依赖收集根据约定的目录控制好范围。在客户端,使用少量运行时和 Preact,完成 hydrate。
<!-- 这是 SSR 生成的 HTML 片段 --><div> <p>Welcome to `fresh`. Try updating this message in the ./routes/index.tsx file, and refresh.</p> <!--frsh-counter:0--> <div> <p>3</p> <button disabled>-1</button> <button disabled>+1</button> </div> </!--frsh-counter:0--></div>
除了 Islands Architecture 之外,Fresh 的 Just-in-time rendering + Edge Runtime 设计,也是对 “强动态化页面” 很好的优化,虽然有一定的门槛。
5.孤岛架构优缺点Islands 架构结合了不同渲染技术的想法,例如:服务器端渲染、静态站点生成和部分水合作用。 实施孤岛的一些潜在好处如下。
性能减少发送到客户端的 JavaScript 代码量。 发送的代码仅包含交互组件所需的脚本,这比为整个页面重新创建虚拟 DOM 并重新水合页面上的所有元素所需的脚本要少得多。 较小的 JavaScript 自动对应于更快的页面加载和交互时间 (TTI)。
Astro 与为 Next.js 和 Nuxt.js 创建的文档网站的比较表明,JavaScript 代码减少了 83%。
SEO由于所有静态内容都在服务器上渲染,页面对 SEO 友好。同时,允许开发者优先考虑重要内容,关键内容几乎可以立即提供给用户。
可访问性/基于组件使用标准静态 HTML 链接访问其他页面有助于提高网站的可访问性,孤岛架构保证了基于组件架构的所有优点,例如:可重用性和可维护性。
孤岛架构不足尽管孤岛架构提供了诸多好多,但这个概念仍处于初级阶段,仍然需要不断完善。
开发人员实现孤岛架构可选择框架太少,开发成本较高孤岛架构不适合高度交互的页面,例如:可能需要数千个孤岛的社交媒体应用程序。孤岛架构强调使用 SSR 渲染静态内容,同时支持通过动态组件进行交互,将对页面性能的影响降至最低。 希望将来在这个领域看到更多的参与者。
5.本文总结本文主要和大家介绍最近大火的孤岛架构 。因为篇幅有限,文章并没有过多展开,如果有兴趣,可以在我的主页继续阅读,同时文末的参考资料提供了大量优秀文档以供学习。最后,欢迎大家点赞、评论、转发、收藏!
https://markojs.com/docs/getting-started/
https://astro.build/
https://docs.astro.build/en/getting-started/
https://markus.oberlehner.net/blog/building-partially-hydrated-progressively-enhanced-static-websites-with-isomorphic-preact-and-eleventy/#lazy-hydration
https://qwik.builder.io/docs/getting-started/
https://keenwon.com/better-react-ssr/
https://keenwon.com/fresh-introduction/
https://github.com/denoland/fresh
英文原文地址:https://www.patterns.dev/posts/islands-architecture
翻译说明:高级前端进阶翻译、修改