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