renderToReadableStream
renderToReadableStream отображает дерево React в потоке чтения веб-страницы.
const stream = await renderToReadableStream(reactNode, options?)
- Ссылка
-
Использование
- Отображение дерева React в формате HTML в потоке чтения веб-страницы
- Потоковая передача дополнительного контента по мере загрузки
- Определение того, что входит в оболочку
- Регистрация сбоев на сервере
- Восстановление от ошибок внутри оболочки
- Восстановление от ошибок вне оболочки
- Установка кода состояния
- Обработка различных ошибок различными способами
- Ожидание загрузки всего контента для роботов-поисковиков и статического генерирования
- Прерывание рендеринга на сервере
Примечание
Этот API зависит от потоков веб-API. Для Node.js используйте renderToPipeableStream вместо этого.
Ссылка
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: Массив строк URL для тегов<script>для вывода на странице. Используйте это для включения<script>, который вызываетhydrateRoot. Опустите, если вы не хотите запускать React на клиенте вообще. -
необязательный
bootstrapModules: КакbootstrapScripts, но выводит теги<script type="module">вместо этого. -
необязательный
identifierPrefix: Строковый префикс, используемый React для ID, генерируемых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: Обратный вызов, который срабатывает при возникновении ошибки на сервере, будь то восстанавливаемая или нет. По умолчанию это вызывает толькоconsole.error. Если вы переопределите его для регистрации отчетов о сбоях, убедитесь, что вы все равно вызываетеconsole.error. Вы также можете использовать его для регулирования кода состояния перед выводом оболочки. -
необязательный
progressiveChunkSize: Количество байтов в фрагменте. Подробнее о дефолтном эвристическом алгоритме. -
необязательный
signal: сигнал прерывания, позволяющий прервать рендеринг на сервере и выполнить рендеринг остальной части на клиенте.
-
необязательный
Возвращаемое значение
renderToReadableStream возвращает Promise:
- Если рендеринг оболочки выполнен успешно, этот Promise разрешится в поток чтения веб-страницы.
- Если рендеринг оболочки завершится ошибкой, Promise будет отклонен. Используйте это для вывода резервной оболочки.
Возвращаемый поток имеет дополнительное свойство:
-
allReady: Promise, который разрешается при завершении всего рендеринга, включая как оболочку, так и весь дополнительный контент. Вы можетеawait stream.allReadyперед возвратом ответа для роботов-поисковиков и статического генерирования. Если вы это сделаете, прогрессивной загрузки не будет. Поток будет содержать окончательный 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' },
});
} Вместе с компонентом корня вам нужно предоставить список путей загрузки <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 внедрит тип документа и ваши теги загрузки <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>
<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, поэтому ошибок гидратации нет.
Потоковая передача дополнительного контента по мере загрузки
Потоковая передача позволяет пользователю начать просмотр контента, даже если все данные еще не загружены на сервере. Например, рассмотрим страницу профиля, на которой отображаются обложка, боковая панель с друзьями и фотографиями, а также список постов:
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> границы таким образом, чтобы оболочка выглядела минимальной, но полной — как каркас всего макета страницы.
Асинхронный вызов 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 для отчёта об ошибках и используйте 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 выбросит ошибку на клиенте. Как и все ошибки, выброшенные во время отрисовки, ближайшая родительская граница обработки ошибок определяет, как отобразить ошибку пользователю. На практике это означает, что пользователь увидит индикатор загрузки, пока не станет ясно, что ошибка не может быть исправлена.
Если повторная отрисовка Posts на клиенте завершится успешно, резервная загрузка с сервера будет заменена результатом отрисовки на клиенте. Пользователь не будет знать, что произошла ошибка на сервере. Тем не менее, серверный onError обратный вызов и клиентский onRecoverableError обратные вызовы будут вызваны, чтобы вы могли быть уведомлены об ошибке.
Указание кода состояния
Потоковая передача представляет собой компромисс. Вы хотите начать передачу страницы потоком как можно раньше, чтобы пользователь мог видеть контент быстрее. Однако, как только вы начнете передачу потоком, вы больше не сможете установить код состояния ответа.
Разделив ваше приложение на оболочку (над всеми <Suspense> границами) и остальной контент, вы уже частично решили эту проблему. Если оболочка даст сбой, ваш catch блок будет выполнен, что позволит установить код состояния ошибки. В противном случае вы знаете, что приложение может восстановиться на клиенте, поэтому вы можете отправить «OK».
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 блок. Это связано с тем, что React попытается восстановиться от этой ошибки на клиенте, как описано выше.
Однако, если хотите, вы можете использовать тот факт, что произошла ошибка, чтобы установить код состояния:
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 подклассы и использовать оператор instanceof, чтобы проверить, какая ошибка была сгенерирована. Например, вы можете определить пользовательскую NotFoundError и выбросить её из вашего компонента. Затем вы можете сохранить ошибку в 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' },
});
}
} Помните, что после отправки оболочки и начала потоковой передачи вы не сможете изменить код состояния.
Ожидание загрузки всего контента для роботов-поисковиков и статической генерации
Потоковая передача обеспечивает лучший пользовательский опыт, так как пользователь может видеть контент по мере его доступности.
Однако, когда робот-поисковик посещает вашу страницу, или если вы генерируете страницы во время сборки, вы можете захотеть сначала загрузить весь контент, а затем сгенерировать окончательный HTML-выход, вместо того, чтобы отображать его по частям.
Вы можете дождаться загрузки всего контента, дождавшись stream.allReady Promise:
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 и попытается отрисовать остальную часть на клиенте.
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/reference/react-dom/server/renderToReadableStream