
В современной веб-разработке качественная документация так же важна, как и качественный код. Когда ваше приложение разрастается до десятков или сотен компонентов, функций и модулей, становится практически невозможно удерживать в памяти все детали их работы. Хорошая документация не только облегчает поддержку проекта в долгосрочной перспективе, но и значительно ускоряет вхождение новых разработчиков в команду.
В этой статье мы рассмотрим два популярных подхода к документированию фронтенд-кода: 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, с помощью специальных тегов можно описать функцию, рассмотрим несколько часто используемых тегов:
@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: интерактивная документация.
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.
