[Из песочницы] Headless CMS. Почему я пишу свою

habr.png

Всем привет!

Написать эту публикацию меня побудила вот эта недавняя статья (вчера увидел).

Пересказывать основные признаки Headless/content-first/api-first и т.п. CMS я не буду, материала полно и наверняка уже многие знакомы с этим трендом. А хочу я рассказать почему и как я пишу свою систему, почему не смог выбрать из имеющихся, что я думаю о других системах, с которыми сталкивался ранее и какие вообще на все это перспективы вижу. Чтиво будет объемное (ибо материал за два года), но постараюсь написать побольше интересного и полезного. Кому интересно, прошу под кат.
Вообще история реально долгая и я постараюсь ее рассказать сначала. То ли чтобы было более понятно каковы истинные причины для создания этого своего движка, то ли потому что просто без этого сложно будет на местах пояснять почему я делаю это именно так, а не эдак.

Но для начала я все-таки коротко распишу основные критерии отбора современной Headless-CMS лично для меня, почему же я все-таки так и не смог выбрать для себя уже готовое решение. Просто чтобы народ не обламывался читать много буков, не понимая, что же в итоге-то будет рассказано.

Коротко: хотелось чтобы все было в одном месте: и бэк, и фронт (а не или то, или другое), и GraphQL-API, и чтобы база данных управляемая была и еще много всего, включая кнопку «Сделать красиво». Вот этого и не нашел. Сам я такое тоже не сделал еще, но в целом уже получилось очень даже не мало, а главное, позволяет делать реальные проекты.

И так, мой подход скорее всего вряд ли можно назвать научным и обоснованным. Дело в том, что я вообще очень часто пишу что-то свое. Нравится вот мне программировать. И два года назад (и до этого еще 8 лет) я сидел на MODX CMF (под которую тоже много своих костылей изобрел). И вот три года мы затеяли один довольно масштабный проект, под который, как мне казалось, я смогу использовать MODX. Но как оказалось, не смог… Основная причина была в том, что это был стартап без какого-либо ТЗ, с кучей идей, которые менялись и дополнялись каждый день (и по нескольку раз на день). И вот каждый раз, когда под новую идею надо было добавить какую-то новую сущность, прописать/изменить поля для имеющихся, создать/удалить/изменить связи между этими сущностями (соответственно с изменением структуры базы данных), у меня в какой-то момент стало уходить по несколько часов на изменение этих сущностей. Ведь помимо того, что надо было прописать эти изменения в схеме, надо было изменить базу данных (практически вручную), актуализировать API, переписать программный код и т.д и т.п. Соответственно, и фронт надо было под все это актуализировать. В итоге я решил, что надо искать что-то новое, более удобное, позволяющее как-то все это упростить. Еще раз уточню, что на тот момент я был php бэкэндщиком, так что не удивляйтесь и не смейтесь, что я стал открывать для себя различные сборщики фронта, less-процессоры, npm и т.д. и т.п. Но так или иначе, постепенно в нашем проекте появился фронт на react+less, API на GraphQL, а сервер на express.

Но не все так было радужно, как это многим показалось бы сейчас. Напомню, это было более двух лет назад. Если вы в современном JS-вебе менее двух лет, советую к прочтению вот эту статью: N причин, чтобы использовать Create React App (habr). Кому лень, коротко: с появлением react-scripts можно не заморачиваться с конфигурированием webpack и т.п. Все это уходит на задний план. Добрые ребята уже сконфигурировали webpack так, чтобы на нем почти гарантированно работало большинство react-проектов, а конечный разработчик сосредотачивался непосредственно на программировании конечного продукта, а не конфигурировании кучи зависимостей, loader-ов и т.п. Но это уже позже. А до этого мне как раз приходилось конфигурировать этот webpack, следить за обновлении кучи всего того, что летело с ним в догонку и т.д. и т.п. А ведь это только часть работы, только по сути фронт. А еще нужен сервер. А еще нужно API. А еще нужно SSR (Server-side rendering), который, к слову, react-script до сих пор не обеспечивает, на сколько я знаю. В общем, все было тогда гораздо сложнее, чем сейчас, многого не было, и каждый костылил как мог. А как я тогда костылил…

Только представьте:

  • Собственная конфигурация webpack отдельно для фронта и сервера.
  • Собственная реализация SSR, так, чтобы async нормально работал с react-server, и стили сразу готовые прилетали, и индексировалось нормально, и статусы серверные для ненайденных страниц отдавались.
  • No-redux. Ну не понравился мне redux вот сразу. Больше понравилась идея использовать родной реактовый flux (хотя и его пришлось под себя чуть-чуть переписать).
  • Вручную прописанные GraphQL-схемы и резолверы, без автоматического деплоя базы данных (API-сервер использовался как миддл для MODX-сайта).
  • Никаких react-apollo/apollo-client и т.п. Все написано самостоятельно с запросами через fetch, хранилищами в браузере на базе custom flux.


