رندر سمت سرور و پیش‌رندر جزئی

⁦runtime⁩ = "⁦server⁩": مسیرهای رندرشده در سرور و ارسال جریانی آن‌ها به‌صورت فریم؛ پوسته‌های پیش‌رندرشدهٔ کنسول.

درخت React یک مسیر را در سرور رندر کنید و فریم‌های آن را به کنسول جریان دهید. نخستین فریم هر مسیر ایستا را در HTML آن پیش‌رندر کنید.

دو محیط اجرا

هر صفحه در یکی از دو مکان رندر می‌شود:

کلاینت (پیش‌فرض)سرور (runtime = "server")
محل اجرای درخت Reactزبانهٔ مرورگرtext dev / text start / آداپتور bun (Bun یا Node)
دریافت مرورگرکل برنامه: React، ⁦Yoga WASM⁩، مسیرهای شماپخش‌کنندهٔ فریم، حدود 21 kB با gzip
دسترسی به دادهfetch() به کنترل‌گرهای مسیرمستقیم: پایگاه‌های داده، فایل‌ها، node:*، اطلاعات محرمانه
کامپوننت‌های ناهمگاماستفاده از use() یا افکت‌هاپشتیبانی از کامپوننت‌های async function
تأخیر فشردن کلیدهمان تسک، همگامیک رفت‌وبرگشت به سرور
اجرا روی میزبان ایستابلهخیر، به text start یا bun dist/server.js نیاز دارد

هر دو محیط اجرا فریم‌های یکسانی تولید می‌کنند. پخش‌کنندهٔ فریم سلول‌ها را با همان CssConsoleDriver کدگذاری می‌کند و با همان سرعت حدود 15 فریم‌برثانیه پیش می‌رود، بنابراین فریم رندرشده در سمت سرور بایت‌به‌بایت با فراخوانی console.log در کلاینت مطابقت دارد.

فعال‌سازی

متغیر runtime را از یک صفحه صادر کنید:

app/status/page.tsxtsx
import { Suspense, type ReactNode } from "react";import os from "node:os";import { Box, Text } from "@textjs/app";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>  );}

متغیر runtime برای هر صفحه، به‌صورت یک رشتهٔ ایستای صادرشده خوانده می‌شود (اعلان نوع، as const یا صدور مجدد پذیرفته است؛ مقدار محاسبه‌شده پذیرفته نیست). چیدمان‌ها، loading.tsx، error.tsx و not-found.tsx هر جا که صفحه‌شان رندر شود رندر می‌شوند: چیدمان مشترک میان صفحات کلاینت و سرور در هر دو مکان اجرا و به مرورگر ارسال می‌شود، درحالی‌که چیدمان‌ها و مرزهای بخشی که فقط صفحات سرور دارد در سرور می‌مانند. هوک‌ها، فوکوس، useInput، <Link> و useRouter() همان رفتار کلاینت را دارند.

زمان استفاده از هر محیط اجرا

از محیط اجرای کلاینت (پیش‌فرض) برای موارد زیر استفاده کنید:

  • رابط‌های کاربری با تایپ زیاد که باید هر بار فشردن کلید را در همان تسک ترسیم کنند، مانند ویرایشگرها و فیلترها.
  • برنامه‌هایی که روی میزبان ایستا یا CDN مستقر می‌شوند.
  • ابزارهایی با قابلیت کار آفلاین.

از محیط اجرای سرور برای موارد زیر استفاده کنید:

  • داشبوردها و نماهای مدیریتی که پایگاه‌های داده، فایل‌ها، صف‌ها یا APIهای خصوصی را می‌خوانند. اطلاعات احراز هویت در سرور می‌مانند.
  • مسیرهایی با وابستگی‌های سنگین (SDKها، تجزیه‌گرها، جدول‌های نحو) که نمی‌خواهید در بستهٔ مرورگر باشند.
  • صفحاتی که باید در شبکه‌های کند سریع بارگیری شوند. مرورگر به‌جای کل برنامه، پخش‌کنندهٔ 21 kB (gzip) را دریافت می‌کند.

می‌توانید در یک برنامه از محیط‌های اجرای مختلف استفاده کنید. ناوبری بین آن‌ها، URL مقصد را به‌صورت یک سند جدید بارگذاری می‌کند. باندل مرورگر هر صفحهٔ سرور را با یک جای‌نگهدار جایگزین می‌کند، بنابراین ماژول‌های سرور و هر چیزی که وارد می‌کنند هرگز به کلاینت ارسال نمی‌شوند.

ماژول‌های مختص سرور را با الگوی *.server.ts نام‌گذاری کنید. اگر کدی که به مرورگر ارسال می‌شود (صفحهٔ کلاینت یا چیدمانی که صفحهٔ کلاینت استفاده می‌کند) یک ماژول *.server.* یا ماژول داخلی node:* را وارد کند، ساخت شکست می‌خورد و نام واردکننده را اعلام می‌کند.

