Отримання результатів рендерингу з боку сервера для додатку React 18 за допомогою Express та Babel
Перетворіть проект Create React App на серверну рендерингову версію за допомогою Express та Babel: renderToString, hydrateRoot, MemoryRouter, а також дізнайтеся, як Remix та Gatsby реалізують SSR.
Джерела: A Hands-on Guide for a Server-Side Rendering React 18 App
Рендеринг з боку сервера для React суттєво змінився з появою версії React 18. Наведена нижче інструкція орієнтована на React 18, Create React App 5 та React Router 6; вона починається з стандартного проекту Create React App, який поступово перетворюється. Рендеринг має відбуватися на сервері, тому потрібен процес Node. Звичайний Node підходить, але nodemon зручний під час розробки, оскільки він перезапускає додаток щоразу, коли змінюється файл. Глобальна установка виглядає так:
% npm install -g nodemon
CSR проти SSR у кількох реченнях
При клієнтському рендерингу (CSR) браузер отримує майже порожню HTML-сторінку, а JavaScript заповнює її контентом. Перше завантаження може бути повільним, але подальше навігування відбувається швидко, і сервер несе невеликий тягар, оскільки роботу виконують бібліотеки JavaScript. Недоліком є оптимізація під пошукові системи, адже пошуковий бот не знаходить статичного контенту для читання.
При серверському рендерингу браузер отримує вже готову HTML-сторінку, яку обробив сервер. Це сприяє SEO, і перше відображення сторінки відбувається швидко. Однак недолік полягає у тому, що для змін часто потрібно повне перезавантаження сторінки, що може збільшити навантаження на сервер.
Тут стануть у пригоді кілька пов’язаних термінів:
- Багатосторінковий додаток (SPA) використовує CSR. Замість того, щоб надавати окрему HTML-сторінку для кожного маршруту, він динамічно генерує кожен маршрут прямо в браузері.
SSR насправді є старшим за CSR. Він повернувся у сучасній формі, побудованій на універсальному JavaScript, і саме це сьогодні зазвичай мають на увазі під SSR.
Базовий додаток CSR
Створіть новий проект:
% yarn create react-app react-ssr
% cd react-ssr
Почніть з npm start. У вкладці Elements браузера крутячий логотип та текст з’являються як HTML, створений за допомогою JavaScript.
Це класичний приклад CSR: скрипти генерують вміст HTML. Вкладка Network показує, що насправді надіслав сервер.
У тілі сторінки знаходиться пакет JavaScript, а не справжній вміст, тому пошуковим ботам майже немає чого індексувати.
Надання версії для продакшну
Для SSR потрібна версія для продакшну, тому першим кроком є створення невеликого сервера Express, який надає скомпільований вихід React. Раніше описана налаштування React для продакшну створює необхідну основу. Файл сервера існує у двох форматах: один — CommonJS, server/index.js, а інший — ES module, server/index.mjs.
Виконайте npm run build, щоб створити результат компіляції, а потім запустіть сервер за допомогою nodemon server або nodemon server/index.mjs. Тепер вкладка Network показує один JavaScript-бандл (рядок 12) та порожній маркап (рядок 17). Поки що не відбувається жодної обробки на сервері, тому це все ще CSR.
Три кроки для SSR у Express
Щоб зробити SSR функціональним у сервері Express, потрібно виконати три кроки.
Крок 1: гідратація на клієнті. ReactDOM.hydrateRoot() схожий на ReactDOM.createRoot(), маючи сигнатуру hydrateRoot(container, element[, options]). Він призначений для контейнера, HTML якого вже було створено за допомогою ReactDOMServer. Оскільки вузол вже містить маркап, отриманий на сервері, React зберігає його та лише додає обробники подій, що прискорює початкове завантаження. Цей виклик знаходиться на 7-й рядку файлу src/index.js.
Крок 2: рендеринг на сервері. React постачає об’єкт ReactDOMServer, який перетворює компоненти на статичний маркап, тож браузер отримує сторінку, вже заповнену даними. Оскільки код React — це універсальний JavaScript, однакові компоненти працюють як на сервері, так і на клієнті.
Крок 3: обробка специфічних потреб сторінки. Кожна маршрутка може вимагати окремої уваги, як покажуть наступні розділи.
Як змусити Node розуміти JSX
Існує кілька пакетів, які дозволяють запустити SSR. @babel/register є одним із найпростіших. Встановіть його разом із babel-plugin-transform-assets:
% yarn add -D @babel/register
% yarn add -D babel-plugin-transform-assets
Обидва потрапляють у devDependencies:
"devDependencies": {
"@babel/register": "^7.18.9",
"babel-plugin-transform-assets": "^1.0.2"
}
@babel/register компілює файли в реальному часі та може бути налаштований для перетворення JSX та ресурсів. Існує одне обмеження: він не може компілювати нативні ES-модулі Node.js в реальному часі, оскільки поки що немає стабільного API для підключення до процесу завантаження ES-модулів. Саме тому експерименти з SSR тут використовують файли CommonJS.
React 18 переробив свої серверні API та перемістив їх під react-dom/server. Функція renderToString все ще працює, хоча підтримка Suspense у ній є обмеженою. Новіші API, зокрема renderToPipeableStream, повністю підтримують Suspense на сервері разом із потоковим SSR.
Огляд сервера SSR
Оновлений файл server/index.js пояснюється нижче за номерами рядків.
- Рядки від 1 до 15 налаштовують Babel через гак
require, тож файли компілюються під час їх завантаження.
@babel/preset-env дозволяє коду використовувати найновішу версію JavaScript без необхідності вручну вибирати трансформації синтаксису для кожного цільового середовища. @babel/preset-react автоматично імпортує функції, до яких компілюється JSX.babel-plugin-transform-assets, який визначає, як перетворюються статичні медіафайли. Без нього тег svg спричиняє помилку SyntaxError: Unexpected token '<'.React, у рядку 17 — ReactDOMServer, а у рядку 18 — значення за замовчуванням, експортоване з src/App.js.Тут важлива послідовність. Оператор app.use() на рядку 47 має виконуватися після app.get() на рядках 25–45. Якби він був раніше, він би обслуговував статичні файли, включаючи index.html, для кореневого маршруту, і app.get() би ніколи не отримав можливості виконатися.
На рядку 26 фіксується URL кожного запиту. Для проекту Create React App список виглядає так:
Request URL = /
Request URL = /static/js/main.b92cc87b.js
Request URL = /static/css/main.073c9b0a.css
Request URL = /static/media/logo.06e73328.svg
Request URL = /static/css/main.073c9b0a.css.map
Request URL = /static/js/main.b92cc87b.js.map
Request URL = /static/media/logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg
Request URL = /favicon.ico
Request URL = /manifest.json
Request URL = /logo192.png
Рядки 27–29 забезпечують те, щоб лише кореневий маршрут оброблявся функцією app.get(), а статичні ресурси передавалися до наступного мідлвейру на рядку 47.
На рядку 30 викликається функція ReactDOMServer.renderToString(element) для створення HTML на сервері. Було б природно написати ReactDOMServer.renderToString() безпосередньо, але тоді JSX довелося б зберігати у іншому файлі, ніж той, де налаштовано @babel-register, тому використовується окремий елемент.
На рядку 31 виводиться маркап, створений сервером:
<div class="App"><header class="App-header"><img src="static/media/logo.06e73328.svg" class="App-logo" alt="logo"/><p>Edit <code>src/App.js</code> and save to reload.</p><a class="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">Learn React</a></header></div>
Рядок 33 завантажує продакшновий файл index.html, а рядки з 34 по 44 читають його вміст. Якщо помилок немає, створений маркап (рядок 42) вставляється у кореневий тег, і готовий index.html стає відповіддю на перше запитання.
Перевірка роботи
Запустіть nodemon server та знову перегляньте вкладку Network. Завантажений документ тепер містить маркап, створений сервером. Рядки з 17 по 27 тіла сторінки містять всю сторінку, що дає інструментам SEO щось реальне для аналізу.
Для більш точного тесту відкрийте chrome://settings/content/javascript та вимкніть JavaScript.
Сторінка продовжує рендеруватися без JavaScript.
Це і є SSR.
Додавання маршрутів
Односторінковий додаток — це просто, тож наступне питання стосується кількох маршрутів. Почніть з встановлення react-router-dom:
% yarn add react-router-dom
Воно додається до dependencies:
"dependencies": {
"react-router-dom": "^6.11.2"
}
Потім відредагуйте src/App.js. У рядку 1 імпортується React, який необхідний для SSR. У рядку 2 імпортується MemoryRouter замість BrowserRouter. Браузерний маршрутизатор спирається на API історії HTML5 pushState, якого немає в Node.js. MemoryRouter зберігає історію URL у пам’яті, не впливаючи на поле адреси, що підходить для тестів та середовищ, що не є браузерами. Іншим варіантом є StaticRouter з пакета react-router-dom/server, хоча він не призначений для складних маршрутів. Рядки 34–40 використовують MemoryRouter.
Знову зберіть проект за допомогою npm run build, запустіть nodemon server та відвідайте http://localhost:8080.
SSR працює, але є одна особливість: URL у полі адреси не оновлюється.
Де самостійний SSR стає складним
Маршрутизація працює з цим обмеженням, але залишаються інші проблеми, такі як отримання даних та Redux. Робота на серверній стороні є складнішою, ніж на клієнтській, і кожна сторінка може потребувати окремого підходу.
Немає потреби вирішувати все це вручну, адже кілька фреймворків та бібліотек вже включають SSR. Remix — це фреймворк повної стекової архітектури, який починається з основ веб-технологій та розробляється до рівня користувацького інтерфейсу, включаючи SSR та інші функції без зайвого коду. Його клієнтський код викликає hydrateRoot на 4-му рядку, а серверський код — renderToPipeableStream на 4-му рядку.
У React 18 було введено функцію renderToPipeableStream(element, options). Якщо передати їй елемент React, вона генерує початковий HTML та повертає потік. У цьому потоці є метод pipe(res) для подальшої передачі результату та метод abort() для скасування запиту. Повністю підтримується механізм Suspense, а HTML із затриманим вмістом формує потоки без пізнього „появлення“ елементів, оскільки вбудовані теги <script> заповнюють прогалини пізніше.
На вкладці Network тіло сторінки-індексу Remix вже містить повну версію сторінки, тому це є прикладом SSR.
Gatsby, Next.js, NuxtJS, Quasar та SvelteKit також надають функції SSR та пов’язані з ними можливості без додаткових налаштувань, хоча кожен з них визначає SSR трохи по-різному. Сторінка-індекс Gatsby також відображає те саме повне тіло на вкладці Network.
Gatsby — це швидкий та надійний генератор статичних сайтів, який використовує React для відображення статичного контенту, і дозволяє змінювати статичний HTML під час його обробки на сервері.
SSR чи статичне відображення
Коли сторінка містить лише статичні дані, статичне відображення є швидшим. Якщо відповідь є динамічною, кращим вибором є SSR. Іноді найкращим рішенням є гібридний підхід. SSR відбувається за потребою, щоразу, коли запитується файл, тоді як статичне відображення відбувається один раз під час створення проекту. Обидва підходи є придатними для SEO, оскільки у тілі HTML у обох випадках міститься повний контент.
Підсумок
Налаштування SSR для Create React App зводиться до трьох елементів:
- Відображення маркапу, обробленого на сервері, за допомогою
ReactDOM.hydrateRoot(). - Відображення компонентів у статичний маркап за допомогою об’єкта
ReactDOMServer, чиї API підтримують як SSR, так і статичне відображення.
Для справжнього проекту розумним вибором є фреймворк чи бібліотека, які вже реалізують це.
Want to Connect?
If you are interested, check out my directory of web development articles.