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.
Также подписывайтесь на наш телеграм‑канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными статьями.
s1967
А зачем тут bun? Все эти задачи примерно в столько же строк кода решаются на проверенном годами webpack
technokratiya Автор
Материал для тех, кто хочет опробовать Bun на практике. Конечно, никто не запрещает пользоваться webpack