Николай Ланец
15 мар. 2021 г., 18:27

MODX + React + NextJS + Prisma-2 + Nexus. gorodskie-bani.ru v3

Всем привет!

Сегодня хочу немного рассказать про успешный запуск очередного долгостроя - обновление сайта https://gorodskie-bani.ru. Это мой личный проект и про него я уже ранее писал. Сначала я его переносил с WordPress на MODX. Затем фронт был переписан на NodeJS+GraphQL+React. При этом бОльшая часть логики по-прежнему выполнялась на стороне MODX, а реализация GraphQL была очень тяжелая (потому что тогда еще только появлялись решения типа Apollo, а я все писал сам) и запущенный сервер сжирал больше 2 Гб оперативы (а на ранних стадиях и вовсе под 4 ГБ).

Теперь же я переписал практически с нуля весь фронт и полностью написал бэк, практически отделившись от MODX. От него пока что остались только коннекторы на ресайз картинок. То есть пока не получилось полностью избавиться от MODX, но все же он тут на уровне статистической погрешности, к тому же он теперь не виден из фронта, то есть напрямую к нему теперь никак не обратиться. В целом же бэк весь теперь написан на JS, а работа с базой данных реализована через Prisma-2 (и немного Knex). И в процессе реализации проекта я освоил много новых техник, чем и хочу поделиться.

Во-первых, как я и сказал, пришлось осваивать Prisma-2. Уточню, что текущая версия freecode.academy работает на первой призме, с которой я работаю уже не один год. Почему же здесь пришлось осваивать именно вторую призму? Дело в том, что первая призма - это для проектов с нуля. В данном случае речь именно про базу данных. Дело в том, что с призмой принцип следующий: мы описываем наши модели, затем говорим призме "деплой", она коннектится с базой данных, создает или редактирует в ней таблицы, затем генерит методы для работы с этой базой (включая GraphQL схему). И все это работает только с чистой базой. Нельзя присосаться к уже существующей. А задача стояла именно в этом, то есть работать с уже существующей базой данных. И вот как раз у второй призмы есть метод introspection. Он как раз и позволяет заработать с другого входа, то есть проанализировать целевую базу данных, сгенерировать GraphQL-модели, сгенерировать миграционные SQL-скрипты и сгенерировать конечный JS-код для работы с этой БД. В итоге получилась вот такая модель и вот такой первичный SQL структуры базы данных. К сожалению, без шероховатостей не обошлось в этом, но пофиксил первичную схему довольно быстро, и в дальнейшем с ней все ОК уже было.

Приятность еще здесь в том, что Призма-2 далее можно вести развитие схемы, добавляя новые связи и прочие изменения, и призма умеет генерировать миграции (можно и самому ручками миграции дописывать). К примеру, вот миграция создания вторичного ключа в связке site_content::site_tmplvar_contentvalues (Ресурсы::TV-параметры). То есть если рассматривать привычный процесс работы с MODX, то если мы что-то на локале редактировали в БД, то потом при переносе на рабочий сервер, нам приходилось думать о том, чтобы не забыть внести изменения в БД еще и там (на самом деле мы чаще просто работали прям на самом сайте боевом). А если представить о том, что с сайтом в единицу времени работает сразу несколько программистов (каждый на своей локальной копии), то процесс свести все воедино просто обречен на возникновение ошибок.

Здесь же я просто в докере в скрипте прописал yarn prisma:deploy, и еще на этапе сборки контейнера будет выполнена попытка миграции в БД, и если она не пройдет успешно, то контейнер не будет собран и не будет замещен текущий рабочий (то есть не получится, что контейнер встал на место работающего, миграция развалилась и сайт перестал работать). И даже если несколько разрозненных программистов выполнят каждый на своей машине собственные миграции, все они в итоге выполнятся последовательно при сведении всех коммитов. Вот такая радость.

К слову, да, проект упакован в докер. За основу был взят этот проект: https://github.com/Fi1osof/docker-lnmp (склонированный и доработанный с другого :)). Мне он понадобился тогда, когда старый клиент обратился с просьбой доработок на MODX-сайте. Конечно же я сначала должен был выполнить доработки на локале, а потом перенести на боевой. Но вот на локале у меня не получилось запустить, потому что MySQL там 5 версии, php тоже древний. То есть их уже под мою версию убунты просто нет. В итоге решил собрать докер-проект для запуска старых MODX-сайтов. Собрал. Работает. Если кому такое понадобится, пишите в комментах. Если потребность есть, напишу отдельную статью, объясню что и как там работает. Для локальной разработки вполне может пригодиться (особенно при работе с разными сайтами на разных версиях php и MySQL).

Кстати, для этого я в MODX-конфиг прописал переменные окружения, а сам конфиг добавил в гит-проект. Планирую вообще весб MODX-сайт добавить в гит (ну, кроме динамики и кеша, конечно же).

