Документирование фронтенд-приложений: обзор JSDoc и Storybook

Две фиолетовые 3D-шестерёнки с логотипами Telegram и Flutter
В современной веб-разработке качественная документация так же важна, как и качественный код. Когда ваше приложение разрастается до десятков или сотен компонентов, функций и модулей, становится практически невозможно удерживать в памяти все детали их работы. Хорошая документация не только облегчает поддержку проекта в долгосрочной перспективе, но и значительно ускоряет вхождение новых разработчиков в команду.

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

Читать на Habr
Читать на VC
Читать на Дзен

JSDoc: Документирование JavaScript-кода с помощью комментариев

JSDoc — это система документирования для JavaScript, которая использует специально форматированные комментарии для описания кода. Она похожа на JavaDoc и другие системы документирования, но адаптирована специально для JavaScript.

Если по какой-то причине ваше приложение не использует TypeScript и обходится только JavaScript'ом, тогда JSDoc является отличным вариантом для документирования. Этот инструмент достаточно популярный, что подтверждается значительным количеством скачиваний с npmjs.com.
Для демонстрации полезности JSDoc, рассмотрим практический пример. Представим, что у нас есть функция calculateDistance, которая проводит какие-то вычисления, тому кто её написал всё предельно понятно, но увидев эту функцию впервые могут возникнуть вопросы - что как и почему.
const calculateDistance = (firstPoint, secondPoint) => {
   const deltaX = secondPoint.x - firstPoint.x;
   const deltaY = secondPoint.y - firstPoint.y;
   return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
};
(Понятно, что для примера взята простая функция, но так далеко не всегда).
При использовании такой функции IDE не даст никакой подсказки, что там происходит.
Тут нам на помощь приходит JSDoc, с помощью специальных тегов можно описать функцию, рассмотрим несколько часто используемых тегов:
1) @param - описывает входные параметры
/**
* @param {тип} название - описание
*/

2) @returns - описывает возвращаемое значение
/**
* @param {тип} - описание
*/

3) @example - описывает пример использования
/** @example
* Пример использования.
*/

4) @typedef - описывает пользовательский тип
/**
* @typedef {тип} Название
*/

5) @property - описывает поля объектов
/**
* @typedef {тип} Название
* @property {тип} Название - описание.
*/

Сочетание этих тегов позволяет создать подробную документацию для кода, которая будет доступна разработчикам непосредственно в IDE при работе с функциями и объектами.
Теперь используя вышеупомянутые теги JSDoc, напишем несколько комментариев над функцией, тем самым создав её описание, получаем следующее:
/** @module Helpers */


/**
* @typedef {Object} Point
* @property {number} x - Координата X.
* @property {number} y - Координата Y.
*/


/**
* Рассчитывает расстояние между двумя точками.
*
* @param {Point} firstPoint - Первая точка.
* @param {Point} secondPoint - Вторая точка.
* @returns {number} Расстояние между точками.
*
* @example
* const firstPoint = { x: 0, y: 0 };
* const secondPoint = { x: 3, y: 4 };
* calculateDistance(firstPoint, secondPoint); // Возвращает 5
*/
const calculateDistance = (firstPoint, secondPoint) => {
   const deltaX = secondPoint.x - firstPoint.x;
   const deltaY = secondPoint.y - firstPoint.y;
   return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
};
код название конст должно быть читаемо В итоге у нас есть хорошее описание входных параметров, результата выполнения и примера использования — всё это будет отображаться в IDE при наведении курсора на функцию.
Меню в telegram по созданию бота
Также для более эффективного использования JSDoc можно автоматически генерировать документацию на основе этих комментариев. Существует множество инструментов, которые позволяют это сделать, например:

  • JSDoc CLI — официальный инструмент для генерации документации;
  • ESDoc — генератор документации для JavaScript-проектов;
  • TypeDoc — поддерживает работу с JSDoc и TypeScript
