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

Простой подход к SSR с React 19 и esbuild

Данная статья описывает создание простого, гибкого React-приложения с минимальным количеством инструментов и фреймворков, используя последние возможности React. Хотя Next.js и Remix отлично подходят для многих задач, это руководство ориентировано на разработчиков, желающих иметь полный контроль над процессом, сохраняя при этом функциональность, аналогичную этим фреймворкам.

В связи с выходом React 19 и новых функций поддержки SSR, был проведен эксперимент по созданию приложения с SSR, используя минимальный набор инструментов. Прежде чем начать, рассмотрим ключевые возможности Next.js, которые нужно учитывать:

Особенность Next.js
SSR (серверный рендеринг) Встроенный, с минимальной настройкой.
SSG (Статическая генерация сайта) Встроенный с getStaticProps.
Маршрутизация Маршрутизация на основе файлов.
Разделение кода Автоматический.
Оптимизация изображения Встроенный с next/image.
Оптимизация производительности Автоматически (например, предварительная загрузка, критический CSS).
SEO Встроенные инструменты, такие как next/head.

На основе этих требований будет создана минимальная конфигурация; ниже представлено пошаговое руководство:

Примечание: полный код этого руководства доступен в репозитории https://github.com/willyelm/react-app

Настройка среды

Для начала необходимо установить Node.js и следующие пакеты:

  • react: библиотека для создания пользовательских интерфейсов на основе компонентов.
  • react-dom: отвечает за восстановление содержимого SSR с использованием возможностей React.
  • react-router-dom: обработка маршрутизации в React-приложениях.
  • express: легковесный веб-сервер Node.js для статических файлов и REST API.
  • esbuild: инструмент для транспиляции TypeScript и объединения файлов JS, CSS, SVG и других ресурсов.
  • typescript: добавление статической типизации в код.

В данном приложении маршрутизация осуществляется с помощью express для обслуживания статических файлов, публичных ресурсов и REST API. Обработка запросов выполняется react-router-dom. После загрузки в браузере, клиентский пакет гидратирует предварительно рендеренный контент, делая компоненты интерактивными. Схема работы представлена ниже:

Схема работы приложения демонстрирует обработку запросов сервером Express: предварительная визуализация компонентов React на сервере и отправка HTML клиенту. На стороне клиента React «гидратирует» этот предварительно отрисованный контент, обеспечивая интерактивность.

С учетом этой схемы, структура проекта будет следующей:

react-app/             # This will be our workspace directory.
  - public/            
  - scripts/           
    - build.js         # Bundle our server and client scripts.
    - config.js        # esbuild config to bundle.
    - dev.js           # Bundle on watch mode and run server.
  - src/
    - App/             # Our components will be here.
      - App.tsx        # The main application with browser routing.
      - Home.tsx.      # Our default page component.
      - NotFound.tsx   # Fallback page for unmatched routes.
    - index.tsx        # Hydrate our pre-rendered client app.
    - main.tsx         # Server app with SSR components.
    - style.css        # Initial stylesheet.   
  package.json
  tsconfig.json

Далее, добавим зависимости и настроим package.json:

{
  "name": "react-app",
  "type": "module",
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/node": "^22.10.2",
    "@types/react": "^19.0.2",
    "@types/react-dom": "^19.0.2",
    "esbuild": "^0.24.2",
    "typescript": "^5.7.2"
  },
  "dependencies": {
    "express": "^4.21.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-router-dom": "^7.1.0"
  },
  "scripts": {
    "build": "node scripts/build",
    "dev": "node scripts/dev",
    "start": "node dist/main.js"
  }
}
Примечание: свойство "type": "module" необходимо для запуска ESM-скриптов Node.js.

Для работы с TypeScript, настроим tsconfig.json:

{
  "compilerOptions": {
    "esModuleInterop": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "baseUrl": ".",
    "paths": {
      "src": [
        "./src/"
      ]
    }
  },
  "include": [
    "src"
  ],
  "exclude": [
    "node_modules"
  ]
}

Настройка esbuild

Выбор esbuild обусловлен его минимализмом, высокой скоростью (на сегодняшний день — самый быстрый бандлер) и встроенной поддержкой TypeScript и ESM.

В данном проекте esbuild используется для создания скриптов разработки и сборки, а также для транспиляции клиентского и серверного кода. В этом разделе мы будем работать с папкой scripts.

scripts/config.js: этот файл содержит базовую конфигурацию для клиентского и серверного кода, используемую нашими скриптами.

import path from 'node:path';
// Working dir
const workspace = process.cwd();
// Server bundle configuration
export const serverConfig = {
  bundle: true,
  platform: 'node', 
  format: 'esm',        // Support esm packages
  packages: 'external', // Omit node packages from our node bundle
  logLevel: 'error',
  sourcemap: 'external',
  entryPoints: {
    main: path.join(workspace, 'src', 'main.tsx') // Express app
  },
  tsconfig: path.join(workspace, 'tsconfig.json'),
  outdir: path.join(workspace, 'dist')
};

// Client bundle configuration
export const clientConfig = {
  bundle: true,
  platform: 'browser',
  format: 'esm',
  sourcemap: 'external',
  logLevel: 'error',
  tsconfig: path.join(workspace, 'tsconfig.json'),
  entryPoints: {
    index: path.join(workspace, 'src', 'index.tsx'), // Client react app
    style: path.join(workspace, 'src', 'style.css')  // Stylesheet
  },
  outdir: path.join(workspace, 'dist', 'static'),    // Served as /static by express
};

