Bun — «швейцарский нож» для JavaScript, который все ждали, наконец релизнулся и уже стал геймченджером. Bun представляет собой универсальную среду выполнения JavaScript и набор инструментов, рассчитанный на высокую скорость работы. В его состав входят бандлер, тест-раннер, встроенная поддержка TypeScript и JSX и даже менеджер пакетов, совместимый с Node.js.

Дисклеймер: это вольный перевод статьи из блога Алекса Кейтса. С оригинальным постом можно ознакомиться здесь.

В этом руководстве мы погрузимся в функционал Bun 1.0, чтобы раскрыть весь его потенциал. Мы рассмотрим:

?️ Процесс установки Bun

? Генерация проекта Bun

?️ Создание первого сервера Bun

? SSR с помощью Bun и React

? Получение сторонних данных и рендеринг на стороне сервера

Весь код, используемый в данном руководстве, можно найти по ссылке: https://github.com/alexkates/ssr-bun-react 

Настройка проекта

Установка Bun

Вы можете установить Bun рядом с установленной нодой, не нарушая при этом работу других репозиториев.

# Install Bun
curl -fsSL https://bun.sh/install | bash

Инициализация проекта Bun

Далее инициализируем новый проект Bun.

# Project setup
mkdir bun-httpserver
cd bun-httpserver
bun init

Использование bun init приводит к созданию схемы проекта, как показано на следующем скриншоте. Вы заметите новый файл, bun.lockb, который заменяет файлы блокировки yarn, npm или pnpm. Кроме того, index.ts и tsconfig.json по умолчанию являются готовыми, что означает поддержку TypeScript, не требующую дополнительных настроек.

Ваш первый Bun-сервер

Создать свой первый Bun-сервер очень просто, буквально за несколько строк кода.

const server = Bun.serve({
  port: 3000,
  fetch(req) {
    return new Response(`Bun!`);
  },
});

console.log(`Listening on http://localhost:${server.port} ...`);

Внимательно рассмотрите каждую строку…

  • const server = Bun.serve({ ... });: Эта строка инициализирует сервер с помощью Bun.serve() и задает ему режим прослушивания на порту 3000.

  • port: 3000,: Указывает, что сервер должен прослушивать порт 3000.

  • fetch(req) { ... }: Определяет функцию, которая будет обрабатывать все поступающие HTTP-запросы. При поступлении запроса она возвращает новый HTTP-ответ с текстом "Bun!".

  • return new Response(Bun!);: Создает новый объект HTTP-ответа с текстом "Bun!".

  • console.log(Listening on localhost:${server.port} ...);: Выводит в консоль сообщение о том, что сервер прослушивается. Для динамической вставки номера порта используются шаблонные строки.

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

Реализация рендеринга на стороне сервера (SSR) с помощью React и Bun

Теперь начинается настоящее веселье: Реализация рендеринга на стороне сервера (Server-Side Rendering, SSR) с помощью React и Bun. В этом разделе мы погрузимся в тонкости Server-Side Rendering, или, как его часто сокращают, SSR, используя React и Bun.

Добавление пакетов в Bun

Чтобы добавить пакеты в Bun, просто воспользуйтесь командой add. Хотите получить пакет как dev-зависимость? Просто добавьте флаг -d.

bun add react react-dom
bun add @types/react-dom -d

Переход на JSX

Далее мы переименуем существующий серверный файл index.ts в файл index.tsx. Это позволит нам напрямую возвращать элементы JSX.

mv index.ts index.tsx

Погружение в наш новый индекс index.tsx

В этом обновленном файле index.tsx мы используем renderToReadableStream из react-dom/server для рендеринга нашего компонента Pokemon. Затем мы оборачиваем этот поток в объект Response, обеспечивая установку типа содержимого "text/html".

import { renderToReadableStream } from "react-dom/server";
import Pokemon from "./components/Pokemon";