Как результат: до сих пор на одной из первых версий этого работает один проект с посещаемостью 500+, а в сезон (зимой) 1000–1700 уников в день. Uptime 2 месяца. Это потому что сам я вручную сервер перезагружал после профилактического обновления ПО. А до этой перезагрузки uptime еще 6+ месяцев был. Но самое интересное — это потребление памяти. В настоящий момент почти 700 мегабайт js-процесс. Да-да, я здесь тоже с вами смеюсь:) Конечно же это очень много. А до этого я еще небольшую профилактику делал и улучшил этот показатель. Раньше было и вовсе 1000M+ на процесс… Тем не менее оно работало и вполне так себе сносно. При чем до того, как в ноябре гугл изменил алгоритмы PageSpeed Insights, у сайта был показатель производительности 97/100. Пруф.

Промежуточный вывод на на основании этого проекта на базе системы, которая развивалась далее уже без этого проекта (проект остался позади):

Плюсы

  1. API проекта стало более гибкое за счет использования GraphQL, а количество запросов на сервер значительно сократилось.
  2. Проекту открыт доступ к огромному количеству компонентов на npm.
  3. Управление проектом стало более прозрачным за счет использования зависимостей, гита и т.п.
  4. Сбилденные в кучу скрипты и стили конечно же больше радуют, чем куча отдельных скриптов на старых сайтах, когда и не знаешь что из этого зоопарка можно убрать без последствий (и не редко наблюдаешь на одном сайте несколько версий жучки).
  5. Сайт стал более интерактивный, страницы работают без перезагрузки, возврат на ранее просмотренные страницы не требует повторных обращений к серверу.
  6. Редактирование данных происходит прям на странице, по принципу «редактируй то, что видишь и там, где видишь», без какой-либо отдельной админки.


Минусы (в основном для разработчика)

  1. Все очень сложно. Реально. Подключить к проекту какого-то стороннего разработчика просто нереально. Я и сам с трудом разбирался что и как работает и откуда растут ноги. Если смотреть п. 3 из плюсов, где сказано про прозрачность, то прозрачность только в том, что если где-то что-то зацепишь, сразу видно, что сломано (скрипты не билдятся и т.п.), а по коммитам и диффам можно найти где что зацепил. Ну и если удалось добавить что-то новое и оно работает, хотя бы понимаешь четко, что да, все залетело нормально. Но в целом это все равно адский ад.
  2. Сложности с кешированием. Это потом я уже для себя открыл apollo-client. А до этого, как я и говорил, писал свои хранилища на базе flux. За счет этих хранилищ получалось из разных компонентов получать нужные данные для рендеринга, но объем кеша на стороне клиента был очень большой (для каждого набора сущностей типовых было свое хранилище). В итоге, сложно было обеспечить проверку того, был ли объект запрошен ранее или нет (то есть стоит ли запрос на сервер делать чтобы его найти), все ли данные связанные имеются в наличии и т.п.
  3. Сложности со схемами, структурой БД и резолверами (API-функциями получения/изменения данных). Как я и говорил, схемы я писал вручную, и резолверы тоже. При чем в резолверах я пытался и кеширование обеспечить, и обработку вложенных запросов и прочие тонкости. В тот момент мне пришлось очень глубоко погрузиться в суть и программный код GraphQL. Плюс на выходе в том, что я в целом довольно хорошо понимаю, как работает GraphQL, какие у него плюсы и минусы и как его лучше готовить. Минус в том, что конечно же нельзя в одного написать все те удобства и плюшки, написанные командами типа apollo. В итоге, когда я открыл для себя apollo, конечно же я с большим удовольствием стал использовать их компоненты (но в основном на фронте, ниже расскажу почему).


В общем, этот проект на устаревших технологиях лично мой на 100%, поэтому я могу себе позволить его забросить до лучших времен. Но есть другие проекты, ради которых пришлось идти дальше и платформу развивать. И несколько раз приходилось переписывать все практически с нуля. Далее я буду более подробно рассказывать об отдельных задачах, с которыми сталкивался и какие решения в итоге разрабатывал и применял.

Schema-first. Сначала схема, а потом все остальное

Сайт (веб-интерфейс, тонкий клиент и т.п.) — это все отображение информации (ну и управление информацией, если позволено и функционал позволяет). Но сначала все-таки база данных (таблицы, колонки и т.п.). Встретив на своем пути несколько различных подходов к работе с базой данных, мне больше всего понравился подход Schema-first. То есть описываешь схему сущностей и типов данных (вручную или через интерфейс), деплоишь схему и у вас сразу в базе данных применяются описанные изменения (создаются/удаляются таблицы и колонки, а так же связи между ними). В зависимости от реализации у вас еще и будут сгененрированы все необходимые функции-резолверы для управления этими данными. Больше всех в этом направлении мне понравился проект prisma.io.

С вашего позволения, так как даже на хабре я не встретил ни одной статьи про призму, я чуть-чуть заострю на них внимание, так как проект действительно очень интересный, и без них у меня бы не было сейчас такой платформы, которая меня так радовала. Собственно, поэтому я свою платформу и назвал prisma-cms, потому что prisma.io играет очень большую роль в нем.

Собственно, prisma.io — это SaaS-проект, но с большой оговоркой: практически все, что они делают, они выкладывают на гитхаб. То есть, вы можете воспользоваться их серверами за вполне разумную плату (и сконфигурировать под себя собственную базу данных и API в считанные минуты), а можете полностью развернуть все у себя. При этом призму логически надо поделить на две важных отдельных части:

  1. Prisma-server, то есть тот сервер, где еще и база данных крутится.
  2. Prisma-client. Это по сути тоже сервер, но по отношению к источнику данных (prisma-серверу) является клиентом.


Сейчас я постараюсь объяснить эту запутанную ситуацию. В общем, суть призмы в том, чтобы используя один API-endpoint, можно было работать с различными источниками данных. Да, здесь любой скажет, что это все придумали в GraphQL и prisma тут не нужна. В целом все будут правы, но есть серьезный момент: GraphQL только определяет принципы и общую работу, но сам по себе он из коробки не обеспечивает работу с конечными источниками данных. Он говорит «Вы можете создать API-схему, чтобы описать какие запросы смогут слать пользователи, но как вы будете обрабатывать эти запросы, это уже вы сами будете заморачиваться». И призма тоже конечно же использует GraphQL (к слову, и много еще чего другого, включая различные apollo-продукты). Но призма плюс к этому как раз и обеспечивает работу с базой данных. То есть описывая схему и деплоя ее, в указанной базе данных сразу будут созданы нужные таблицы и колонки (а так же связи между ними), да еще и генерит сразу все необходимые CRUD-функции. То есть с призмой вы не просто получаете GraphQL-сервер, а полноценное работающее API, которое сразу позволяет работать с базой данных. Так вот, Prisma-server обеспечивает работу базу данных и взаимодействие с ней, а prisma-client позволяет писать свои резолверы и слать запросы на prisma-server (или еще куда-нить, хоть на несколько prisma-server). И вот получается, что у себя вы можете развернуть только prisma-client (а в качестве prisma-server будет использоваться SaaS prisma.io), а можете развернуть prisma-server у себя, и вообще никаким образом не зависеть от призмы, это будет все ваше.

Вот призму я для себя и выбрал, как основу для своей платформы. Но далее пришлось и это докручивать для себя, чтобы получить полноценную платформу.

1. Мерж схемы


На тот момент призма не умела объединять схемы. То есть задача в следующем:

У вас в одном модуле описана модель пользователя

type User {
  id: ID! @unique
  username: String! @unique
  email: String @unique
}


и в другом модуле

type User {
  id: ID! @unique
  username: String! @unique
  firstname: String
  lastname: String
} 


В рамках одного проекта вы хотите объединить эти две схемы автоматически, чтобы получить на выходе

type User {
  id: ID! @unique
  username: String! @unique
  email: String @unique
  firstname: String
  lastname: String
}


Вот тогда этого призма не умела делать. Получилось это реализовать с помощью библиотеки merge-graphql-schemas.

Работа с произвольным prisma-server.


В призме конфигурация прописывается в специальном конфиг-файле. Если хочешь изменить адрес используемого призма-сервера, надо править файл. Мелочь, а не приятная. Хотелось сделать, чтобы УРЛ можно было указывать в команде, например endpoint=http://endpoint-address yarn deploy (yarn start). Вот на это было убито несколько дней… Но зато теперь можно один призма-проект использовать для любого количества эндпоинтов. К слову, до сих пор prisma-cms легко работает хоть с локальной базой, хоть с SaaS серверами призмы.

Модули/плагины


