流式 HTML – 无需 JavaScript 即可实现异步 DOM 更新

关键要点

  • 当页面快速加载并显示附加数据时,Web 应用程序可提供最佳的用户体验。
  • 传统的使用 JavaScript 异步显示数据的方法功能强大,但与传统的服务器端渲染相比增加了额外的复杂性。
  • 声明式影子 DOM 允许开发人员使用模板和插槽显示无序内容。
  • HTTP 流响应允许开发人员在数据可用时向用户发送 HTML 页面的增量元素。
  • 我们展示了一个 Go 语言的示例应用程序,该应用程序使用带有 HTTP 流响应的声明式影子 DOM 来快速加载页面,并在没有 JavaScript 的情况下显示可用的附加数据。

开发人员努力开发响应式 Web 应用程序以提供最佳用户体验。Web 应用程序用户希望页面能够快速加载,但如果页面需要来自较慢源的数据或执行计算密集型操作,则很难实现这一点。在这些情况下,开发人员可能最初使用基本样式和快速加载数据加载页面,然后在较慢的数据可用时异步更新页面。

在数据可用时更新页面几乎总是需要使用 JavaScript。单页应用程序 (SPA) 最常用,但与服务器端渲染良好交互的新框架(如 Remix、Next.js、HTMX 或 Turbo)正变得越来越普遍。但是,每种 JavaScript 解决方案都会给应用程序带来额外的复杂性。

随着声明式 Shadow DOM 和流式 HTTP 主体的发展,开发人员拥有了无需 JavaScript 即可异步更新页面的新技术。这些技术可应用于服务器端渲染的应用,以提高响应能力,同时保持较低的复杂性。

JavaScript 解决方案

单页应用程序

单页应用程序(例如使用 React 构建)仍然是开发人员用于异步更新页面的最常见架构。SPA 的优势在于被广泛使用且为大多数开发人员所熟悉。它们非常灵活,可实现丰富且响应迅速的用户交互。

然而,SPA 带来了相当大的复杂性。SPA 是一个独立于后端的应用程序。构建和维护 SPA 的复杂性与移动应用程序的复杂性相似。SPA 必须进行彻底的独立测试并与应用程序后端集成。

很难找到精通 SPA 和后端框架的开发人员,因此开发人员通常分为前端和后端团队。

这种分离需要增加沟通、增加团队内部依赖性,并且降低对系统功能如何运作的理解。

服务器端渲染 React

HTTP 流式传输主体允许浏览器在收到整个响应之前渲染 HTML 文档的部分内容。Remix和Next.js等较新的框架利用此功能,在服务器上渲染大部分页面,并在其他数据可用时沿同一连接流式传输。每个框架还提供客户端 JavaScript,用于读取流式传输的数据并更新 DOM。

这些框架比 SPA 更简单,但保留了 SPA 的灵活性,允许开发人员在浏览器中运行任意代码。它们有点复杂,因为应用程序的大部分代码必须能够同时在浏览器和后端运行。此外,后端必须用 JavaScript 或可编译为 JavaScript 的语言编写,这对许多团队来说可能并不理想。

轻量级前端库

HTMX和Turbo等轻量级前端库提供了一种解决方案,允许开发人员使用 HTML 属性进行 HTTP 调用,并用其响应替换部分 DOM。他们可以在无需自定义 JavaScript 代码的情况下为服务器端呈现的应用程序添加一层轻量级交互性。这些库很简单,但与 JavaScript 解决方案相比,它们的交互性有限。它们也很难测试,因为必须通过使用类似于 Cypress 或 Playwright 的框架来驱动浏览器来进行测试。

不同的方法

声明式Shadow DOM

Shadow DOM 引入的模板和插槽功能为 HTML 添加了可重复使用的模板。不幸的是,直到最近,使用 Shadow DOM 的唯一方法是通过 JavaScriptattachShadow函数。现代浏览器现在支持声明式 Shadow DOM,它允许开发人员直接在 HTML 中构建影子根。

