Загрузка данных в серверных компонентах

Fresh — современный фреймворк для разработки веб-приложений на JavaScript и TypeScript, ориентированный на скорость, простоту и использование возможностей серверного рендеринга. Одной из ключевых особенностей является работа с серверными компонентами (Server Components), где обработка данных происходит на сервере до отправки HTML на клиент.

Серверные компоненты и их преимущества

Серверные компоненты в Fresh выполняются на сервере и сразу возвращают готовый HTML. Основные преимущества такого подхода:

  • Скорость загрузки страницы — HTML формируется на сервере, поэтому клиент получает готовую разметку без ожидания загрузки большого количества JS.
  • Безопасность данных — чувствительная информация обрабатывается на сервере и не попадает в клиентский код.
  • Меньше нагрузки на клиент — нет необходимости рендерить компоненты полностью на стороне пользователя.

Основные принципы загрузки данных

Загрузка данных в серверных компонентах Fresh строится вокруг функции handler и специальных методов fetch, а также встроенной поддержки асинхронного кода. Пример базовой структуры:

import { Handlers, PageProps } from "$fresh/server.ts";

export const handler: Handlers = {
  async GET(_req, ctx) {
    const data = await fetch("https://api.example.com/items")
      .then(res => res.json());
    
    return ctx.render({ items: data });
  },
};

export default function Page({ data }: PageProps) {
  return (
    <div>
      {data.items.map(item => (
        <p key={item.id}>{item.name}</p>
      ))}
    </div>
  );
}

Ключевые моменты:

  • handler отвечает за обработку HTTP-запроса.
  • ctx.render передает данные в компонент для рендеринга.
  • Все асинхронные операции выполняются на сервере, клиент получает уже готовый HTML.

Использование асинхронных функций

В Fresh серверные компоненты могут использовать async/await для загрузки данных из внешних API, баз данных или локальных файлов:

export default async function Users() {
  const response = await fetch("https://api.example.com/users");
  const users = await response.json();

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.username}</li>
      ))}
    </ul>
  );
}

В этом примере HTML формируется только после завершения запроса к API, что гарантирует, что клиент увидит полный контент сразу.

Работа с базами данных

Для серверных компонентов характерна интеграция с базами данных напрямую на сервере. Например, используя Deno и PostgreSQL:

import { Client } from "https://deno.land/x/postgres/mod.ts";

export default async function Posts() {
  const client = new Client({ user: "user", database: "blog", hostname: "localhost", password: "pass" });
  await client.connect();

  const result = await client.queryArray("SELECT id, title FROM posts");
  await client.end();

  return (
    <div>
      {result.rows.map(([id, title]) => (
        <h2 key={id}>{title}</h2>
      ))}
    </div>
  );
}

Особенности:

  • Подключение к базе и выполнение запроса происходит на сервере.
  • Данные передаются компоненту через возвращаемый результат функции.
  • Серверные компоненты исключают необходимость передачи SQL-запросов на клиент.

Кэширование и оптимизация

Для увеличения производительности важно использовать кэширование данных. Fresh позволяет комбинировать серверные компоненты с заголовками кэширования HTTP:

export const handler: Handlers = {
  async GET(_req, ctx) {
    const data = await fetch("https://api.example.com/data").then(res => res.json());

    const response = ctx.render({ data });
    response.headers.set("Cache-Control", "s-maxage=60, stale-while-revalidate=30");
    return response;
  },
};

Пояснение:

  • s-maxage указывает, сколько времени кэшировать на сервере.
  • stale-while-revalidate позволяет отдавать устаревший контент, пока обновляется новый.

Комбинирование серверных и клиентских компонентов

Хотя серверные компоненты отвечают за загрузку данных и рендеринг HTML, их можно сочетать с клиентскими компонентами для интерактивности:

import Counter from "../islands/Counter.tsx";

export default async function Dashboard() {
  const stats = await fetch("https://api.example.com/stats").then(res => res.json());

  return (
    <div>
      <h1>Статистика</h1>
      <p>Пользователи: {stats.users}</p>
      <Counter />
    </div>
  );
}

Заметки:

  • Серверная часть загружает и отображает статические данные.
  • Клиентская компонента (Counter) добавляет интерактивность без перезагрузки страницы.
  • Такой подход позволяет минимизировать объем JS, отправляемого на клиент.

Работа с ошибками при загрузке данных

Серверные компоненты позволяют обрабатывать ошибки до передачи данных клиенту:

export const handler: Handlers = {
  async GET(_req, ctx) {
    try {
      const data = await fetch("https://api.example.com/items").then(res => res.json());
      return ctx.render({ items: data });
    } catch (err) {
      return ctx.render({ error: "Не удалось загрузить данные" });
    }
  },
};

export default function Page({ data }) {
  if (data.error) return <p>{data.error}</p>;
  return <ul>{data.items.map(item => <li key={item.id}>{item.name}</li>)}</ul>;
}

Преимущество:

  • Обработка ошибок происходит на сервере, клиент получает корректный HTML, даже если загрузка данных не удалась.

Итоговая структура загрузки данных в Fresh

  1. Объявление серверного компонента — асинхронная функция или handler.
  2. Получение данных — API-запросы, база данных или локальные файлы.
  3. Обработка ошибок и кэширование — через try/catch и заголовки Cache-Control.
  4. Передача данных в компонент — через ctx.render или возвращаемый результат асинхронной функции.
  5. Комбинация с клиентскими компонентами — для интерактивных элементов без дополнительной нагрузки на сервер.

Загрузка данных в серверных компонентах Fresh обеспечивает высокую производительность, безопасность и минимизацию объема кода на клиенте, делая приложения быстрыми и отзывчивыми даже при сложной структуре данных.