renderToReadableStream
renderToReadableStream מעבד עץ React לזרם אינטרנט קריא.
const stream = await renderToReadableStream(reactNode, options?)הפניה
renderToReadableStream(reactNode, options?)
התקשר ל-renderToReadableStream כדי לעבד את עץ ה-React שלך כ-HTML לתוך זרם אינטרנט קריא.
import { renderToReadableStream } from 'react-dom/server';
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}בלקוח, התקשר ל-hydrateRoot כדי להפוך את ה-HTML שנוצר על ידי השרת לאינטראקטיבי.
פרמטרים
-
reactNode: צומת React שברצונך להציג ל-HTML. לדוגמה, אלמנט JSX כמו<App />. הוא צפוי לייצג את המסמך כולו, ולכן הרכיבAppצריך לעבד את התג<html>. -
אופציונלי
options: אובייקט עם אפשרויות סטרימינג.- אופציונלי
bootstrapScriptContent: אם צוין, מחרוזת זו תמוקם בתג<script>מוטבע. - אופציונלי
bootstrapScripts: מערך של כתובות אתרים של מחרוזות לתגיות<script>לפליטה בדף. השתמש בזה כדי לכלול את<script>שקורא ל-hydrateRoot. השמט אותו אם אינך רוצה להפעיל את React על הלקוח בכלל. - אופציונלי
bootstrapModules: כמוbootstrapScripts, אבל פולט<script type="module">במקום זאת. - אופציונלי
identifierPrefix: קידומת מחרוזת React uses עבור מזהים שנוצרו על ידיuseId. שימושי כדי למנוע התנגשויות בעת שימוש במספר שורשים באותו עמוד. חייבת להיות אותה קידומת כמו שהועברה ל-hydrateRoot. - אופציונלי
namespaceURI: מחרוזת עם השורש שם URI עבור הזרם. ברירת המחדל היא HTML רגילה. עוברים'http://www.w3.org/2000/svg'עבור SVG או'http://www.w3.org/1998/Math/MathML'עבור MathML. - אופציונלי
nonce: מחרוזתnonceכדי לאפשר סקריפטים עבורscript-srcContent-Security-Policy. - אופציונלי
onError: התקשרות חוזרת הנפתחת בכל פעם שיש שגיאת שרת, בין אם ניתנת לשחזור או לא. כברירת מחדל, זה קורא רק __.K_T אם תעקוף אותו ליומן דוחות קריסה, ודא שאתה עדיין קורא ל-console.error. אתה יכול גם use אותו כדי להתאים את קוד המצב לפני שהקליפה תיפלט. - אופציונלי
progressiveChunkSize: מספר הבתים בנתח. קרא עוד על היוריסטית ברירת המחדל. - אופציונלי
signal: אות ביטול המאפשר לך להפסיק את עיבוד השרת ולעבד את השאר בלקוח.
- אופציונלי
מחזירה
renderToReadableStream מחזיר הבטחה:
- אם רינדור ה-shell יצליח, ההבטחה הזו תיפתר ל-זרם אינטרנט קריא.
- אם עיבוד הקליפה נכשל, ההבטחה תידחה. השתמש בזה כדי להפיק מעטפת חלופית.
לזרם המוחזר יש מאפיין נוסף:
allReady: הבטחה שנפתרת כאשר כל העיבוד הושלם, כולל הן את המעטפת וכל התוכן הנוסף. אתה יכולawait stream.allReadyלפני החזרת תגובה לסורקים [עבור סורקים]. generation.](#waiting-for-all-content-to-load-for-crawlers-and-static-generation) אם תעשה זאת, לא תקבל שום טעינה מתקדמת. הזרם יכיל את ה-HTML הסופי.
שימוש
עיבוד עץ React כ-HTML לזרם אינטרנט קריא
התקשר ל-renderToReadableStream כדי לעבד את עץ ה-React שלך כ-HTML לתוך זרם אינטרנט קריא:
import { renderToReadableStream } from 'react-dom/server';
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}יחד עם רכיב השורש, עליך לספק רשימה של נתיבי bootstrap <script>. רכיב השורש שלך צריך להחזיר את המסמך כולו כולל תג השורש <html>.
לדוגמה, זה עשוי להיראות כך:
export default function App() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css"></link>
<title>My app</title>
</head>
<body>
<Router />
</body>
</html>
);
}React יזריק את doctype ואת תגי bootstrap <script> שלך לזרם HTML שיתקבל:
<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>בלקוח, סקריפט האתחול שלך צריך לייבש את כל document עם קריאה ל-hydrateRoot:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);פעולה זו תצרף מאזיני אירועים ל-HTML שנוצר על ידי השרת ויהפוך אותו לאינטראקטיבי.
Deep Dive
כתובות ה-URL הסופיות של הנכסים (כמו קבצי JavaScript וCSS) עוברות גיבוב לעתים קרובות לאחר הבנייה. לדוגמה, במקום styles.css אתה עלול לקבל styles.123456.css. גיבוב של שמות קבצים סטטיים של נכסים מבטיח שלכל מבנה נפרד של אותו נכס יהיה שם קובץ שונה. זהו useמלא כי use הוא מאפשר לך לאפשר בבטחה שמירת מטמון לטווח ארוך עבור נכסים סטטיים: קובץ עם שם מסוים לעולם לא ישנה תוכן.
עם זאת, אם אינך מכיר את כתובות ה-URL של הנכסים עד לאחר הבנייה, אין לך דרך להכניס אותם לקוד המקור. לדוגמה, קידוד קשיח של "/styles.css" ל-JSX כמו קודם לא יעבוד. כדי להרחיק אותם מקוד המקור שלך, רכיב השורש שלך יכול לקרוא את שמות הקבצים האמיתיים ממפה שהועברה כאביזר:
export default function App({ assetMap }) {
return (
<html>
<head>
<title>My app</title>
<link rel="stylesheet" href={assetMap['styles.css']}></link>
</head>
...
</html>
);
}בשרת, עבד את <App assetMap={assetMap} /> והעביר את ה-assetMap שלך עם כתובות ה-URL של הנכס:
// You'd need to get this JSON from your build tooling, e.g. read it from the build output.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['/main.js']]
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}מכיוון שהשרת שלך מעבד כעת <App assetMap={assetMap} />, עליך לרנדר אותו עם assetMap גם בלקוח כדי למנוע שגיאות הידרציה. אתה יכול לעשות סדרה ולהעביר את assetMap ללקוח כך:
// You'd need to get this JSON from your build tooling.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
// Careful: It's safe to stringify() this because this data isn't user-generated.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}בדוגמה שלמעלה, האפשרות bootstrapScriptContent מוסיפה תג <script> מוטבע נוסף שקובע את המשתנה הגלובלי window.assetMap בלקוח. זה מאפשר לקוד הלקוח לקרוא את אותו assetMap:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);גם הלקוח וגם השרת מעבדים את App עם אותו אבזר assetMap, כך שאין שגיאות הידרציה.
הזרמת תוכן נוסף תוך כדי טעינתו
סטרימינג מאפשר ל-user להתחיל לראות את התוכן עוד לפני שכל הנתונים נטענו על השרת. לדוגמה, שקול דף פרופיל המציג שער, סרגל צד עם חברים ותמונות ורשימת פוסטים:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}תאר לעצמך שטעינת נתונים עבור <Posts /> לוקחת קצת זמן. באופן אידיאלי, תרצה להציג את שאר תוכן דף הפרופיל ל-user מבלי לחכות לפוסטים. לשם כך, עטפו את Posts בגבול <Suspense>:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}זה אומר לReact להתחיל להזרים את ה-HTML לפני שPosts יטען את הנתונים שלו. React ישלח תחילה את ה-HTML עבור החזרת הטעינה (PostsGlimmer), ולאחר מכן, כאשר Posts יסיים לטעון את הנתונים שלו, React ישלח את ה-HTML הנותר יחד עם תג <script> מוטבע שמחליף את ה-fallback הטעינה ב-__TK_12 הזה. מנקודת המבט של ה-user, הדף יופיע תחילה עם ה-PostsGlimmer, מאוחר יותר מוחלף ב-Posts.
אתה יכול להמשיך לקנן <Suspense> גבולות כדי ליצור רצף טעינה מפורט יותר:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}בדוגמה זו, React יכול להתחיל להזרים את הדף אפילו מוקדם יותר. רק ProfileLayout וProfileCover חייבים לסיים את הרינדור תחילה כיuse הם אינם עטופים בשום גבול <Suspense>. עם זאת, אם Sidebar, Friends, או Photos צריכים לטעון נתונים מסוימים, React ישלח את ה-HTML עבור BigSpinner החזרה במקום זאת. לאחר מכן, ככל שיהיו יותר נתונים זמינים, תוכן נוסף ימשיך להיחשף עד שכולו יהיה גלוי.
סטרימינג לא צריך לחכות לטעינת React עצמה בדפדפן, או שהאפליקציה שלך תהפוך לאינטראקטיבית. תוכן HTML מהשרת ייחשף בהדרגה לפני טעינת כל אחד מהתגים <script>.
קרא עוד על אופן הפעולה של סטרימינג HTML.
ציון מה נכנס למעטפת
החלק של האפליקציה שלך מחוץ לכל גבולות <Suspense> נקרא המעטפת:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}זה קובע את הטעינה המוקדמת ביותר של state שה-user עשוי לראות:
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>אם תעטפו את האפליקציה כולה לתוך גבול <Suspense> בשורש, המעטפת תכיל רק את הספינר הזה. עם זאת, זו לא חווית user נעימה מכיוון שuse לראות ספינר גדול על המסך יכול להרגיש איטי יותר ומעצבן יותר מאשר לחכות עוד קצת ולראות את הפריסה האמיתית. זו הסיבה שבדרך כלל תרצה למקם את גבולות <Suspense> כך שהמעטפת תרגיש מינימלית אך שלמה - כמו שלד של פריסת העמוד כולה.
הקריאה האסינכרונית ל-renderToReadableStream תפתור ל-stream ברגע שהמעטפת כולה טופלה. בדרך כלל, לאחר מכן תתחיל להזרים על ידי יצירה והחזרה של תגובה עם ה-stream הזה:
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}עד להחזרת ה-stream, ייתכן שרכיבים בגבולות <Suspense> מקוננים עדיין טוענים נתונים.
הרישום קורס בשרת
כברירת מחדל, כל השגיאות בשרת נרשמות למסוף. אתה יכול לעקוף התנהגות זו כדי לרשום דוחות קריסה:
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}אם אתה מספק יישום onError מותאם אישית, אל תשכח גם לרשום שגיאות למסוף כמו לעיל.
שחזור משגיאות בתוך המעטפת
בדוגמה זו, המעטפת מכילה ProfileLayout, ProfileCover ו-PostsGlimmer:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}אם מתרחשת שגיאה במהלך רינדור הרכיבים האלה, ל-React לא יהיה שום HTML משמעותי לשלוח ללקוח. עטוף את הקריאה renderToReadableStream שלך ב-try...catch כדי לשלוח HTML מיתון שלא מסתמך על עיבוד שרת כמוצא אחרון:
async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}אם יש שגיאה בעת יצירת המעטפת, גם onError וגם בלוק catch שלך יופעלו. השתמש ב-onError לדיווח על שגיאות וב-use בלוק catch כדי לשלוח את המסמך HTML החלפה. HTML לא חייב להיות דף שגיאה. במקום זאת, תוכל לכלול מעטפת חלופית שמציגה את האפליקציה שלך בלקוח בלבד.
שחזור משגיאות מחוץ למעטפת
בדוגמה זו, הרכיב <Posts /> עטוף ב-<Suspense> כך שהוא לא חלק מהקליפה:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}אם מתרחשת שגיאה ברכיב Posts או במקום כלשהו בתוכו, React ינסה להתאושש ממנו:
- הוא יפלוט את החזרת הטעינה עבור גבול
<Suspense>הקרוב ביותר (PostsGlimmer) לתוך HTML. - זה “יוותר” על הניסיון לרנדר את התוכן
Postsבשרת יותר. - כאשר הקוד JavaScript נטען על הלקוח, React תנסה לעבד את
Postsבלקוח.
אם ניסיון חוזר לעיבוד Posts בלקוח גם נכשל, React יזרוק את השגיאה על הלקוח. כמו בכל השגיאות שנזרקו במהלך העיבוד, גבול שגיאת האב הקרובה ביותר קובע כיצד להציג את השגיאה ל-user. בפועל, זה אומר שה-user יראה מחוון טעינה עד שיהיה בטוח שלא ניתן לשחזר את השגיאה.
אם ניסיון חוזר לעיבוד Posts בלקוח יצליח, הטעינה הנפילה מהשרת תוחלף בפלט העיבוד של הלקוח. ה-user לא יידע שהייתה שגיאת שרת. עם זאת, ההתקשרות חזרה של השרת onError והלקוח onRecoverableError יופעלו כדי שתוכל לקבל הודעה על השגיאה.
הגדרת קוד המצב
סטרימינג מציג פשרה. אתה רוצה להתחיל להזרים את הדף מוקדם ככל האפשר כדי שה-user יוכל לראות את התוכן מוקדם יותר. עם זאת, ברגע שתתחיל להזרים, לא תוכל עוד להגדיר את קוד סטטוס התגובה.
על ידי חלוקת האפליקציה שלך לתוך המעטפת (מעל כל גבולות <Suspense>) ושאר התוכן, כבר פתרת חלק מהבעיה הזו. אם המעטפת שגיאה, בלוק catch שלך יפעל, מה שמאפשר לך להגדיר את קוד מצב השגיאה. אחרת, אתה יודע שהאפליקציה עשויה להתאושש בלקוח, כך שתוכל לשלוח “אישור”.
async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}אם רכיב מחוץ למעטפת (כלומר בתוך גבול <Suspense>) זורק שגיאה, React לא יפסיק לעבד. המשמעות היא שההתקשרות חוזרת onError תידלק, אבל הקוד שלך ימשיך לפעול מבלי להיכנס לבלוק catch. הסיבה לכך היאuse React ינסה להתאושש מהשגיאה הזו בלקוח, כמתואר לעיל.
עם זאת, אם תרצה, תוכל use את העובדה שמשהו השתבש כדי להגדיר את קוד המצב:
async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}זה יתפוס רק שגיאות מחוץ למעטפת שקרו בזמן יצירת תוכן המעטפת הראשוני, כך שזה לא ממצה. אם לדעת אם אירעה שגיאה עבור תוכן מסוים היא קריטית, אתה יכול להעביר אותה למעלה לתוך המעטפת.
טיפול בשגיאות שונות בדרכים שונות
אתה יכול ליצור תת-מחלקות Error משלך ו-use האופרטור instanceof כדי לבדוק איזו שגיאה נזרק. לדוגמה, אתה יכול להגדיר NotFoundError מותאם אישית ולזרוק אותו מהרכיב שלך. לאחר מכן תוכל לשמור את השגיאה ב-instanceof ולעשות משהו שונה לפני החזרת השגיאה: onError
async function handler(request) {
let didError = false;
let caughtError = null;
function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
}
}זכור שברגע שאתה פולט את המעטפת ומתחיל להזרים, לא תוכל לשנות את קוד הסטטוס.
ממתין עד שכל התוכן ייטען עבור סורקים ויצירת סטטי
סטרימינג מציע חוויית user טובה יותר מכיוון שuse ה-user יכול לראות את התוכן כשהוא הופך זמין.
עם זאת, כאשר סורק מבקר בדף שלך, או אם אתה יוצר את הדפים בזמן הבנייה, ייתכן שתרצה לתת לכל התוכן להיטען תחילה ולאחר מכן להפיק את הפלט הסופי HTML במקום לחשוף אותו בהדרגה.
אתה יכול להמתין עד שכל התוכן ייטען על ידי המתנה להבטחה stream.allReady:
async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
let isCrawler = // ... depends on your bot detection strategy ...
if (isCrawler) {
await stream.allReady;
}
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}מבקר קבוע יקבל זרם של תוכן שנטען בהדרגה. סורק יקבל את הפלט הסופי HTML לאחר כל טעינת הנתונים. עם זאת, זה גם אומר שהסורק יצטרך להמתין לכל הנתונים, שחלקם עשוי להיות איטי בטעינה או שגיאה. בהתאם לאפליקציה שלך, תוכל לבחור לשלוח את המעטפת גם לסורקים.
ביטול עיבוד השרת
אתה יכול לאלץ את העיבוד של השרת “לוותר” לאחר פסק זמן:
async function handler(request) {
try {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 10000);
const stream = await renderToReadableStream(<App />, {
signal: controller.signal,
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
// ...React ישחק את שאר הטעינה הנפילות כ-HTML, וינסה לעבד את השאר בלקוח.