Миграция с Nuxt

Фреймворк Fresh представляет собой современную платформу для разработки веб-приложений на JavaScript с использованием Deno. При переходе с Nuxt важно понимать различия в архитектуре, рендеринге и управлении состоянием, чтобы корректно перенести существующий проект.


Структура проекта

Nuxt использует традиционную структуру pages, components, store, plugins и assets. В Fresh структура упрощена:

  • routes/ – папка для всех маршрутов, аналог Nuxt pages. Каждая страница соответствует файлу с расширением .tsx или .ts.
  • components/ – хранение переиспользуемых компонентов. Компоненты могут быть как функциональными, так и с поддержкой серверного рендеринга.
  • islands/ – ключевая особенность Fresh. “Острова” — интерактивные части страницы, которые рендерятся на клиенте. Это аналог Nuxt-компонентов с client-only, но с более строгим разделением клиентского и серверного кода.

Особенность: в Fresh отсутствует глобальный стор. Управление состоянием клиентских компонентов выполняется через собственные хуки или сторонние библиотеки (например, Zustand, Redux Toolkit).


Рендеринг страниц

Nuxt поддерживает SSR и SSG, включая гибридные подходы. Fresh ориентирован на SSR по умолчанию, с возможностью статической генерации страниц через deno task prebuild. Основные моменты при миграции:

  • Файлы маршрутов (routes/*.tsx) экспортируют функцию handler(req: Request), которая возвращает HTML.
  • Для страниц, которые должны быть интерактивными, используются islands. Их можно подключать в JSX с помощью <Island />, где Island — компонент, импортированный из islands/.
  • Статические данные можно получать через серверные функции внутри маршрута, используя API Deno (fetch, Deno.readTextFile, и т.д.), что заменяет Nuxt asyncData и fetch.

Пример серверной функции в Fresh:

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

Компоненты и Islands

Nuxt компоненты рендерятся как на сервере, так и на клиенте. Fresh разделяет:

  • Серверные компоненты — рендерятся полностью на сервере, не имеют состояния.
  • Islands — интерактивные клиентские компоненты, подключаются на серверной странице и инициализируются на клиенте.

Пример Island:

/** islands/Counter.tsx */
import { useState } from "preact/hooks";

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onCl ick={() => setCount(count + 1)}>
      Счётчик: {count}
    </button>
  );
}

Использование в маршруте:

/** routes/index.tsx */
import Counter from "../islands/Counter.tsx";

export default function Home() {
  return (
    <div>
      <h1>Главная страница</h1>
      <Counter />
    </div>
  );
}

Управление состоянием и хуки

Nuxt предлагает Vuex или Composition API. Fresh не имеет встроенного хранилища, поэтому миграция потребует:

  • Использовать локальное состояние компонентов через хуки Preact (useState, useEffect).
  • Для глобального состояния можно подключить сторонние библиотеки, совместимые с Preact, либо реализовать контекст с createContext и useContext.

Пример глобального состояния через Context:

/** components/Store.tsx */
import { createContext } from "preact";
import { useState } from "preact/hooks";

export const CounterContext = createContext(null);

export function CounterProvider({ children }) {
  const [count, setCount] = useState(0);
  return (
    <CounterContext.Provider value={{ count, setCount }}>
      {children}
    </CounterContext.Provider>
  );
}

Работа с маршрутизацией

В Nuxt маршруты генерируются автоматически по файлам в pages/. Fresh использует похожий принцип, но с расширением .tsx и явным экспортом обработчика. Отличия:

  • Динамические маршруты создаются через квадратные скобки: [id].tsx.
  • Query-параметры и POST-запросы обрабатываются через объект Request внутри handler.

Пример динамического маршрута:

/** routes/posts/[id].tsx */
export const handler: Handlers = {
  async GET(req, ctx) {
    const { id } = ctx.params;
    const post = await fetch(`https://api.example.com/posts/${id}`).then(res => res.json());
    return ctx.render({ post });
  },
};

Асинхронные данные

В Nuxt asyncData выполняется перед рендерингом страницы. В Fresh аналогично:

  • Серверная функция handler может извлекать данные через fetch или работу с файловой системой.
  • Для изоляции логики можно создавать отдельные сервисные функции, аналог Nuxt services.

Пример сервисного слоя:

// services/posts.ts
export async function getPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`);
  return res.json();
}

Использование в маршруте:

import { getPost } from "../services/posts.ts";

export const handler: Handlers = {
  async GET(req, ctx) {
    const post = await getPost(ctx.params.id);
    return ctx.render({ post });
  },
};

Миграция стилей и ассетов

  • Nuxt поддерживает SCSS, PostCSS и другие препроцессоры. Fresh ориентирован на нативные CSS-модули или Tailwind.
  • Статические файлы помещаются в static/, аналог Nuxt static.
  • Импорт CSS возможен напрямую в компоненты через import "./style.css";.

API и middleware

Nuxt предоставляет serverMiddleware и api/. Fresh использует обработчики (Handlers) в маршрутах. Разница:

  • Middleware выполняется через функции, оборачивающие обработчики.
  • Можно создавать кастомные middleware для аутентификации, логирования или кэширования.

Пример middleware:

export function authMiddleware(handler: Handlers): Handlers {
  return {
    async GET(req, ctx) {
      const token = req.headers.get("Authorization");
      if (!token) return new Response("Unauthorized", { status: 401 });
      return handler.GET(req, ctx);
    },
  };
}

Оптимизация и производительность

Fresh ориентирован на минимизацию JavaScript на клиенте. Основные принципы:

  • Только интерактивные компоненты загружаются на клиент (islands).
  • SSR обеспечивает быстрый первый рендер и SEO.
  • Отсутствие глобального состояния снижает нагрузку на браузер.

При миграции следует выделять только действительно интерактивные части приложения в islands и максимально переносить логику на сервер.


Миграция с Nuxt на Fresh требует пересмотра подхода к рендерингу, состоянию и структуре проекта. Основное внимание уделяется разделению серверной и клиентской логики, использованию islands для интерактивности и организации маршрутов через обработчики. Такой подход обеспечивает высокую производительность и упрощает поддержку больших приложений.