← 部落格

主控台中的伺服器端渲染

Text.js 0.4 可在伺服器上渲染頁面,並以儲存格差異串流傳送影格至主控台。靜態路由也會將第一個影格預先渲染至 HTML。

Adam

Text.js 0.4 新增了第二個頁面渲染位置。預設情況下,頁面的 React 樹在瀏覽器分頁中執行。匯出 runtime = "server" 的頁面則在伺服器上執行,由瀏覽器接收影格。

app/status/page.tsxtsx
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

伺服器只會傳送雜湊值相較於該連線上次收到的影格有所變動的列。每列以文字區段傳送,樣式則參照僅能附加的樣式表。寬字素、Emoji 叢集和半透明色彩都會完整保留。

影格之間至少間隔 66 ms,每個影格都會與通訊端上次接受的內容比對差異。當緩慢的連線無法繼續送出緩衝資料時,系統會跳過中間狀態,下一個影格直接跳到最新狀態。

如果串流中斷,播放器會在 x-textjs-live 標頭中帶上工作階段 ID 重新連線,從新的關鍵影格恢復同一個工作階段,並保留完整狀態。工作階段 ID 是儲存在記憶體中的 192 位元隨機值,絕不放在網址中。沒有連線的工作階段會停止渲染,並在 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 也改為附加影格而非清除,執行 hydration 的渲染器會以 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 則會關閉預先渲染。

選擇執行環境

用戶端執行環境仍是預設選項。按鍵輸入會在同一個任務中繪製,應用程式可部署至任何靜態網站託管服務。伺服器執行環境適合需要讀取私有資料的儀表板和管理介面,以及不希望將相依套件放入瀏覽器套件組合的路由。同一個應用程式可以混用兩者,在兩者之間導覽時,會將目標網址載入為新文件。

完整參考資料請見伺服器端渲染。