renderToPipeableStream
renderToPipeableStream
рендерить дерево React у трубчастий потік Node.js.
const { pipe, abort } = renderToPipeableStream(reactNode, options?)
Цей API є специфічним для Node.js. Середовища з веб-потоками, такі як Deno та сучасні периферійні середовища виконання, повинні використовувати renderToReadableStream
замість цього.
Довідник
renderToPipeableStream(reactNode, options?)
Викличте renderToPipeableStream
, щоб відрендерити ваше React-дерево як HTML у Node.js Stream.
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>
.опція
опцій
: Об'єкт з опціями потокового передавання.- опція
bootstrapScriptContent
: якщо вказано, цей рядок буде поміщено у вбудований тег<script>
. - optional
bootstrapScripts
: Масив рядкових URL-адрес для тегів<script>
, які слід виводити на сторінці. Використовуйте його, щоб включити<script>
, який викликаєhydrateRoot
. Пропустіть його, якщо ви не хочете запускати React на клієнті взагалі. - optional
bootstrapModules
: ЯкbootstrapScripts
, але видає<script type="module">
замість цього. - optional
identifierPrefix
: Рядковий префікс, який React використовує для ідентифікаторів, згенерованихuseId
. Корисно для уникнення конфліктів при використанні декількох коренів на одній сторінці. Має бути той самий префікс, що й уhydrateRoot
. .
- optional
namespaceURI
: Рядок з кореневим URI простору імен для потоку. За замовчуванням - звичайний HTML. Передайте'http://www.w3.org/2000/svg'
для SVG або'http://www.w3.org/1998/Math/MathML'
для MathML. - optional
nonce
: Рядокnonce
для дозволу скриптів дляscript-src
політики-безпеки-вмісту. - optional
onAllReady
: Зворотний виклик, який виконується після завершення рендерингу, включаючи оболонку і весь додатковий вміст. Ви можете використовувати його замістьonShellReady
для краулерів і статичної генерації. Якщо ви почнете потік тут, ви не отримаєте ніякого прогресивного завантаження. Потік буде містити фінальний HTML. - optional
onError
: Зворотний виклик, який виконується щоразу, коли виникає помилка сервера, незалежно від того, чи є вона виправною або невиправною За замовчуванням, він викликає лишеconsole.error
. Якщо ви перевизначите його для реєструвати звіти про аварії, переконайтеся, що ви все ще викликаєтеconsole.error
. Ви також можете використовувати його для коригування коду стану перед видачею оболонки. - optional
onShellReady
: Зворотний виклик, який виконується одразу після рендерингу початкової оболонки. Ви можете встановити код стану і викликатиpipe
тут, щоб розпочати потік. React буде передавати додатковий контент після оболонки разом з вбудованими тегами<script>
, які заміняють альтернативні варіанти завантаження HTML на контент. - optional
onShellError
: Функція зворотного виклику, яка спрацьовує, якщо виникла помилка під час рендерингу початкової оболонки. Вона отримує помилку як аргумент. З потоку ще не було виведено жодного байта, і ніonShellReady
, ніonAllReady
не буде викликано, тому ви можете вивести запасну HTML-оболонку. - опція
progressiveChunkSize
: Кількість байт у чанку. Детальніше про евристику за замовчуванням.
- опція
Повернення
renderToPipeableStream
повертає об'єкт з двома методами:
pipe
виводить HTML у наданий Writable Node.js Stream. Викличтеpipe
уonShellReady
, якщо ви хочете увімкнути потік, або уonAllReady
для кроулерів та статичної генерації.abort
дозволяє вам припинити серверний рендеринг і продовжити його на клієнті.
Використання
Відображення React-дерева як HTML у потоці Node.js
Викличте renderToPipeableStream
, щоб відрендерити ваше React-дерево як HTML у Node.js Stream:
"], [2, 6, "['/main.js']"]]" class="language-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);
}
});
});
Разом з <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 html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>
На клієнтській стороні ваш скрипт завантаження повинен завантажити весь документ
викликом hydrateRoot
:
"]]" class="language-js">import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
Це додасть слухачів подій до згенерованого сервером HTML і зробить його інтерактивним.
Читання шляхів до CSS та JS ресурсів з виведення збірки
Остаточні URL-адреси ресурсів (наприклад, JavaScript та CSS-файли) часто хешуються після збирання. Наприклад, замість styles.css
ви можете отримати styles.123456.css
. Хешування статичних назв файлів ресурсів гарантує, що кожна окрема збірка того самого ресурсу матиме іншу назву файлу. Це корисно, оскільки дозволяє безпечно увімкнути довготривале кешування статичних ресурсів: файл з певною назвою ніколи не змінить вміст.
Втім, якщо ви не знаєте 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
пропом, тому помилок гідратації немає.
Передавати більше вмісту під час завантаження
Потокова передача дозволяє користувачеві почати бачити вміст ще до того, як всі дані будуть завантажені на сервер. Наприклад, розглянемо сторінку профілю, яка показує обкладинку, бічну панель з друзями і фотографіями, а також список дописів:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}
Уявіть, що завантаження даних для <Posts />
займає деякий час. В ідеалі, ви хотіли б показати користувачеві решту вмісту сторінки профілю, не чекаючи на повідомлення. Для цього обгорніть 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>
, який замінить резервне завантаження на цей HTML. З точки зору користувача, сторінка спочатку з'явиться з 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
повинні закінчити рендеринг першими, оскільки вони не обгорнуті жодною межею <Suspense>
. Однак, якщо Sidebar
, Friends
або Photos
потрібно завантажити якісь дані, React надішле HTML для резервного варіанту BigSpinner
. Потім, по мірі того, як буде доступно більше даних, буде продовжувати показуватися більше контенту, поки він не стане видимим повністю.
Потокове передавання не потребує очікування завантаження самого React у браузері або переходу вашого застосунку в інтерактивний режим. HTML-контент з сервера буде поступово розкриватися до того, як будь-який з тегів <script>
завантажиться.
Довідка про те, як працює потоковий HTML.
Лише джерела даних з увімкненим режимом очікування активують компонент очікування. До них належать:
- Отримання даних за допомогою фреймворків з підтримкою Suspense, таких як Relay та Next.js
- Ліниве завантаження коду компонента за допомогою
lazy
- Зчитування значення обіцянки за допомогою
використання
Призупинення не виявляє, коли дані отримуються всередині ефекту або обробника події.
Точний спосіб завантаження даних у компонент Posts
залежить від вашого фреймворку. Якщо ви використовуєте фреймворк із підтримкою Suspense, ви знайдете подробиці у його документації щодо отримання даних.
Отримання даних у стані очікування без використання фреймворку з думкою наразі не підтримується. Вимоги до реалізації джерела даних з підтримкою Suspense є нестабільними і недокументованими. Офіційний API для інтеграції джерел даних з Suspense буде випущено в наступній версії React.
Вказівка того, що буде передано в оболонку
Частина вашого застосунку поза будь-якими <Suspense>
межами називається оболонкою:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}
Він визначає найперший стан завантаження, який може побачити користувач:
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>
Якщо ви обгорнете весь застосунок у межу <Suspense>
в корені, то оболонка міститиме лише цей спінер. Однак, це не дуже приємний користувацький досвід, оскільки бачити великий спінер на екрані може здаватися повільнішим і більш дратівливим, ніж зачекати трохи довше і побачити реальну компоновку. Ось чому зазвичай вам слід розташувати межі <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
для повідомлення про помилки, а onShellError
- для надсилання резервного HTML-документа. Ваш запасний HTML-документ не обов'язково має бути сторінкою помилок. Замість цього ви можете включити альтернативну оболонку, яка відображатиме вашу програму лише на клієнті.
Відновлення після помилок за межами оболонки
У цьому прикладі компонент <Posts />
обгорнуто у <Suspense>
, тому він не є частиною оболонки:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
Якщо у компоненті Posts
або десь всередині нього трапиться помилка, React спробує відновитися після неї:
- Він буде випромінювати у HTML запасний варіант завантаження для найближчої
<Suspense>
межі (PostsGlimmer
). - Він "припинить" спроби відрендерити
Posts
вміст на сервері. - Коли JavaScript-код завантажиться на клієнтській стороні, React повторить спробу рендерингу
Posts
на клієнтській стороні.
Якщо повторна спроба рендерингу Posts
на стороні клієнта також не вдасться, React видасть помилку на клієнтській стороні. Як і у випадку з усіма помилками, що виникають під час рендерингу, найближча батьківська межа помилки визначає, як представити помилку користувачеві. На практиці це означає, що користувач бачитиме індикатор завантаження, доки не переконається, що помилку не можна виправити.
Якщо повторна спроба рендерингу Posts
на стороні клієнта буде успішною, резервне завантаження з сервера буде замінено на виведення рендерингу клієнта. Користувач не знатиме, що сталася помилка на сервері. Однак серверний onError
та клієнтський onRecoverableError
зворотні виклики буде виконано, щоб ви могли отримати сповіщення про помилку.
Встановлення коду стану
Потокове передавання передбачає компроміс. Ви хочете почати трансляцію сторінки якомога раніше, щоб користувач зміг побачити її вміст раніше. Однак після початку потокової передачі ви більше не зможете встановити код стану відповіді.
Розділивши вашу програму на оболонку (передусім межі <Suspense>
) та решту вмісту, ви вже вирішили частину цієї проблеми. Якщо оболонка помиляється, ви отримаєте зворотний виклик onShellError
, який дозволить вам встановити код статусу помилки. В іншому випадку, ви знаєте, що програма може відновитися на стороні клієнта, тому ви можете надіслати "OK".
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
. Це тому, що React спробує відновитися після цієї помилки на стороні клієнта, як описано вище.
Втім, за бажанням, ви можете використати той факт, що сталася помилка, для встановлення коду стану:
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
підкласи і використовувати оператор instanceof
для перевірки того, яка помилка виникає. Наприклад, ви можете визначити власний NotFoundError
і згенерувати його з вашого компонента. Тоді ваші виклики onError
, onShellReady
та onShellError
можуть робити щось інше, залежно від типу помилки:
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);
}
});
Майте на увазі, що після того, як ви випустите оболонку і почнете трансляцію, ви не зможете змінити код стану.
Очікування завантаження всього вмісту для сканерів та статичної генерації
Потокова передача пропонує кращий користувацький досвід, оскільки користувач може бачити вміст, коли він стає доступним.
Втім, коли вашу сторінку відвідує пошуковий робот, або якщо ви генеруєте сторінки під час збирання, ви можете дозволити спочатку завантажити весь вміст, а потім створити остаточний 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, і спробує відрендерити решту на клієнтській стороні.