⁦Middleware⁩ همان‌طور که از سندها محافظت می‌کند، از مسیرهای سرور هم محافظت می‌کند: برای مسیر مقصد، پیش از باز شدن نشست، پیش از هر ناوبری درون نشست (router.push، بازگشت و رفتن به جلو)، و دوباره هر زمان که کوکی‌های نشست یا Authorization تغییر کنند اجرا می‌شود. اگر قرار باشد تغییرمسیر دهد، رد کند یا بازنویسی کند، مرورگر به‌جای آن URL را به‌صورت سند بارگذاری می‌کند و middleware به آن درخواست پاسخ می‌دهد.

کامپوننت‌های ناهمگام سرور

در سرور، هر کامپوننت async function تا زمان تکمیل promise خود در نزدیک‌ترین مرز <Suspense> یا loading.tsx معلق می‌شود:

app/deployments/live/page.tsxtsx
export const runtime = "server";async function Deployments({ team }: { team: string }) {  const rows = await db.deployments.list({ team });  return <Table data={rows} columns={COLUMNS} />;}

هر promise برای هر نشست کش می‌شود و کلید آن بر اساس کامپوننت و props تعیین می‌شود، بنابراین رندر مجدد از درخواست در حال اجرا استفاده می‌کند. با تغییر مسیر و فراخوانی router.refresh() کش پاک می‌شود. برای اجرای دوبارهٔ دسترسی به داده پس از ورودی، تابع router.refresh() را فراخوانی کنید:

const router = useRouter();useInput((e) => e.input === "r" && router.refresh());

⁦props⁩ ارسالی به کامپوننت‌های ناهمگام باید سریال‌سازی‌پذیر باشند: رشته‌ها، اعداد، اشیای ساده، آرایه‌ها، Date، URL، Map، Set و عناصر. props تابعی بخشی از کلید نیستند (داده ارسال کنید، نه callback). هر نشست حداکثر ⁦256 promise⁩ را کش می‌کند. همچنین می‌توانید یک کامپوننت را به‌صورت دستی با serverComponent(fn) از @textjs/app/server-components بپوشانید.

هوک‌ها (useState، useInput، …) درون کامپوننت async function کار نمی‌کنند. برای تعامل، یک کامپوننت معمولی پیرامون یا زیر آن رندر کنید.

نحوهٔ کار

browser tab ──(GET /__textjs/live, server-sent events)──▶ frame deltasbrowser tab ──(POST /__textjs/live, JSON)──────────────▶ keys, paste, wheel, resize, focus, Back/Forward
  • یک نشست برای هر زبانه. هر بارگذاری صفحه، نشست مختص خود را باز می‌کند: یک ریشهٔ React، یک تاریخچهٔ مسیریاب در حافظه و یک بافر سلول روی سرور. شناسه‌های نشست مقادیر تصادفی 192 بیتی هستند که مرورگر فقط در حافظه نگه می‌دارد.
  • تغییرات سلول‌ها. سرور فقط ردیف‌هایی را می‌فرستد که هش آن‌ها از آخرین فریمی که آن اتصال دریافت کرده تغییر کرده است. هر ردیف به‌صورت قطعه‌های متن با ارجاع به جدول سبک‌هایی ارسال می‌شود که فقط می‌توان به انتهای آن افزود، و نویسه‌های عریض، خوشه‌های ایموجی و رنگ‌های نیمه‌شفاف دقیقاً حفظ می‌شوند.
  • فشار برگشتی. فریم‌ها دست‌کم 66 ms فاصله دارند و تفاوت هر فریم با آخرین چیزی که سوکت پذیرفته محاسبه می‌شود. وقتی تخلیهٔ داده در یک اتصال کند متوقف می‌شود، حالت‌های میانی کنار گذاشته می‌شوند و فریم بعدی مستقیم به آخرین حالت می‌رود.
  • اتصال مجدد. اگر جریان قطع شود، پخش‌کننده با شناسهٔ نشست خود در سرآیند x-textjs-live (هرگز در URL) دوباره متصل می‌شود و همان نشست را، با حفظ وضعیت آن، از یک فریم کلیدی تازه ادامه می‌دهد. به‌روزرسانی‌های ازدست‌رفتهٔ نوار نشانی دوباره اعمال می‌شوند. نشستی که اتصالی ندارد رندر را متوقف می‌کند و پس از 30 ثانیه از حالت نصب خارج می‌شود؛ اگر سرور آن را فراموش کرده باشد، پخش‌کننده نشست جدیدی باز می‌کند.
  • تاریخچه. بازگشت و رفتن به جلو هر دو تاریخچه را به تعداد یکسانی از ورودی‌ها جابه‌جا می‌کنند و فراخوانی router.back() روی سرور، تاریخچهٔ مرورگر را هم جابه‌جا می‌کند.
  • امنیت. هر دو نقطهٔ پایانی درخواست‌های بین‌سایتی را رد می‌کنند (Sec-Fetch-Site و Origin؛ سرآیند X-Forwarded-Host فقط با server.trustProxy لحاظ می‌شود). درخواست‌های POST ورودی به بدنهٔ JSON و سرآیند x-textjs-live نیاز دارند. هر رویداد با یک طرح‌وارهٔ ثابت اعتبارسنجی می‌شود و مقصدهای ناوبری باید مسیرهایی در مبدأ شما باشند. فقط صفحه‌های موجود در مانیفست app/ شما قابل رندر هستند. نشست‌ها به کوکی‌هایی مقید هستند که با آن‌ها باز شده‌اند.
  • محدودیت‌ها. حداکثر 64 رویداد در هر POST و حدود 240 رویداد در ثانیه برای هر نشست (429 برای بیشتر از آن)، مهلت 5 ثانیه‌ای برای بدنه، 32 نشست برای هر نشانی کلاینت و maxSessions برای هر فرایند (نشستی که بیشترین مدت قطع اتصال را داشته، زودتر حذف می‌شود).
