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

Я frontend-разработчик SimbirSoft Эллина, и в этой статье расскажу, как создать внутренний пакет компонентов в React с помощью инструмента сборки Rollup, а также как сделать его более качественным и удобным для использования. Материал будет полезен frontend-разработчикам уровней junior+ и middle.

Для чего это вообще нужно? 

Внутренний пакет компонентов в React влияет на эффективность разработки и успех проекта в целом. Рассмотрим, какие практические преимущества он может принести как самому проекту, так и бизнесу:

1. Экономия времени и улучшение качества за счет повторного использования компонентов.

В IT достаточно часто случаются ситуации, когда один и тот же элемент дизайна используется в нескольких местах/проектах, при этом на каждое такое использование есть своя реализация. Очевидно, что работать с такими компонентами довольно сложно и дорого: уйма времени тратится на повторные реализации, тестирование одинаковой функциональности, обновление логики, поддержку.

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

2. Единообразный дизайн и стиль для согласованного пользовательского опыта.

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

3. Оптимизация производительности и скорости разработки.

Использование внутреннего пакета компонентов может существенно улучшить производительность и скорость разработки. Заранее оптимизированные и проработанные компоненты помогают избежать дублирования кода и снижают вероятность появления ошибок. Более того, готовые компоненты упрощают процесс разработки новой функциональности в проекте — вместо того чтобы писать все с нуля, разработчику достаточно подключить нужный компонент и настроить его в соответствии с требованиями.

4. Централизованное управление и обновление.

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

Как понять, нужен ли проекту такой пакет?

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

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

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

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

Основные моменты при подготовке качественного пакета

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

О чем стоит подумать перед созданием пакета:

  • Типизация

Типизация — один из ключевых аспектов при создании качественного пакета компонентов. Хотя в некоторых случаях типизация может быть необязательной, в современной разработке она становится все более значимой. Использование типов позволяет заранее обнаруживать ошибки в коде, ускоряет процесс разработки и обеспечивает понимание того, что ожидать от кода. Вы можете выбрать удобные вам инструменты, но в нашем случае мы остановимся на Typescript.

  • Тестирование

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

  • Документация

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

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

  • Версионирование

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

Инструкция к сборке

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

1. Создание локального репозитория

Для начала добавим новую папку для нашего пакета и инициализируем пакет с помощью консольной команды, которая запросит информацию о проекте:

yarn init

После выполнения этой команды в корневой папке будет создан файл package.json, в котором будут указаны введенные вами имя проекта, версия, описание, автор и другое. 

Далее инициализируем гит:

git init

И наполняем .gitignore:

node_modules/

2. Установка React

Теперь установим зависимости для React:

yarn add -D react react-dom @types/react

Установка пакетов с флагом -D (или --dev) указывает, что они являются зависимостями разработки: эти пакеты не требуются в процессе работы и использования самого пакета, который мы собираем, но они необходимы при разработке или сборке пакета.

После установки они отобразятся как devDependencies в ранее созданном package.json.

Важно! Так как пакет будет использоваться в React-проекте, то необходимо поместить некоторые пакеты из зависимостей разработки devDependencies в зависимости peerDependencies.

Peer Dependencies – это зависимости, которые должны быть установлены и поддерживаться внешними проектами, использующими наш пакет. Помещение этих пакетов в peerDependencies позволит избежать дублирования зависимостей при установке, что поможет уменьшить размер нашего пакета. Кроме того, это обеспечит совместимость между зависимостями нашего пакета и внешними проектами, которые смогут выбирать и устанавливать соответствующие версии этих зависимостей.

Добавим в package.json блок peerDependencies и поместим туда react и react-dom. Для гибкой совместимости мы не будем фиксировать конкретную версию:

...
"peerDependencies": {
  "react": ">=18.0.0",
  "react-dom": ">=18.0.0"
}
...

3. Настройка Typescript

Установим Typescript с помощью команды:

yarn add -D typescript

Создадим tsconfig.json со следующим содержимым:

{
    "compilerOptions": {
      "target": "ESNext", 
      "lib": ["es6", "dom", "es2016", "es2017"],
      "jsx": "react",
      "module": "ESNext",
      "moduleResolution": "node",
      "declaration": true,
      "sourceMap": true,
      "declarationDir": "dist",
      "allowSyntheticDefaultImports": true,
      "esModuleInterop": true,
      "forceConsistentCasingInFileNames": true,
      "skipLibCheck": true
      },
    "include": ["src/**/*"],
    "exclude": [
      "node_modules",
      "dist"
    ]
}

Конфигурация выше – минимум, необходимый для понимания материала статьи, её можно донастроить под ваши нужды. В данном случае мы пропускаем проверки внешних зависимостей для ускорения сборки с помощью skipLibCheck. Для избежания транспиляции лишних файлов мы указываем в exclude папки node_modules и dist (папка, куда будет собираться проект).