Рассмотрим поподробнее JSDoc CLI, чтобы воспользоваться этим инструментом, его необходимо установить в проект.
npm install jsdoc --save-dev
После чего можно добавить в package.json и выполнить следующий скрипт.
"jsdoc index.js -d docs"
Где index.js — файл, на основе которого будет генерироваться документация.

-d docs — папка с результатом.

Также index.js можно заменить, например, на название папки, допустим, src, и тогда документация будет генерироваться на основе содержимого этой папки.

После запуска скрипта в папке с результатами откройте файл index.html — в браузере откроется сгенерированная документация. Вот пример того, что получилось на основе нашей функции.
Меню в telegram по созданию бота
Для более детальной настройки JSDoc, можно использовать файл конфигурации в формате json или модуля common js, например мы создали файл .jsdoc.conf.json, и настроили паттерн файлов, которые будет обрабатывать JSDoc.
"source": {
 "includePattern": ".+\\.js(doc|x)?$",
 "excludePattern": "(^|\\/|\\\\)_"
},
Такой способ документирования выглядит удобным, но на практике имеет следующие недостатки:
  • Увеличение размера кодовой базы: простые функции могут стать слишком громоздкими из-за комментариев.
  • Ограниченная поддержка сложных типов.
  • Избыточность на проектах с TypeScript.
  • Необходимость постоянно актуализировать комментарии при внесении изменений.

Storybook: интерактивная документация.

Storybook представляет куда более практичное, обширное и удобное средство для документирования кода на фронтенде а также тестирования и визуализации реальных элементов ui . Эта библиотека пользуется большой популярностью и доступна для большинства (если не всех) современных фронтенд фреймворков. Мы же рассмотрим этот инструмент на примере Next с Typescript.

Количество скачиваний на npmjs.com уже в разы больше чем у JSDoc
Меню в telegram по созданию бота
Для того чтобы начать работу со Storybook, практически ничего не нужно. Достаточно выполнить команду npm create storybook@latest, после чего Storybook инициализируется и создаст минимальный жизнеспособный конфиг, с которым уже можно будет писать свои “сторисы” — компоненты в изоляции.

Скорее всего, все, кто использует Storybook, расширяют его конфиг. Это нужно для базовых вещей вашего проекта — подключить SVG, SCSS, Redux, тему и т. п. К счастью, у Storybook отличная документация, и такие улучшения делаются без особых проблем.

Например, у нас использовался SVGR, и чтобы Storybook умел с этим работать, нужно было написать свой webpack.config.ts и добавить в него правила.
config.module.rules.push({
   test: /\.svg$/,
   use: ['@svgr/webpack'],
});
Также в нашем случае необходимо было подключить SCSS, тему и Redux. С SCSS всё просто — для этого в preview.ts нужно было импортировать файл со стилями. А чтобы использовать тему и Redux для наших изолированных компонентов, можно добавлять декораторы для конкретного варианта компонента (это будет показано в примере ниже).

Если с импортом всё понятно, то про декоратор можно сказать, что это функция, позволяющая обернуть истории в дополнительные элементы или контекст. Например, мы создали моковые провайдеры темы и Redux, а также декоратор для Storybook, чтобы все сторисы, которые мы будем писать далее, можно было бы создавать в двух вариантах — в соответствии с темами приложения — и наполнять их разными наборами данных. Это удобно, когда нужно отобразить компоненты в разных состояниях.
const ThemeDecoratorProvider = ({ theme, children }: ThemeDecoratorComponentProps) => {
   useEffect(() => {
       if (theme) {
           document.documentElement.setAttribute('data-theme', theme);

           return () => {
               document.documentElement.removeAttribute('data-theme');
           };
       }
       return undefined;
   }, [theme]);

   return children;
};

