renderToPipeableStream
renderToPipeableStream отображает React-дерево в потоковом Node.js Stream.
const { pipe, abort } = renderToPipeableStream(reactNode, options?)
- Справочник
-
Использование
- Отображение React-дерева как HTML в Node.js Stream
- Потоковая передача дополнительного контента по мере загрузки
- Определение того, что входит в оболочку
- Ведение журнала ошибок на сервере
- Восстановление от ошибок внутри оболочки
- Восстановление от ошибок вне оболочки
- Установка кода состояния
- Обработка различных ошибок различными способами
- Ожидание загрузки всего контента для роботов-индексаторов и статической генерации
- Прерывание серверного рендеринга
Примечание
Этот API специфичен для Node.js. Среды с Web Streams, такие как 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>. -
Необязательно
options: Объект с параметрами потоковой передачи.-
Необязательно
bootstrapScriptContent: Если указано, эта строка будет помещена в тег inline<script>. -
Необязательно
bootstrapScripts: Массив строк URL для тегов<script>для вывода на странице. Используйте это для включения<script>, который вызываетhydrateRoot. Опустите, если вы не хотите запускать React на клиенте. -
Необязательно
bootstrapModules: АналогичноbootstrapScripts, но выводит<script type="module">вместо этого. -
Необязательно
identifierPrefix: Префикс строки, который React использует для идентификаторов, сгенерированных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: Обратный вызов, который срабатывает после завершения всего рендеринга, включая как оболочку, так и все дополнительные данные. Вы можете использовать его вместоonShellReadyдля роботов-индексаторов и статической генерации. Если вы начнете потоковую передачу здесь, вы не получите пошаговой загрузки. Поток будет содержать окончательный HTML. -
Необязательно
onError: Обратный вызов, который срабатывает всякий раз, когда возникает ошибка сервера, будь то восстанавливаемая или нет. По умолчанию это только вызываетconsole.error. Если вы переопределите его для записи отчетов об ошибках, убедитесь, что вы все равно вызываетеconsole.error. Вы также можете использовать его для корректировки кода состояния до вывода оболочки. -
Необязательно
onShellReady: Обратный вызов, который срабатывает сразу после рендеринга начальной оболочки. Вы можете установить код состояния и вызватьpipeздесь, чтобы начать потоковую передачу. React будет потоково передавать дополнительные данные после оболочки вместе с тегами inline<script>, которые заменяют HTML-заглушки контентом. -
Необязательно
onShellError: Обратный вызов, который срабатывает, если произошла ошибка при рендеринге начальной оболочки. Он получает ошибку в качестве аргумента. Ни один байт еще не был выведен из потока, и ниonShellReady, ниonAllReadyне будут вызваны, поэтому вы можете вывести HTML-заглушку. -
Необязательно
progressiveChunkSize: Количество байтов в пакете. Подробнее о дефолтном эвристике.
-
Необязательно
Возвращаемое значение
renderToPipeableStream возвращает объект с двумя методами:
-
pipeвыводит HTML в предоставленный Writable Node.js Stream. ВызовитеpipeвonShellReady, если хотите включить потоковую передачу, или вonAllReadyдля роботов-индексаторов и статической генерации. -
abortпозволяет прервать серверный рендеринг и отобразить остальное на клиенте.
Использование
Отображение React-дерева как HTML в Node.js Stream
Вызовите renderToPipeableStream для отображения вашего React-дерева в виде HTML в Node.js Stream:
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);
}
});
}); Вместе с компонентом корня вам нужно предоставить список путей для загрузки <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 и ваши теги загрузки <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 и сделает его интерактивным.
Подробное описание
Чтение путей к файлам 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 добавляет дополнительный тег inline <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 активируют компонент Suspense. К ним относятся:
- Загрузка данных с использованием фреймворков с поддержкой Suspense, таких как Relay и Next.js
- Ленивая загрузка кода компонента с помощью
lazy - Чтение значения Promise с помощью
use
Suspense не обнаруживает, когда данные загружаются внутри обработчика эффекта или события.
Точный способ загрузки данных в компоненте Posts зависит от вашего фреймворка. Если вы используете фреймворк с поддержкой Suspense, подробности вы найдете в документации по загрузке данных.
Загрузка данных с поддержкой 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 по умолчанию не обязан быть страницей с ошибкой. Вместо этого вы можете включить альтернативную оболочку, которая отображает ваше приложение только на клиенте.
Восстановление после ошибок за пределами оболочки
В этом примере компонент <Posts /> заключен в <Suspense>, поэтому он не является частью оболочки:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
} Если в компоненте Posts или где-то внутри него произойдет ошибка, React будет пытаться восстановиться:
- Он отправит загрузку по умолчанию для ближайшей
<Suspense>границы (PostsGlimmer) в HTML. - Он прекратит попытки отобразить содержимое
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 и попытается отобразить остальное на клиенте.
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://18.react.dev/reference/react-dom/server/renderToPipeableStream