DevGang
Авторизоваться

Серверный рендеринг и потоковая передача UI с нуля в React с использованием Suspense

В этом посте мы с нуля реализуем server-side рендеринг (SSR) и потоковую передачу пользовательского интерфейса (UI) в React, используя express.js в качестве серверной платформы.

Одной из самых интересных функций Next.js является потоковая передача UI, которая позволяет нам отображать частичный контент наряду с мгновенной загрузкой тех частей, для которых все еще требуется получение данных непосредственно с сервера. Несмотря на простоту использования потоковой передачи в Next.js, все же неплохо бы попробовать реализовать нашу собственную версию потоковой передачи. Итак, давайте начнем.

Начальная настройка

$ mkdir react-streaming
$ cd react-streaming
$ mkdir src
$ npm init -y
$ touch src/server.js

Теперь давайте установим наши зависимости:

$ npm i --save express dotenv chalk@4.1.2
$ npm i -D @babel/core @babel/node @babel/preset-env @babel/preset-react

Конфигурация babel .babelrc:

{
    "ignore": ["node_modules"],
    "presets": [
        ["@babel/preset-env", {
            "targets": {
                "node": "current"
            }
        }], "@babel/preset-react"
    ]
}

Инициализация нашего-сервера Express:

// src/server.js
import chalk from 'chalk';
import express from 'express';
import { config as env } from 'dotenv';

// * initialization
env();
const app = express();
const port = process.env.PORT ?? 3000;

app.use(express.static('public'));

app.get('/', (_req, res) => {
  res.status(200).json({ message: "Success" })
});

app.listen(port, () => {
  console.log(chalk.blueBright(`Server running at http://localhost:${port}`));
});

Чтобы выполнить рендеринг нашего приложения React на стороне сервера, нам нужна функция renderToPipeableStream из react-dom/server. Итак, давайте установим его вместе с react и создадим некоторые необходимые компоненты:

$ npm i react react-dom
$ mkdir src/client
$ touch src/client/App.jsx
$ touch src/client/Html.jsx
// src/client/App.jsx
import * as React from 'react';

const App = () => {
  return (<div>This is our React application</div>);
};

export default App;
// src/client/Html.jsx
import * as React from 'react';

// * initial HTML boilerplate
const Html = ({ children }) => {
  return (
    <html>
      <head>
        <meta charSet='UTF-8' />
        <meta name='viewport' content='width=device-width, initial-scale=1.0' />
        <title>Document</title>
      </head>
      <body>
        <div id='app'>{children}</div>
      </body>
    </html>
  );
};

export default Html;

Далее мы добавим логику рендеринга на стороне сервера на наш сервер Express:

// src/server.js
import * as React from 'react';
import { renderToPipeableStream } from 'react-dom/server';

// * components
import Html from './client/Html';
import App from './client/App';

app.get('/', (_req, res) => {
  const stream = renderToPipeableStream(
    <Html>
      <App />
    </Html>,
    {
      onShellReady() {
        stream.pipe(res);
      },
    }
  );
});

Добавьте скрипт NPM для запуска сервера:

// package.json
{
  "scripts": {
    "start:server": "npx babel-node src/server.js"
  }
}
$ npm run start:server

Откройте http//:localhost:3000 в своем браузере и просмотрите исходный код страницы. Вы увидите, что ваше приложение React успешно отображается на сервере.

Теперь у нас есть приложение React, визуализируемое на стороне сервера, но у нас есть две основные проблемы:

  • В нашем приложении React нет никакой интерактивности.
  • Мы не осуществляем потоковую передачу компонентов, хотя используем renderToPipeableStream.

Давайте сначала рассмотрим проблему интерактивности.

Интерактивность

Создание приложения React, отображаемого на стороне сервера, состоит из двух шагов:

  • Отобразите приложение на сервере и обслуживайте его.
  • Пусть React гидратирует серверный HTML и добавит интерактивности.

Чтобы позволить React гидратировать приложение, мы используем функцию hydarteRoot, предоставляемую response-dom/client, чтобы React взял на себя управление после того, как сервер завершит обработку HTML.

Для достижения этой цели мы используем функцию hydrateRoot, предоставляемую react-dom/client, позволяющую React взять на себя управление после того, как сервер завершит обработку HTML — исходного HTML в случае потоковой передачи.

Итак, давайте создадим index.jsx:

$ touch src/client/index.jsx
// src/client/index.jsx
import * as React from 'react';
import { hydrateRoot } from 'react-dom/client';

// * components
import App from './App';

hydrateRoot(document.getElementById('app'), <App />);

