原文:https://lamplightdev.com/blog/2024/01/10/streaming-html-out-of-order-without-javascript/
让我们从一个演示开始:https://ooo.lamplightdev.workers.dev:
这是一个简单的页面,显示了一个包含10个项目的列表。尝试在浏览器中启用和禁用JavaScript,你会注意到几个事情:
'应用外壳'首先渲染 - 你会看到标题和页脚,但列表项目将要渲染的地方有一个加载占位符。
一秒钟后,加载占位符被列表项目替换 - 但每个项目本身都有一个加载占位符。
然后项目内容无序渲染,替换加载占位符 - 你首先看到项目5,然后随着它们的生成,看到其他项目。
如果查看页面源代码,你会发现HTML按发送顺序排列 - 而不是渲染顺序。
该页面使用了无自定义元素的Shadow DOM。
挺不错的,对吧?虽然这可能是一个刻意构建的例子,但这是一种有趣的技术,以前不使用JavaScript是不可能实现的。
查看此演示的代码,或者继续阅读以了解其工作原理。
背景
HTML流式传输
流式传输HTML的概念 - 将HTML从Web服务器发送到浏览器时,将其分块发送 - 并不新鲜。在现代前端框架和单页面应用盛行的初期,它似乎被搁置了一旁 - 在那里,整个页面在浏览器中生成 - 但随着摆锤向着使用全栈框架进行服务器端渲染的方向摆动,流式响应再次变得流行起来。
与等待整个响应生成后再将其发送到浏览器相比,流式传输HTML的优势是显而易见的 - 你可以立即渲染一些内容,向用户指示正在发生的事情,并且你可以在等待响应的耗时部分生成时提前下载CSS和JavaScript等资源。
直到目前为止,缺乏的是一种无序流式传输HTML的方法 - 也就是说,按照生成的顺序将HTML分块流式传输,而不用担心将分块按顺序发送到浏览器 - 并且仍然使浏览器按照正确的顺序渲染HTML块,就像上面的演示一样。
现代全栈框架通过使用各种聪明的技术实现了这种功能,所有这些技术都需要对特定框架的支持以及大量的JavaScript。如果你的用例可以接受这一点,那可能没什么问题,但是如果我们能够在没有任何JavaScript或框架的情况下实现相同的效果会怎样呢? 现在你可以了。
Shadow DOM
Shadow DOM是一种在页面的其余部分隔离渲染DOM的方法。虽然通常与自定义元素相关联,但Shadow DOM可以与任何HTML标签一起使用,比如普通的<div>
标签。
它还有插槽的概念 - 标记为插槽的标签可以从父标签的其他位置渲染HTML进来,方法是在要渲染的标签上指定一个slot
属性。在这里,Shadow 根节点附加到外部<div>
标签,并且内部<div>
标签被渲染到该Shadow 根节点中的插槽中:
<div>
#shadowroot
<header>标题</header>
<main>
<slot name="content"></slot>
</main>
<footer>页脚</footer>
<div slot="content">
这个div将被渲染到上面的插槽中。神奇!
</div>
</div>
要求
那么,如何使用Shadow DOM无序流式传输HTML呢?你需要一些东西:
- 支持流式响应的http服务器。你很幸运,几乎所有语言都普遍支持这一点。我选择了Hono,因为它是一个轻量级服务器,基于Web标准构建,在node上运行以及各种边缘平台上运行。值得注意的是,这并不依赖于JavaScript后端 - 同样的效果可以在PHP、Java、Go等上实现。
注意
所有浏览器都支持流式传输HTML,但是Safari似乎对何时触发流式传输有较高的阈值 - 这似乎在512字节左右。这样做的结果是,Safari会缓冲内容,直到达到该阈值,然后渲染它所拥有的内容再进行流式传输剩余内容。
- 支持流式传输的模板语言
。理论上,你不需要模板语言 - 你可以手工编写HTML并手动管理流式传输 - 但那是很多工作。在JavaScript世界中,没有多少独立的模板语言支持流式传输,但最近有一个叫做SWTL的项目支持。SWTL被创建用于Service Workers,但由于我们始终使用Web标准,因此也可以在服务器上使用。SWTL的另一个好处是你几乎可以将任何东西都放入其中 - 异步函数、生成器、数组、响应 - 它都能处理。
- 声明性Shadow DOM - 直到最近,自定义Shadow DOM是一种仅限浏览器的技术 - 只能使用JavaScript在浏览器中创建Shadow DOM - 但现在,得益于声明性Shadow DOM(DSD),你可以在服务器上创建Shadow DOM,而浏览器会通过在
<template>
标签上使用新的shadowrootmode
属性来渲染它而无需JavaScript。然后,Shadow 根节点会自动附加到包含元素上:
<div>
<template shadowrootmode="open">
<header>标题</header>
<main>
<slot name="content"></slot>
</main>
<footer>页脚</footer>
</template>
<div slot="content">
这个div将在没有JavaScript的情况下渲染到上面的插槽中。更多魔法!
</div>
</div>
浏览器支持
截至撰写本文时,DSD在Chrome和Safari中得到支持。Firefox也即将支持,预计将在2024年2月/3月(版本123/124)发布 - 因此,如果你的目标是面向现代浏览器,这很快将成为一个你可以自由使用的技术。
对于尚未支持它的浏览器,有一个polyfill可用,如果你确实需要它的话。
将其组合在一起
那么,初始演示是如何创建的呢?让我们通过一个简化的代码示例来分解它:
import { Hono } from 'hono';
import { stream } from 'hono/streaming';
import { render, html } from 'swtl';
import {
delayed,
createReadableStreamFromAsyncGenerator
} from './utils.js';
const app = new Hono();
app.get('/', (ctx) => {
/*
由SWTL提供的`html`标记模板字面量允许传入异步函数。
在这里,插槽内容被包裹在一个引入人为延迟的函数中。
*/
const template = ({ name }) => html`
<html>
<head>
<title>流式示例>
</head>
<body>
<div>
<template shadowrootmode="open">
<header>标题</header>
<main>
<slot name="content"></slot>
</main>
<footer>页脚</footer>
</template>
<!--
上面的HTML首先发送给浏览器
-->
<!--
添加了人为延迟的插槽内容
模拟服务器响应缓慢的情况:
-->
${delayed(1000, html`
<p slot="content">
你好,${name}!
</p>
`)}
<!--
一旦延迟内容已发送,剩余的HTML将发送给浏览器
-->
</div>
</div>
</body>
</html>
`;
return stream(ctx, async (stream) => {
ctx.res.headers.set('Content-Type', 'text/html');
/*
最后,`render`方法将输出转换为异步生成器,
然后将其转换为编码流并作为生成时管道传输到响应中。
*/
await stream.pipe(
createReadableStreamFromAsyncGenerator(
render(
template({ name: 'Ada' })
)
)
);
});
});
export default app;
就是这样!浏览此演示的代码并自己尝试一下。我很乐意听到你对这种技术的想法 - 以及你能想到的任何新奇用例 - 所以请联系我。下次见👋。
感谢@passle,SWTL的作者,为本文进行校对和反馈。_