Коммуникация
Сквозные задачи (Cross-Cutting Concerns)
Аналитика и логирование
shared/lib/
├── analytics/
│ ├── analytics.ts
│ └── events.ts
└── logger/
└── logger.ts
// shared/lib/analytics/analytics.ts
export const analytics = {
track: (event: string, properties?: Record<string, any>) => {
// Отправк а события в аналитическую систему
}
};
// features/article-favorite/model/use-toggle-favorite.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { analytics } from '@/shared/lib/analytics';
export function useToggleFavorite(slug: string) {
return useMutation({
mutationFn: toggleFavorite,
onSuccess: () => {
analytics.track('article_favorited', { slug });
}
});
}
Провайдеры и инициализация
// app/providers/analytics.tsx
import { useEffect, type PropsWithChildren } from 'react';
import { analytics } from '@/shared/lib/analytics';
import { config } from '@/shared/config';
type Props = PropsWithChildren;
export function AnalyticsProvider({ children }: Props) {
useEffect(() => {
analytics.init(config.analyticsId);
}, []);
return <>{children}</>;
}
Таблица размещения Cross-Cutting Concerns
| Тип функциональности | Размещение | Обоснование | Пример |
|---|---|---|---|
| Инфраструктурный клиент (SDK) | shared/lib/ | Не содержит бизнес-логики, переиспользуется | shared/lib/analytics/analytics.ts |
| Провайдер/инициализация | app/providers/ | Глобальная настройка приложения | app/providers/analytics.tsx |
| Доменное событие | entities/ или features/ | Привязано к бизнес-контексту | analytics.track('article_favorited') в features/article-favorite |
| Feature flags (клиент) | shared/lib/feature-flags/ | Технический механизм | shared/lib/feature-flags/client.ts |
| Feature flags (проверка) | Там, где используется | Близко к месту применения | if (isFeatureEnabled('newEditor')) в features |
| Error tracking (Sentry SDK) | shared/lib/error-tracking/ | Инфраструктурный клиент | shared/lib/sentry/client.ts |
| Error boundaries | app/ или pages/ | Глобальные/локальные обработчики | app/error-boundary.tsx |
| Авторизация (проверка токена) | entities/user/ или features/auth/ | Доменная логика пользователя | entities/user/lib/check-auth.ts |
| Авторизация (guards) | Там, где используется | Близко к защищаемому роуту | pages/*/lib/require-auth.ts |
| Логирование (logger) | shared/lib/logger/ | Технический механизм | shared/lib/logger/logger.ts |
| Логирование (вызовы) | Там, где нужно | По месту использования | logger.error('Failed to load') в features |
Принцип размещения:
- Инфраструктура →
shared/lib/: Если это SDK, клиент, или технический ме ханизм - Инициализация →
app/: Если это глобальная настройка при старте приложения - Использование → Локально: Вызовы размещаются там, где они нужны (features, entities, pages)
Когда НЕ использовать Cross-Cutting паттерн:
FORBIDDEN: Создавать "универсальный" слой для функциональности, используемой в одном месте - размещайте код локально.
// ❌ Плохо - создан "cross-cutting" слой для одного использования
// shared/lib/article-tracking/track-article-view.ts
export function trackArticleView(slug: string) {
analytics.track('article_viewed', { slug });
}
// ✅ Хорошо - вызов напрямую там, где нужен
// features/article-detail/model/use-article-detail.ts
useEffect(() => {
analytics.track('article_viewed', { slug });
}, [slug]);
Коммуникация между фичами (Cross-Feature Communication)
См. также: Слои и архитектура - почему импорты между features запрещены.
Проблема: Как две фичи могут взаимодействовать?
❌ FORBIDDEN: Прямой импорт между features
// ❌ features/article-favorite/model/use-toggle-favorite.ts
import { showNotification } from '@/features/notifications'; // FORBIDDEN!
export function useToggleFavorite() {
const t = useTranslations('article');
return useMutation({
onSuccess: () => {
showNotification(t('favorited')); // Нарушение изоляции
}
});
}
Как выбрать правильное решение?
Используйте эту схему для принятия решения:
✅ Решение 1: Композиция на более высоком уровне (Lifting Up)
// ✅ widgets/article-detail/ui/article-detail.tsx
import { useTranslations } from 'next-intl';
import { FavoriteButton } from '@/features/article-favorite';
import { showNotification } from '@/features/notifications';
export function ArticleDetail({ article }: Props) {
const t = useTranslations('article');
const handleFavoriteSuccess = () => {
showNotification(t('favorited'));
};
return (
<div>
<ArticleCard article={article} />
<FavoriteButton
article={article}
onSuccess={handleFavoriteSuccess}
/>
</div>
);
}
✅ Решение 2: Dependency Inversion через props
// ✅ features/article-favorite/ui/favorite-button.tsx
import { useTranslations } from 'next-intl';
interface FavoriteButtonProps {
article: Article;
onSuccess?: () => void; // Инверсия зависимости
onError?: (error: Error) => void;
}
export function FavoriteButton({ article, onSuccess, onError }: Props) {
const { mutate } = useToggleFavorite({
onSuccess,
onError
});
const t = useTranslations('common');
return (
<button onClick={() => mutate(article)}>
{t('actions.favorite')}
</button>
);
}
// pages/article-details/ui/article-details-page.tsx
import { useTranslations } from 'next-intl';
import { FavoriteButton } from '@/features/article-favorite';
import { toast } from '@/shared/ui/toast';
export function ArticleDetailsPage() {
const t = useTranslations('article');
return (
<FavoriteButton
article={article}
onSuccess={() => toast.success(t('favorited'))}
onError={(e) => toast.error(e.message)}
/>
);
}
Основные подходы:
- Lifting Up (Композиция): Родительский слой (widget/page) комбинирует независимые модули
- Dependency Inversion: Модуль принимает зависимость через props/контекст вместо прямого импорта
📌 Ключевые моменты:
- FORBIDDEN: Прямой импорт между features
- SHOULD: Используйте Lifting Up если parent-слой может координировать
- SHOULD: Используйте Dependency Inversion через props/callbacks для переиспользуемых features
- MUST: Композиция решается на уровне widgets/pages
Public API и изоляция модулей
См. также: Слои и архитектура - правила импортов между слоями.
Правило Public API
MUST: Каждый слайс имеет единую точку входа - публичный контракт (обычно index.ts).
❌ FORBIDDEN: Импорт внутренних файлов
// ❌ FORBIDDEN - импорт из внутренних файлов
import { ArticleCard } from '@/entities/article/ui/article-card';
import { useArticle } from '@/entities/article/model/use-article';
✅ MUST: Импорт через Public API
// ✅ MUST - импорт только из корня модуля
import { ArticleCard, useArticle } from '@/entities/article';
Визуальная схема Public API
Ключевое правило: Всегда импортируйте из корня модуля через index.ts, никогда не обходите Public API напрямую.
Пример Public API
// entities/article/index.ts
// Public API модуля article
// UI Components
export { ArticleCard } from './ui/article-card';
export { ArticleMeta } from './ui/article-meta';
export { ArticleList } from './ui/article-list';
// Model
export { useArticle, useArticles } from './model/use-article';
export type { Article, ArticleFormData } from './model/article.types';
// Lib (только публичные утилиты)
export { formatArticleDate } from './lib/format-date';
export { slugify } from './lib/slugify';
// API НЕ экспортируется - это внутренняя деталь реализации
Преимущества Public API
- Инкапсуляция: Внутренние детали скрыты от внешнего кода
- Гибкость: Можно изменить внутреннюю структуру без влияния на внешний код
- Контроль: Явно определяем, что является публичным контрактом
- Простота: Легко понять, что можно использовать снаружи
📌 Ключевые моменты:
- MUST: Импортируйте только из корня модуля через
index.ts- FORBIDDEN: Глубокие импорты (
@/entities/article/ui/article-card)- MUST: Public API определяет контракт модуля
- MUST: Внутренние файлы остаются приватными