Без NextJS или других фреймворков.
React — это библиотека, обычно используемая для разработки одностраничных приложений (SPA), которые в значительной степени зависят от JavaScript.
На самом деле, когда запрос делается из браузера, React возвращает пустую HTML-страницу, а контент фактически загружается с помощью JavaScript после начальной загрузки страницы.
Хотя этот подход работает, следует учитывать несколько существенных недостатков.
- Плохо для поисковой оптимизации (SEO), поскольку исходный HTML, возвращаемый React, пуст, что затрудняет ранжирование в поисковых системах.
- Время ожидания начального рендеринга страницы может быть больше из-за выборки больших пакетов JavaScript.
Чтобы противостоять этим проблемам, мы можем прибегнуть к рендерингу на стороне сервера (SSR).
Проверьте код на Github, если вы хотите сразу перейти к коду https://github.com/rajgaur98/react-ssr/tree/main.
Рендеринг на стороне сервера
Рендеринг на стороне сервера (SSR) — это метод, который отображает веб-страницу на сервере и отправляет ее обратно клиенту.
SSR возвращает браузеру полностью отрендеренную HTML-страницу, что может помочь смягчить проблемы, упомянутые ранее, такие как плохое SEO и более длительное время ожидания начальной отрисовки страницы.
Такие фреймворки, как NextJS, уже поддерживают SSR «из коробки», но его также можно реализовать с нуля. В этом процессе участвуют три шага:
- Генерация HTML на сервере
- Рендеринг HTML, полученного с сервера, на клиенте
- Добавление JavaScript в статический HTML на клиенте (также известное как гидратация)
Это станет более ясно, когда мы начнем писать код.
Настраивать
Мы будем использовать ExpressJS для настройки нашего веб-сервера и, конечно же, React и ReactDOM для нашего интерфейса.
Кроме того, нам также понадобятся Babel и Webpack для транспиляции и объединения кода нашего модуля в скрипты, понятные браузерам.
Чтобы установить необходимые зависимости, выполните следующую команду:
yarn add express react react-dom
Чтобы установить зависимости разработки, выполните следующую команду:
yarn add -D @babel/core @babel/preset-env @babel/preset-react @webpack-cli/generators babel-loader webpack webpack-cli
Создание внешнего интерфейса
Мы будем хранить все наши интерфейсные файлы в папке client
внутри нашей корневой папки. Эта папка будет содержать типичное приложение React и ничего сложного.
Давайте создадим файл index.jsx
, который будет служить корнем нашего приложения React.
import React from "react"; import { hydrateRoot } from "react-dom/client"; import { App } from "./App.jsx"; hydrateRoot(document, <App />);
Обратите внимание, что мы используем функцию hydrateRoot
вместо функции createRoot
. Это потому, что у нас уже есть HTML-документ из серверной части, и теперь нам нужно только прикрепить к нему JavaScript.
Прежде чем мы двинемся дальше, давайте больше разберемся в гидратации.
Когда мы визуализируем наши веб-страницы на сервере, сервер выводит простой статический HTML-документ.
Хотя это подходит для показа начального рендеринга пользователю, сервер не прикрепляет прослушиватели событий или JavaScript к коду. Он возвращает только статическую страницу.
Ответственность за присоединение JavaScript к этому HTML-коду, отображаемому сервером, лежит на клиентской части. Поэтому процесс прикрепления JavaScript к этой статической, сухой странице называется Hydration.
Итак, как видно из последней строки кода, мы используем функцию hydrateRoot для присоединения JavaScript к отображаемому сервером document
с помощью компонента App
.
Теперь давайте создадим компонент приложения.
import React from "react"; export const App = () => { return ( <html> <head> </head> <body> <div id="root">App</div> </body> </html> ); };
Важно отметить, что весь HTML-код включен в компонент приложения, так как он будет отображаться на сервере, и мы хотим, чтобы сервер возвращал полный HTML-код.
Причина этого в том, что сервер может использовать структуру HTML для вставки в HTML необходимых импортов JavaScript и CSS в виде скриптов и тегов ссылок.
А пока давайте сосредоточимся на рендеринге этого простого приложения на стороне сервера, так как позже мы добавим в приложение больше сложностей.
Управление активами
Включение ресурсов, таких как CSS, в рендеринг на стороне сервера можно выполнить либо путем добавления тега link
в head
HTML, либо с помощью подключаемых модулей Webpack.
Например, вы можете использовать css-loader
и MiniCssExtractPlugin
, чтобы извлечь CSS из файлов .jsx
и внедрить их как тег link
. Эти плагины позволяют использовать импорт CSS в файлах .jsx
следующим образом:
import React from "react" import "../styles/main.css"
Однако в этом случае мы использовали ручной подход для внедрения листов в HTML, как показано ниже:
<head> <link rel="stylesheet" href="styles/Root.css"></link> </head>
Для изображений мы использовали статические пути ExpressJS вместо их импорта в .jsx
файлов.
<img src="optimus.png" alt="Profile Picture" />
Если вы предпочитаете импортировать изображения в .jsx
, вы можете использовать следующее решение Webpack:
module: { rules: [ { test: /\.(png|svg|jpg|jpeg|gif)$/i, type: 'asset/resource', }, ], },
Создание сервера
Мы будем хранить файлы нашего сервера в папке src
в нашем корневом каталоге. Сервер прост, состоит из двух файлов: index.js
и render.js
.
index.js
содержит стандартный экспресс-код с одним маршрутом.
const express = require("express"); const path = require("path"); const app = express(); const { render } = require("./render"); app.use(express.static(path.resolve(__dirname, "../build"))); app.use(express.static(path.resolve(__dirname, "../assets"))); app.get("/", (req, res) => { render(res); }); app.listen(3000, () => { console.log("listening on port 3000"); });
Давайте еще посмотрим на файл render.js
, он интересный.
import React from "react"; import { renderToPipeableStream } from "react-dom/server"; import { App } from "../client/App.jsx"; export const render = (response) => { const stream = renderToPipeableStream(<App />, { bootstrapScripts: ["client.bundle.js"], onShellReady() { response.setHeader("content-type", "text/html"); stream.pipe(response); }, }); };
Обратите внимание, что мы можем использовать операторы ESM import
внутри файла render.js
, потому что мы перенесем его позже с babel
.
Импорт React вверху необходим, иначе веб-страница не будет отображаться и будет выдана ошибка.
Затем мы импортируем renderToPipeableStream
, который будет использоваться для рендеринга нашего компонента App
в HTML.
Функция renderToPipeableStream
принимает два параметра: компонент App
и опции.
Параметр bootstrapScripts
представляет собой массив, содержащий пути к сценариям, которые будут обрабатывать HTML.
Сценарий client.bundle.js
— это выходной пакет нашей точки входа client/index.jsx
. Если вы помните, функция hydrateRoot
находится внутри client/index.jsx
.
Функция onShellReady
запускается после того, как исходный HTML-код готов. Мы можем начать потоковую передачу HTML, используя stream.pipe(response)
, чтобы постепенно отправлять его в браузер.
Это простое приложение SSR завершено и готово к работе! Однако запуск всего кода как есть приведет к ошибкам. Сначала нам нужно связать клиентский и серверный код.
Объединение модулей
Нам понадобятся файлы конфигурации babel.config.json
и webpack.config.js
в корневой папке, чтобы транспилировать и связать наш код.
// babel.config.json { "presets": ["@babel/preset-env", "@babel/preset-react"] }
Babel будет обрабатывать импорт модуля ECMAScript (ESM), JSX и другой нестандартный код JavaScript, используя предустановки, указанные в файле выше.
// webpack.config.js const path = require("path"); const clientConfig = { target: "web", entry: "./client/index.jsx", output: { filename: "client.bundle.js", path: path.resolve(__dirname, "build"), }, module: { rules: [ { test: /\.(js|jsx)$/i, loader: "babel-loader", }, ], }, }; const serverConfig = { target: "node", entry: "./src/index.js", output: { filename: "server.bundle.js", path: path.resolve(__dirname, "build"), }, module: { rules: [ { test: /\.(js|jsx)$/i, loader: "babel-loader", }, ], }, }; module.exports = [clientConfig, serverConfig];
Для конфигурации клиента мы устанавливаем свойство target
на web
, точку entry
на client/index.jsx
и output
на client.bundle.js
.
Для конфигурации сервера мы устанавливаем свойство target
на node
, точку entry
на src/index.js
и output
на server.bundle.js
.
Мы также добавим скрипты в package.json
для сборки и запуска нашего кода.
// package.json { // ... "scripts": { "start": "yarn build:dev && node build/server.bundle.js", "build": "webpack --mode=production --node-env=production", "build:dev": "webpack --mode=development", "build:prod": "webpack --mode=production --node-env=production" } }
Теперь, когда мы запускаем yarn start
, клиентский и серверный код должны быть объединены в папку build
, и наш сервер должен работать! Если мы сделаем запрос к http://localhost:3000
, мы должны увидеть наше простое приложение в браузере.
Изучив вкладку сети, мы должны увидеть сгенерированный сервером HTML-код из localhost:3000
. На вкладке элементов мы должны увидеть тег <script src=”client.bundle.js” async=””></script>
, вставленный перед тегом </body>
.
Поддержка саспенса
Давайте начнем добавлять контент в наше приложение. Мы добавим боковую панель, страницу блога и компонент «Похожие блоги». Чтобы организовать наш код, мы реорганизуем App.jsx
в отдельные компоненты.
Мы также реорганизуем нашу HTML-разметку в компонент с именем Html
. Это означает, что компонент App
теперь должен выглядеть следующим образом:
import React, { lazy, Suspense } from "react"; import { Html } from "./Html.jsx"; import { Loading } from "./components/Loading.jsx"; const Sidebar = lazy(() => import("./components/Sidebar.jsx" /* webpackPrefetch: true */) ); const Main = lazy(() => import("./components/Main.jsx" /* webpackPrefetch: true */) ); const Extra = lazy(() => import("./components/Extra.jsx" /* webpackPrefetch: true */) ); export const App = () => { return ( <Html> <Suspense fallback={<Loading />}> <Sidebar></Sidebar> <Suspense fallback={<Loading />}> <Main></Main> <Suspense fallback={<Loading />}> <Extra></Extra> </Suspense> </Suspense> </Suspense> </Html> ); };
Наше приложение теперь имеет некоторые интересные функции. Разберем их:
Sidebar
,Main
иExtra
являются стандартными компонентами React, поэтому мы пока не будем вдаваться в подробности их кода.lazy
: используется для ленивой загрузки компонентов. Это означает, что они импортируются только тогда, когда их необходимо визуализировать. Это приводит к разделению кода в Webpack, создавая отдельные файлы пакетов для каждого компонента в папкеbuild
. Эти файлы загружаются после загрузки пакетаclient.bundle.js
в браузере.Suspense
: это компонент React, который обрабатывает одновременную выборку данных и рендеринг. Он ожидает загрузки компонентовlazy
по запросу и показывает индикатор загрузки (используя реквизитfallback
), пока они не будут готовы.
В нашем случае это играет важную роль в потоковой передаче HTML с сервера. HTML-код отправляется постепенно в виде потока, при этом компонент Suspense
ожидает загрузки компонентов по мере необходимости.
Например, сначала отправляется HTML до корня div
, затем компонент Sidebar
, затем компонент Main
и, наконец, компонент Extra
.
Это улучшает взаимодействие с пользователем, поскольку пользователю не нужно ждать, пока все компоненты загрузятся одновременно. Компоненты отображаются постепенно по мере их появления.
Чтобы увидеть это в действии, вы можете искусственно задержать загрузку компонента в компоненте App
.
const Sidebar = lazy( () => new Promise((resolve) => { setTimeout( () => resolve( import("./components/Sidebar.jsx" /* webpackPrefetch: true */) ), 1000 ); }) ); const Main = lazy( () => new Promise((resolve) => { setTimeout( () => resolve(import("./components/Main.jsx" /* webpackPrefetch: true */)), 2000 ); }) ); const Extra = lazy( () => new Promise((resolve) => { setTimeout( () => resolve(import("./components/Extra.jsx" /* webpackPrefetch: true */)), 3000 ); }) );
Управление потоком HTML
Вы можете дополнительно управлять потоковой передачей HTML с помощью параметров в методе renderToPipableStream
.
Вы можете использовать onAllReady
вместо опции onShellReady
, если хотите передать весь HTML-код сразу после загрузки всей страницы, а не при первоначальном рендеринге.
const stream = renderToPipeableStream(<App />, { bootstrapScripts: ["client.bundle.js"], onAllReady() { response.setHeader("content-type", "text/html"); stream.pipe(response); }, });
Рендеринг на стороне сервера также можно остановить, чтобы остальная часть рендеринга могла выполняться на стороне клиента. Вы можете добиться этого, используя следующее:
const stream = renderToPipeableStream(<App />, { bootstrapScripts: ["client.bundle.js"], onShellReady() { response.setHeader("content-type", "text/html"); stream.pipe(response); }, }); setTimeout(() => { stream.abort() }, 10000)
Заключение
Вот и все, ребята. Я надеюсь, что эта статья помогла вам понять сложности рендеринга компонентов React на стороне сервера. Спасибо за чтение!
Полезные ссылки
Найдите больше подобного контента на SliceOfDev.com ✨