export const ThemeDecorator = (theme?: Theme) => (StoryComponent: StoryFn) => (
   <ThemeDecoratorProvider theme={theme}>
       <StoryComponent />
   </ThemeDecoratorProvider>
);


export const StoreProvider = (props: StoreProviderProps) => {
   const { children, initialState } = props;

   const store = makeStore(initialState as StateSchema);

   return <Provider store={store}>{children}</Provider>;
};
В идеале для компонентов Storybook стоит добавить все обёртки (провайдеры), используемые в вашем приложении, на этапе настройки. Это поможет избежать проблем в будущем, когда Storybook «не узнает» о каком-либо провайдере при написании сторисов.

После выполнения всех этих настроек попробуем написать истории для компонента страницы — BidsPage. Создаём соответствующий файл BidsPage.stories.tsx и заполняем его следующим образом:
export default {
    title: 'pages/BidsPage',
    component: BidsPage,
    parameters: {
        layout: 'centered',
    },
    argTypes: {},
} satisfies Meta<typeof BidsPage>;

const Template: StoryFn = () => <BidsPage />;

const state: DeepPartial<StateSchema> = {
    bids: {
        entities: {
            1: {...данные заявки},
        },
        page: 0,
        isNextPage: false,
        isInitialLoading: false,
        isRefetchCache: false,
        pageSize: 9,
        isLoadingMore: false,
        ids: [1],
    },
    folders: {
        currentFolder: 'actual',
        folders: [
            {...данные папки},
        ],
        isFoldersError: false,
        isFoldersInitialLoading: false,
    },
    sellerBids: {
        entities: {
            1: {...данные заявки},
        },
        page: 0,
        isNextPage: false,
        isInitialLoading: false,
        isRefetchCache: false,
        pageSize: 9,
        isLoadingMore: false,
        ids: [1],
    },
    subscriptions: {
        subscriptions: [
            {...данные подборки},
        ],
        currentSubscription: 'actual',
        isSubscriptionsInitialLoading: false,
        isSubscriptionsError: false,
    },
};

export const BidsPageCustomer = Template.bind({});
BidsPageCustomer.decorators = [ThemeDecorator(Theme.CUSTOMER), AppLayoutDecorator(state)];

export const BidsPageSeller = Template.bind({});
BidsPageSeller.decorators = [ThemeDecorator(Theme.SELLER), AppLayoutDecorator(state)];
Дефолтный экспорт

Это основной объект метаданных для истории в Storybook. Он описывает, как Storybook должен работать с компонентом: задаёт название, компонент, параметры, аргументы и т.д.

const Template

Шаблонная функция для рендера компонента. Используется для создания различных вариаций (сторисов) на основе одного и того же компонента с разными данными или контекстом.

const state

Моковая часть Redux-стейта — набор данных, необходимый для корректной работы страницы. Используется при создании сторисов, чтобы воспроизвести поведение компонента в нужном состоянии.

BidsPageCustomer

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

По-хорошему, таких историй, как BidsPageCustomer, следует создавать столько, сколько существует вариантов отображения компонента в зависимости от его пропсов и других условий.
Если указать в tags объекта meta ‘autodocs’, тогда storybook создаст дополнительную страницу с описание компонента, для которого мы писали истории, там будут описаны все пропсы компонента, основываясь на типе, который мы для них создали.

После написания историй можно запустить Storybook с помощью команды: storybook dev -p 6006 -c ./config/storybook
Меню в telegram по созданию бота
После запуска storybook, откроется страница с нашими сторисами, вот как они будут выглядеть в нашем случае. (рис. 3)
Меню в telegram по созданию бота
Здесь мы получаем два варианта страницы — с разной темой и различным набором данных из Redux-стейта. Таким образом, можно создать большое количество сторисов этой страницы в разных её состояниях.

Например, просто изменив набор данных в state для конкретного сториса, можно посмотреть, как будет выглядеть страница при отсутствии элементов в списке заявок.
Меню в telegram по созданию бота