下面的示例展示了直接在 HTML 中声明的影子根。元素template包含slot由元素用slot属性填充的标签。

section>
    <template shadowrootmode="open">
        <slot name="heading"></slot>
    </template>
    <h1 slot="heading">Hello world</h1>
</section>
The browser renders this as follows.
<section>
    #shadow-root
    <h1 slot="heading">Hello world</h1>
</section>

添加流媒体

模板通过使用来自另一个元素的内容填充一个位置的元素来促进异步页面加载。假设我们正在构建一个应用程序,其中检索用户的电子邮件地址是一项缓慢的操作,我们定义标记如下。

<header><!-- header content -->/header>
<footer><!-- footer content -->/footer>
<main>
    <template shadowrootmode="open">
        <slot name="heading">
            <h1>Loading</h1>
        </slot>
    </template>

    <!-- additional main content -->

    <h1 slot="heading">Welcome, user@example.com!</h1>
</main>

这样,我们就可以在呈现用户的电子邮件之前,使用快速加载的数据呈现整个页面。使用 HTTP 流,我们首先将标签之前的所有内容发送

到浏览器,这样浏览器就可以呈现页面的部分视图。

<header><!-- header content -->/header>
<footer><!-- footer content -->/footer>
<main>
    #shadow-root
    <h1>Loading</h1>

    <!-- additional main content -->

一旦我们检索到用户的电子邮件,我们就会将其发送到流上并关闭连接,以便用户可以看到页面的其余内容。

<header><!-- header content -->/header>
<footer><!-- footer content -->/footer>
<main>
    #shadow-root
    <h1 slot="heading">Welcome, user@example.com!</h1>

    <!-- additional main content -->
</main>

设置 Shadow DOM 的样式

Shadow DOM 的一个强大之处(或令人抓狂之处,取决于您的用例)在于每个影子根都封装了自己的样式。这意味着影子 DOM 中呈现的元素不受全局 CSS 样式的影响,并且应用于给定影子根的样式不会影响影子根之外的元素。如果您的所有样式都仅限于组件,那么这可能是理想的选择,但实际上,大多数应用程序都有一些应该应用于所有地方的全局样式。

规范维护者之间正在持续讨论,寻找允许全局样式影响模板化 HTML 文档的最佳方法,但目前,建议的方法是使用link引用与头部相同文件的标签在每个影子根中复制全局样式。

<link rel="stylesheet" href="style.css">
<header><!-- header content -->/header>
<footer><!-- footer content -->/footer>
<main>
    <template shadowrootmode="open">
        <link rel="stylesheet" href="style.css">
        <slot name="heading">
            <h1>Loading</h1>
        </slot>
    </template>

    <!-- additional main content -->

    <h1 slot="heading">Welcome, user@example.com!</h1>
</main>

由于浏览器已经获取并解析了全局样式文件,因此不会对性能造成影响。缺点是这对开发人员来说很麻烦,而且全局样式不会跨光/影 DOM 边界应用。

好处

这种方法的主要优点是简单。大多数应用程序代码与原始服务器端渲染 (SSR) 应用程序相同。与其他 SSR 一样,由于所有代码都在同一个位置运行,因此调试更简单。

此外,该应用程序的测试非常简单。它们可以像任何其他服务器端渲染应用程序的测试一样编写,查看模板渲染的最终输出,而不必担心流。

用这种方法构建的应用程序速度很快。所有数据都在一个请求中获取,并在准备就绪后显示在屏幕上。在传统的 SPA 中,我们必须等待初始请求在浏览器中呈现,然后再发出其他请求来获取更多数据。

缺点

这种方法的一个主要缺点是它尚未得到广泛使用。使用它的技术的框架并不多,而且一些模板语言也不支持它。由于声明式 Shadow DOM 相对较新,因此文档很少。

此外,使用这种方法构建的应用程序无法实现与 SPA 相同的交互性。初始 HTTP 请求关闭后,应用程序的行为将与传统的 SSR 应用程序一样。

