Стартуем библиотеку компонентов на React и TypeScript

в 8:23, , рубрики: create react library, javascript, ReactJS, библиотека компонентов
Стартуем библиотеку компонентов на React и TypeScript - 1

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

TL/DR: Код готовой к старту библиотеки можно посмотреть на github

К задаче можно подойти с двух сторон:

  1. Найти готовый starter-kit, Boilerplate или cli-генератор
  2. Собрать всё самому

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

Вначале я пошёл первым путём, но затем решил обновить зависимости и прикрутить ещё пару пакетов, и тут посыпались всякие ошибки и несостыковки. В итоге закатал рукава и сделал всё сам. Но генератор библиотек таки упомяну.

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

Ответим на предложенные вопросы

CLR questions

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

С определённой структурой

CLR tree

А потом что-то пошло не так...

На сегодняшний день зависимости немного устаревшие, поэтому я решил обновить их всех до последних версий с помощью 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 добавим поля с распложением собранных бандлов библиотеки, как рекомендует нам rolluppkg.module:

   "main": "dist/index.js",
   "module": "dist/index.es.js",
   "jsnext:main": "dist/index.es.js"

Создадим конфигурационный файл rollup.config.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-файл самого компонента.

ExampleComponent.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).

typings.d.ts

/**
 * 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, то он будет выкинут из сборки, что прекрасно.

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

Добавим конфиг для небольшого уточнения правил форматирования.

.prettierrc.json

{
   "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 с помощью конфигурационного файла.

.eslintrc.json

{
   "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 команду linteslint 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.

jest.config.js

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 в его каталоге.

ExampleComponent.test.tsx

import { ExampleComponent } from './ExampleComponent';

describe('ExampleComponent', () => {
   it('is truthy', () => {
      expect(ExampleComponent).toBeTruthy();
   });
});

Добавим в поле scripts из package.json команду testnpm 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.

webpack.config.js

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.

config.ts

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

ExampleComponent.stories.tsx

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"
  />
  ~~~

`
});

Мы использовали аддоны:

  • knobs — позволяет в режиме реального вермени менять props в отображемом в Storybook компоненте. Для этого необходимо оборачивать props в специальные функции в stories
  • info — позволяет добавлять документацию и описание props на страницу story

Теперь обратим внимание, что скрипт инициализации 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!

Автор: frozen_coder

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js