我编写了一个简单的 NodeJS 代码来处理 HTML 流,类似于 Next.js 的做法,但没有 React。代码首先将占位符 HTML 作为部分 HTML 片段发送,然后停止请求,并发送修改占位符的
<script>
标记。
https://gist.github.com/peat-psuwit/53a0b13dfda8781ac34cf6e66cc92ec0(编辑:也包含在下面)
在 Chrome 上,这显示了预期结果:在 HTTP 请求停止时显示文本“正在加载...”。然后,5 秒后,在同一请求中发送修改单词“Loading...”的脚本,以及单词“Hello, world!”显示。
但是,在 Firefox 上,它只是等待整个 HTTP 请求完成后再渲染。这意味着它只显示白色页面 5 秒钟,然后跳到“Hello, world!”状态。
我的理解是,正如我所说的
res.write()
,NodeJS 自动使用 HTTP 的分块传输功能将骨架、部分 HTML 代码首先传输到浏览器,从而允许看到“正在加载...”文本。我的问题是为什么它不适用于 Firefox?或者也许我应该做一些额外的事情来使它工作,并且它意外地在 Chrome 上工作?
这是 Ubuntu 22.04 上的 Chrome (Chromium) 124.0.6367.60 和 Firefox 124.0.2。
有问题的示例代码:
import { setTimeout } from "node:timers/promises";
import express from "express";
/* Simple HTML streaming concept, inspired by now NextJS do it, but without React.
* TODO: figure out why in the world does Firefox not show the "Loading..." as soon
* as it's received.
*/
async function getContent() {
await setTimeout(5000);
return {
id: "main-content",
innerHTML: "<h1>Hello, world!</h1>",
};
}
const app = express();
app.get("/", async (req, res) => {
res.status(200);
// TODO: maybe de-indent.
res.write(`
<!DOCTYPE html>
<html>
<head>
<title>Hello, world</title>
</head>
<body>
<div id="main-content">Loading...</div>
`);
let content = await getContent();
res.write(`
<script>
document.getElementById(${JSON.stringify(content.id)}).innerHTML = ${JSON.stringify(content.innerHTML)};
</script>
`);
res.write(`
</body>
</html>
`);
res.end();
});
console.log("http://localhost:3000");
app.listen(3000);
回答我自己的问题,原来“附加的东西”是这一行:
res.header('Content-Type', 'text/html; charset=utf-8');
我的逻辑是,如果没有这个标头,Firefox 可能无法推断部分响应是 HTML 或它是什么字符集。因此,Firefox 可能会缓冲响应,直到它对内容类型或字符集有足够的信心。有了标头,Firefox 就不必进行猜测,并且可以立即开始将响应解析为 HTML。
我已将修复程序以及更多评论包含在新要点中here,并且还将在下面重现。
/*
* Simple HTML streaming concept, inspired by how NextJS do it, but without
* React.
*
* It works by initially sending a skeleton HTML to the browser, but,
* importantly, this skeleton is unclosed, which allows server to stream in
* more content. However, you cannot go back to edit the HTML content you've
* already sent. Or can you???
*
* Turns out, if the additional content is a <script> tag which changes the
* innerHTML of an existing node, then you _can_ change anything you've already
* sent. This is essentially now Next.js can send stream additional stuff to the
* page under the same connection without additional requests, however RSC
* (React Server Component) payload is used instead of raw HTML.
*
* Have fun!
*
* Author: Ratchanan Srirattanamet
* SPDX-License-Identifier: CC0-1.0
*/
import { setTimeout } from "node:timers/promises";
import express from "express";
async function getContent() {
// Simulates querying DB, doing formatting etc. (Exacerbated).
await setTimeout(5000);
return {
id: "main-content",
innerHTML: "<h1>Hello, world!</h1>",
};
}
const app = express();
app.get("/", async (req, res) => {
res.status(200);
// Without this, browsers might not be able to infer from the partial response
// that this is an HTML and might not start rendering.
res.header('Content-Type', 'text/html; charset=utf-8');
// The skeleton HTML.
// TODO: maybe de-indent.
res.write(`
<!DOCTYPE html>
<html>
<head>
<title>Hello, world</title>
</head>
<body>
<div id="main-content">Loading...</div>
`);
// Now let's generate the actual content.
let content = await getContent();
// And then send the JS to instruct the browser to plug the content into the
// skeleton.
res.write(`
<script>
document.getElementById(${JSON.stringify(content.id)}).innerHTML = ${JSON.stringify(content.innerHTML)};
</script>
`);
// Now that everything is sent to the client. Be courteous and send the
// closing tags to the client. This is probably not strictly needed, but why
// not?
res.write(`
</body>
</html>
`);
// And finally, closes the request-response cycle.
res.end();
});
console.log("http://localhost:3000");
app.listen(3000);