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

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

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 при наведении курсора на функцию.

Также для более эффективного использования 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 — в браузере откроется сгенерированная документация. Вот пример того, что получилось на основе нашей функции.

Для более детальной настройки JSDoc можно использовать файл конфигурации в формате JSON или модуля CommonJS. Например, мы создали файл .jsdoc.conf.json и настроили паттерн файлов, которые будет обрабатывать JSDoc.

"source": {
 "includePattern": ".+\\.js(doc|x)?$",
 "excludePattern": "(^|\\/|\\\\)_"
},

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

  • Увеличение размера кодовой базы: простые функции могут стать слишком громоздкими из-за комментариев.

  • Ограниченная поддержка сложных типов.

  • Избыточность на проектах с TypeScript.

  • Необходимость постоянно актуализировать комментарии при внесении изменений.

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

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

Количество скачиваний на npmjs.com уже в разы больше, чем у JSDoc.

Для того чтобы начать работу со 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 для наших изолированных компонентов, можно добавлять декораторы для конкретного варианта компонента (это будет показано в примере ниже).

Если с импортом всё понятно, то про декоратор можно сказать, что это функция, позволяющая обернуть истории в дополнительные элементы или контекст. Например, мы создали моковый провайдер и декоратор темы, а также декоратор для остальных используемых в приложении провайдеров — AppLayoutDecorator, чтобы все сторисы, которые мы будем писать далее, можно было создавать в двух вариантах — в соответствии с темами приложения — и наполнять их разными наборами данных. Это удобно, когда нужно отобразить компоненты в разных состояниях.

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 AppLayoutDecorator =
   (state: DeepPartial<StateSchema>) => (StoryComponent: StoryFn) => {
       const queryClient = new QueryClient();
       return (
           <SessionProvider session={null}>
               <QueryClientProvider client={queryClient}>
                   <StoreProvider initialState={state}>
                       <WebSocketProvider>
                           <MainLayoutDecorator>
                               <StoryComponent />
                           </MainLayoutDecorator>
                       </WebSocketProvider>
                   </StoreProvider>
               </QueryClientProvider>
           </SessionProvider>
       );
   };

В идеале для компонентов 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.

тема покупатель
тема покупатель

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

тема продавец
тема продавец

Здесь мы получаем два варианта страницы — с разной темой и различным набором данных из Redux-стейта. Таким образом можно создать большое количество сторисов этой страницы в разных её состояниях.

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

пустой список
пустой список

Рассмотрим ещё один пример. Для этого напишем новый сторис для новой страницы — BidDetail. Всё делается по уже знакомому сценарию: создаём файл BidDetail.stories.tsx и заполняем его. Однако в данном примере будет небольшое различие: через пропсы BidDetail получает набор данных, необходимых для отображения этой страницы, поэтому создаём тестовый объект с данными, который передаём в наш компонент. Получаем следующий код:

export default {   
title: 'pages/BidDetail',
   component: BidDetail,
   parameters: {
       layout: 'centered',
   },
   argTypes: {},
} satisfies Meta<typeof BidDetail>;


const Template: StoryFn<BidDetailProps> = (args: BidDetailProps) => <BidDetail {...args} />;


const args: BidDetailProps = {
   id: 1,
   count: 1,
   created_date: new Date().toDateString(),
   bid_photos: [],
   customer: {...данные покупателя},
   delivery_place: 'Москва',
   delivery_type: { id: 4, name: 'ПЭК' },
   description: 'Вот такое описание',
   find_only_in_my_city: true,
   name: 'Название',
   number: 4,
   offer_id: 5,
   offers_count: 6,
   originality: true,
   photos: [],
   published: true,
   request_on_order: true,
   spare_part_type: { id: 7, name: 'Запчасть' },
   taxation: 8,
   relevance_in_days: 9,
   technique_card: {...данные о технической карточке},
   is_favorite: true,
};


export const BidDetailSeller = Template.bind({});
BidDetailSeller.args = args;
BidDetailSeller.decorators = [ThemeDecorator(Theme.SELLER), AppLayoutDecorator({})];


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

В этом случае отличается Template компонента, поскольку BidDetail имеет пропсы, необходимо сообщить об этом Storybook и соответственно типизировать аргументы в шаблоне. Создаётся объект args такого же типа, как и пропсы BidDetail, и передаётся в сторисы.

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

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

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

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

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


  1. Metotron0
    08.08.2025 15:31

    Что именно замеряли в npmjs для JSDoc? Это же просто комментарии в коде, что там можно скачивать?