Bun.serve({
  async fetch(request) {

    const stream = await renderToReadableStream(<Pokemon />);

    return new Response(stream, {
      headers: { "Content-Type": "text/html" },
    });
  },
});

console.log("Listening ...");

Так, здесь происходит несколько важных моментов. Давайте рассмотрим подробнее.

  • import { renderToReadableStream } from "react-dom/server";: Импортирует функцию renderToReadableStream из пакета react-dom/server для рендеринга React на стороне сервера.

  • import Pokemon from "./components/Pokemon";: Импортирует компонент React с именем Pokemon из относительного пути к файлу.

  • Bun.serve({ ... });: Использует метод Bun.serve() для настройки HTTP-сервера. Он включает в себя асинхронную функцию fetch для обработки входящих HTTP-запросов.

  • async fetch(request) { ... }: Асинхронная функция, которая будет запускаться для каждого HTTP-запроса, поступающего на сервер.

  • const stream = await renderToReadableStream(<Pokemon />);: Асинхронно рендерит React-компонент Pokemon в читаемый поток.

  • return new Response(stream, { ... });: Возвращает новый объект HTTP Response с читаемым потоком и устанавливает заголовок "Content-Type" в значение "text/html".

  • console.log("Listening ...");: Выводит в консоль сообщение о том, что сервер прослушивает входящие запросы.

Создание потокового компонента React

Наконец, мы построим простой компонент React. Этот компонент будет рендериться на стороне сервера (SSR) и передаваться обратно клиенту.

import React from "react";

type PokemonProps = {
  name?: string;
};

function Pokemon() {
  return <div>Bun Forrest, Bun!</div>;
}

export default Pokemon;

Запуск Bun-сервера

Далее начинается самое интересное - запускаем наш сервер Bun и смотрим, как все складывается!

bun index.tsx

Перейдите по ссылке http://localhost:3000 и вы увидите наш SSR компонент Pokemon!

Построение динамических маршрутов с покемонами

Пора перейти к более сложным задачам. В этом разделе мы создадим два отдельных маршрута: /pokemon и /pokemon/[pokemonName].

  • Переход по адресу /pokemon вызывает запрос на получение информации из Pokémon API и выдает результаты в виде списка тегов с якорями, по которым можно кликнуть.

  • При нажатии на любой из этих тегов вы перейдете на страницу /pokemon/[pokemonName], где будет получен конкретный покемон, отрендерен на стороне сервера (SSR) и затем передан обратно клиенту.

Более пристальный взгляд на наш усовершенствованный индекс index.tsx

В этой обновленной версии наш index.tsx может быть более сложным. Теперь в нем реализована динамическая маршрутизация для отображения списка покемонов, полученного из Pokémon API, или для отображения конкретного покемона на о��нове URL. Будь то список или отдельный покемон, компонент рендерится на стороне сервера и затем передается обратно клиенту.

import { PokemonResponse } from "./types/PokemonResponse";
import { PokemonsResponse } from "./types/PokemonsResponse";
import { renderToReadableStream } from "react-dom/server";
import Pokemon from "./components/Pokemon";
import PokemonList from "./components/PokemonList";

