Документирование фронтенд-приложений: обзор JSDoc и Storybook
- суббота, 9 августа 2025 г. в 00:00:09
 

В современной веб-разработке качественная документация так же важна, как и качественный код. Когда ваше приложение разрастается до десятков или сотен компонентов, функций и модулей, становится практически невозможно удерживать в памяти все детали их работы. Хорошая документация не только облегчает поддержку проекта в долгосрочной перспективе, но и значительно ускоряет вхождение новых разработчиков в команду.
В этой статье мы рассмотрим два популярных подхода к документированию фронтенд-кода: JSDoc и Storybook. Они решают схожие задачи, но совершенно разными способами.
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, с помощью специальных тегов можно описать функцию, рассмотрим несколько часто используемых тегов:
@param - описывает входные параметры.                                                                                   /**                                                                                                                                                              * @param {тип} название - описание 
*/
@returns - описывает возвращаемое значение
/**
 * @param {тип} - описание
 */
@example - описывает пример использования
/**
* @example
* Пример использования.
*/
@typedef - описывает пользовательский тип
/**
 * @typedef {тип} Название
*/
@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 представляет куда более практичное, обширное и удобное средство для документирования кода на фронтенде. Эта библиотека пользуется большой популярностью и доступна для большинства (если не всех) современных фронтенд-фреймворков. Мы же рассмотрим этот инструмент на примере 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.