Типичная фронтовая команда
Типичная фронтовая команда

Что имеем?

А имеем мы довольно крупную команду фронтендеров, которая раздедлена на небольшие подгруппы, каждая из которых отвечает за свои микросервисы. В конечном итоге, эти сервисы интегрируются в единый масштабный проект. Мы придерживаемся общей дизайн-системы, стандартизированных правил, описываем все процессы и тд.

Однако, несмотря на наличие единой системы, мы сталкивались с рядом проблем:

  • Разнообразие реализации компонентов. Несмотря на стандарты, каждая команда имела собственный подход к созданию компонентов, что приводило к разнородности кода, дублированию логики и сложностям при интеграции.

  • Человеческий фактор. Процесс создания новых компонентов мог растягиваться из-за ошибок или несоблюдения ключевых соглашений. Это особенно остро ощущалось при расширении команды или при найме новых сотрудников, каждый из которых приносил свои привычки и методы работы.

  • Ручной процесс создания компонентов. Сборка компонентов и связанных с ними файлов (стили, тесты, Storybook) занимала больше времени, поскольку всё делалось вручную, что повышало риск допущения ошибок и снижало скорость разработки.

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

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

Как было предложено решать данную проблему?

Чтобы ускорить разработку и устранить неконсистентность в файлах, папках, структуре и содержимом компонентов между сервисами, было предложено внедрить автоматическую генерацию шаблонов компонентов и других файлов с базовым наполнением.

Основные требования к генератору, выдвинутые после обсуждения с командой:

  1. Единый подход к организации компонентов. Генератор должен автоматически создавать структуру папок и файлов в соответствии с общими правилами команды. Это уменьшит вероятность ошибок и расхождений между проектами.

  2. Соответствие стандартам проекта. Все компоненты, создаваемые генератором, должны строго соответствовать установленным правилам именования и стандартам проекта. Это гарантирует консистентность и облегчает поддержку кода.

  3. Независимость генератора. Генератор должен быть оформлен в виде независимой библиотеки, что позволит легко интегрировать его во все сервисы проекта.

  4. Гибкость и конфигурируемость. Несмотря на стандартизацию, генератор должен быть достаточно гибким для настройки под отдельные случаи, где структура компонентов может отличаться по объективным причинам. Это позволяет автоматизировать рутину даже в условиях различий между сервисами.


Начинаем начинать

Что выбрать?

Конечно же сразу пошли искать готовые решения, потому что задача не настолько тривиальная и не мы первые о таком задумались. На текущий момент есть два основных популярных решения: 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', // Указание кастомного базового пути для компонентов
    );
};

Что имеем в итоге?

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

Также хочется отметить, тот факт, что новым сотрудникам стало значительно проще проходить онбординг и погружаться в процессы

На текущий момент данная библиотека успешно внедрена в тестовом режиме в основные сервисы для того, чтобы собрать обратную связь и понять, чего еще не хватает и где можно еще доработать. В планах на будущее сделать аналогичную генерацию "заглушек" не только для компонентов, но в том числе и для других файлов: утилит, хуков, тестов и тд. Также есть мысли дать возможность пользоваться библиотекой не только внутри команды, но и подключать ее в других сервисах компании (почему бы и нет?).

Скрытый текст

Пользуясь случаем, укажу свой тг канал, в котором открываю что-то новое для себя и делюсь с остальными, если интересно, то велком, как говорится ❤️

Пишите в комментариях, что думаете о такой автоматизации, делитесь своими кейсами
???

Комментарии (5)


  1. markelov69
    18.10.2024 13:41

    export default {{pascalCase name}};

    Оторвать руки за export default сразу и вон из профессии. Жесть, на дворе почти 2025, а кто-то ещё до сих пор стреляет себе в ногу и всем тем, кто унаследует его "прекрасный" код и херачит export default.

    pascalCase - серьезно? Как насчет того, что это все же PascalCase.


    1. and-kushnir Автор
      18.10.2024 13:41

      На вкус и цвет — все фломастеры разные

      Не до конца понимаю, чем вам не нравится `export default`, такой подход используют очень многие большие и современные ui библиотеки смотрим пример

      По поводу pascalCase - да серьезно, это встроенный метод и именно так он пишется. Описано в документации, предлагаю ознакомиться, прежде чем гневно кричать


  1. StiPAFk
    18.10.2024 13:41

    cпасибо за статью. пользуюсь таким же подходом для генерации кода в проектах. хотим создать NPM репозиторий и вынести созданий страниц/экранов сущностей и фичей которые сейчас есть в проектах на основе Feature Sliced Design. стайл гайд может быть любым. мы генерируем сразу несколько разных файлов. компонент, типы, константы. всё отдельными файлами и структуру под них в зависимости от директории (фича, страница, сущность и т.д.)
    очень удобно!


    1. and-kushnir Автор
      18.10.2024 13:41

      Спасибо)

      У нас еще реализована кодогенерация редакс кода на основе openapi — сильно упрощает жизнь. То есть ты по итогу используешь готовые слайсы - очень удобно и не приходится писать однотипный код


  1. danielsedoff
    18.10.2024 13:41

    Материал интересный. Я про plop впервые услышал, мне нравится. Даже на tg не поленился подписаться