Вторая технология, которую пришлось тоже осваивать - Nexus. Смысл в том, что нельзя просто так как есть выставить наружу сгенерированный призма-проект. Согласитесь, не круто будет, если все методы чтения на все таблицы и поля будут доступны наружу, да еще и все методы на обновление всех данных :). То есть нужны средства для написания промежуточного API, публичного. Но тут момент: нужна двусторонняя типизация. То есть фронт тянет структуру из фронтового API и все методы завязаны на него. Если что-то поменяется в API и это затрагивает фронт (то есть могут сломаться API-запросы), то фронт должен это узнать (а разработчик получить ошибки еще на уровне анализа TypeScript). В свою очередь фронтовое API завязано на бэковое API и так же должно быть в курсе изменений в нет, и если мы в своих методах пытаемся запросить некорректно данные, мы так же должны еще на уровне TypeScript узнать об этом. Вот Nexus это и обеспечивает: мы описываем модели и методы для фронта, а он проверяет все ли у нас корректно. Штука довольно сложная и капризная, но крайне полезная. С ней разработка проекта в комплексе (API+фронт) сильно надежней получается.

Но не все тут так просто. К примеру, есть у меня вот такая страница: https://gorodskie-bani.ru/ratings (рейтинги бань, завездочками в карточках выводятся). То есть мне надо было сделать выборку всех голосов и сгруппировать по типам рейтинга и заведениям, да еще и отсортировать. И вот здесь-то призма и не справилась. То есть в общих чертах она в группировки умеет, но она не умеет сортировать по этим полям. Она умеет сортировать только по тем полям, которые перечислены в критериях группировки, но не умеет по самим суммам. Плюс к этому не умеет в результаты вывести данные других таблиц. В итиге задачка с расчетным временем в часик-полтора вылилась в 6+ часов довольно сложных исследований. Но на выходе получилось довольно интересное решение с чистыми SQL-запросами и типизацией по ним. И здесь как раз пригодился Knex.

Вообще, Knex интересен еще и тем, что с ним не обязательно выполнять коннект к базе данных. Его можно использовать просто для генерации безопасного SQL. А сам SQL выполнять через ту же призму, для этого у нее есть метод выполнения чистых SQL-запросов $queryRaw(sql). И я даже сначала так и сделал. Но тут возникла проблема с типизацией, так как Knex именно при написании .then() генерирует конечное описание типов для TS с учетом прописанных полей в select(), и именно там понятно какие типы данных будут возвращены. Скорее всего через ReturnType и прочее TS-колдунство можно было вычленить эти типы, но, к сожалению, в TS я пока не эксперт и не смог этого сделать, поэтому пришлось выполнять запросы через Knex, но по идее это не особая проблема.

Ну а в остальном (в плане фронта и в целом работы сайта) все было написано на базе заготовки https://github.com/prisma-cms/nextjs. То есть общий движок как есть из коробки. Дописывались только конечные страницы и компоненты, а сам механизм API, роутинг и т.п. - все базовое.

Из интересного здесь вот этот общий роутер для всех страниц: /src/pages/Resource/index.tsx. То есть в MODX же как? Есть таблица site_content, где хранятся все вообще ресурсы. В админке мы редактируем структуру сайта, создавая эти ресурсы с вложенностью. УРЛы этих страниц генерятся из алиасов родителей, а можем мы и вручную задать УРЛ конкретной страницы (заморозив УРЛ). При сохранении страницы MODX переобходит все дерево документов, генерирую карту ресурсов (Привет работа с десятками тысяч страниц и генерация кеша при каждом сохранении!))). То есть в MODX у нас чаще всего отсутствует какая-либо структурность в УРЛ (SEOшники ликуют, потому что за это они MODX и любят - возможность любой странице задать любой SEO-frendly адрес). Вот это наследие я и получил - все страницы разрознены и нет возможности по адресу сразу определить что это за страница и какой шаблон для нее применить (и какие допольнительные данные запросить). К примеру, города могут иметь УРЛ типа /city/zlatoust (и тут все понятно), а могут находиться и в корне сайта, как /moscow, а может в корне и статья быть, как то /predlozhenie-dlya-vladelcev-saun (к которой еще и комменты сразу запросить надо, а иначе SSR будет не полный).

Вот и получается, что надо иметь возможность написать единую точку входя для всех неизвестных страниц и там уже, получив данные страницы, определить что это за страница и вызывать нужную для нее вьюху (а может еще и данные допросить). Вот /src/pages/Resource/index.tsx как раз и является такой единой точкой. Но самая изюминка сокрыта именно в механизме определения типа документа. Сначала все эти документы были суть Resource и логику я прописал по условию template (шаблон). Но мне такая логика не очень понравилась, тем более что на выходе я имею только возможность какую-то логику рулить уже на фронте и только с общими полученными данными. Мне же хотелось сразу с сервера получать разный набор данных в зависимости от типа токумента (хоть они и есть все суть одни данные из одной таблицы). И тут на помощь приходят GraphQL union types. Это позволяет прописать сразу несколько типов с разным набором полей и использовать для получения всех или некоторых типов одним запросом. При чем в этом же запросе просписать разный набор полей для разных типов. Вот пример такого запроса:
fragment resource on ResourceInterface { ...resourceNoNesting CreatedBy @include(if: $withCreatedBy) { ...user_ } ...TopicReviewFragment ... on Company { ...CompanyFields } ... on City { ...city } }
То есть есть какой-то базовый набор полей, а есть дополнительно поля для городов, компаний, топиков и т.п. Таким образом в единой точке получается не только определить что это за ресурс и отправить данные в нужную вьюху, но и данные получить свои для разных типов ресурсов.

