Благодаря постоянному развитию экосистемы JavaScript мы почти каждый день видим новый способ создания внешнего интерфейса, будь то новый фреймворк или новый шаблон/библиотека. Однажды мой друг задал мне этот вопрос: как перенести работающее интерфейсное приложение, чтобы воспользоваться преимуществами нового фреймворка, не переписывая все приложение?

Отправная точка

Начнем с приложения Next.js в монорепозитории. Мы хотим перенести наше приложение Next.js на Astro. Итак, наш репозиторий будет содержать следующие проекты:

2 hosts проекта:

  • nextjs приложение Next.js
  • astro приложение Astro

2 apps проекта:

  • docs веб-приложение для документации на основе Storybook
  • home код веб-приложения, используемый каждым ведущим проектом

Список packages внизу:

  • data список типов и констант, которые можно использовать в репозитории
  • eslint-config-custom конфигураций для eslint
  • functions список функций, которые можно использовать во всем репозитории
  • layouts список макетов, используемых на страницах
  • next-ui-wrapper набор компонентов, характерных для Next.js (построен на основе библиотеки пользовательского интерфейса)
  • tsconfig конфигурация по умолчанию для проектов TypeScript
  • ui список компонентов, используемых в приложении

Исходный код размещен на GitHub здесь: https://github.com/Odonno/monorepo-migration.

Н.Б. Эта структура папок предназначена только для демонстрационных целей. Некоторые части этой архитектуры могут быть добавлены или удалены в более конкретном приложении.

Приложения с отслеживанием состояния

Это простое представление о современных веб-приложениях, но даже с учетом множества интерфейсных фреймворков (а теперь и мета-фреймворков) мы можем разделить страницы на 2 категории: статические страницы/страницы без сохранения состояния и страницы с состоянием.

Создание и миграция страниц без сохранения состояния довольно проста. Однако со страницами с состоянием нам нужно позаботиться о состоянии и убедиться, что мы используем правильную абстракцию, а не слишком привязаны к фреймворку. Давайте посмотрим, что можно считать состоянием:

  • Удаленные состояния: состояние из вызова API, состояние сохраняется в URL-адресе (параметры маршрута или запроса)
  • Локальные состояния: внутреннее состояние компонента, глобальное состояние в памяти в приложении (например, Redux)

Если глобальное состояние в памяти делает вас слишком привязанным к фреймворку, то лучшим противодействием будет сохранение этого состояния в локальном хранилище. Таким образом, любой другой фреймворк сможет интерпретировать данные в локальном хранилище и «перезапустить» оттуда.

Миграция страниц

Чтобы сделать миграцию всех страниц нашего приложения максимально безболезненной, нам нужно как можно меньше кода. Надеюсь, с Next.js и Astro это будет довольно легко.

Повторное использование компонента страницы React на странице Next.js:

export { default } from "home/pages"; // pages/index.tsx

Повторное использование компонента страницы React на странице Astro:

---
import Layout from "../layouts/Layout.astro";
import Home from "home/pages";
---

<Layout>
  <Home />
</Layout>

В Astro этот шаблон хорошо работает для чисто статических компонентов, но если у вас есть динамические компоненты, вам нужно будет использовать директивы шаблона.

Повторное использование компонента динамической страницы React на странице Astro:

---
import Layout from "../layouts/Layout.astro";
import Forms from "home/pages/forms";
---

<Layout>
  <Forms client:load />
</Layout>

Помните, это самый простой способ. Astro обеспечивает более развязанную структуру компонентов. Вы можете удалить проект home, содержащий страницы React, и напрямую использовать компоненты React. Или, что еще лучше, перепишите компоненты React в Astro. Это прекрасно, но тогда вы снова привязаны к фреймворку.

Миграция конечных точек API

Еще одной важной особенностью метафреймворка являются конечные точки API. Также можно выполнить их мягкую миграцию, но с немного большим количеством стандартного кода.

Начнем с примера Next.js:

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<GridApiResponse>
) {
  switch (req.method) {
    case "GET":
      convertApiResponse(res, GameApi.get(response));
    case "POST":
      convertApiResponse(res, GameApi.post(response, req.body));
    case "DELETE":
      convertApiResponse(res, GameApi.del(response));
    default:
      res.setHeader("Allow", ["GET", "POST", "DELETE"]).status(405);
  }
}

Код для каждой конечной точки API доступен в общем пакете. Нам все еще нужно сопоставить возвращаемое значение каждой функции API (get, post, del) с помощью функции сопоставления с именем convertApiResponse.

export const convertApiResponse = <T>(
  res: NextApiResponse<T>,
  apiResponse: ApiResponse
) => {
  res.status(apiResponse.status || 200).json(apiResponse.response);
};

Это специальная функция сопоставления, и она будет работать только для Next.js. Когда мы будем работать с Astro, нам придется немного изменить его:

export const convertApiResponse = (apiResponse: ApiResponse) => {
  return {
    status: apiResponse.status || 200,
    body: JSON.stringify(apiResponse.response),
  };
};

Выходя за рамки

Из того, что мы видели, миграция большой кодовой базы стала довольно простой. Использование инструмента монорепозитория, такого как turborepo, для такого рода задач может открыть некоторые возможности:

  • Мощный инструмент для создания и разработки сложных приложений.
  • С помощью подходящего инструмента (например, переключения функций) мы можем протестировать активацию/деактивацию хост-проекта в режиме реального времени, чтобы при необходимости можно было легко вернуться к предыдущему моделированию хоста.
  • Мы даже можем поддерживать работу двух хостов одновременно и выбирать, какая страница должна отображаться на каком хосте, используя балансировщик нагрузки.

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

В этой статье мы описали только один способ выполнить миграцию с хоста React на хост, поддерживающий React. Во-первых, это не статья, доказывающая, что Astro лучше, чем Next.js, или наоборот. Тогда мы могли бы выбрать другой метафреймворк, построенный на React или поддерживающий его, например Remix.

Особая благодарность моему другу Jean-Baptiste Vigneron.