العرض على الخادم والعرض المسبق الجزئي
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 من صفحة:
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 العرض عند أقرب <Suspense> أو loading.tsx حتى يُحسم الوعد الخاص به:
export const runtime = "server";async function Deployments({ team }: { team: string }) { const rows = await db.deployments.list({ team }); return <Table data={rows} columns={COLUMNS} />;}يُخزّن كل وعد مؤقتًا لكل جلسة، بمفتاح يعتمد على المكوّن وخصائصه، لذا تعيد محاولة العرض استخدام الطلب الجاري بالفعل. تُمسح الذاكرة المؤقتة عند تغير المسار وعند استدعاء router.refresh(). لإعادة الوصول إلى البيانات بعد الإدخال، استدعِ router.refresh():
const router = useRouter();useInput((e) => e.input === "r" && router.refresh());ينبغي أن تكون الخصائص الممررة إلى المكوّنات غير المتزامنة قابلة للتسلسل: سلاسل نصية، أرقام، كائنات عادية، مصفوفات، Date، URL، Map، Set وعناصر. لا تدخل خصائص الدوال في المفتاح (مرّر بيانات، لا دوال رد نداء). تخزّن كل جلسة 256 وعدًا على الأكثر مؤقتًا. يمكن أيضًا تغليف مكوّن يدويًا باستخدام 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لكل عملية (تُزال أولًا الجلسة التي مضى على انقطاعها أطول وقت).
export default defineConfig({ live: { maxSessions: 512, reconnectGraceMs: 60_000 },});العرض المسبق الجزئي
يعرض text build كل مسار ثابت مرة واحدة، بحجم viewport المضبوط، ويضمّن ذلك الإطار الأول داخل HTML للمسار كعنصر <script> تقليدي صغير. تعرض وحدة التحكم الإطار قبل تحميل أي وحدة:
- الغلاف الثابت. كل ما يُعرض بشكل متزامن. يعرض كل حد
<Suspense>لا يزال ينتظر محتواه البديل، بما في ذلك مكوّنات الخادم غير المتزامنة (التي لا تعمل أثناء عرض الغلاف)، وuse()والوحدات المحمّلة عند الحاجة. لا يُحسمfetch()مطلقًا أثناء عرض الغلاف، لذا تبقى البيانات المحمّلة في التأثيرات في حالة التحميل أيضًا. - الفجوات الديناميكية. عند بدء 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).