Bun.serve({
  async fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === "/pokemon") {
      const response = await fetch("https://pokeapi.co/api/v2/pokemon");

      const { results } = (await response.json()) as PokemonsResponse;

      const stream = await renderToReadableStream(<PokemonList pokemon={results} />);

      return new Response(stream, {
        headers: { "Content-Type": "text/html" },
      });
    }

    const pokemonNameRegex = /^\/pokemon\/([a-zA-Z0-9_-]+)$/;
    const match = url.pathname.match(pokemonNameRegex);

    if (match) {
      const pokemonName = match[1];

      const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonName}`);

      if (response.status === 404) {
        return new Response("Not Found", { status: 404 });
      }

      const {
        height,
        name,
        weight,
        sprites: { front_default },
      } = (await response.json()) as PokemonResponse;

      const stream = await renderToReadableStream(<Pokemon name={name} height={height} weight={weight} img={front_default} />);

      return new Response(stream, {
        headers: { "Content-Type": "text/html" },
      });
    }

    return new Response("Not Found", { status: 404 });
  },
});

console.log("Listening ...");

Здесь происходит много интересного. Давайте поподробнее остановимся на самых интересных моментах.

  • Инициализация HTTP-сервера с помощью Bun: Метод Bun.serve() устанавливает HTTP-сервер и задает асинхронную функцию fetch для обработки входящих запросов, фактически выступая в качестве точки входа для всего HTTP-трафика.

  • Маршрут для всех покемонов: Когда URL-адрес имеет значение /pokemon, сервер получает список покемонов из внешнего API и отображает компонент PokemonList React в HTML. Затем этот HTML отправляется обратно клиенту.

  • Маршрут для конкретных покемонов: Код использует регулярное выражение для поиска путей URL, в которых указано имя конкретного покемона (например, /pokemon/pikachu). Если такой путь обнаружен, сервер получает подробную информацию о конкретном покемоне и отображает ее с помощью React-компонента Pokemon.

  • Рендеринг React на стороне сервера: Для общего и специфического маршрутов покемонов функция renderToReadableStream преобразует компоненты React в читаемый поток, который затем возвращается в виде HTML-ответа.

  • Обработка ошибок: В коде предусмотрена специальная обработка ошибок 404. Если покемон не найден в API или если URL не соответствует ожидаемым маршрутам, возвращается сообщение "Not Found" с кодом состояния 404.

Компонент PokemonList

Этот компонент получает список покемонов и превращает их в элементы списка, на которые можно нажать. Каждый элемент списка представляет собой ссылку, которая при клике направляет пользователя на страницу /pokemon/[name], где отображается подробная информация о каждом покемоне.

import React from "react";

function PokemonList({ pokemon }: { pokemon: { name: string; url: string }[] }) {
  return (
    <ul>
      {pokemon.map(({ name }) => (
        <li key={name}>
          <a href={`/pokemon/${name}`}>{name}</a>
        </li>
      ))}
    </ul>
  );
}

export default PokemonList;

Компонент Pokemon

Компонент Pokemon отвечает за получение данных о росте, весе, имени и URL-адресе изображения отдельного покемона и возвращает именно то, как мы хотим отобразить одного покемона.

import React from "react";

function Pokemon({ height, weight, name, img }: { height: number; weight: number; name: string; img: string }) {
  return (
    <div>
      <h1>{name}</h1>
      <img src={img} alt={name} />
      <p>Height: {height}</p>
      <p>Weight: {weight}</p>
    </div>
  );
}

export default Pokemon;

Повторный запуск сервера с помощью HMR

Пришло время перезапустить наш сервер, но на этот раз давайте добавим флаг --watch для Hot Module Reloading (HMR). Хорошие новости - Bun все предусмотрел, так что с nodemon можно попрощаться.

bun --watch index.tsx

Динамические маршруты в действии

На первом скриншоте показано, что происходит при переходе по адресу /pokemon. Как видите, появляется список покемонов, каждый из которых является ссылкой, на которую можно нажать. Все это происходит благодаря нашему компоненту PokemonList, который получает и отображает имена покемонов.

На втором скриншоте мы видим /pokemon/charmander. На этот раз компонент Pokemon занимает центральное место, показывая рост, вес и изображение Чармандера - разумеется, все это красиво отрисовано на стороне сервера.

Вот и все, друзья!

Если вы занимались написанием кода, похлопайте сами себе! Вы только что:

?️ Установили и инициализировали новый блестящий проект Bun.

? Создали свой собственный HTTP-сервер.

?️ Использовали рендеринг на стороне сервера (SSR) для потоковой передачи простого компонента React.

?️ Построил два отдельных маршрута для получения данных и SSR различных компонентов React.


Также подписывайтесь на наш телеграм‑канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными статьями.