renderToPipeableStream
renderToPipeableStream מעבד עץ React לזרם Node.js שניתן להזרים.
const { pipe, abort } = renderToPipeableStream(reactNode, options?)הפניה
renderToPipeableStream(reactNode, options?)
התקשר ל-renderToPipeableStream כדי להפוך את עץ ה-React שלך כ-HTML לזרם Node.js.
import { renderToPipeableStream } from 'react-dom/server';
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});בלקוח, התקשר ל-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. - אופציונלי
onAllReady: התקשרות חוזרת המופעלת כאשר כל העיבוד הושלם, כולל הן את המעטפת והן את כל ה-[content] הנוסף.(#streaming-more-content-as-it-loads) אתה יכול useK this place of [useK this] generation.](#waiting-for-all-content-to-load-for-crawlers-and-static-generation) אם תתחיל להזרים כאן, לא תקבל שום טעינה מתקדמת. הזרם יכיל את ה-HTML הסופי. - אופציונלי
onError: התקשרות חוזרת המופעלת בכל פעם שיש שגיאת שרת, בין אם ניתנת לשחזור או לא. כברירת מחדל, זה קורא רק 22.K אם תעקוף אותו ליומן דוחות קריסה, ודא שאתה עדיין קורא ל-console.error. אתה יכול גם use את זה כדי להתאים את קוד המצב לפני פליטת המעטפת. - אופציונלי
onShellReady: התקשרות חוזרת המופעלת מיד לאחר עיבוד המעטפת הראשונית. אתה יכול להגדיר את קוד הסטטוס ולהתקשר ל-pipeכאן כדי להתחיל בסטרימינג. React תזרים את התוכן הנוסף אחרי המעטפת יחד עם תגיות ה-<script>המוטבעות שמחליפות את נפילות הטעינה של HTML בתוכן. - אופציונלי
onShellError: התקשרות חוזרת המופעלת אם הייתה שגיאה בעיבוד המעטפת הראשונית. הוא מקבל את השגיאה כארגומנט. עדיין לא נפלטו בתים מהזרם, ולאonShellReadyוגםonAllReadyלא ייקראו, אז אתה יכול להוציא מעטפת HTML fallback. - אופציונלי
progressiveChunkSize: מספר הבתים בנתח. קרא עוד על היוריסטית ברירת המחדל.
- אופציונלי
מחזירה
renderToPipeableStream מחזיר אובייקט בשתי שיטות:
pipeמוציא את ה-HTML לזרם ניתן לכתיבה Node.js. התקשר ל-pipeב-onShellReadyאם ברצונכם לאפשר סטרימינג, או ב-onAllReadyעבור סורקים ויצירה סטטית.abortמאפשר לך להפסיק את עיבוד השרת ולעבד את השאר בלקוח.
שימוש
עיבוד עץ React כ-HTML לזרם Node.js
התקשר ל-renderToPipeableStream כדי להפוך את עץ ה-React שלך כ-HTML לזרם Node.js:
import { renderToPipeableStream } from 'react-dom/server';
// The route handler syntax depends on your backend framework
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});יחד עם רכיב השורש, עליך לספק רשימה של נתיבי 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>
...
<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'
};
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});מכיוון שהשרת שלך מעבד כעת <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'
};
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<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']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});בדוגמה שלמעלה, האפשרות 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> כך שהמעטפת תרגיש מינימלית אך שלמה - כמו שלד של פריסת העמוד כולה.
ההתקשרות חזרה onShellReady מופעלת כאשר כל המעטפת טופלה. בדרך כלל תתחיל להזרים אז:
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});עד שה-onShellReady יופעל, ייתכן שרכיבים בגבולות <Suspense> מקוננים עדיין טוענים נתונים.
הרישום קורס בשרת
כברירת מחדל, כל השגיאות בשרת נרשמות למסוף. אתה יכול לעקוף התנהגות זו כדי לרשום דוחות קריסה:
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});אם אתה מספק יישום onError מותאם אישית, אל תשכח גם לרשום שגיאות למסוף כמו לעיל.
שחזור משגיאות בתוך המעטפת
בדוגמה זו, המעטפת מכילה ProfileLayout, ProfileCover ו-PostsGlimmer:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}אם מתרחשת שגיאה בזמן רינדור הרכיבים האלה, ל-React לא יהיה שום HTML משמעותי לשלוח ללקוח. עוקף את onShellError כדי לשלוח מיתוס HTML שאינו מסתמך על עיבוד שרת כמוצא אחרון:
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});אם יש שגיאה במהלך יצירת המעטפת, גם onError וגם onShellError יופעלו. השתמש ב-onError לדיווח על שגיאות וב-use onShellError כדי לשלוח את המסמך 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>) ושאר התוכן, כבר פתרת חלק מהבעיה הזו. אם המעטפת שגיאה, תקבל את ההתקשרות חזרה onShellError המאפשרת לך להגדיר את קוד מצב השגיאה. אחרת, אתה יודע שהאפליקציה עשויה להתאושש בלקוח, כך שתוכל לשלוח “אישור”.
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = 200;
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});אם רכיב מחוץ למעטפת (כלומר בתוך גבול <Suspense>) זורק שגיאה, React לא יפסיק לעבד. המשמעות היא שההתקשרות חוזרת onError תפעל, אבל עדיין תקבל onShellReady במקום onShellError. הסיבה לכך היאuse React ינסה להתאושש מהשגיאה הזו בלקוח, כמתואר לעיל.
עם זאת, אם תרצה, תוכל use את העובדה שמשהו השתבש כדי להגדיר את קוד המצב:
let didError = false;
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});זה יתפוס רק שגיאות מחוץ למעטפת שקרו בזמן יצירת תוכן המעטפת הראשוני, כך שזה לא ממצה. אם לדעת אם אירעה שגיאה עבור תוכן מסוים היא קריטית, אתה יכול להעביר אותה למעלה לתוך המעטפת.
טיפול בשגיאות שונות בדרכים שונות
אתה יכול ליצור תת-מחלקות Error משלך ו-use האופרטור instanceof כדי לבדוק איזו שגיאה נגררת. לדוגמה, אתה יכול להגדיר NotFoundError מותאם אישית ולזרוק אותו מהרכיב שלך. ואז TK_3_4, __K שלך יכולים להתקשר בחזרה, onError, __K שונים על סוג השגיאה:
let didError = false;
let caughtError = null;
function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});זכור שברגע שאתה פולט את המעטפת ומתחיל להזרים, לא תוכל לשנות את קוד הסטטוס.
ממתין עד שכל התוכן ייטען עבור סורקים ויצירת סטטי
סטרימינג מציע חוויית user טובה יותר מכיוון שuse ה-user יכול לראות את התוכן כשהוא הופך זמין.
עם זאת, כאשר סורק מבקר בדף שלך, או אם אתה יוצר את הדפים בזמן הבנייה, ייתכן שתרצה לתת לכל התוכן להיטען תחילה ולאחר מכן להפיק את הפלט הסופי HTML במקום לחשוף אותו בהדרגה.
אתה יכול להמתין עד שכל התוכן ייטען באמצעות ההתקשרות חזרה onAllReady:
let didError = false;
let isCrawler = // ... depends on your bot detection strategy ...
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
if (!isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onAllReady() {
if (isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});מבקר קבוע יקבל זרם של תוכן שנטען בהדרגה. סורק יקבל את הפלט הסופי HTML לאחר כל טעינת הנתונים. עם זאת, זה גם אומר שהסורק יצטרך להמתין לכל הנתונים, שחלקם עשוי להיות איטי בטעינה או שגיאה. בהתאם לאפליקציה שלך, תוכל לבחור לשלוח את המעטפת גם לסורקים.
ביטול עיבוד השרת
אתה יכול לאלץ את העיבוד של השרת “לוותר” לאחר פסק זמן:
const { pipe, abort } = renderToPipeableStream(<App />, {
// ...
});
setTimeout(() => {
abort();
}, 10000);React ישחק את שאר הטעינה הנפילות כ-HTML, וינסה לעבד את השאר בלקוח.