
Большую часть свой работы, я пишу бэкенды, но вот на днях появилась задача начать библиотеку компонентов на React. Несколько лет назад, когда версия React была такой же маленькой, как и мой опыт фронтенд-разработки, я уже делал подход к снаряду и получилось неумело и коряво. Принимая во внимание зрелость текущей экосистемы React и мой подросший опыт, я воодушевился уж в этот-то раз сделать всё хорошо и удобно. В результате у меня появилась заготовка для будущей библиотеки, а чтобы ничего не забыть и собрать всё в одном месте, была написана эта статья-шпаргалка, которая также должна помочь тем, кто не знает, с чего начать. Посмотрим, что же у меня получилось.
TL/DR: Код готовой к старту библиотеки можно посмотреть на github
К задаче можно подойти с двух сторон:
- Найти готовый starter-kit, Boilerplate или cli-генератор
- Собрать всё самому
Первый способ хорош для быстрого старта, когда вы совершенно не хотите разбираться с конфигурированием и подключением необходимых пакетов. Также этот вариант подойдёт для новичков, кто не знает с чего начать и в чём должно быть отличие бибилиотеки от обычного приложения.
Вначале я пошёл первым путём, но затем решил обновить зависимости и прикрутить ещё пару пакетов, и тут посыпались всякие ошибки и несостыковки. В итоге закатал рукава и сделал всё сам. Но генератор библиотек таки упомяну.
Create React Library
Большинство разработчиков, которые имели дело с React слышали про удобный стартер приложений на React, который позволяет свести конфигурацию проекта к минимуму и предоставляет разумные дефолты — Create React App (CRA). В принципе, его можно было бы использовать и для библиотеки (и есть статья на хабре). Однако, структура проекта и подход к разработке ui-kit немного отличается от обычного SPA. Нам нужен отдельный каталог с исходниками компонентов (src), песочница для их разработки и отладки (example), инструмент документирования и демонстрации ("витрина") и отдельный каталог с подготовленными к экспорту файлами (dist). Также компоненты библиотеки не будут складываться в SPA приложение, а будут экспортироваться через индексный файл. Подумав об этом, я отправился на поиски и быстро обнаружил подобный CRA пакет — Creat React Library (CRL).
CRL, также как и CRA, является "easy-to-use" CLI-утилитой. С помощью неё можно сгенерировать проект. Он будет содержать:
- настроенный Rollup для сборки cjs и es модулей и source map с поддержкой css-модулей
- babel для транспиляции в ES5
- Jest для тестов
- TypeScript (TS) как опция, которой мы хотели бы воспользоваться
Для генерации проекта библиотеки выполним(npx позволяет не устанавливать пакеты глобально):
npx create-react-library

И в результате работы утилиты получим сгенерированный и готовый к работе проект библиотеки компонентов.