Теперь нам нужен способ транспилировать и связывать index.jsx, чтобы его можно было включить в HTML-код, отправляемый с сервера. Для этого используем webpack и babel-loader:

$ npm i -D webpack webpack-cli babel-loader
// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/client/index.jsx',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'public'),
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env', '@babel/preset-react'],
          },
        },
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
};
{
  "scripts": {
    "build:client": "npx webpack --mode production",
    "start:server": "npx babel-node src/server.js",
    "start": "npm run build:client && npm run start:server"
  }
}

Затем запустите $ npm run build:client, чтобы веб-пакет сгенерировал bundle.js внутри public папки.

Последний шаг по добавлению интерактивности включает в себя включение bundle.js в HTML, сгенерированный на сервере, что довольно просто благодаря React. Просто включите файл сценария в поле bootstrapScripts параметров функции renderToPipeableStream.

app.get('/', (_req, res) => {
  const stream = renderToPipeableStream(
    <Html>
      <App />
    </Html>,
    {
      bootstrapScripts: ['/bundle.js'],
      onShellReady() {
        stream.pipe(res);
      },
    }
  );
});

Проверим интерактивность:

$ touch src/client/Counter.jsx
// src/client/Counter.jsx
import * as React from 'react';

const Counter = () => {
  const [count, setCount] = React.useState(0);

  const decrement = () => setCount(prev => prev - 1);
  const increment = () => setCount(prev => prev + 1);

  return (
    <div className='counter' style={{ display: 'flex', gap: '0.5rem' }}>
      <button type='button' onClick={decrement}>
        decrement
      </button>
      {count}
      <button type='button' onClick={increment}>
        increment
      </button>
    </div>
  );
};

export default Counter;
// src/client/App.jsx
import * as React from 'react';

// * components
import List from './List';
import Counter from './Counter';

const App = () => {
  return (
    <main>
      <p style={{ marginBottom: '1rem' }}>this is the app component</p>
      <Counter />
      <React.Suspense
        fallback={
          <div style={{ marginTop: '1rem' }}>Loading the List...</div>
        }
      >
        <List />
      </React.Suspense>
    </main>
  );
};

export default App;

Теперь запустите проект с помощью $ npm run start и проверьте счетчик. Вы увидите, что он полностью интерактивный.

Потоковая передача

Последний шаг этого руководства — обеспечить потоковую передачу компонентов, которым необходимы данные, и отображение состояний загрузки в ожидании получения данных.

При такой настройке мы можем использовать границы React Suspense для отображения состояний загрузки с помощью резервного свойства, но проблема в том, что дочерние элементы React не могут быть промисами, а значением ожидающего запроса на выборку является Promise. Таким образом, мы получим ошибку. Чтобы решить эту проблему, мы используем экспериментальный хук use React для обработки обещания, который доступен только в канале выпуска React canary.

Чтобы использовать этот хук, нам сначала нужно обновить react и react-dom на версию canaray:

$ npm install react@canary react-dom@canary

Далее нам нужен компонент для потоковой передачи:

$ touch src/client/List.jsx
// src/client/List.jsx
import * as React from 'react';

// * data
const list = [
  {
    userId: 1,
    id: 1,
    title: 'sunt aut facere repellat provident occaecati excepturi optio',
    body: 'quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto',
  },
  {
    userId: 1,
    id: 2,
    title: 'qui est esse',
    body: 'est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla',
  },
  {
    userId: 1,
    id: 3,
    title: 'ea molestias quasi exercitationem repellat qui ipsa sit aut',
    body: 'et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut',
  },
];

const fetchList = () => {
  return new Promise(resolve => setTimeout(() => resolve(list), 3000));
};

const List = () => {
  const [items] = React.useState(React.use(fetchList()));

  return (
    <div
      className='list'
      style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}
    >
      {items.map(item => (
        <div
          key={item.id}
          style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}
        >
          <h3>{item.title}</h3>
          <p>{item.body}</p>
        </div>
      ))}
    </div>
  );
};

export default List;

Теперь нам нужно обернуть компонент List границей Suspense:

// src/client/App.jsx
import * as React from 'react';

// * components
import List from './List';

const App = () => {
  return (
    <main>
      this is the app component
      <React.Suspense fallback={<div>Loading the List...</div>}>
        <List />
      </React.Suspense>
    </main>
  );
};

export default App;
$ npm run start

Откройте http://localhost:3000, подождите 3 секунды и та-да!

Теперь у вас есть работающее приложение React, отображаемое на стороне сервера, которое поддерживает потоковую передачу пользовательского интерфейса и мгновенные состояния загрузки.

#JavaScript #React #Express
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу