[Из песочницы] Конструктор шаблонов проектов или сложности управления большими проектами
Предыстория
Современный фронтенд активно двигается в сторону усложнения, и эту сложность необходимо контролировать. Для начала хочется отметить, что проблема существует давно, и было опробовано и даже адаптировано несколько её решений, но конкретная идея должна была выкристаллизоваться и оформиться. О чем и пойдет речь далее.
Компания, в которой я сейчас работаю, занимается разработкой фронтенда для различных заказчиков, и размеры проектов варьируются от совсем небольших до весьма солидных (например, Enterprise-системы). Мы столкнулись с тем, что решения, которые хорошо подходят для крупных проектов, плохо адаптируются для маленьких, и наоборот — то, что в маленьких проектах работает очень хорошо, не соответствует требованиям больших.
В духе времени можно сказать, что тут нужен свой собственный бойлерплейт для проектов, позволяющий эффективно структурировать проекты разного масштаба так, чтобы держать под контролем их сложность и не давать ей произвольно расти, т.к. это может привести либо к срыву сроков проекта, либо к авралам. И, разумеется, не стоит забывать о стоимости поддержки и разработки.
Первоначальная реализация предполагала просто разделение проекта на независимые части, и основной упор делался на развитие инструмента, облегчающего работу с проектом. Проект начал представлять собой монорепозиторий под управлением lerna, который формировался на основании общих модулей, открыто публикующихся на github, а также специфических модулей, находящихся в самом проекте. Для общения между этими модулями использовалась шина событий, и в целом данная реализация стабильно работала. Однако у такой структуры был существенный недостаток: модули, которые не планировалось изменять в рамках проекта, всё равно нужно было подключать как часть монорепозитория, так как иначе мы получали разные версии одного и того же пакета в разных местах. Это, конечно, не проблема, но только если это не пакет с конфигами.
Второй важной проблемой стало описание способа написания плагинов, ведь при всей своей модульности они всё равно оказались сильно связанными, а общая концепция общения между модулями отсутствовала. Нужна была некая целостная спецификация для написания таких плагинов любым человеком, которая также позволила бы решить вопрос с разрешением зависимостей на уровне старта приложения.
Далее я постараюсь описать наш вариант устранения этих проблем и начну с описания идеального возможного результата.
Disclaimer
Написанное ниже не претендует на истину в последней инстанции и является просто ещё одним мнением. Мы ищем свой путь и хотим сделать мир чуточку лучше и удобнее, хотя бы для себя.
Идеальный возможный результат
1. Модульность
Проблема: Монолитность приложения. Сейчас уже предпринимаются шаги к разделению клиентского кода на отдельные части в рамках некоторого монолита, но по своей сути проект так и остаётся единым целым, и в нём сложно быстро заменить или удалить определённую часть, так как она не имеет четких границ.
Решение: Разбить проект на физически разграниченные модули с чётко очерченными границами и ответственностью. Также необходима возможность конфигурирования поведения одного модуля из другого.
Профит: Проект можно собирать как конструктор. То есть модуль сам обеспечивает свою работоспособность в рамках существующей инфраструктуры. Хорошим примером тут является настройка линтинга. Конфиг самого линтера можно собирать по частям из разных пакетов, но вот используемые плагины нужно подключать в самом проекте. Идеальный модуль, подключающий линтер, вместе с собой принесёт все необходимые плагины, и при использовании в проекте сам внесёт изменения для своего подключения. В случае eslint
это команды в package.json
и сам конфиг .eslintrc
.
2. Интерфейс взаимодействия
Проблема: Усложнение внутренней структуры приложения приводит к сложностям в управлении зависимостями, конфигурацией проекта и запуском команд.
Решение: Необходимо проработанное cli
приложение, которое предоставляет простой и понятный интерфейс для управления, расширения и поддержки проекта. Команды должны быть лаконичны и интуитивны.
Профит: Разработчик тратит намного меньше времени на рутинные операции — например, на создание типовых компонентов для интерфейса.
3. Автоматизация рутинных задач
Проблема: Большинство шаблонов заканчивают своё участие в проекте сразу после генерации проекта или клонирования шаблона. Иногда добавляется инструмент, который умеет генерировать атомарные части проекта.
Решение: Каждый модуль должен содержать определённый набор шаблонов своих составных частей, которые можно быстро добавлять в проект.
Профит: Мощность автоматизации рутинных задач — это ответственность самого проекта, а не шаблона. То есть чем сложнее проект, тем более сложные генераторы он будет требовать, но их добавление никак не затронет, например, другой, более локальный проект. Но при необходимости его можно будет легко подключить как отдельный модуль.
Кратко: набор генераторов в проекте определяется набором установленных модулей.
4. Обновление шаблона
Проблема: После старта проекта на определённом шаблоне обновление используемых пакетов, а так же структуры проекта остаётся за разработчиками проекта.
Решение: Необходимо предоставлять возможность быстрого обновления зависимостей и проведения миграций кодовой базы в случае необходимости.
Профит: Миграция ломающих версий может производиться только в одном месте. В конечных проектах произойдет просто обновление версий зависимостей проекта с дополнительной миграцией кодовой базы, если необходимо (стоит сказать, что, так как эта задача пока не решена, её определение и способ реализации расплывчаты).
Как это работает
Получается, что проект Rispa можно разбить на несколько уровней:
cli
приложение,- инфраструктура для поддержки модульности проекта,
- модули-плагины:
- общая функциональность,
- проект-специфичная логика.
cli
приложение
Главная особенность этого приложения заключается в том, что набор исполняемых команд зависит от самого проекта. То есть существует два типа команд:
- Команды управления проектом:
ris add
,ris update
,ris assemble
и другие; - Команды из плагинов, описанные как
scripts
вpackage.json
. Является расширением командыris run [plugin-name-or-alias] [command-name-from-scripts]
в которойrun
можно опустить
Более подробно про cli можно прочитать в описании.
Модульная инфраструктура
Наиболее очевидным решением для организации такой инфраструктуры видится использование в конечном проекте формата монорепозитория. Изначально использовался инструмент lerna.js
, однако он лучше подходит для управления проектами разработки библиотек — таких, как, например, jest
, babel
и многие другие. Основной используемой фичей было разрешение зависимостей в рамках монорепозитория, однако сейчас, с появлением yarn workspaces
, переключились на использования именно их, ведь это позволяет решить проблемы организации проекта как монорепозитория наиболее эффективно.
Второй частью модульности приложения можно назвать rispa-core. Этот пакет предоставляет возможность организовывать взаимодействие плагинов друг с другом, а также декларирует спецификацию для написания плагинов.
Плагины
На текущий момент сформировался достаточно большой корпус плагинов, однако они в основном обслуживают потребности проектов на React, которые в перспективе могут быть пересмотрены для внедрения поддержки других фреймворков или систем сборки. Вот их краткий перечень:
rispa-config
— хранилище конфигурации проекта, всегда присутствует в проекте и подключается как простая зависимость во всех плагинах;rispa-server
— отвечает за всю рутину, связанную с запуском сервера, конфигурированием сжатия, поддержкой CORS и некоторым дефолтным роутингом. При этом позволяет другим плагинам расширять свои возможности через подключение роутов и других middleware;rispa-client
— отвечает непосредственно за клиентское приложение, которое работает в браузере. Сейчас это React-специфичный клиент, поэтому в дальнейшем тут останется только часть, обеспечивающая работу всей платформы, а React-часть переедет в отдельный плагин;rispa-webpack
— отвечает за работу общей сборки проекта вебпаком, при этом конфиг собирается по частям из существующих плагинов;rispa-eslint-config
— отвечает за подключение и корректную работу линтера в проекте, является просто drop-in зависимостью;rispa-generator
— отвечает за работу генераторов в проекте, сами генераторы также собираются из всех установленных плагинов в проекте.
Экспериментальные плагины:
rispa-render-static
— плагин для осуществления рендеринга существующего приложения в статический сайт, на данный момент в процессе разработки;rispa-render-simple
— плагин для рендеринга страниц без фреймворков, в первоначальной реализации используется pug, вероятно, конечный вариант тоже будет настраиваемым.
Как начать
Предварительно необходимо установить cli
:
$ yarn global add @rispa/cli
После успешной установки нужно запустить ris new
, что позволит создать новый проект, ответить на вопросы, выбрать плагины и дождаться установки всех зависимостей:
$ ris new
$ ris new
[18:22:11] Enter project name [started]
? Enter project name: temp-project-name
[18:22:22] Enter project name [completed]
[18:22:22] Enter remote url [started]
? Enter remote url for project (optional):
[18:22:24] Enter remote url [completed]
[18:22:24] Generate project structure [started]
[18:22:24] Generate project structure [completed]
[18:22:24] Git init [started]
Initialized empty Git repository in /opt/work/temp-project-name/.git/
[master (root-commit) f5af296] Create project 'temp-project-name'
[18:22:24] Git init [completed]
[18:22:24] Fetch plugins [started]
[18:22:28] Fetch plugins [completed]
[18:22:28] Select plugins to install [started]
? Select plugins:
>(*) react-redux v4.1.3 - Rispa Redux plugin
(*) webpack-javascript v4.1.3 - Rispa plugin which provide Webpack JavaScript configuration
(*) babel v4.1.3 - Rispa Babel plugin
(*) @rispa/react-config v4.1.3 - Rispa React Configuration
(*) @rispa/routes v4.1.3 - rispa routes
(*) react-server v4.1.7 - Rispa Server rendering plugin
(*) react-client v4.1.3 - Rispa Client plugin
(Move up and down to reveal more choices)
[18:33:30] Select plugins to install [completed]
[18:33:30] Install plugins [started]
[18:33:30] Install plugin with name @rispa/react-config [started]
...
success Saved lockfile.
$ ris clean-cache
[18:37:38] Read project configuration [started]
[18:37:38] Read project configuration [completed]
[18:37:38] Clean cache [started]
[18:37:38] Clean cache [completed]
Done in 240.41s.
[18:37:39] Install project dependencies [completed]
[18:37:39] Git commit [started]
[master e3223c2] Bootstrap deps and install plugins
[18:37:40] Git commit [completed]
Теперь у нас есть свежесгенерированный и настроенный проект. Однако в нём нет ни одного роута. Нужно его добавить. Для этого воспользуемся генератором feature-plugin
, который создаёт новый модуль и регистрирует его в @rispa/routes
. После ответа на несколько вопросов плагин будет сгенерирован и подключён в проект, а также обновятся зависимости.
$ ris g feature-plugin
$ ris g feature-plugin
[19:07:27] Read project configuration [started]
[19:07:27] Read project configuration [completed]
[19:07:27] Scan plugins [started]
[19:07:27] Scan plugins [completed]
[19:07:27] Init generators [started]
[19:07:28] Init generators [completed]
[19:07:28] Check generator [started]
[19:07:28] Check generator [completed]
[19:07:28] Select plugin [started]
[19:07:28] Select plugin [skipped]
[19:07:28] Enter plugin name [started]
? Enter plugin name: home
[19:07:38] Enter plugin name [completed]
[19:07:38] Run generator [started]
? Enter package name: @project/home
? Enter plugin router path: /
[19:07:58] Run generator [completed]
[19:07:58] Bootstrap plugin dependencies [started]
...
[19:08:26] Read project configuration [started]
[19:08:26] Read project configuration [completed]
[19:08:26] Clean cache [started]
[19:08:26] Clean cache [completed]
Done in 28.54s.
[19:08:27] Bootstrap plugin dependencies [completed]
Теперь у нас есть настроенный плагин, далее нужно доставить основной компонент для страницы. Вновь воспользуемся генератором ris g container
, в процессе будет предложено выбрать, в какой плагин произвести генерацию. Кстати, чтобы увидеть все генераторы, можно просто указать ris g
— будет выдан список генераторов, доступных в проекте.
$ ris g container
$ ris g container
[19:12:20] Read project configuration [started]
[19:12:20] Read project configuration [completed]
[19:12:20] Scan plugins [started]
[19:12:20] Scan plugins [completed]
[19:12:20] Init generators [started]
[19:12:20] Init generators [completed]
[19:12:20] Check generator [started]
[19:12:20] Check generator [completed]
[19:12:20] Select plugin [started]
? Select plugin: @project/home
[19:12:25] Select plugin [completed]
[19:12:25] Enter plugin name [started]
[19:12:25] Enter plugin name [skipped]
[19:12:25] Run generator [started]
? How should it be called? Home
[19:12:29] Run generator [completed]
[19:12:29] Bootstrap plugin dependencies [started]
[19:12:29] Bootstrap plugin dependencies [skipped]
Теперь необходимо связать роут с созданным контейнером, это делается в файле /packages/home/src/register.js
:
import Home from './containers/Home/Home'
// import reducer, { action } from './redux/reducer'
// import { match } from '@rispa/redux'
const registerReducer = () => {
// store.injectReducer('reducerName', reducer)
}
const registerWhen = () => {
// when(match('/'), action)
}
const registerModule = context => {
registerReducer(context)
registerWhen(context)
return Home
}
export default registerModule
Теперь можно запускать проект и после завершения сборки увидеть его в браузере:
$ ris server start-dev
Заключение
Сейчас Rispa позволяет решать задачи по упрощению старта проекта, а также оптимизации рабочего процесса. Дальнейшее развитие видится именно в расширении возможностей и поддержке новых фреймворков, а кроме того, в поиске других возможных применений для самой базы в виде rispa-cli + rispa-core
.
Если вам понравилась идея данного проекта, то о чем хотелось бы еще прочитать, например, особенности организации плагинной инфраструктуры поверх монорепозитория, или туториал по разработке плагинов и экскурс в окружения или режимы, с которыми работает cli с проектом, или архитектура клиентского приложения, но пока она react специфичная, она остается достаточно стандартной. Так как проект развивается, и нет возможности протестировать все во различных окружениях, то не исключены проблемы, о которых можно рассказать через issues. Также приглашаем желающих присоединиться к данному проекту, чтобы развивать его на благо разработчиков.
Некоторые планы на будущее
- Расширение списка поддерживаемых фреймворков, планируется добавить Angular, vue.js;
- Завершение переезда ярда и плагинов на TypeScript и типизация всех интерфейсов;
- Добавление подробной документации для плагинов;
- Внедрение плагинной инфраструктуры на клиенте.