Вот этого вообще сильно не хватало. Как я говорил, основная задача призмы — это обеспечивать работу с различными базами данных. И они с этим отлично справляются. Уже сейчас они поддерживают работу с MySQL, PostgreSQL, Amazon RDS и MongoDB, еще несколько типов источников на подходе. Но они не дают никакой модульной инфраструктуры. Нет пока никакого маркетплейса или типа того. Есть только несколько типовых заготовок. Но вы не можете из нескольких заготовок выбрать две-три и установить на один проект. Придется выбирать какую-то одну. Я же хотел, чтобы можно было на конечном проекте устанавливать различное количество модулей, и чтобы при деплое схемы и резолверы мержились и получался такой единый проект с суммарным функционалом. И хотя какого-то графического интерфейса пока нет, есть уже больше двух десятков работающих модулей и компонентов, которые можно комбинировать на конечном проекте. Здесь сразу немного определюсь с личными определениями: модуль — это то, что устанавливается на бэк (расширяя базу данных и API), а компонент, это то, что устанавливается во фронт (для добавления различных элементов интерфейса). Пока что для подключения модулей тоже нет графического интерфейса, но мне не сложно прописывать так (это же не часто делается):

  constructor(options = {}) {

    super(options);

    this.mergeModules([
      LogModule,
      MailModule,
      UploadModule,
      SocietyModule,
      EthereumModule,
      WebrtcModule,
      UserModule,
      RouterModule,
    ]);

  }


После добавления новых модулей достаточно просто выполнить опять деплой одной командой и все, вот у нас уже новые таблицы/колонки и дополненный функционал.

5 фронт, реагирующий на изменения в бэкэнде


Вот этого вообще не хватало. Тут последует лирическое отступление. Дело в том, что все API-first CMS, что я видел, говорят «Мы офигенно обеспечиваем API, а вы прикручивайте какой хотите фронт». Вот это их «прикручивайте какой хотите» на самом деле по факту означает «заморачивайтесь как хотите». Ровно так же, как UI-фреймворки говорят «смотрите какие мы классные кнопочки и все такое делаем, а с бэкэндом заморочьтесь сами». Вот это всегда убивало. Хотелось найти просто комплексную CMS, написанную на javascript, использующую GraphQL и обеспечивающую сразу и бэк, и фронт. Но вот не нашел я такой. А очень хотелось, чтобы изменения API сразу актуально воспринимались и на фронте. И вот для этого было еще выполнено несколько подшагов:

5.1 Генерация API-фрагментов


На фронте в запросах прописаны фрагменты из схема-файла. Когда на сервере API пересобирается, генерируется и новый JS-файл с API-фрагментами. А в запросах прописано типа такого:

const {
  UserNoNestingFragment,
  EthAccountNoNestingFragment,
  NotificationTypeNoNestingFragment,
  BatchPayloadNoNestingFragment,
} = queryFragments;

const userFragment = `
  fragment user on User {
    ...UserNoNesting
    EthAccounts{
      ...EthAccountNoNesting
    }
    NotificationTypes{
      ...NotificationTypeNoNesting
    }
  }

  ${UserNoNestingFragment}
  ${EthAccountNoNestingFragment}
  ${NotificationTypeNoNestingFragment}
`;


const usersConnection = `
  query usersConnection (
    $where: UserWhereInput
    $orderBy: UserOrderByInput
    $skip: Int
    $after: String
    $before: String
    $first: Int
    $last: Int
  ){
    objectsConnection: usersConnection (
      where: $where
      orderBy: $orderBy
      skip: $skip
      after: $after
      before: $before
      first: $first
      last: $last
    ){
      aggregate{
        count
      }
      edges{
        node{
          ...user
        }
      }
    }
  }

  ${userFragment}
`;


5.2 Единый контекст для всех компонентов


В react 16.3 появилось новое контекст-API. Его я и заюзал, чтобы в дочерних компонентах на любом уровне можно было получить доступ к единому контексту без перечисления заранее желаемых типов из контекста, а просто указывая static contextType = PrismaCmsContext и получая все прелести через this→context (включая API-клиент, схему, запросы и т.п.).

5.3 динамические фильтры


Вот это тоже очень хотелось. GraphQL позволяет строить сложные запросы с вложенной структурой. Хотелось, чтобы фильтры тоже были динамическими, формируемыми из API-схемы, и позволяли делать вложенные условия. Вот что получилось:


5.4 Конструктор сайтов


Ну и напоследок, чего мне не хватало, так это внешнего редактора сайта, то есть конструктора. Хотелось, чтобы на сервере необходимо было только минимум действий выполнять, а все конечное оформление выполнялось бы на фронте (включая настройку роутинга, формирование выборок и т.п.). Это тема для отдельной статьи, потому что помимо всего прочего я еще и написал под это свой костыльный wysiwyg-редактор на чистом contentEditable, а там очень много тонкостей. Если меня восстановят в правах и кому будет интересно, я напишу отдельную статью.

Ну, а напоследок коротенькое демо-видео конструктора в действии. Пока еще совсем сырой, но мне нравится.


На том пока закончу. Много еще не написал, что хотелось бы написать, но и так много получилось. Буду рад комментариям.

P.S.: все исходники, включая исходники самого сайта, находятся здесь.

© Habrahabr.ru