4. Добавление компонентов

Есть разные способы стилизовать компоненты, но в данной статье рассмотрим реализацию с помощью библиотеки styled-components. Для её установки запустим следующую команду:

yarn add -D styled-components @types/styled-components

Теперь добавим тестовый компонент — карточку. Для этого создадим файлы по следующей структуре:

src/

  Card/

      Card.styles.ts    // Здесь будут располагаться стили

      Card.tsx         // Здесь будет располагаться код компонента

      Card.types.ts   // Здесь будут располагаться типы

      index.ts       // Нужен для экспорта компонента и его типов

  index.ts         // Нужен для экспорта всех компонентов, входящих в пакет

Добавим контент:

src/Card/Card.styles.ts

import styled from "styled-components";

import { CardProps } from './Card.types';

type CardContainerProps = Pick<CardProps, 'disabled'>;

export const CardContainer = styled.div<CardContainerProps>`
  border: 2px dashed #0ebeff;
  border-radius: 20px;
  padding: 12px 24px;
  ${({ disabled }) => disabled ? `pointer-events: none;` : `cursor: pointer;`}
`;

export const CardTitle = styled.span`
  font-size: 24px;
  font-weight: bold;
`;

src/Card/Card.tsx

import React, { FC } from "react";

import { CardProps } from './Card.types';
import { CardContainer, CardTitle } from './Card.styles';

/** Интерактивная карточка */
export const Card: FC<CardProps> = ({ title, onClick, disabled }) => (
  <CardContainer
    onClick={onClick}
    disabled={disabled}
  >
    <CardTitle>{title}</CardTitle>
  </CardContainer>
);

src/Card/Card.types.ts

export interface CardProps {
    /** Заголовок карточки */
    title: string;
    /** Обработчик клика по карточке */
    onClick: () => void;
    /** Флаг блокировки карточки  */
    disabled?: boolean;
}

src/Card/index.ts

export { Card } from "./Card";
export { CardProps } from "./Card.types";

src/index.ts

export * from "./Card";

Здесь src/index.ts файл, который будет экспортировать все компоненты нашего пакета. Именно с этим файлом мы будем работать в дальнейшем — он будет отправной точкой в нашей сборке, и Rollup будет обрабатывать те файлы, которые в нём указаны.

5. Настройка сборки

Давайте наконец перейдем непосредственно к сборке. Установим Rollup и некоторые его плагины, которые пригодятся в работе:

yarn add -D rollup rollup-plugin-peer-deps-external @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript @rollup/plugin-terser rollup-plugin-dts

Работа этих плагинов заключается в следующем:

  • rollup-plugin-peer-deps-external потребуется для того, чтобы исключать из сборки зависимости, которые мы ранее перенесли в peerDependencies. Это поможет нам уменьшить объем нашего пакета

  • @rollup/plugin-commonjs позволит нам поддерживать формат CommonJS

  • @rollup/plugin-node-resolve поможет Rollup работать с внешними модулями (node_modules)

  • @rollup/plugin-typescript поддерживает работу Rollup с Typescript

  • @rollup/plugin-terser минифицирует код сборки

  • rollup-plugin-dts соберет все типы в единый файл

В корневой папке создадим файл rollup.config.mjs и наполним его следующим содержимым:

import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import terser from "@rollup/plugin-terser";
import peerDepsExternal from "rollup-plugin-peer-deps-external";

const packageJson = require("./package.json");

export default [
  {
    input: "src/index.ts",
    output: [
      {
        file: packageJson.main,
        format: "cjs",
        sourcemap: true,
      },
      {
        file: packageJson.module,
        format: "esm",
        sourcemap: true,
      },
    ],
    plugins: [
      peerDepsExternal(),
      resolve(),
      commonjs(),
      typescript({ tsconfig: "./tsconfig.json" }),
      terser(),
    ],
    external: ["react", "react-dom", "styled-components"],
  },
  {
    input: "src/index.ts",
    output: [{ file: packageJson.types, format: "es" }],
    plugins: [dts.default()],
  },
];

Рассмотрим заданные настройки. 

  • В input указывается входная точка для сборки, для нас это будет корневой индексный файл, который импортирует наши компоненты и их типы.

  • output указывает, куда будет помещен обработанный код. Мы настроим сборку так, чтобы получились два билда в разных форматах: CommonJS (cjs) и ES Modules (esm); теперь пакет смогут поддержать инструменты, которые работают с форматом CommonJS (например, Webpack, Node.js), и инструменты, которые работают с ES Modules (Webpack 2+, Rollup).  Некоторые значения мы импортируем из package.json, которые добавим позже.

  • В  plugins мы подключаем упомянутые выше плагины. В зависимости от нужд вашего пакета, вам могут понадобиться и другие плагины Rollup, которых на npm достаточно много.

  • В external указываются пакеты, которые не должны быть включены  в сборку, в нашем случае это пакеты из peerDependencies.