На сегодняшний день зависимости немного устаревшие, поэтому я решил обновить их всех до последних версий с помощью npm-check:
npx npm-check -u
Ещё одним печальным фактом является то, что приложение-песочница в каталоге example генерируется на js. Придётся руками переписать его на TypeScript, добавив tsconfig.json и некоторые зависимости (например, сам typescript и основные @types).
Также пакет react-scripts-ts объявлен deprecated и больше не поддерживается. Вместо него следует установить react-scripts, потому что с некоторых пор CRA (чьим пакетом является react-scripts) поддерживает TypeScript из коробоки (с помощью Babel 7).
В итоге я не осилил натягивание конфига react-scripts на мое представление о библиотеке. Насколько я помню, Jest из этого пакета требовал опции компилятора isolatedModules, которая шла в разрез с моим желанием генерировать и экспортировать d.ts из библиотеки (всё это как-то связано с ограничениями Babel 7, который используется Jest и react-scripts для компиляции TS). Поэтому я сделал eject для react-scripts, посмотрел на результат и переделал всё руками, о чём и пишу далее.
Берём управление в свои руки
А что, если пойти вторым путём и собрать всё самому? Итак, начнём с начала. Запускаем npm init и генерируем package.json для библиотеки. Добавим туда немного информации о нашем пакете. Например, пропишем минимальные версии для node и npm в поле engines. Собранные и эспортируемые файлы будем помещать в каталог dist. Укажем это в поле files. Мы создаём библиотеку компонентов react, поэтому рассчитываем на то, что у пользователей будут стоять необходимые пакеты — прописываем в поле peerDependencies минимальные необходимые версии react и react-dom.
Теперь установим пакеты react и react-dom и необходимые types (т.к. мы будем пилить компоненты на TypeScript) как devDependencies (как и все пакеты в этой статье):
npm install --save-dev react react-dom @types/react @types/react-dom
Установим TypeScript:
npm install --save-dev typescript
Создадим файлы конфигурации для основного кода и тестов: tsconfig.json и tsconfig.test.json. Наш target будет в es5, будем генерировать sourceMap и т.д. С полным списком возможных опций и их значений можно ознакомиться в документации. Не забудем в include указать каталог с исходниками, а в exclude добавить каталоги node_modules и dist. В package.json укажем в поле typings, где брать типы для нашей библиотеки — dist/index.
Создадим каталог src для исходников компонентов библиотеки. Добавим всякие мелочи, вроде .gitignore, .editorconfig, файла с лицензией и README.md.
Rollup
Для сборки будем использовать Rollup, как предлагал CRL. Необходимые пакеты и конфиг, я подсматривал также у CRL. Вообще слышал мнение, что Rollup хорош для библиотек, а Webpack для приложений. Однако, я не конфигурировал Webpack (мне хватало того, что делает за меня CRA), но Rollup действительно хорош, прост и красив.
Установим:
npm install --save-dev rollup rollup-plugin-babel rollup-plugin-commonjs rollup-plugin-node-resolve rollup-plugin-peer-deps-external rollup-plugin-postcss rollup-plugin-typescript2 rollup-plugin-url @svgr/rollup
В package.json добавим поля с распложением собранных бандлов библиотеки, как рекомендует нам rollup — pkg.module:
"main": "dist/index.js", "module": "dist/index.es.js", "jsnext:main": "dist/index.es.js"
import typescript from 'rollup-plugin-typescript2'; import commonjs from 'rollup-plugin-commonjs'; import external from 'rollup-plugin-peer-deps-external'; import postcss from 'rollup-plugin-postcss'; import resolve from 'rollup-plugin-node-resolve'; import url from 'rollup-plugin-url'; import svgr from '@svgr/rollup'; import pkg from './package.json'; export default { input: 'src/index.tsx', output: [ { file: pkg.main, format: 'cjs', exports: 'named', sourcemap: true }, { file: pkg.module, format: 'es', exports: 'named', sourcemap: true } ], plugins: [ external(), postcss({ modules: false, extract: true, minimize: true, sourceMap: true }), url(), svgr(), resolve(), typescript({ rollupCommonJSResolveHack: true, clean: true }), commonjs() ] };
Конфиг представляет собой js-файл, а точнее экспортируемый объект. В поле input указываем файл, в котором прописаны экспорты для нашей библиотеки. output — описывает наши ожидания на выходе — в модуль какого формата скомпилировать и куда его положить.
- rollup-plugin-peer-deps-external — позволяет исключить
peerDependenciesизbundle, чтобы уменьшить его размер. Это резонно, ибо наличиеpeerDependenciesожидается от пользователя библиотеки - rollup-plugin-postcss — интегрирует PostCss и Rollup. Тут мы отключаем css-modules, включаем в экспортную поставку от нашей библиотеки css, минимизируем его и включаем создание sourceMap. Если вы не экспортируете никакого css, кроме используемого компонентами библиотеки, то
extractможно избежать — необходимый в компонентах css будет по необходимости добавлен в тег head на странице в конечном итоге. Однако в моём случае необходимо раздавать ещё некоторый дополнительный css (сетка, цвета и т.п.), и клиенту придётся явно подключать себе css-bundle библиотеки. - rollup-plugin-url — позволяет экспортировать различные ресурсы, вроде картинок
- svgr — трансформирует svg в React-компоненты
- rollup-plugin-node-resolve — определяет расположение сторонних модулей в node_modules
- rollup-plugin-typescript2 — подключает компилятор TypeScript и предоставляет возможность для его конфигурации
- rollup-plugin-commonjs — конвертирует commonjs-модули зависимостей в es-модули, чтобы их можно было включить в bundle
Добавим в поле scripts package.json команду для сборки ("build": "rollup -c") и запуска сборки в watch-режиме во время разработки ("start": "rollup -c -w && npm run prettier-watch").
Первый компонент и экспортный файл
Теперь напишем простейший react-компонент, чтобы проверить как работает наша сборка. Каждый компонент в библиотеке будем помещать в отдельный каталог в родительском каталоге — src/components/ExampleComponent. В этом каталоге будут содержаться все связанные с компонентом файлы — tsx, css, test.tsx и проч.
Создадим какой-нибудь файл стилей для компонента и tsx-файл самого компонента.
/** * @class ExampleComponent */ import * as React from 'react'; import './ExampleComponent.css'; export interface Props { /** * Simple text prop **/ text: string; } /** My First component */ export class ExampleComponent extends React.Component<Props> { render() { const { text } = this.props; return ( <div className="test"> Example Component: {text} <p>Coool!</p> </div> ); } } export default ExampleComponent;
Также в src надо создать файл с общими для библиотеками типами, где будет объявлен тип для css и svg (подсмотрено у CRL).
/** * Default CSS definition for typescript, * will be overridden with file-specific definitions by rollup */ declare module '*.css' { const content: { [className: string]: string }; export default content; } interface SvgrComponent extends React.FunctionComponent<React.SVGAttributes<SVGElement>> {} declare module '*.svg' { const svgUrl: string; const svgComponent: SvgrComponent; export default svgUrl; export { svgComponent as ReactComponent }; }
Все экспоритруемые компоненты и css должны быть указаны в экспортном файле. У нас это — src/index.tsx. Если какой-то css не используется в проекте и не указан в составе импорируемых в src/index.tsx, то он будет выкинут из сборки, что прекрасно.
import { ExampleComponent, Props } from './ExampleComponent'; import './export.css'; export { ExampleComponent, Props };
Теперь можно попробовать собрать библиотеку — npm run build. В результате запуститься rollup и соберёт нашу библиотеку в бандлы, которые мы найдём в каталоге dist.
Далее добавим несколько инструментов для повышения качества нашего процесса разработки и его результата.
Забываем о форматировании кода с Prettier
Терпеть не могу в code-review указывать на небрежное или нестандартное для проекта форматирование, а тем более спорить про него. Подобные недочёты естественно должны быть исправлены, однако разработчикам лучше сосредоточиться на том, что и как код делает, а не как он выглядит. Эти исправления — первый кандидат на автоматизацию. Есть прекрасный пакет под эту задачу — prettier. Установим его:
npm install --save-dev prettier
Добавим конфиг для небольшого уточнения правил форматирования.
{ "tabWidth": 3, "singleQuote": true, "jsxBracketSameLine": true, "arrowParens": "always", "printWidth": 100, "semi": true, "bracketSpacing": true }
Посмотреть значение доступных правил можно в документации. WebStrom после создания файла конфигурации сама предложит использовать prettier при запуске форматирования через IDE. Чтобы форматирование не тратило время впустую, добавим в исключения каталог /node_modules и /dist с помощью файла .prettierignore (формат аналогичен .gitignore). Теперь можно запустить prettier, применив правила форматирования к исходному коду:
prettier --write "**/*"
Чтобы не запускать команду явно каждый раз руками и быть уверенным, что код остальных разработчиков проекта также будет отформатирован prettier, добавим запуск prettier на precommit-hook для файлов, отмеченных как staged (через git add). Для такого дела нам нужно два инструмента. Во-перых — это hasky, ответсвенного за выполнение каких-либо команд перед коммитом, пушем и т.п. А во-вторых — это lint-staged, который может запускать разные линтеры на staged файлы. Нам нужно выполнить всего лишь одну строчку, чтобы поставить эти пакеты и добавить команды запуска в package.json:
npx mrm lint-staged
Мы можем не ждать форматирования до коммита, а сделать так, чтобы prettier постоянно срабатывал на изменённые файлы в процессе нашей работы. Да, нам нужен ещё один пакет — onchange. Он позволяет следить за изменениями файлов в проекте и тут же выполнять необходимую для них команду. Устанавливаем:
npm install --save-dev --save-exact onchange
Затем в команды поля scripts в package.json добавляем:
"prettier-watch": "onchange 'src/**/*' -- prettier --write {{changed}}"
На этом все споры о форматирвании в проекте можно считать закрытыми.
Избегаем ошибок с ESLint
ESLint уже давно стал стандартом и его можно встретить практически во всех js и ts-проектах. Нам он тоже поможет. В конфигурировании ESLint я доверяю CRA, поэтому просто возьмём необходимые пакеты из CRA и подключим в нашу библиотеку. Кроме того, добавим конфиги для TS и prettier (чтобы избежать конфиктов между ESLint и prettier):
npm install --save-dev eslint eslint-config-react-app eslint-loader eslint-plugin-flowtype eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks @typescript-eslint/eslint-plugin @typescript-eslint/parser babel-eslint eslint-config-prettier eslint-plugin-prettier
Настроим ESLint с помощью конфигурационного файла.
{ "extends": [ "plugin:@typescript-eslint/recommended", "react-app", "prettier", "prettier/@typescript-eslint" ], "plugins": [ "@typescript-eslint", "react" ], "rules": { "@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-member-accessibility": "off" } }
Добавим в поле scripts из package.json команду lint — eslint src/**/* --ext .ts,.tsx --fix. Теперь можно запустить eslint через npm run lint.
Тестируем с Jest
Чтобы писать модульные тесты для компонентов библиотеки, установим и сконфигурируем Jest — библиотеку тестирования от facebook. Однако, т.к. мы компилируем TS не через babel 7, а через tsc, то нам нужно также установить пакет ts-jest:
npm install --save-dev jest ts-jest @types/jest
Чтобы jest нормально воспринимал импорты css или других файлов, необходимо подменить их моками. Создаём каталог __mocks__ и создаём там два файла.
styleMock.ts:
module.exports = {};
fileMock.ts:
module.exports = 'test-file-stub';
Теперь создаём конфиг jest.
module.exports = { preset: 'ts-jest', testEnvironment: 'node', moduleNameMapper: { '\\.(css|less|sass|scss)$': '<rootDir>/__mocks__/styleMock.ts', '\\.(gif|ttf|eot|svg)$': '<rootDir>/__mocks__/fileMock.ts' } };
Напишем простейший тест для нашего ExampleComponent в его каталоге.
import { ExampleComponent } from './ExampleComponent'; describe('ExampleComponent', () => { it('is truthy', () => { expect(ExampleComponent).toBeTruthy(); }); });
Добавим в поле scripts из package.json команду test — npm run lint && jest. Для надёжности ещё и прогоним линтер. Теперь можно запустить наши тесты и удостовериться, что они проходят — npm run test. А чтобы при сборке тесты не попали в dist, добавим в конфиге Rollup плагину typescritp поле exclude — ['src/**/*.test.(tsx|ts)']. Укажем запуск тестов в husky pre-commit hook перед запуском lint-staged — "pre-commit": "npm run test && lint-staged".
Разрабатываем, документируем и любуемся компонентами с Storybook
Каждая библиотека нуждается в хорошей документации для её успешного и продуктивного использвания. Что касается библиотеки компонентов интерфейса, то про них не только хочется прочитать, но и посмотреть как они выглядят, а лучше всего потрогать и поизменять. Для поддержки такой хотелки есть несколько решений. Раньше я использовал Styleguidist. Этот пакет позволяет писать документацию в формате markdown, а также вставлять в неё примеры описываемых React-компонентов. Далее документация собирается и из неё получается сайт-витрина-каталог, где можно найти компонент, прочитать документацию о нём, узнать о его параметрах, а также потыкать в него палочкой.
Однако в этот раз я решил присмотреться к его конкуренту — Storybook. На сегодняшний момент он кажется более мощным с его системой плагинов. Кроме того, он постоянно развивается, имеет большое сообщество и скоро также начнёт генерировать свои страницы документации с помощью markdown-файлов. Ещё одно достоинство Storybook это то, что он является песочницей — средой для изолированной разработки компонентов. Это означает, что нам не нужны никакие полноценные приложения-примеры для разработки компонентов (как это предлагает CRL). В storybook мы пишем stories — ts-файлы, в которых мы передаём наши компоненты с некоторыми входыми props в специальные функции (лучше посмотреть на код, чтобы стало понятнее). В итоге из этих stories собирается приложение-витрина.
Запустим скрипт, который выполнит инициализацию storybook:
npx -p @storybook/cli sb init
Теперь подружим его с TS. Для этого нам нужно ещё немного пакетов, а заодно поставим пару полезных аддонов:
npm install --save-dev awesome-typescript-loader @types/storybook__react @storybook/addon-info react-docgen-typescript-loader @storybook/addon-actions @storybook/addon-knobs @types/storybook__addon-info @types/storybook__addon-knobs webpack-blocks
Скрипт создал каталог с конфигурацией storybook — .storybook и каталог с примером, который мы безжалостно удаляем. А в каталоге конфигураций меняем расширение addons и config на ts. В файл addons.ts подключим аддоны:
import '@storybook/addon-actions/register'; import '@storybook/addon-links/register'; import '@storybook/addon-knobs/register';
Теперь, надо помочь storybook с помощью конфига webpack в каталоге .storybook.
module.exports = ({ config }) => { config.module.rules.push({ test: /\.(ts|tsx)$/, use: [ { loader: require.resolve('awesome-typescript-loader') }, // Optional { loader: require.resolve('react-docgen-typescript-loader') } ] }); config.resolve.extensions.push('.ts', '.tsx'); return config; };
Немного подправим конфиг config.ts, добавив декораторы для подключения аддонов ко всем нашим stories.
import { configure } from '@storybook/react'; import { addDecorator } from '@storybook/react'; import { withInfo } from '@storybook/addon-info'; import { withKnobs } from '@storybook/addon-knobs'; // automatically import all files ending in *.stories.tsx const req = require.context('../src', true, /\.stories\.tsx$/); function loadStories() { req.keys().forEach(req); } configure(loadStories, module); addDecorator(withInfo); addDecorator(withKnobs);
Напишем нашу первую story в каталоге компонента ExampleComponent
import * as React from 'react'; import { storiesOf } from '@storybook/react'; import { ExampleComponent } from './ExampleComponent'; import { text } from '@storybook/addon-knobs/react'; const stories = storiesOf('ExampleComponent', module); stories.add('ExampleComponent', () => <ExampleComponent text={text('text', 'Some text')} />, { info: { inline: true }, text: ` ### Notes Simple example component ### Usage ~~~js <ExampleComponent text="Some text" /> ~~~ ` });
Мы использовали аддоны:
Теперь обратим внимание, что скрипт инициализации storybook добавил к нам в package.json команду storybook. Воспользуемся ей для запуска npm run storybook. Storybook соберётся и запустится по адресу http://localhost:6006. Не забудем таже добавить в исключение для модуля typescriptв конфиге Rollup — 'src/**/*.stories.tsx'.
Разрабатываем
Итак, окружив себя множеством удобных инстументов и приготових их к работе, можно приступить к разработке новых компонентов. Каждый компонент будем помещать в свой каталог в src/components с названием компонента. В нём будут находится все связанные с ним файлы — css, сам компонент в tsx-файле, тесты, stories. Запускаем storybook, создаём для компонента stories, там же пишем к нему документацию. Создаём тесты и тестируем. Импорт-экспорт готового компонента записываем в index.ts.
Кроме того, можно авторизоваться в npm и опубликовать свою библиотеку как новый npm-пакет. А можно подключать её прямо из git-репозитория как из master, так и из других веток. Например, для моей заготовки можно выполнить:
npm i -s git+https://github.com/jmorozov/react-library-example.git
Чтобы в приложении-потребителе библиотеки в каталоге node_modules было только содержимое каталога dist в собранном состоянии, необходимо добавить в поле scripts команду "prepare": "npm run build".
Также, благодаря TS, будет работать автодополнение в IDE.
Подводим итоги
В середине 2019 года можно довольно быстро начать разрабатывать свою библиотеку компонентов на React и TypeScript, пользуясь удобными инструментами разработки. Этого результата можно достичь как с помощью автоматизированной утилиты, так и в ручном режиме. Второй путь является предпочтительным, если нужны актуальные пакеты и больше контроля. Главно знать куда копать, а с помощью примера в этой статье, я надюсь, это стало несколько проще.
Вы также можете взять получившуюся заготовку тут.
Кроме всего прочего, я не претендую на истину в последней инстанции и, вообще, занимаюсь фронтендом постольку-поскольку. Вы можете выбрать альтернативные пакеты и опции конфигурации и также достичь успеха в создании своей библиотеки компонентов. Буду рад, если вы поделитесь в комментариях своими рецептами. Happy coding!
ссылка на оригинал статьи https://habr.com/ru/post/461439/
Добавить комментарий