Что имеем?
А имеем мы довольно крупную команду фронтендеров, которая раздедлена на небольшие подгруппы, каждая из которых отвечает за свои микросервисы. В конечном итоге, эти сервисы интегрируются в единый масштабный проект. Мы придерживаемся общей дизайн-системы, стандартизированных правил, описываем все процессы и тд.
Однако, несмотря на наличие единой системы, мы сталкивались с рядом проблем:
Разнообразие реализации компонентов. Несмотря на стандарты, каждая команда имела собственный подход к созданию компонентов, что приводило к разнородности кода, дублированию логики и сложностям при интеграции.
Человеческий фактор. Процесс создания новых компонентов мог растягиваться из-за ошибок или несоблюдения ключевых соглашений. Это особенно остро ощущалось при расширении команды или при найме новых сотрудников, каждый из которых приносил свои привычки и методы работы.
Ручной процесс создания компонентов. Сборка компонентов и связанных с ними файлов (стили, тесты, Storybook) занимала больше времени, поскольку всё делалось вручную, что повышало риск допущения ошибок и снижало скорость разработки.
Поддержка старого кода. При работе с устаревшими сервисами, несоответствие компонентов новым стандартам вызывало трудности в поддержке и внесении изменений, что замедляло обновления и рост проекта.
Эти проблемы вносили значительный вклад в нарастание технического долга и замедляли процесс разработки, создавая неэффективность и затрудняя масштабирование проекта.
Как было предложено решать данную проблему?
Чтобы ускорить разработку и устранить неконсистентность в файлах, папках, структуре и содержимом компонентов между сервисами, было предложено внедрить автоматическую генерацию шаблонов компонентов и других файлов с базовым наполнением.
Основные требования к генератору, выдвинутые после обсуждения с командой:
Единый подход к организации компонентов. Генератор должен автоматически создавать структуру папок и файлов в соответствии с общими правилами команды. Это уменьшит вероятность ошибок и расхождений между проектами.
Соответствие стандартам проекта. Все компоненты, создаваемые генератором, должны строго соответствовать установленным правилам именования и стандартам проекта. Это гарантирует консистентность и облегчает поддержку кода.
Независимость генератора. Генератор должен быть оформлен в виде независимой библиотеки, что позволит легко интегрировать его во все сервисы проекта.
Гибкость и конфигурируемость. Несмотря на стандартизацию, генератор должен быть достаточно гибким для настройки под отдельные случаи, где структура компонентов может отличаться по объективным причинам. Это позволяет автоматизировать рутину даже в условиях различий между сервисами.
Начинаем начинать
Что выбрать?
Конечно же сразу пошли искать готовые решения, потому что задача не настолько тривиальная и не мы первые о таком задумались. На текущий момент есть два основных популярных решения: Plop и Hygen. Сравнив две библиотеки, было решено использовать plop, так как он оказался более гибким в настройке, имеет поддержку ts, поддерживает интерактивные команды в CLI, а также имеет более развитой сообщество и подробную документацию.
Hygen на мой взгляд более сложный в понимании, но у него есть одна фича, которая позволяет вставлять блоки кода в уже существующие файлы (поэтому если кому-то нужно такое, то конечно же предпочтительнее будет использовать данную библиотеку).
Как использовать-то?
Для начала установим сам plop:
npm install --save-dev plop
# или
yarn add --dev plop
Описание всей логики и вообще всех взаимодействий происходит в файле plopfile.js
Приведу для примера минимальную реализация самого файла, чтобы было понимание что это такое
module.exports = function (plop) {
// Создаем новый генератор с именем 'component'
plop.setGenerator('component', {
// Описание генератора
description: 'Создать новый компонент',
// Промпты для ввода данных
prompts: [
{
type: 'input',
name: 'name',
message: 'Введите имя компонента:',
}
],
// Действия для создания файла на основе шаблона
actions: [
{
type: 'add',
// Путь до создаваемого файла
path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.tsx',
// Указываем шаблон
templateFile: 'plop-templates/component.hbs',
}
],
});
};
Давайте разберем, что делает этот код:
plop.setGenerator('component', { ... })
— мы создаем новый генератор с именемcomponent
. Когда мы запустим Plop, можно будет вызвать его командойplop component
.prompts
— это массив с вопросами, которые Plop задаст в итерактивном режиме в консоли. В данном случае мы используем один вопрос: запрос имени компонента. Формат имени будет введен пользователем, а затем автоматически преобразован в форматPascalCase
.actions
— это действия, которые Plop выполнит после получения данных. В нашем примере это создание файла в директорииsrc/components/{{pascalCase name}}
. Путь файла будет динамически изменяться в зависимости от введенного имени компонента. Мы также используем шаблонcomponent.hbs
, который создадим дальше.
Создание шаблонов
Шаблоны мы можем описывать как в формате .hbs так и в обычном .js/.ts. В моем случае я предпочел описывать в формате .ts, так как в некоторые шаблоны мне было необходимо передавать параметры и внутри шаблона на основании параметров по условию изменять блоки. Самый простой пример, который у меня был - это в зависимости от того, выбран ли файл стилей к созданию, в шаблоне добавлять или нет строку импорта и подключения стилей (пример шаблона из нашего генератора будет ниже).
Пример простого .hbs шаблона на примере:
import React from 'react';
const {{pascalCase name}} = () => {
return (
<div>
Implement me!
</div>
);
};
export default {{pascalCase name}};
Добавляем больше возможностей и интерактива
Пример одного из шаблонов для создания React компонента из нашего генератора
export default function componentTemplate(
name: string,
withStyle: boolean = false,
withQaAttributes: boolean = false,
): string {
const importStyle = withStyle ? `\n\nimport cx from './${name}.scss';` : '';
const importWithClassName = withStyle
? `\n\nimport {type IWithClassName} from 'types/withClassName';`
: '';
const importQaAttributes = withQaAttributes
? `\n\nimport {
type IWithQaAttributes,
prepareQaAttributes,
} from 'utilities/qaAttributes/qaAttributes';`
: '';
const interfaceExtensions = [
withStyle ? 'IWithClassName' : '',
withQaAttributes ? 'IWithQaAttributes' : '',
]
.filter(Boolean)
.join(', ');
const classNameProp = withStyle ? 'className' : '';
const classNameUsage = withStyle
? ` className={cx('root', className)}`
: '';
const qaAttributesUsage = withQaAttributes
? ' {...prepareQaAttributes(props)}'
: '';
return `import {type FC, memo} from 'react';${importWithClassName}${importQaAttributes}${importStyle}
export interface I${name}Props${
interfaceExtensions ? ` extends ${interfaceExtensions}` : ''
} {
// define your props here
}
const ${name}: FC<I${name}Props> = props => {
const {${classNameProp}} = props;
return (
<div${classNameUsage}${qaAttributesUsage}>
Implement me
</div>
);
};
export default memo(${name});
`;
}
Одним из условий была возможность масштабировать данный функционал на несколько проектов и возможность немного "дотюнить" в рамках конкретных проектов.
Первую проблему решили тем, что обернули утилиту в npm библиотеку, опубликовали ее и используем во всех проектах где нужно. Для этого после установки, нужно создать в проекте файл plopfile.js
и наполнить его следующим содержимым
const {generator} = require('@example/react-components-generator');
module.exports = function (plop) {
generator(plop);
};
После чего в package.json
прописываем новую команду для запуска нашего генератора, указывая в качестве аргумента путь к скрипту, который мы определили выше
"create": "plop --plopfile=scripts/generator/plopfile.js --dest=$PWD"
Чтобы была возможность в конкретных проектах переопределить шаблон или дефолтный путь, мы типизируем возможные шаблоны и прокидываем в основной файл генератора те шаблоны, которые хотим переопределить. Тоже самое делаем с дефолтным путем.
interface ITemplatesTypes {
componentTemplate?: (name: string, withStyle?: boolean) => string;
storybookV6Template?: (name: string) => string;
storybookV8Template?: (name: string) => string;
styleTemplate?: () => string;
}
export function generator(
plop: NodePlopAPI,
customTemplates?: ITemplatesTypes,
customBasePath?: string,
): void {
const templates = {
componentTemplate,
storybookV6Template,
storybookV8Template,
styleTemplate,
...customTemplates,
};
const basePath = customBasePath || './src';
...
}
Для того чтобы использовать такое переопределение, мы в целевом проекте создаем шаблоны и используем скрипт (plopfile.js
) следующим образом:
import {generator} from '@example/react-components-generator';
import customComponentTemplate from './custom-templates/componentTemplate';
import customStyleTemplate from './custom-templates/styleTemplate';
module.exports = function (plop) {
generator(
plop,
{
// Переопределение дефолтных шаблонов своими
componentTemplate: customComponentTemplate,
styleTemplate: customStyleTemplate,
},
'./custom/src', // Указание кастомного базового пути для компонентов
);
};
Что имеем в итоге?
Внедрение генератора компонентов значительно улучшило рабочие процессы команды. Автоматизация создания шаблонов компонентов позволила избавиться от ручной рутины, стандартизировать структуру файлов и повысить их согласованность между сервисами. Что позволило уменьшить количество ошибок, итерация ревью и как следствие появляется больше времени пилить фичи.
Также хочется отметить, тот факт, что новым сотрудникам стало значительно проще проходить онбординг и погружаться в процессы
На текущий момент данная библиотека успешно внедрена в тестовом режиме в основные сервисы для того, чтобы собрать обратную связь и понять, чего еще не хватает и где можно еще доработать. В планах на будущее сделать аналогичную генерацию "заглушек" не только для компонентов, но в том числе и для других файлов: утилит, хуков, тестов и тд. Также есть мысли дать возможность пользоваться библиотекой не только внутри команды, но и подключать ее в других сервисах компании (почему бы и нет?).
Скрытый текст
Пользуясь случаем, укажу свой тг канал, в котором открываю что-то новое для себя и делюсь с остальными, если интересно, то велком, как говорится ❤️
Пишите в комментариях, что думаете о такой автоматизации, делитесь своими кейсами
👇👇👇