
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 и отображает компонентPokemonListReact в 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.
Также подписывайтесь на наш телеграм‑канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными статьями.
