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 повертає об'єкт з двома методами:


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

Відображення 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);
    }
  });
});

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

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