К слову, в этом же роутере я прописал и 301-ый редирект по маске.
/** * Редиректим со страниц с даннми координат */ if (res && Array.isArray(query.path) && query.path.length) { const path = [...query.path] const coordsPath = path.find((n) => /^@\d+/.test(n)) if (coordsPath) { path.pop() res.redirect(301, '/' + path.join('/')) return {} } }
NextJS и в такое умеет.


Резюме: в общем, движок прошел проверку в бою. Городские бани - проект сильно сложнее какого-либо сайта-визитки, и здесь не нашлось ни одной задачи, которую не получилось решить.

Но тут усматривается еще один момент - это технолгия постепенного переезда с MODX-сайта на более современные технологии. Ведь такое совсем не редкость, когда сайт стартует с минимальными требованиями, а потом компания вырастает, ей надо и сайт развивать, а это уже не получается просто так делать и часто встает вопрос о том, чтобы переписать сайт с нуля. Но переписать с нуля - это не только лишние финансовые и временнЫе затраты, но и большие риски. Прикол в том, что изначально практически невозможно оценить объем работ. Пробежались по сайту, посмотрели то да се, и вроде все ясно-понятно, но как взялись за работу, выяснилось, что там двусторонняя интеграция с 1С, импорт данных с десятка источников, а еще 20 девушек-манагеров сидят с сайтом в админке постоянно работают (и не хотят переучиваться на новую админку), а так же хитрости всякие с УРЛами, да еще и дорогостоящие SEO-кампании настроены с интеграциями во всякие AMO-CRM, Bitrix-24 и т.п. В общем, в таком случае переписать сайт и сохранить его структурную работу на 100% - практически нулевой шанс.

Вот и я, даже со своим сайтом, который я сам же писал, и который, казалось бы, должен знать как свои 5 пальцев, не смог даже перенести на другой сервер:) То есть на чистый сервер я смог бы перенести, но хотелось избавиться от лишнего сервера (за который платилось $50 в месяц), и перекинуть его на общий сервер с другими проектами (там ресурсы позволяют). Вот я его и упаковал в докер и локально он у меня работал, но на боевом сервере полноценно не заработал (логические ошибки в маршрутизации). А под капотом все оказалось очень сложно, чтобы вот так вот после двухлетней паузы залезть и все подправить. Это было 5 месяцев назад и в итоге на это время заброшено. Но тут я освоил новые подходящие технолгии, и решил, что пришло время попробовать опять. Но как и говорил выше, возникли подводные камни. Пришлось еще технологии новые осваивать. Освоил. И они помогли. Но где гарантия, что с любым проектом так успешно закончится, а не будет потрачено время, все сломано и в итоге заброшено?

Вот и получается, что такой подход постепенного переезда - баланс трудозатрат и результата. Тут и админка осталась старая (если встанет вопрос продолжить наполнение сайта старыми средствами, ее все еще можно использовать). И данные никак не пострадали (база данных та же самая используется). И фронт совершенно новый (Конечным пользователям должно сильно комфортней быть, потому что раньше JS-бандл весил почти 5 метров, а сейчас 300 кило, да и новые плюшки можно сейчас хоть каждый день вводить). А на выходе есть еще и актуальная картина что и как работает, какие технологии использованы, каковы шансы на окончательный переезд и т.п. В итоге я переписал его за две недели частичной занятости (в основном в выходные, так как в будни у меня основная работа). Вот здесь можно посмотриеть все текущие и завершенные задачи: https://freecode.academy/office/projects/cjov5i9mv10ui0889c2hzuf1t

Если у кого есть интерес к этому направлению, пишите комментарии, обсудим.

Очень интересно! Николай, будешь пробовать администрирование прикручивать к проекту или пока оставишь как есть, с адмикой MODX?
Я как обычно придерживаюсь принципа больше во фронте редактировать. Где вижу - там и редактирую. Так что как таковой админки скорее всего не будет (как ее и здесь нет). А редактирование скоро сделаю.

Имел в виду именно схему редактирования, а не прям админку-админку))
Ааа, ну это скоро будет. Просто там не часто сейчас пишут, так что это не критично, если недельку не будет редактирования. А задача стояла в первую очередь перенести сайт, чтобы освободить и удалить лишний сервер.

Добавить комментарий