Помимо этого доработаем package.json, добавив следующие строки:

...
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "types": "dist/types.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "rollup -c --bundleConfigAsCjs"
  }
...

Как я отметила ранее, строки main и module фигурируют в конфиге Rollup. Они указывают на файлы — входные точки наших двух билдов. В зависимости от инструмента, который будет работать с пакетом, он выберет main (CommonJS) или module (ES Modules). Строка types указывает на общий файл с типами пакета. 

Для того чтобы опубликовать только определенные файлы (например, только те, которые мы получили после сборки в папке dist), мы добавляем строку files.

В блоке scripts мы описали команду сборки пакета с помощью Rollup. Теперь запустим команду в консоли:

yarn build

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

$ yarn build
yarn run v1.22.19 
$ rollup -c
src/index.ts → dist/cjs/index.js, dist/esm/index.js... 
created dist/cjs/index.js, dist/esm/index.js in 4.1s 
Done in 5.14s.

Совет! Конечно, чтобы использовать пакет на проекте, его нужно опубликовать. Однако мы можем проверить корректность собранного пакета локально.

Для этого запустим следующую команду для архивации репозитория:

yarn pack

Затем перейдем в репозиторий нужного проекта, перенесем полученный архив в корневую папку и установим наш пакет с помощью команды:

yarn add file:./component-library-v1.0.0.tgz

где component-library-v1.0.0.tgz — название полученного архива. 

Проверку выполнить просто: импортируем компонент Card так, как сделали бы это с внешним пакетом:

import React, { FC } from 'react';
import { Card } from 'component-library';

import { Container } from './styles';

export const BasePage: FC = () => {
  return (
    <Container>
      <Card title="Заголовок" onClick={() => console.log('Успех!')} />
    </Container>
  );
};

После этого проект должен собираться без каких-либо ошибок, а компонент отображаться так, как вы планировали.

6. Публикация пакета

Для начала нам потребуется создать аккаунт в npm, затем ввести в консоли логин и электронную почту аккаунта для yarn с помощью команды:

yarn login

Для публикации запустим следующую команду:

yarn publish

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

7. Версионирование

Давайте настроим версионирование, которым мы сможем управлять самостоятельно, при этом генерировать CHANGELOG.md автоматически. Для этого можно воспользоваться библиотекой, которая работает по принципу SemVer (Семантическое Версионирование) и Conventional Commits (Соглашение о коммитах). Установим ее с помощью команды:

yarn add -D standard-version

Чтобы наши коммиты попадали в список СhangeLog, их комментарии следует начинать со следующих ключевых слов:

  • chore — изменение конфигурации, обновление зависимостей и так далее

  • docs — изменения только в документации

  • feat — новая функция, модуль и так далее

  • fix — исправление ошибки

  • refactor — изменение кода, которое не исправляет ошибку и не добавляет новую функцию

  • test — добавление отсутствующих тестов или исправление существующих тестов

Например, если мы добавим новые пропсы для нашего компонента Card, мы можем закоммитить их следующим образом:

git commit -m "feat: Добавляет пропс фона компонента Cards"

Теперь в package.json добавим дополнительные скрипты:

...
  "scripts": {
      …
    "dryrelease": "standard-version --dry-run",
    "release": "standard-version -a"
  }
...

Команду dryrelease удобно использовать в случае, когда вы еще не готовы поднимать версию пакета, но хотите проверить какие изменения войдут в релиз. После запуска этой команды в консоли будет выведен отчёт, схожий с тем, что вы получите после релиза:

$ yarn dryrelease
yarn run v1.22.19
$ standard-version --dry-run
√ bumping version in package.json from 1.0.0 to 1.1.0
√ created CHANGELOG.md
√ outputting changes to CHANGELOG.md
---
## 1.1.0 (2023-07-26)

### Features
* Добавляет пропс фона компонента Cards a330789

### Bug Fixes
* Исправляет отступы в компоненте Card 40010c8
---
√ committing package.json and CHANGELOG.md
√ tagging release v1.1.0
i Run `git push --follow-tags origin HEAD && npm publish` to publish
Done in 1.22s.

Удостоверившись, что всё так, как нам нужно, мы можем выпустить релиз командой release. 

$ yarn release
yarn run v1.22.19
$ standard-version -a
√ bumping version in package.json from 1.0.0 to 1.1.0
√ created CHANGELOG.md
√ outputting changes to CHANGELOG.md
√ committing package.json and CHANGELOG.md and all staged files
warning: LF will be replaced by CRLF in CHANGELOG.md.
The file will have its original line endings in your working directory
√ tagging release v1.1.0
i Run `git push --follow-tags origin d && npm publish` to publish
Done in 7.10s.

