renderToReadableStream

renderToReadableStream рендерить дерево React у читабельний веб-потік.

const stream = await renderToReadableStream(reactNode, options?)

Цей 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>.

  • опція опцій: Об'єкт з опціями потокового передавання.

    • опція 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 onError: Зворотний виклик, який виконується щоразу, коли виникає помилка сервера, незалежно від того, чи є вона виправною або невиправною За замовчуванням, він викликає лише console.error. Якщо ви перевизначите його для реєструвати звіти про аварії, переконайтеся, що ви все ще викликаєте console.error. Ви також можете використовувати його для коригування коду стану перед видачею оболонки.
    • опція progressiveChunkSize: Кількість байт у чанку. Детальніше про евристику за замовчуванням.
    • опціонально signal: сигнал переривання, який дозволяє вам перервати рендеринг на сервері і продовжити рендеринг на клієнтській стороні.

Повернення

renderToReadableStream повертає обіцянку:

Потік, що повертається, має додаткову властивість:

  • allReady: Обіцянка, яка буде виконана після завершення рендерингу, включаючи оболонку і весь додатковий контент. Ви можете await stream.allReady перед поверненням відповіді для сканерів і статичної генерації. Якщо ви це зробите, то не отримаєте жодного прогресивного завантаження. Потік буде містити фінальний HTML.

Використання

Перетворення дерева React як HTML у читабельний веб-потік

.

Викличте renderToReadableStream, щоб відрендерити ваше React-дерево як HTML у читабельний веб-потік:

"], [2, 5, "['/main.js']"]]" class="language-js">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' },
  });
}

Разом з кореневим компонентом </CodeStep> , потрібно вказати список <CodeStep data-step="2">bootstrap <code><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 додасть тип та ваші теги <код><сценарій> завантажувача у результуючий HTML-потік:

<!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>
        <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, таких як 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> таким чином, щоб оболонка виглядала мінімальною, але завершеною - як каркас усього макету сторінки.

Виклик асинхронізації до renderToReadableStream перетвориться на потік щойно буде відображено всю оболонку. Зазвичай, ви почнете потоковий виклик, створивши і повернувши відповідь з цим потоком:

async function handler(request) {
  const stream = await renderToReadableStream(<App />, {
    bootstrapScripts: ['/main.js']
  });
  return new Response(stream, {
    headers: { 'content-type': 'text/html' },
  });
}

На момент повернення потоку компоненти у вкладених <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 спробує відновитися після неї:

  1. Він буде випромінювати у HTML запасний варіант завантаження для найближчої <Suspense> межі (PostsGlimmer).
  2. Він "припинить" спроби відрендерити Posts вміст на сервері.
  3. Коли 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 Обіцянку:

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, і спробує відрендерити решту на на клієнтській стороні.