text.config.tsts
export default defineConfig({  live: { maxSessions: 512, reconnectGraceMs: 60_000 },});

پیش‌رندر جزئی

دستور text build هر مسیر ایستا را یک بار در اندازهٔ پیکربندی‌شدهٔ viewport رندر می‌کند و نخستین فریم را به‌صورت یک <script> کلاسیک کوچک در HTML مسیر قرار می‌دهد. کنسول پیش از بارگیری هر ماژولی ترسیم می‌شود:

  1. پوستهٔ ایستا. هر چیزی که همگام رندر می‌شود. هر مرز <Suspense> که هنوز منتظر است جایگزین خود را نشان می‌دهد، از جمله کامپوننت‌های ناهمگام سرور (که هنگام رندر پوسته اجرا نمی‌شوند)، use() و ماژول‌های تنبل. فراخوانی fetch() هرگز هنگام رندر پوسته تکمیل نمی‌شود، بنابراین داده‌های بارگیری‌شده در افکت‌ها هم در حالت بارگیری می‌مانند.
  2. حفره‌های پویا. وقتی JavaScript راه‌اندازی می‌شود، رندرکنندهٔ کلاینت یا پخش‌کنندهٔ فریم، فریم واقعی را جریان می‌دهد. این فریم به انتهای پوسته افزوده می‌شود و پردهٔ خود فریم هیدراته‌شونده، پوسته را در همان نوبت ترسیم DevTools از دید خارج می‌کند. پوسته زمان ترسیم خود را ثبت می‌کند، بنابراین نخستین فریم هیدراته‌شونده منتظر پایان بازهٔ پس از پاک‌سازی DevTools می‌ماند تا وارد مسیر کند آن نشود.

هنگام بارگیری، فقط پوسته کنسول را پاک می‌کند. رندرکنندهٔ هیدراته‌کننده در حالت افزودن به انتها (initialClear: false) آغاز می‌شود، بنابراین چیزی چشمک نمی‌زند و پوسته فقط در مسیری ترسیم می‌شود که برای آن رندر شده است. مسیرهای پویا ([id]، force-dynamic) پوسته‌ای رندر نمی‌کنند.

 ✓ Prerendering console shells (5/5) · 1 server-rendered, frame player 56.5 kB (21.3 kB gzip)

برای غیرفعال کردن آن، از ppr: false در فایل text.config.ts استفاده کنید. پیش‌رندر، کامپوننت‌های شما را هنگام ساخت اجرا می‌کند، بنابراین در کد سطح بالای ماژول از متغیرهای سراسری مختص مرورگر استفاده نکنید. مسیری که هنگام پیش‌رندر خطا پرتاب کند، redirect() / notFound() را فراخوانی کند یا ناوبری انجام دهد، پوسته‌ای دریافت نمی‌کند (صفحهٔ خطا هرگز در HTML گنجانده نمی‌شود) و فرایند ساخت دلیل آن را به شما می‌گوید.

پوسته در اندازهٔ viewport.width × viewport.height (پیش‌فرض 120 × 36) رندر می‌شود، زیرا فرایند ساخت نمی‌تواند اندازهٔ کنسول بازدیدکننده را بداند؛ سپس فریم هیدراته‌شونده به اندازهٔ واقعی کنسول تغییر می‌کند. پوسته یک <script> درون‌خطی است: اگر خط‌مشی امنیت محتوا اسکریپت‌های درون‌خطی را ممنوع می‌کند، یا با هش به آن اجازه دهید یا ppr: false را تنظیم کنید.

استقرار

دستور text build فایل‌های زیر را می‌نویسد:

فایلکاربرد
index.html، <route>/index.htmlمسیرهای کلاینت با پوسته‌هایشان. روی هر میزبان ایستا کار می‌کند.
assets/player-[hash].jsپخش‌کنندهٔ فریم.
.text/server/live.mjsمسیرهای سرور، ⁦App Router⁩ و رندرکنندهٔ کنسول.
.text/pages/…، .text/live.htmlاسناد مسیرهای سرور.
.text/ansi/…نخستین فریم‌ها برای curl و دیگر کلاینت‌های ترمینال.

دستور text start و آداپتور bun همهٔ آن‌ها را سرو می‌کنند. مسیرهای سرور به یک فرایند طولانی‌مدت نیاز دارند: آداپتورهای static، vercel و cloudflare ساخت را با شکست متوقف می‌کنند (TX030).

ویرایش این صفحه در GitHub