scripts/dev.js: этот скрипт объединяет клиентское и серверное приложения и запускает основной серверный скрипт в режиме наблюдения.

import { spawn } from 'node:child_process';
import path from 'node:path';
import { context } from 'esbuild';
import { serverConfig, clientConfig } from './config.js';
// Working dir
const workspace = process.cwd();
// Dev process
async function dev() {
  // Build server in watch mode
  const serverContext = await context(serverConfig);
  serverContext.watch();
  // Build client in watch mode
  const clientContext = await context(clientConfig);
  clientContext.watch();
  // Run server
  const childProcess = spawn('node', [
    '--watch',
    path.join(workspace, 'dist', 'main.js')
  ], {
    stdio: 'inherit'
  });
  // Kill child process on program interruption
  process.on('SIGINT', () => {
    if (childProcess) {
      childProcess.kill();
    }
    process.exit(0);
  });
}
// Start the dev process
dev();

Запуск осуществляется командой npm run dev, как определено в package.json.

scripts/build.js: аналогичен dev.js, но включает минификацию.

import { build } from 'esbuild';
import { clientConfig, serverConfig } from './config.js';
// build process
async function bundle() {
  // Build server
  await build({
    ...serverConfig,
    minify: true
  });
  // Build client
  await build({
    ...clientConfig,
    minify: true
  });
}
// Start the build process
bundle();

Этот скрипт создаёт готовый к продакшену пакет dist, запускаемый командами npm run build и npm start.

После настройки esbuild для объединения клиентского и серверного кода, перейдём к созданию сервера Express и реализации React SSR.

Сервер Express и маршрутизация

Простое приложение использует Express для обслуживания статических файлов, обработки серверных маршрутов и маршрутизации с помощью react-router-dom.

src/main.tsx: основное приложение Node.js, инициализирующее сервер, обрабатывающее маршруты с помощью Express и реализующее React SSR.

import path from 'node:path';
import express, { type Request, type Response } from 'express';
import { renderToPipeableStream } from 'react-dom/server';
import { StaticRouter } from 'react-router';
import { App } from './App/App';

const app = express(); // Create Express App
const port = 3000; // Port to listen
const workspace = process.cwd(); // workspace
// Serve static files like js bundles and css files
app.use('/static', express.static(path.join(workspace, 'dist', 'static')));
// Server files from the /public folder
app.use(express.static(path.join(workspace, 'public')));
// Fallback to render the SSR react app
app.use((request: Request, response: Response) => {
  // React SSR rendering as a stream
  const { pipe } = renderToPipeableStream(
    <html lang="en">
      <head>
        <meta charSet="UTF-8" />
        <link rel='stylesheet' href={`/static/style.css`} />
      </head>
      <body>
        <base href="/" />
        <div id="app">
          <StaticRouter location={request.url}>
            <App />
          </StaticRouter>
        </div>
      </body>
    </html>,
    { 
      bootstrapModules: [ `/static/index.js` ], // Load script as modules
      onShellReady() {
        response.setHeader('content-type', 'text/html');
        pipe(response);
      }
    });
});
// Start server
app.listen(port, () => {
  console.log(`[app] listening on port ${port}`)
});

src/index.tsx: на стороне клиента, для активации и обеспечения интерактивности компонентов, необходима «гидратация».

import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import { App } from './App/App';
// Hydrate pre-renderer #app element
hydrateRoot(
  document.getElementById('app') as HTMLElement,
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

React-приложение

React-приложение использует react-router-dom для маршрутизации. Приложение состоит из страниц Home и NotFound для проверки гидратации. На странице Home добавлена кнопка счётчика, а мета-теги title и description обновляются с использованием возможностей React 19.

src/App/Home.tsx: минимальный функциональный компонент.

import { useState, type FunctionComponent } from 'react';
// Home Page Component
export const Home: FunctionComponent = () => {
  const [count, setCount] = useState(0);
  const update = () => {
    setCount((prev) => prev + 1);
  }
  return <>
    <title> App | Home </title>
    <meta name='description' content='This is my home page' />
    <h1> Home Page </h1>
    <p> Counter: {count} </p>
    <button onClick={() => update()}>Update</button>
  </>;
}

src/App/NotFound.tsx: функциональный компонент для отображения страницы 404.

import { type FunctionComponent } from 'react';
// Not Found Page Component
export const NotFound: FunctionComponent = () => {
  return <>
    <title> App | Not Found </title>
    <meta name='description' content='Page not found' />
    <h1>404</h1>
    <p> Page Not Found</p>
  </>;
}

src/App/App.tsx: настройка приложения с использованием react-router-dom.

import { type FunctionComponent } from 'react';
import { Routes, Route } from 'react-router';
import { Home } from './Home';
import { NotFound } from './NotFound';
// App Component
export const App: FunctionComponent = () => {
  return <Routes>
    <Route path="/">
      <Route index element={<Home />} />
      <Route path="*" element={<NotFound />} />
    </Route>
  </Routes>;
}

Запуск приложения

После настройки скриптов esbuild, сервера Express и реализации React SSR, запустите сервер командой:

npm run dev

В консоли появится сообщение "[app] listening on port 3000". Откройте http://localhost:3000 в браузере для проверки.

Проверка метатегов SSR и SEO:

Исходный код доступен в репозитории willyelm/react-app

В этом руководстве создано минималистичное и гибкое приложение React с SSR, используя esbuild и Express. Применение современных инструментов, таких как React 19, React Router DOM 7 и esbuild, обеспечило быстрый и эффективный рабочий процесс без накладных расходов, характерных для крупных фреймворков.

Источник:

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

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

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

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