Перейти к содержанию

resume

resume передаёт предварительно отрендеренное дерево React в читаемый веб-поток.

1
const stream = await resume(reactNode, postponedState, options?)

Примечание

Этот API зависит от Web Streams. Для Node.js используйте resumeToNodeStream.

Описание

resume(node, postponedState, options?)

Вызовите resume, чтобы возобновить рендеринг предварительно отрендеренного дерева React в HTML в читаемый веб-поток.

1
2
3
4
5
6
7
8
import { resume } from 'react-dom/server';
import {getPostponedState} from './storage';

async function handler(request, writable) {
  const postponed = await getPostponedState(request);
  const resumeStream = await resume(<App />, postponed);
  return resumeStream.pipeTo(writable)
}

Больше примеров ниже.

Параметры

  • reactNode: Узел React, с которым вы вызывали prerender. Например, JSX-элемент <App />. Ожидается, что он представляет весь документ, поэтому компонент App должен вывести тег <html>.
  • postponedState: Непрозрачный объект postpone, возвращённый API пререндера и загруженный оттуда, где вы его сохранили (например, redis, файл или S3).
  • опционально options: Объект с опциями потоковой передачи.

Возвращаемое значение

resume возвращает промис:

У возвращённого потока есть дополнительное свойство:

  • allReady: Промис, который разрешается, когда весь рендеринг завершён. Можно выполнить await stream.allReady перед возвратом ответа для краулеров и статической генерации. Если сделать так, прогрессивной загрузки не будет. Поток будет содержать окончательный HTML.

Предупреждения

  • resume не принимает опции bootstrapScripts, bootstrapScriptContent и bootstrapModules. Эти опции нужно передать в вызов prerender, который создаёт postponedState. Содержимое бутстрапа также можно вручную вставить в записываемый поток.
  • resume не принимает identifierPrefix, потому что префикс должен совпадать в prerender и resume.
  • Поскольку nonce нельзя передать в пререндер, передавайте nonce в resume, только если вы не передаёте скрипты в пререндер.
  • resume заново рендерит дерево от корня, пока не найдёт компонент, который не был полностью предварительно отрендерен. Полностью пререндеренные компоненты (сам компонент и его дочерние элементы закончили пререндер) пропускаются целиком.

Использование

Возобновление пререндера

1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <iframe id="container"></iframe>
</body>
</html>
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
import {
    flushReadableStreamToFrame,
    getUser,
    Postponed,
    sleep,
} from "./demo-helpers";
import { StrictMode, Suspense, use, useEffect } from "react";
import { prerender } from "react-dom/static";
import { resume } from "react-dom/server";
import { hydrateRoot } from "react-dom/client";

function Header() {
    return <header>Me and my descendants can be prerendered</header>;
}

const { promise: cookies, resolve: resolveCookies } = Promise.withResolvers();

function Main() {
    const { sessionID } = use(cookies);
    const user = getUser(sessionID);

    useEffect(() => {
        console.log("reached interactivity!");
    }, []);

    return (
        <main>
            Hello, {user.name}!
            <button onClick={() => console.log("hydrated!")}>
                Clicking me requires hydration.
            </button>
        </main>
    );
}

function Shell({ children }) {
    // In a real app, this is where you would put your html and body.
    // We're just using tags here we can include in an existing body for demonstration purposes
    return (
        <html>
            <body>{children}</body>
        </html>
    );
}

function App() {
    return (
        <Shell>
            <Suspense fallback="loading header">
                <Header />
            </Suspense>
            <Suspense fallback="loading main">
                <Main />
            </Suspense>
        </Shell>
    );
}

async function main(frame) {
    // Layer 1
    const controller = new AbortController();
    const prerenderedApp = prerender(<App />, {
        signal: controller.signal,
        onError(error) {
            if (error instanceof Postponed) {
            } else {
                console.error(error);
            }
        },
    });
    // We're immediately aborting in a macrotask.
    // Any data fetching that's not available synchronously, or in a microtask, will not have finished.
    setTimeout(() => {
        controller.abort(new Postponed());
    });

    const { prelude, postponed } = await prerenderedApp;
    await flushReadableStreamToFrame(prelude, frame);

    // Layer 2
    // Just waiting here for demonstration purposes.
    // In a real app, the prelude and postponed state would've been serialized in Layer 1 and Layer would deserialize them.
    // The prelude content could be flushed immediated as plain HTML while
    // React is continuing to render from where the prerender left off.
    await sleep(2000);

    // You would get the cookies from the incoming HTTP request
    resolveCookies({ sessionID: "abc" });

    const stream = await resume(<App />, postponed);

    await flushReadableStreamToFrame(stream, frame);

    // Layer 3
    // Just waiting here for demonstration purposes.
    await sleep(2000);

    hydrateRoot(frame.contentWindow.document, <App />);
}

main(document.getElementById("container"));
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export async function flushReadableStreamToFrame(readable, frame) {
    const document = frame.contentWindow.document;
    const decoder = new TextDecoder();
    const reader = readable.getReader();

    while (true) {
        const {done, value} = await reader.read();
        if (done) {
            break;
        }
        const partialHTML = decoder.decode(value, {stream: true});
        document.write(partialHTML);
    }

    document.write(decoder.decode());
}

// This doesn't need to be an error.
// You can use any other means to check if an error during prerender was
// from an intentional abort or a real error.
export class Postponed extends Error {}

// We're just hardcoding a session here.
export function getUser(sessionID) {
    return {
        name: "Alice",
    };
}

export function sleep(timeoutMS) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve();
        }, timeoutMS);
    });
}

Дополнительно

Возобновление ведёт себя как renderToReadableStream. Больше примеров — в разделе использования renderToReadableStream. Раздел использования prerender содержит примеры того, как использовать именно prerender.

Источник — https://react.dev/reference/react-dom/server/resume

Комментарии