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.


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

Комментарии (2)


  1. s1967
    20.09.2023 05:24

    А зачем тут bun? Все эти задачи примерно в столько же строк кода решаются на проверенном годами webpack


    1. technokratiya Автор
      20.09.2023 05:24

      Материал для тех, кто хочет опробовать Bun на практике. Конечно, никто не запрещает пользоваться webpack