控制台中的服务端渲染
Text.js 0.4 可在服务器上渲染页面,并以单元格增量的形式将帧流式传输到控制台。静态路由还会将首帧预渲染到 HTML 中。
Text.js 0.4 为页面增加了第二个渲染位置。默认情况下,页面的 React 树在浏览器标签页中运行。导出 runtime = "server" 的页面则在服务器上运行,浏览器接收其帧。
import { Suspense, type ReactNode } from "react";import os from "node:os";import { Box, Text } from "textjs";export const runtime = "server";async function Load(): Promise<ReactNode> { const [one, five] = os.loadavg(); return <Text>{`load ${one.toFixed(2)} ${five.toFixed(2)}`}</Text>;}export default function StatusPage(): ReactNode { return ( <Box flexDirection="column"> <Text bold>Status</Text> <Suspense fallback={<Text color="gray">Reading host…</Text>}> <Load /> </Suspense> </Box> );}Load 是一个 async function 组件。在服务器上,它会挂起到最近的 <Suspense> 或 loading.tsx,直到其 promise 完成,并且可以直接读取文件、数据库和 node:* 模块。异步组件负责加载数据,不能使用 Hook。周围的常规组件仍可使用 Hook、焦点、useInput、<Link> 和 useRouter(),这些功能在两种运行时中的行为相同。
浏览器下载的内容
服务端渲染的页面不会下发 React、Yoga 或你的路由。浏览器加载一个 gzip 压缩后约 21 kB 的帧播放器,其中包含一个 CellBuffer,以及客户端渲染器使用的同一个 CssConsoleDriver。它同样以 15 fps 的节奏播放帧,因此服务端渲染的帧产生的 console.log 调用与客户端相同。
协议
每个标签页都在服务器上建立独立会话,包含一个 React 根节点、一份内存中的路由历史记录和一个单元格缓冲区。一个端点承载双向通信:
browser tab ──(GET /__textjs/live, server-sent events)──▶ frame deltasbrowser tab ──(POST /__textjs/live, JSON)──────────────▶ keys, paste, wheel, resize, focus, Back/Forward服务器只发送自该连接收到上一帧以来哈希值发生变化的行。每一行以连续文本片段的形式发送,样式引用一个只追加的样式表。宽字素、表情符号簇和半透明颜色都会完整传输。
帧之间至少间隔 66 ms,每一帧都会与套接字最后接受的内容做差异比较。当慢速连接无法继续排出数据时,中间状态会被跳过,下一帧直接跳到最新状态。
如果流断开,播放器会在 x-textjs-live 请求头中携带会话 ID 重新连接,并从一个新的关键帧恢复同一会话,保留原有状态。会话 ID 是保存在内存中的 192 位随机值,从不放入 URL。没有连接的会话会停止渲染,并在 30 秒后卸载。
两个端点都拒绝跨站请求,每个输入事件都会按固定模式校验。一个会话每次 POST 最多接受 64 个事件,每秒约接受 240 个事件。
异步组件与数据
每个异步组件的 promise 都按会话缓存,以组件及其 props 为键,因此重试渲染时会复用正在进行的请求。路由变化或调用 router.refresh() 时,缓存会被清除:
const router = useRouter();useInput((e) => e.input === "r" && router.refresh());仅限服务端的模块以 *.server.ts 命名。如果下发到浏览器的代码导入了这类模块,或导入了 node:* 内置模块,构建就会失败,并指出导入方。
部分预渲染
部分预渲染无需服务器即可工作。text build 现在会为每个静态路由渲染一次,并将首帧作为一个小型传统 <script> 内联到路由的 HTML 中,因此控制台会在任何模块加载之前绘制。
外壳包含所有同步渲染的内容。仍在等待的 <Suspense> 边界会显示其回退内容,异步组件也包含在内,而 fetch() 在外壳渲染期间始终不会完成。
JavaScript 启动后,客户端渲染器或帧播放器会流式传输实际帧。由于 0.4 也采用追加帧而非清空的方式,执行水合的渲染器会以 initialClear: false 启动,在外壳下方输出日志,其幕布会在同一次 DevTools 绘制中将外壳移出视野。控制台仅由外壳清空一次。
✓ Prerendering console shells (5/5) · 1 server-rendered, frame player 56.5 kB (21.3 kB gzip)外壳按配置的 viewport 尺寸渲染,默认为 120 × 36,因为构建时无法知道访客的控制台尺寸。启用 viewport.responsive 后,第一个实时帧会调整尺寸以适应停靠的控制台。动态路由不渲染外壳,在 text.config.ts 中设置 ppr: false 可关闭预渲染。
选择运行时
客户端运行时仍是默认选项。按键输入在同一任务中完成绘制,应用可部署到任何静态托管服务。服务端运行时适合读取私有数据的仪表盘和管理视图,也适合那些你不希望将其依赖打包到浏览器中的路由。一个应用可以混用两种运行时,在两者之间导航时,会将目标 URL 作为新文档加载。
完整参考文档见服务端渲染。