这种方法的错误处理也非常困难。它保持页面的初始 HTTP 连接打开,直到数据加载速度变慢。连接打开的时间越长,连接中断的风险就越大,可能会导致页面处于部分加载状态。在这种情况下,应用程序必须找到一种向用户显示错误的方法。

使用 Go 的示例

接下来,我们将考虑一个使用声明式 Shadow DOM 和流式 HTTP 响应的示例应用程序和代码库,以了解如何在实践中应用此技术。大多数 HTTP 服务器都支持开箱即用的 HTTP 流,因此我们应该能够使用任何语言/框架组合构建示例。Go 是一个自然的选择,因为它具有内置的并发http.Server原语,并且 Go 的模板在解析模板时会逐步将输出写入 Writer 。

缓冲写入器

Gohttp.Server通过 4kB 缓冲写入器将响应主体写入连接。即使我们逐步写入解析模板的结果,在写入 4kB 数据之前,结果也不会发送给用户。

为了确保更新尽快发送给用户,我们必须在等待长时间运行的操作之前刷新缓冲的写入器数据。这将在等待较慢的数据解析时为用户呈现尽可能多的页面。

该http.ResponseWriter接口没有 flush 方法,但大多数实现都有(包括 提供的方法http.Server)。如果可能,请使用flushon 方法http.ResponseController来安全地刷新响应编写器。

请注意,浏览器或其他网络层元素也可能缓冲响应主体流,因此刷新不能保证它们会真正将数据发送给用户。使用此方法的应用程序应在类似生产的基础架构上进行测试,以确保其正常运行。实际上,这种方法往往得到很好的支持。

一个简单的处理程序

我们的示例应用程序有一个索引处理程序,用于显示一条简单消息。为了说明我们的 Go 服务器如何传输缓慢的响应,我们为消息提供程序添加了一个人工的一秒延迟。

data := make(chan []string)
go func() {
	data <- provider.FetchAll()
}()

_ = websupport.Render(w, Resources, "index", model{Message: deferrable.New(w, data)})

在后台,我们等待消息,并在准备好后将其发送到通道。在将通道传递给模板模型之前,它会被包装在一个可延迟对象中。

可延期

可延迟结构接受一个写入器和一个通道作为属性。一旦GetOne被调用,可延迟结构就会刷新写入器(使用http.ResponseController如上所述的方法),然后等待通道的结果并返回它。

type Deferrable[T any] struct {
	writer  http.ResponseWriter
	channel chan T
}

func (d Deferrable[T]) GetOne() T {
	d.flush()
	return <-d.channel
}

func (d Deferrable[T]) flush() {
	_ = http.NewResponseController(d.writer).Flush()
}

刷新允许在等待慢速消息之前呈现模板,这意味着用户可以在等待消息时查看内容。

流式传输模板

如上所述,模板以声明方式创建影子根并包含全局样式。插槽包含占位符内容,这些内容会一直呈现,直到稍后定位插槽为止。我们调用GetOne此方法,以便在向用户发送慢速消息之前刷新编写器。

一旦收到消息,GetOne就返回,并为用户呈现模板的其余部分(包括插槽内容)。

<template shadowrootmode="open">
    <link rel="stylesheet" href="/static/style/application.css">
    <!-- header content -->
    <section>
        <slot name="content">
            <h2>Wait for it...</h2>
        </slot>
    </section>
</template>
{{ $items := .Message.GetOne }}
<div slot="content">
    <h2>
        Success!
    </h2>

    <ul class="bulleted">
        {{range $item := $items}}
        <li>{{$item}}</li>
        {{end}}
    </ul>
</div>

就是这样!应用程序的其余部分由标准 Go 服务器完成。

结论

本文展示了一个实际示例,说明如何使用 Shadow DOM、模板插槽和流式响应主体构建无需 JavaScript 的响应式应用程序。下次您想要为应用程序添加一些响应性时,请考虑使用此方法,然后再转向更重的基于 JavaScript 的解决方案。它为开发人员提供了服务器端呈现应用程序的简单性、可测试性和可维护性,同时提供了更好的用户体验。

  • 20
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢.锋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值