Вокруг все говорят о серверных компонентах реакта, о серверном рендеринге, и разных новшествах в мире фронтенде. Как будто JQuery в один миг взял и исчез. Несмотря ни на что он всё ещё остаётся самой популярной библиотекой 😅.
Сегодня я вам расскажу, как мы постепенно мигрируем с JQuery на React.
Если вам понравится эта статья, загляните в мой Telegram-канал — там я делюсь полезными материалами и мыслями о программировании.
Почему мигрируем
Тут всё довольно стандартно и понятно:
- Разработка идёт медленно
- Код сложно читать и поддерживать
- XXS уязвимости подстерегают на каждом шагу
- Сложно полноценно использовать NPM из-за ограничений пространств имён (namespace) в TypeScript
Исходный стек
Миграция началась примерно в 2019 году. Наш стек выглядел так:
- ASP.NET Core
- JQuery
- TypeScript c пространства имён вместо ESM
- Немного Vanilla JS
- LESS для стилей
Весь этот стек важен, ведь каждая из его частей накладывает некоторые ограничения, от которых зависят принимаемые решения.
Прежде чем говорить о миграции, давайте расскажу, как мы писали код до React. Если не интересно — сразу переходите к основной части.
Жизнь до реакта — JQuery
В упрощённом виде наши компоненты выглядели вот так:
Мы с завистью смотрели на React, поэтому писали компоненты в похожем стиле:
- Интерфейс с пропсами
- Метод render
- Render Props функции
Что такое неймспейсы
Если вы знакомы с C# или давно используете TypeScript, то должны знать что такое неймспейсы. При компиляции они превращается в JavaScript объект, а все export
-элементы внутри пространства имён становятся свойствами этого объекта.
Например:
Компилируется в IIFE:
React-like контекст
Мы даже изобрели что-то вроде React контекста. Данные сохраняются в DOM-элементе, с помощью JQuery метода $('.container').data(dataName, value)
. А достаются (аналогично React-контексту) из любого дочерного DOM узла с помощью метода findData
.
Миграция
Почему React?
Мы не собирались переписывать всё с нуля, поэтому Angular нам точно не подходил — выбор стоял между React и Vue. Так как у нас в команде был разработчик с опытом миграции с JQuery на React, то выбор пал именно на него.
Подход к миграции
Дело в том, что в одном проекте нельзя использовать одновременно неймспейсы и ES-модули. Никакого инструмента для авто-конвертации тоже нет. Команда TypeScript писала внутренний инструмент для конвертации кодовой базы TypeScript’а (TypeScript написан на TypeScript’е!) на модули на основе AST.
В общем, не получалось просто взять и добавить React в существующий проект. нас было 2 варианта:
- Переписываем существующий код на ES-модули и интегрируем React
- Создать отдельный проект, где писать будем только на реакте. Никакого JQuery!
Но существующая кодовая база достаточно большая, поэтому мы решили идти вторым путём.
После того как определились с подходом решили не писать webpack-конфиг с нуля — просто взяли Create React App (CRA) и сделали Eject. Потом мы сделали форк, чтобы было проще обновляться на новые версии, тогда CRA был ещё жив 🪦.
Отдельный проект — библиотека компонентов
Про реакт из каждого утюга говорят, что это просто View слой, поэтому его можно легко использовать в существующем проекте.
Мы решили, что наш реакт проект будет своего рода библиотекой компонентов.
То есть все новые компоненты мы пишем в новом проекте и просто встраиваем в существующий. Для рендера реакт-компонентов в DOM-дерево, нам нужно использовать функцию createRoot
(до React18 — ReactDOM.render
).
Все вы видели следующий код:
Именно он находится в index.tsx
файле вашего проекта. Точно также мы и будем встраивать наши компоненты в существующее приложение.
В React17 для рендера и обновления компонента можно было использовать ReactDOM.render
, главное передавать один и тот же DOM-элемент. При миграции на React 18 нам пришлось написать функцию-обёртку renderComponent
для удобного использования createRoot()
. Код целиком можно найти здесь.
Весь публичный API нашей библиотеки находится в файле library.tsx
. В основном это функции-обёртки для рендера реакт компонентов, которые выглядят следующим образом:
Тут же мы используем динамические импорты для код сплитинга, чтобы подгружать код по мере необходимости.
Недавно наткнулся на статью The anatomy of a React Island, где описывается такой же подход.
Конфигурация Webpack
По умолчанию Create React App — приложение, и чтобы сделать из него библиотеку мы немного изменили webpack-конфиг. Возможность запускать CRA как приложение мы также оставили, но для чего — немного позже.
Упрощенно конфиг библиотеки выглядит так:
Быстро пробежимся по конфигу.
entry: { 'lib': './src/library.tsx' }
— src/library.tsx
- основной файл нашей библиотеки. Тут мы указываем, что будет доступно в существующем проекте.
После билда мы получим файл с именем lib.[contenthash].js
(например, lib.94c4847c.js
), который нужно будет подгрузить в основное приложение.
output.library.name
: ‘myLib’ — имя объекта, в котором будет доступно всё, что экспортируется из library.tsx
.
output.library.type
: ‘umd’, тип модулей совместимый с большинством популярных загрузчиков. Нас интересует только возможность работать с библиотекой как с глобальной переменной, поэтому значения window или var тоже бы подошли.
Проще говоря, всё, что мы экспортируем из src/library.tsx
будет упаковано в объект myLib
и доступно глобально. В существующем проекте мы сможем вызвать renderAppHeader
вот так:
Интегрируем
Библиотека компонентов есть, но как её интегрировать в существующий проект?
Обычно для загрузки скриптов в index.html
используется HtmlWebpackPlugin
, это очень удобно, а когда мы используем contenthash
— жизненно необходимо.
Но мы не разрабатываем приложение с нуля. Мы интегрируем реакт в существующее ASP.NET приложение, где для написания разметки используются шаблонизатор Razor, а файлы имеют расширение cshtml. При компиляции ASP.NET приложения, cshtml файлы будут включены в dll сборку.
Мы могли бы генерировать cshtml файл с помощью HtmlWebpackPlugin
’а и затем подключать его через Html.PartialAsync
.
Но тогда, на каждый билда фронта, нам придётся запускать и билд ASP.NET приложения. Всё из-за того, что имена js
файлов будут всё время меняться из-за использования contenthash’а. Избежать этого нам поможет “манифест”, для этого нам и нужен WebpackManifestPlugin
в конфиге выше.
Манифест выглядит примерно вот так (на реальном проекте он будет намного больше):
Он содержит название нашего основного бандла lib.js
и путь с самому файлу ./lib.016f9cc5.js
. С помощью манифеста мы можем получить название основного бандла и подгрузить его.
В итоге, интеграция проектов выглядит вот так
Нельзя просто так взять и мигрировать
К сожалению, от легаси «не спрятаться не скрыться, …». Например, иногда приходится использовать сложный компонент написанный на JQuery внутри React компонента.
В этом случае мы просто пишем компоненты-обёртки. Упрощённо они выглядят вот так.
Кастомный генератор типов
Теперь можно сказать, что всё работает и мы можем использовать нашу библиотеку в существующем проекте. Но про кое-что мы забыли. Мы забыли про типы!
Мы любим TypeScript, и не любим писать код без автодополнений и проверки типов. CRA использует babel под капотом, в котором нет проверки типов и потому нет возможности генерировать .d.ts
файлы. Поэтому во время сборки мы запускаем tsc
для генерации типов.
В принципе, такой гибридный подход и рекомендуется в документации TypeScript — Babel for transpiling, tsc for types.
Конфиг выглядит примерно вот так:
declaration
,emitDeclarationOnly
- указываем, что нам нужно сгенерировать только файлы типовoutFile
- путь, по которому будет сгенерирован файл с типамиmoduleResolution
,module
- нужны для корректной обработки импортов
Файл с типами будет выглядеть вот так:
Но это ещё не всё. Помните, мы указали имя библиотеки myLib
? Так tsc
об этом ничего не знает. Как временное решение, мы просто взяли и с помощью регулярок:
- удалили
} declare module "path/to/mo"
- оставшийся импорт (перед которым нем
}
) заменили наdeclare module myLib
- удалили вообще все импорты
В итоге получили:
Но нет ничего более постоянного чем временное, поэтому ничего менять в итоге не стали. Генерируемые типы не совсем корректны, но нам главное, что он даёт базовые автокомплит и проверку типов.
Управление состоянием — Zustand
Изначально у нас вообще не было стейт менеджера, весь код мы писали на useState/useReducer + useContext. Но в этом подходе есть несколько проблем:
- useContext не поддерживает атомарные обновления
- В useReducer нельзя вынести асинхронную логику
В качестве стейтменеджера мы выбрали Zustand. Подробнее почему именно его — можно почитать здесь, но основная причина — его можно использовать вне React, в существующей части проекта.
Выглядит это так:
Теперь в существующей части проекта можно использовать AppStore:
Стили
В новом компоненте мы используем компонентный подход — всё, что относится к компоненту — кладём рядом:
- код компонента
- стили
- тесты
- истории сторибука
Чтобы использовать существующие LESS-переменные и миксины в новом проекте мы используем pnpm-монорепозиторий. Для этого мы создали package.json
в папке со стилями в существующем проекте и добавили зависимость в реакт проекте:
И далее просто импортируем нужный нам файл в стилях.
Тильда ~
перед legacy-styles
говорит вебпаку, что стили находятся в папке node_modules
.
Песочница
Помните я говорил, что мы оставили возможность запускать CRA как приложение? Проблема в том, что в существующем приложении нет Hot Reload’а 😮. Эту ситуацию мы также смогли немного улучшить.
Когда мы запускаем pnpm run build
происходит всё то, что я описал выше. Но при pnpm start
запускается стандартное реакт приложение. Мы называем его песочницей. По той же причине мы используем сторибук, но в нём нельзя выполнять API запросы. Мы добавили реакт роутер, и для каждой фичи создаём песочницу по отдельному урлу.
Заключение
Если подводить итог, то с уверенностью можно сказать, что код стало писать в разы легче.
Но важно помнить, что если в вашей команде не все знакомы с реакт или с другой новой технологией, вам нужно быть очень осторожными и тщательно делать код ревью. Миграция также нужна и для подхода, который используют люди. Если вы пишете на реакте код автоматически не становится идеальным, очень легко написать плохой код на любой технологии.
Если у вас есть опыт миграции на реакт или идеи как это можно было сделать лучше — расскажите в комментариях 🙏.
Код из статьи можно найти на гитхабе.
Если вам понравилась статья подпишитесь на мой телеграмм канал о программировании и не только.