В результате мы получим следующее: 

  • в package.json будет автоматически поднята версия до той, которая соответствует вашим правкам,

  • будет создан CHANGELOG.md, если его нет, а в его контент будут добавлены логи по коммитам с ключевыми словами (подобно тому, что мы видели в консоли после выполнения dryrelease)

  • Все изменения будут закомичены и готовы к публикации.

8. Дополнительно: документация и тесты

Существует множество библиотек, которые предоставляют простые и удобные инструменты для создания документации к коду. Однако зачастую простого описания бывает недостаточно. Так как мы описываем компоненты, было бы полезно отображать их и дать возможность взаимодействовать с ними. С этим нам поможет такой инструмент как Storybook. Более подробную информацию о нем, а также инструкцию по настройке Storybook можно найти в опубликованной нами ранее статье.

Не забудьте добавить в репозиторий подробную документацию и к самому пакету. Разместить её можно в следующих файлах:

  • README.md  — здесь обычно хранится информация для разработчиков-пользователей по знакомству с пакетом, его назначению, основным требованиям к установке и так далее.

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

  • CHANGELOG.md — как упоминалось раннее, в этом файле хранятся версии и их изменения, внесенные в проект.

Для тестирования компонентов также есть различные инструменты. В топе – известные Enzyme и Jest, которые позволяют протестировать не только состояние компонента, но и взаимодействие с ним, вычислить покрытие кода тестами. Существуют и менее популярные альтернативные инструменты для тестирования, например, Ava.js.

Подводя итоги

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

Спасибо за внимание!

Авторские материалы для frontend-разработчиков мы также публикуем в наших соцсетях – ВКонтакте и Telegram.

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


  1. nin-jin
    26.09.2023 14:26
    +2

    А теперь сравните с..

    Создание внутреннего пакета компонентов $mol

    Создание локального репозитория

    mkdir acme
    cd acme
    git init
    cd ..
    

    Установка $mol

    Не дурак, сам поставится.

    Настройка Typescript

    Привет, zero-config.

    Добавление компонентов

    Добавим композицию в файл acme/card/card.view.tree:

    $acme_card $mol_view
        event * click? <=> click? null
        attr * acme_card_enabled <= enabled true
        sub /
            <= Title $mol_view
                sub /
                    <= title \
    

    Добавим оформление в файл acme/card/card.view.css.ts:

    namespace $.$$ {
        $mol_style_define( $acme_card, {
        
            border: {
                width: `2px`,
                style: `dashed`,
                color: `#0ebeff`,
                radius: `20px`,
            },
            
            padding: [ `12px`, `24px`, ],
            
            cursor: `none`,
            
            '@': {
                acme_card_enabled: {
                    true: {
                        cursor: `pointer`,
                    },
                }
            }
            
            Title: {
                font: {
                    size: `24px`,
                    weight: `bold`,
                },
            },
            
        } )
    }
    

    Могли бы добавить и поведение в файл acme/card/card.view.ts, но поведения у этого компонента нет.

    Настройка сборки

    Привет, zero-config.

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

    yarn mam acme/card
    

    Публикация пакета

    git remote set-url origin ...
    git push
    

    Версионирование

    К чёрту версиониирование.

    Дополнительно: документация и тесты

    Добавим демонстрацию использования нашего компонента в файл acme/card/demo/demo.view.tree:

    $acme_card_demo $mol_example_small
    	title \Clickable card
    	sub /
    		<= Enabled $acme_card
    			title \Enabled Card
                enabled true
    			click? <=> run? null
    		<= Disabled $acme_card
    			title \Disabled Card
                enabled false
    			click? <=> run? null
    

    Добавим к демонстрации поведение в файл acme/card/demo/demo.view.ts:

    namespace $.$$ {
        export class $acme_card_demo extends $.$acme_card_demo {
        
            run() {
                alert( 'Run!' )
            }
        
        }
    }
    

    Скомпонуем приложение демонстрирующее все компоненты в файл acme/demo/demo.meta.tree:

    include \/mol/app/demo
    include \/acme/card
    

    Добавим точку входа в демо приложение в файл acme/demo/index.html:

    <!doctype html>
    <html mol_view_root>
    <body mol_view_root="$mol_app_demo">
    <script src="web.js" charset="utf-8"></script>
    

    Можем открыт его по ссылке http://localhost:9080/acme/demo/test.html, чтобы прогнать все тесты и потыкать в демки.

    Подводя итоги

    На выходе будет что-то типа такого:


    1. SimbirSoft_frontend Автор
      26.09.2023 14:26

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