docs: добавить раздел «Данные» и реорганизовать документацию
- Добавлен раздел «Данные»: REST (автоматическая и ручная генерация клиентов, получение данных в server и client компонентах с инкапсуляцией SWR в хуках), Realtime, введение - Прикладные разделы переименованы в «Использование», папка перенесена в `docs/docs/usage/` - Создана группа «Установка и настройка» с папкой `docs/docs/setup/` — туда вынесены PostCSS, Biome, VS Code, алиасы и установка SVG-спрайтов - Подгруппы «Стили» и «SVG-спрайты» в сайдбаре упразднены — страницы установки и использования разнесены по верхнеуровневым группам - Удалён устаревший раздел `applied/api.md` - Перекрёстные ссылки в workflow-разделах и внутри новых страниц синхронизированы с новыми путями - CONTRIBUTING.md обновлён под новую структуру папок
This commit is contained in:
@@ -30,40 +30,63 @@ const sidebar = [
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Прикладные разделы',
|
||||
text: 'Установка и настройка',
|
||||
items: [
|
||||
{ text: 'Структура проекта', link: '/docs/applied/project-structure' },
|
||||
{ text: 'Алиасы', link: '/docs/applied/aliases' },
|
||||
{ text: 'Компоненты', link: '/docs/applied/components' },
|
||||
{ text: 'Страницы (App Router)', link: '/docs/applied/page-level' },
|
||||
{ text: 'Шаблоны и генерация кода', link: '/docs/applied/templates-generation' },
|
||||
{
|
||||
text: 'Стили',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'PostCSS', link: '/docs/applied/styles/postcss' },
|
||||
{ text: 'Использование', link: '/docs/applied/styles/usage' },
|
||||
],
|
||||
},
|
||||
{ text: 'Изображения', link: '/docs/applied/images-sprites' },
|
||||
{
|
||||
text: 'SVG-спрайты',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Установка и настройка', link: '/docs/applied/svg-sprites/setup' },
|
||||
{ text: 'Использование', link: '/docs/applied/svg-sprites/usage' },
|
||||
],
|
||||
},
|
||||
{ text: 'Видео', link: '/docs/applied/video' },
|
||||
{ text: 'API', link: '/docs/applied/api' },
|
||||
{ text: 'Stores', link: '/docs/applied/stores' },
|
||||
{ text: 'Хуки', link: '/docs/applied/hooks' },
|
||||
{ text: 'Шрифты', link: '/docs/applied/fonts' },
|
||||
{ text: 'Локализация', link: '/docs/applied/localization' },
|
||||
{ text: 'Biome', link: '/docs/applied/biome' },
|
||||
{ text: 'Настройка VS Code', link: '/docs/applied/vscode' },
|
||||
{ text: 'Алиасы', link: '/docs/setup/aliases' },
|
||||
{ text: 'Biome', link: '/docs/setup/biome' },
|
||||
{ text: 'PostCSS', link: '/docs/setup/postcss' },
|
||||
{ text: 'SVG-спрайты', link: '/docs/setup/svg-sprites' },
|
||||
{ text: 'VS Code', link: '/docs/setup/vscode' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Использование',
|
||||
items: [
|
||||
{ text: 'Структура проекта', link: '/docs/usage/project-structure' },
|
||||
{ text: 'Компоненты', link: '/docs/usage/components' },
|
||||
{ text: 'Страницы (App Router)', link: '/docs/usage/page-level' },
|
||||
{ text: 'Шаблоны и генерация кода', link: '/docs/usage/templates-generation' },
|
||||
{ text: 'Стили', link: '/docs/usage/styles' },
|
||||
{ text: 'Изображения', link: '/docs/usage/images-sprites' },
|
||||
{ text: 'SVG-спрайты', link: '/docs/usage/svg-sprites' },
|
||||
{ text: 'Видео', link: '/docs/usage/video' },
|
||||
{
|
||||
text: 'Данные',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Введение', link: '/docs/usage/data/' },
|
||||
{
|
||||
text: 'REST',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{
|
||||
text: 'Клиенты',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Автоматическая генерация', link: '/docs/usage/data/rest/clients/auto' },
|
||||
{ text: 'Ручная генерация', link: '/docs/usage/data/rest/clients/manual' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Получение данных',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Серверные компоненты', link: '/docs/usage/data/rest/fetching/server' },
|
||||
{ text: 'Клиентские компоненты', link: '/docs/usage/data/rest/fetching/client' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ text: 'Realtime', link: '/docs/usage/data/realtime' },
|
||||
],
|
||||
},
|
||||
{ text: 'Stores', link: '/docs/usage/stores' },
|
||||
{ text: 'Хуки', link: '/docs/usage/hooks' },
|
||||
{ text: 'Шрифты', link: '/docs/usage/fonts' },
|
||||
{ text: 'Локализация', link: '/docs/usage/localization' },
|
||||
],
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,8 +34,13 @@ docs/
|
||||
│ ├── naming.md
|
||||
│ ├── documentation.md
|
||||
│ └── typing.md
|
||||
└── applied/ # Прикладные разделы
|
||||
├── vscode.md
|
||||
├── setup/ # Установка: разовая настройка проекта
|
||||
│ ├── aliases.md
|
||||
│ ├── biome.md
|
||||
│ ├── postcss.md
|
||||
│ ├── svg-sprites.md
|
||||
│ └── vscode.md
|
||||
└── usage/ # Использование: повседневная работа
|
||||
├── project-structure.md
|
||||
├── components.md
|
||||
├── page-level.md
|
||||
@@ -44,7 +49,7 @@ docs/
|
||||
├── images-sprites.md
|
||||
├── svg-sprites.md
|
||||
├── video.md
|
||||
├── api.md
|
||||
├── data/
|
||||
├── stores.md
|
||||
├── hooks.md
|
||||
├── fonts.md
|
||||
@@ -59,7 +64,7 @@ generate-llms.ts # Скрипт генерации llms.txt и R
|
||||
|
||||
### Добавление нового раздела
|
||||
|
||||
1. Создать `.md`-файл в нужной папке (`docs/docs/basics/` или `docs/docs/applied/`).
|
||||
1. Создать `.md`-файл в нужной папке (`docs/docs/basics/`, `docs/docs/setup/` или `docs/docs/usage/`).
|
||||
2. Добавить пункт в сайдбар — `.vitepress/config.ts`.
|
||||
Сайдбар — единственный источник порядка и группировки для `llms.txt`.
|
||||
3. Запустить `npm run llms` для обновления `llms.txt` и README.
|
||||
|
||||
@@ -77,4 +77,4 @@ keywords: [biome, линтер, форматтер, lint, format, biome.json, "@
|
||||
|
||||
## Интеграция с VS Code
|
||||
|
||||
Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [Настройка VS Code](/docs/applied/vscode).
|
||||
Расширение `biomejs.biome` и автоформатирование при сохранении настраиваются в [Настройка VS Code](/docs/setup/vscode).
|
||||
@@ -7,7 +7,7 @@ keywords: [postcss, postcss.config.mjs, postcss-custom-media, postcss-nesting, a
|
||||
|
||||
Установка и настройка CSS-процессора PostCSS в проекте: набор плагинов, конфиг `postcss.config.mjs`. Выполняется один раз при заведении проекта.
|
||||
|
||||
Правила написания CSS в компонентах — [Использование](/docs/applied/styles/usage).
|
||||
Правила написания CSS в компонентах — [Использование](/docs/usage/styles).
|
||||
|
||||
## Зачем PostCSS
|
||||
|
||||
@@ -68,4 +68,4 @@ export default {
|
||||
|
||||
Опция `importFrom` у `postcss-custom-media` удалена в v10+; её роль теперь выполняет `@csstools/postcss-global-data`.
|
||||
|
||||
Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование](/docs/applied/styles/usage), раздел «Импорт стилей»).
|
||||
Импортировать `media.css` в файлы компонентов **не нужно** и запрещено правилами (см. [Использование](/docs/usage/styles), раздел «Импорт стилей»).
|
||||
@@ -7,7 +7,7 @@ keywords: [svg-sprites, установка, настройка, config, паке
|
||||
|
||||
Первичная настройка пакета `@gromlab/svg-sprites` в проекте. Выполняется один раз при заведении проекта и при смене мажорной версии пакета.
|
||||
|
||||
Что такое спрайты, как с ними работать и как управлять цветом — [Использование](/docs/applied/svg-sprites/usage).
|
||||
Что такое спрайты, как с ними работать и как управлять цветом — [Использование](/docs/usage/svg-sprites).
|
||||
|
||||
## Требования
|
||||
|
||||
@@ -30,7 +30,7 @@ keywords: [svg-sprites, установка, настройка, config, паке
|
||||
mkdir -p src/shared/sprites/icons
|
||||
```
|
||||
|
||||
Источники спрайтов живут в `src/shared/sprites/<group>/` — это слой `shared` SLM-архитектуры (см. [Структура проекта](/docs/applied/project-structure), [Архитектура](/docs/basics/architecture/)). В `src/` посторонних каталогов вне слоёв не заводим.
|
||||
Источники спрайтов живут в `src/shared/sprites/<group>/` — это слой `shared` SLM-архитектуры (см. [Структура проекта](/docs/usage/project-structure), [Архитектура](/docs/basics/architecture/)). В `src/` посторонних каталогов вне слоёв не заводим.
|
||||
|
||||
4. Добавить скрипты в `package.json`:
|
||||
|
||||
50
docs/docs/usage/data/index.md
Normal file
50
docs/docs/usage/data/index.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Введение
|
||||
keywords: [данные, api, rest, realtime, клиент, swr, infrastructure, введение, карта раздела]
|
||||
---
|
||||
|
||||
# Введение
|
||||
|
||||
Работа с источниками данных в проекте: REST, realtime и любые другие каналы, которые появятся в будущем. Раздел описывает, как создаются клиенты для API и как полученные данные доходят до страниц и компонентов.
|
||||
|
||||
## Принципы раздела
|
||||
|
||||
- **Клиент — в `infrastructure/`.** Каждый внешний сервис — отдельный модуль слоя `infrastructure/{service-name}/`.
|
||||
- **Прямой `fetch` запрещён.** Запросы идут только через клиент модуля. Исключения — точечные и обоснованные.
|
||||
- **Источник данных диктует канал.** REST, realtime и т.п. — независимые подразделы, у каждого своя модель клиента и своё потребление.
|
||||
- **Серверные и клиентские компоненты потребляют по-разному.** Server Components — прямой `await` метода клиента, клиентские — через готовые хуки модуля API (`useUserList`, `usePostDetail` и т.п.). SWR инкапсулирован в хуке, компонент про него не знает.
|
||||
|
||||
## Карта раздела
|
||||
|
||||
### REST
|
||||
|
||||
Канал «запрос-ответ» по HTTP. Покрывает большинство API.
|
||||
|
||||
- **Клиенты** — как создаётся клиент REST API:
|
||||
- [Автоматическая генерация](/docs/usage/data/rest/clients/auto) — для API с OpenAPI-спецификацией, через `@gromlab/api-codegen`.
|
||||
- [Ручная генерация](/docs/usage/data/rest/clients/manual) — для API без схемы, клиент пишется и поддерживается руками.
|
||||
- **Получение данных** — как клиент используется в приложении:
|
||||
- [Серверные компоненты](/docs/usage/data/rest/fetching/server) — прямой `await` метода клиента в Server Components.
|
||||
- [Клиентские компоненты](/docs/usage/data/rest/fetching/client) — через готовые хуки модуля API; SWR с кешем, дедупликацией и ревалидацией скрыт внутри хука.
|
||||
|
||||
### Realtime
|
||||
|
||||
Канал push-данных: WebSocket, SSE, событийные шины. Транспорт не зашит в правила — важна абстракция «подписка».
|
||||
|
||||
- [Realtime](/docs/usage/data/realtime) — клиент realtime в `infrastructure/`, потребление через `useSWRSubscription` или прямые подписки.
|
||||
|
||||
## Что даёт раздел
|
||||
|
||||
После прочтения раздела понятно:
|
||||
|
||||
- Где живёт код работы с API и почему именно там.
|
||||
- Когда генерировать клиент автоматически, а когда писать вручную, и как структурирован каждый из вариантов.
|
||||
- Как получать данные на сервере и на клиенте, чтобы не ломать кеш и не плодить лишние запросы.
|
||||
- Как подключать realtime-источники в общую модель работы с данными.
|
||||
- Какие правила обязательны и какие отклонения допустимы.
|
||||
|
||||
## Что не входит в раздел
|
||||
|
||||
- **Глобальное состояние UI** — Stores, формы, фичефлаги. Это [Stores](/docs/usage/stores).
|
||||
- **Доменная логика** — как данные превращаются в сценарии бизнеса. Это слой `business/` в [Архитектуре](/docs/basics/architecture/).
|
||||
- **Хуки общего назначения** — переиспользуемые хуки UI, не привязанные к конкретному API. Это [Хуки](/docs/usage/hooks).
|
||||
80
docs/docs/usage/data/realtime.md
Normal file
80
docs/docs/usage/data/realtime.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
title: Realtime
|
||||
keywords: [realtime, websocket, sse, подписка, swr subscription, useSWRSubscription, push, события]
|
||||
---
|
||||
|
||||
# Realtime
|
||||
|
||||
Канал для push-данных: WebSocket, SSE, событийные шины и любой другой источник, инициирующий передачу со стороны сервера. Транспорт не зашит в правила — важна абстракция «подписка».
|
||||
|
||||
Получение REST-данных — [REST](/docs/usage/data/rest/clients/auto).
|
||||
|
||||
## Принципы
|
||||
|
||||
- **Клиент realtime — в `infrastructure/`** отдельным модулем по имени канала. То же правило, что и для REST: никаких прямых соединений в коде приложения.
|
||||
- **Подписка — единица потребления.** Клиент даёт функцию `subscribe(topic, handler) → unsubscribe`. Внутри — конкретный транспорт.
|
||||
- **Использование на клиенте — два сценария:**
|
||||
- **`useSWRSubscription`** — для данных, которые показываются в UI и должны кешироваться/синхронизироваться с REST.
|
||||
- **Прямая подписка** — для побочных эффектов (тосты, нотификации, аналитика), не привязанных к рендеру.
|
||||
|
||||
## Размещение клиента
|
||||
|
||||
```text
|
||||
src/infrastructure/
|
||||
└── {channel-name}/
|
||||
├── connection.ts # установление соединения, реконнект
|
||||
├── subscribe.ts # subscribe(topic, handler) → unsubscribe
|
||||
├── types.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## Использование через SWR
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import useSWRSubscription from 'swr/subscription'
|
||||
import { subscribe } from 'infrastructure/notifications'
|
||||
|
||||
export function NotificationCounter() {
|
||||
const { data: count } = useSWRSubscription(
|
||||
['notifications', 'count'],
|
||||
(key, { next }) =>
|
||||
subscribe('notifications.count', (value: number) => next(null, value)),
|
||||
)
|
||||
|
||||
return <span>{count ?? 0}</span>
|
||||
}
|
||||
```
|
||||
|
||||
Плюсы: кеш и дедупликация подписки между несколькими местами рендера; единая модель данных с REST.
|
||||
|
||||
## Прямая подписка
|
||||
|
||||
Для побочных эффектов, которые не влияют на состояние UI напрямую:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { subscribe } from 'infrastructure/notifications'
|
||||
import { showToast } from 'ui/toast'
|
||||
|
||||
export function NotificationsToaster() {
|
||||
useEffect(() => {
|
||||
return subscribe('notifications.new', (notification) => {
|
||||
showToast(notification.message)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
Возврат `unsubscribe` из `useEffect` обязателен — иначе утечка подписки.
|
||||
|
||||
## Запрет прямых соединений
|
||||
|
||||
Создавать `new WebSocket(...)`, `new EventSource(...)` или подписываться на событийные шины напрямую в коде приложения — запрещено. Все соединения проходят через клиент в `infrastructure/`.
|
||||
|
||||
Исключения — точечные и обоснованные (например, диагностический скрипт), помечаются комментарием.
|
||||
280
docs/docs/usage/data/rest/clients/auto.md
Normal file
280
docs/docs/usage/data/rest/clients/auto.md
Normal file
@@ -0,0 +1,280 @@
|
||||
---
|
||||
title: Автоматическая генерация
|
||||
keywords: [api, rest, openapi, codegen, генерация, клиент, api-codegen, gromlab, infrastructure, swagger-typescript-api]
|
||||
---
|
||||
|
||||
# Автоматическая генерация
|
||||
|
||||
Если у API есть OpenAPI-спецификация — клиент генерируется утилитой [@gromlab/api-codegen](https://gromlab.ru/gromov/api-codegen) (обёртка над `swagger-typescript-api`). Ручной код для таких API не пишется.
|
||||
|
||||
Когда схемы нет — [Ручная генерация](/docs/usage/data/rest/clients/manual).
|
||||
|
||||
В примерах ниже используется условный API `pet-project-api` (kebab-case в путях) / `petProjectApi` (camelCase в коде). В реальном проекте имена выбираются по конкретному API.
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
npm install -D @gromlab/api-codegen
|
||||
```
|
||||
|
||||
Скрипт генерации в `package.json` — по одному на каждый API:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"codegen:pet-project-api": "api-codegen --config src/infrastructure/pet-project-api/config/pet-project-api.config.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Конфиг и опции — в репозитории [@gromlab/api-codegen](https://gromlab.ru/gromov/api-codegen).
|
||||
|
||||
## Структура модуля
|
||||
|
||||
Клиент кладётся в слой `infrastructure/` отдельным модулем по имени API (kebab-case):
|
||||
|
||||
```text
|
||||
src/infrastructure/
|
||||
└── pet-project-api/
|
||||
├── generated/ # сегмент сгенерированного кода
|
||||
│ └── pet-project-api.generated.ts # сгенерировано — не править
|
||||
├── types/ # расширения сгенерированных типов
|
||||
│ ├── user.ts # declare module + Extended-тип
|
||||
│ └── index.ts # реэкспорт расширений
|
||||
├── hooks/ # SWR-хуки для клиентских компонентов
|
||||
│ ├── use-user-list.hook.ts
|
||||
│ ├── use-user-detail.hook.ts
|
||||
│ └── index.ts # реэкспорт хуков
|
||||
├── config/ # конфиги модуля
|
||||
│ └── pet-project-api.config.ts # конфиг генерации клиента
|
||||
├── client.ts # настройка HttpClient, инстанс Api
|
||||
└── index.ts # публичный API модуля
|
||||
```
|
||||
|
||||
| Файл | Роль | Кто правит |
|
||||
|------|------|-----------|
|
||||
| `generated/{service-name}.generated.ts` | Сгенерированный код: типы, `class Api`, `class HttpClient` | codegen, не править |
|
||||
| `types/{сущность}.ts` | `declare module` + `Extended`-типы по сущности | разработчик |
|
||||
| `types/index.ts` | Реэкспорт публичных расширений | разработчик |
|
||||
| `hooks/use-{action}.hook.ts` | SWR-хук поверх метода клиента | разработчик |
|
||||
| `hooks/index.ts` | Реэкспорт хуков | разработчик |
|
||||
| `config/{service-name}.config.ts` | Параметры генерации для конкретного API | разработчик |
|
||||
| `client.ts` | `baseUrl` из env, конфиг `HttpClient`, инстанс `new Api(...)` | разработчик |
|
||||
| `index.ts` | Публичный API: инстанс сервиса, расширенные типы, хуки | разработчик |
|
||||
|
||||
`client.ts` и `index.ts` — единственные корневые файлы модуля. Все остальные файлы живут в сегментах (`generated/`, `types/`, `hooks/`, `config/`).
|
||||
|
||||
Имя сгенерированного файла — `{service-name}.generated.ts` (имя сервиса в kebab-case + суффикс `.generated.ts`). Суффикс сигнализирует «не править руками».
|
||||
|
||||
## `client.ts`
|
||||
|
||||
Тонкий ручной слой поверх сгенерированного кода. Делает три вещи: читает и нормализует `baseUrl`, конфигурирует `HttpClient`, создаёт **именованный инстанс** сервиса.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/client.ts
|
||||
import { Api, HttpClient } from './generated/pet-project-api.generated'
|
||||
|
||||
const resolvedBaseUrl = process.env.NEXT_PUBLIC_API_URL
|
||||
.replace(/\/+$/, '') // убираем хвостовой слэш
|
||||
.replace(/\/v1$/, '') // версия уже в путях методов — режем дубль
|
||||
|
||||
const httpClient = new HttpClient({
|
||||
baseApiParams: {
|
||||
secure: false,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// кастомные заголовки API — если требуются
|
||||
// 'X-App-Key': '...',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
httpClient.baseUrl = resolvedBaseUrl
|
||||
|
||||
export const petProjectApi = new Api(httpClient)
|
||||
```
|
||||
|
||||
### Имя инстанса = имя сервиса
|
||||
|
||||
Инстанс называется по имени API в camelCase, не унифицированно `api`/`client`. Это даёт **процедурное обращение** и однозначность при работе с несколькими сервисами:
|
||||
|
||||
```ts
|
||||
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||
|
||||
const user = await petProjectApi.user.getUser(id)
|
||||
```
|
||||
|
||||
При нескольких API — каждый со своим именем:
|
||||
|
||||
```ts
|
||||
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||
import { paymentsApi } from 'infrastructure/payments-api'
|
||||
|
||||
const user = await petProjectApi.user.list()
|
||||
const invoice = await paymentsApi.invoices.list()
|
||||
```
|
||||
|
||||
### Нормализация `baseUrl`
|
||||
|
||||
`@gromlab/api-codegen` может включать версию (`/v1`) в `baseUrl` сгенерированного кода, а пути методов уже содержат её — отсюда дубль. Стандартный приём:
|
||||
|
||||
```ts
|
||||
.replace(/\/+$/, '') // хвостовой слэш
|
||||
.replace(/\/v1$/, '') // версия (если фигурирует в путях)
|
||||
```
|
||||
|
||||
Подгоняется под конкретный API: если версия в путях не повторяется — второй `replace` не нужен.
|
||||
|
||||
## Расширения типов
|
||||
|
||||
Автогенерация не покрывает все реальные поля API: иногда тип `object`, иногда поле просто отсутствует. Расширения живут в `types/`, по файлу на сущность.
|
||||
|
||||
Две техники:
|
||||
|
||||
### `declare module` — добавление полей
|
||||
|
||||
Дополняет существующий интерфейс из `generated.ts`. Сама сгенерированная декларация не трогается.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/types/user.ts
|
||||
import type { User } from '../generated/pet-project-api.generated'
|
||||
|
||||
declare module '../generated/pet-project-api.generated' {
|
||||
interface User {
|
||||
avatar?: {
|
||||
file?: string
|
||||
title?: string
|
||||
url?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `Extended` через `Omit & {...}` — переопределение полей
|
||||
|
||||
Когда автогенерация даёт `object` или общий тип, а реально структура известна — создаётся отдельный тип `UserExtended` (по имени сущности + суффикс `Extended`).
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/types/user.ts
|
||||
export type UserExtended = Omit<User, 'roles' | 'tags' | 'fields'> & {
|
||||
roles?: Array<{ _id?: string; id?: string; slug?: string; name?: string }>
|
||||
tags?: Array<{ _id?: string; id?: string; slug?: string; name?: string }>
|
||||
fields?: Record<string, unknown>
|
||||
}
|
||||
```
|
||||
|
||||
### Реэкспорт
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/types/index.ts
|
||||
export type { UserExtended } from './user'
|
||||
```
|
||||
|
||||
### Правила
|
||||
|
||||
- Расширения — **только в `types/`**, не в `client.ts` и не в сгенерированном файле.
|
||||
- Один файл на сущность (имя файла — kebab-case по сущности: `user.ts`, `order.ts`, `invoice.ts`).
|
||||
- При регенерации `generated/{service-name}.generated.ts` файлы в `types/` не затрагиваются.
|
||||
- Если сломался `Extended`-тип после regen — синхронизировать руками.
|
||||
|
||||
## Хуки для клиентских компонентов
|
||||
|
||||
В клиентских компонентах вызовы клиента не делаются напрямую — компонент получает готовый хук, который инкапсулирует SWR + метод клиента. Хуки живут в сегменте `hooks/`, по файлу на операцию.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/hooks/use-user-list.hook.ts
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import { petProjectApi } from '../client'
|
||||
import type { User } from '../generated/pet-project-api.generated'
|
||||
|
||||
/**
|
||||
* Получение списка пользователей.
|
||||
*/
|
||||
export const useUserList = (
|
||||
query?: { limit?: number; offset?: number },
|
||||
config?: SWRConfiguration,
|
||||
) => {
|
||||
return useSWR<User[]>(
|
||||
['pet-project-api', 'user', 'list', query],
|
||||
() => petProjectApi.user.list(query ?? {}),
|
||||
config,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/hooks/use-user-detail.hook.ts
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import { petProjectApi } from '../client'
|
||||
import type { UserExtended } from '../types'
|
||||
|
||||
/**
|
||||
* Получение пользователя по идентификатору.
|
||||
*/
|
||||
export const useUserDetail = (
|
||||
id: string | null,
|
||||
config?: SWRConfiguration,
|
||||
) => {
|
||||
const key = id ? ['pet-project-api', 'user', 'detail', id] : null
|
||||
const fetcher = () => petProjectApi.user.getUser(id!) as Promise<UserExtended>
|
||||
|
||||
return useSWR<UserExtended>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/hooks/index.ts
|
||||
export { useUserList } from './use-user-list.hook'
|
||||
export { useUserDetail } from './use-user-detail.hook'
|
||||
```
|
||||
|
||||
### Правила хуков
|
||||
|
||||
- **Один файл — один хук**, имя файла `use-{action}.hook.ts` ([Именование](/docs/basics/naming)).
|
||||
- **Тонкая обёртка над SWR.** Внутри — построение ключа, fetcher через метод клиента, возврат `useSWR(...)`. Никакой бизнес-логики.
|
||||
- **Ключ начинается с имени сервиса** (`['pet-project-api', ...]`) — изолирует кеш между разными API.
|
||||
- **Условный ключ для опциональных параметров:** `id ? [...key, id] : null`. `null` приостанавливает запрос, пока параметры не готовы.
|
||||
- **Параметр `config?: SWRConfiguration`** — даёт потребителю переопределить ревалидацию, `fallbackData`, `suspense` и т.п. без обёрток.
|
||||
|
||||
## Публичный API модуля
|
||||
|
||||
Из `index.ts` экспортируются инстанс, расширенные типы и хуки. Сырые типы из `generated/` экспортируются по необходимости — точечно.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/index.ts
|
||||
export { petProjectApi } from './client'
|
||||
export type { UserExtended } from './types'
|
||||
export * from './hooks'
|
||||
```
|
||||
|
||||
## Регенерация
|
||||
|
||||
При изменении OpenAPI-схемы:
|
||||
|
||||
```bash
|
||||
npm run codegen:pet-project-api
|
||||
```
|
||||
|
||||
Что меняется:
|
||||
|
||||
- `generated/{service-name}.generated.ts` — перезаписывается полностью, изменения коммитятся.
|
||||
- `client.ts`, `types/`, `config/`, `index.ts` — **не трогаются** автоматически.
|
||||
|
||||
Поломка контракта (изменение типов в схеме) ловится TypeScript при сборке проекта. Если ломаются `Extended`-типы — синхронизировать вручную в соответствующих файлах `types/`.
|
||||
|
||||
## Сгенерированный файл коммитится
|
||||
|
||||
Файл `generated/{service-name}.generated.ts` **не добавляется в `.gitignore`** — попадает в репозиторий вместе с остальным кодом.
|
||||
|
||||
Причины:
|
||||
|
||||
- **Детерминированная сборка.** `npm run build` не зависит от доступности OpenAPI-схемы (обычно она на удалённом сервере). Сервис лёг — прод собирается.
|
||||
- **Видимость изменений в PR.** Diff показывает, что именно поменялось в контракте API между версиями.
|
||||
- **Простой онбординг.** После `git clone` IDE сразу видит типы, без предварительной генерации.
|
||||
- **Фиксация версии контракта.** Пересборка старого коммита даёт ровно тот клиент, что был тогда.
|
||||
|
||||
Регенерация — **ручная команда** при обновлении схемы, не хук `predev`/`prebuild`. Запускается осознанно.
|
||||
|
||||
Исключение возможно, только если OpenAPI-схема лежит **в этом же репозитории** и генерация быстрая, без сети — тогда допустимо добавить сегмент `generated/` в `.gitignore` и хук `prebuild`, по аналогии со спрайтами. На практике встречается редко.
|
||||
366
docs/docs/usage/data/rest/clients/manual.md
Normal file
366
docs/docs/usage/data/rest/clients/manual.md
Normal file
@@ -0,0 +1,366 @@
|
||||
---
|
||||
title: Ручная генерация
|
||||
keywords: [api, rest, клиент, ручной, fetch, infrastructure, api-клиент]
|
||||
---
|
||||
|
||||
# Ручная генерация
|
||||
|
||||
Если у API нет OpenAPI-спецификации — клиент пишется и поддерживается вручную. Цель та же, что и у автогенерации: единая точка работы с API, без прямых `fetch` в коде приложения.
|
||||
|
||||
Когда схема есть — [Автоматическая генерация](/docs/usage/data/rest/clients/auto).
|
||||
|
||||
В примерах ниже используется условный API `pet-project-api` / `petProjectApi`. В реальном проекте имена выбираются по конкретному API.
|
||||
|
||||
## Структура модуля
|
||||
|
||||
Клиент живёт в слое `infrastructure/` отдельным модулем по имени API (kebab-case):
|
||||
|
||||
```text
|
||||
src/infrastructure/
|
||||
└── pet-project-api/
|
||||
├── methods/ # методы по сущностям API
|
||||
│ ├── pages.ts
|
||||
│ ├── posts.ts
|
||||
│ └── forms.ts
|
||||
├── hooks/ # SWR-хуки для клиентских компонентов
|
||||
│ ├── use-post-detail.hook.ts
|
||||
│ ├── use-post-filter.hook.ts
|
||||
│ └── index.ts
|
||||
├── types/ # типы клиента и доменные типы
|
||||
│ ├── client.ts # типы клиента: RequestOptions, ParamValue
|
||||
│ ├── post.ts # доменные типы сущности post
|
||||
│ ├── form.ts # доменные типы сущности form
|
||||
│ └── index.ts # реэкспорт публичных типов
|
||||
├── errors/ # доменные ошибки API
|
||||
│ └── pet-project-api.error.ts
|
||||
├── client.ts # класс клиента: baseUrl, headers, get/post
|
||||
└── index.ts # публичный API модуля
|
||||
```
|
||||
|
||||
| Файл | Роль |
|
||||
|------|------|
|
||||
| `client.ts` | Класс `PetProjectApiClient`: `baseUrl`, общие заголовки, `buildUrl`, базовые `get`/`post` |
|
||||
| `methods/{entity}.ts` | Методы по сущности, экспортируются фабрикой `{entity}Methods(client)` |
|
||||
| `hooks/use-{action}.hook.ts` | SWR-хук поверх метода клиента |
|
||||
| `hooks/index.ts` | Реэкспорт хуков |
|
||||
| `types/client.ts` | Типы инфраструктуры клиента: `RequestOptions`, `PostOptions`, `ParamValue` |
|
||||
| `types/{entity}.ts` | Доменные типы: запросы, ответы, фильтры по сущности |
|
||||
| `types/index.ts` | Реэкспорт публичных типов |
|
||||
| `errors/{service-name}.error.ts` | Доменный класс ошибок API |
|
||||
| `index.ts` | Публичный API: инстанс клиента, хуки, доменные ошибки, типы |
|
||||
|
||||
`methods/`, `hooks/`, `types/`, `errors/` — сегменты модуля по канону SLM. `client.ts` и `index.ts` — единственные корневые файлы.
|
||||
|
||||
## Типы клиента
|
||||
|
||||
Типы, описывающие саму инфраструктуру запросов (опции, параметры) — выносятся в `types/client.ts`. Это держит `client.ts` коротким и не смешивает декларации типов с реализацией класса.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/types/client.ts
|
||||
export type ParamValue = string | number | (string | number)[]
|
||||
|
||||
export type RequestOptions = {
|
||||
params?: Record<string, ParamValue>
|
||||
headers?: Record<string, string>
|
||||
revalidate?: number | false
|
||||
}
|
||||
|
||||
export type PostOptions = RequestOptions & {
|
||||
type?: 'json' | 'formdata'
|
||||
}
|
||||
```
|
||||
|
||||
## Базовый клиент
|
||||
|
||||
Класс с конфигурацией (`baseUrl`, общие заголовки) и базовыми методами `get` / `post`. Конкретные методы API размещаются в сегменте `methods/`, а не на самом классе — это держит `client.ts` коротким и не плодит «бога-класс».
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/client.ts
|
||||
import { PetProjectApiError } from './errors/pet-project-api.error'
|
||||
import type { ParamValue, RequestOptions, PostOptions } from './types/client'
|
||||
|
||||
export class PetProjectApiClient {
|
||||
constructor(
|
||||
private readonly baseUrl: string,
|
||||
private readonly defaultHeaders: Record<string, string> = {},
|
||||
) {
|
||||
this.defaultHeaders = {
|
||||
Accept: 'application/json',
|
||||
...defaultHeaders,
|
||||
}
|
||||
}
|
||||
|
||||
buildUrl(path: string, params?: Record<string, ParamValue>): string {
|
||||
const base = this.baseUrl.replace(/\/+$/, '')
|
||||
const tail = path.replace(/^\/+/, '')
|
||||
const url = `${base}/${tail}`
|
||||
|
||||
if (!params) {
|
||||
return url
|
||||
}
|
||||
|
||||
const search = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => search.append(key, String(v)))
|
||||
} else {
|
||||
search.set(key, String(value))
|
||||
}
|
||||
}
|
||||
|
||||
return `${url}?${search}`
|
||||
}
|
||||
|
||||
async get<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { params, headers, revalidate } = options
|
||||
const response = await fetch(this.buildUrl(path, params), {
|
||||
headers: { ...this.defaultHeaders, ...headers },
|
||||
...(revalidate !== undefined && { next: { revalidate } }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw await PetProjectApiError.fromResponse(response)
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
|
||||
async post<T>(path: string, body: unknown, options: PostOptions = {}): Promise<T> {
|
||||
const { params, headers, revalidate, type = 'json' } = options
|
||||
const isJson = type === 'json'
|
||||
|
||||
const response = await fetch(this.buildUrl(path, params), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...(isJson && { 'Content-Type': 'application/json' }),
|
||||
...headers,
|
||||
},
|
||||
body: isJson ? JSON.stringify(body) : (body as BodyInit),
|
||||
...(revalidate !== undefined && { next: { revalidate } }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw await PetProjectApiError.fromResponse(response)
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ключевые требования к клиенту
|
||||
|
||||
- **Класс с приватным состоянием** (`baseUrl`, `defaultHeaders`) — конфигурация инкапсулирована.
|
||||
- **Типы клиента — в `types/client.ts`**, не в `client.ts`. Реализация и контракты разделены.
|
||||
- **Базовые методы дженерик `<T>` без дефолта.** Вызов без типа невозможен — потребитель обязан указать форму ответа.
|
||||
- **Доменная ошибка вместо `null`.** При не-`ok` бросается `PetProjectApiError`. Возврат `null` глотает причины (404 vs 500 vs 401) — не использовать.
|
||||
- **Дефолт POST — `json`.** `formdata` указывается явно, на конкретных методах (загрузка файлов, отправка форм).
|
||||
- **Нормализация слэшей** в `buildUrl` — `baseUrl` без хвостового `/`, `path` без ведущего `/`.
|
||||
- **`async/await`**, не `.then()` — линейное чтение, простая обработка ошибок.
|
||||
- **Поддержка `next.revalidate`** — клиент знает о Next.js App Router и пробрасывает кеш-флаги.
|
||||
|
||||
## Доменная ошибка
|
||||
|
||||
Сетевая ошибка превращается в класс ошибки модуля. Наружу не выходит сырой `Response`.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/errors/pet-project-api.error.ts
|
||||
export class PetProjectApiError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
public readonly body: string,
|
||||
) {
|
||||
super(`PetProjectApi ${status}: ${body.slice(0, 200)}`)
|
||||
this.name = 'PetProjectApiError'
|
||||
}
|
||||
|
||||
static async fromResponse(response: Response): Promise<PetProjectApiError> {
|
||||
const body = await response.text().catch(() => '')
|
||||
return new PetProjectApiError(response.status, body)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Дополнительные подклассы по необходимости: `PetProjectApiValidationError` (400), `PetProjectApiAuthError` (401/403), `PetProjectApiNotFoundError` (404). Вводятся когда у потребителя есть **разная реакция** на разные коды; иначе хватает базового класса.
|
||||
|
||||
## Доменные типы
|
||||
|
||||
Типы запросов, ответов и фильтров — по файлу на сущность. Типы должны лежать рядом по смыслу: всё, что относится к `posts`, — в `types/post.ts`.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/types/post.ts
|
||||
export type Post = {
|
||||
id: string
|
||||
slug: string
|
||||
title: string
|
||||
content: string
|
||||
publishedAt: string
|
||||
}
|
||||
|
||||
export type PostFilter = {
|
||||
limit?: number
|
||||
categories?: number[]
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/types/index.ts
|
||||
export type * from './post'
|
||||
export type * from './form'
|
||||
// типы клиента — внутренние, наружу не реэкспортируются
|
||||
```
|
||||
|
||||
Типы клиента (`RequestOptions`, `PostOptions`, `ParamValue`) **не реэкспортируются** через `types/index.ts` — они нужны только внутри модуля.
|
||||
|
||||
## Методы
|
||||
|
||||
Методы группируются по сущностям в сегменте `methods/`, экспортируются фабрикой, принимающей клиент. Это даёт **процедурное обращение** в стиле автогенерированного клиента (`petProjectApi.posts.get(slug)`), а не плоский список (`petProjectApi.getPost(slug)`).
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/methods/posts.ts
|
||||
import type { PetProjectApiClient } from '../client'
|
||||
import type { Post, PostFilter } from '../types/post'
|
||||
|
||||
export function postsMethods(client: PetProjectApiClient) {
|
||||
return {
|
||||
/** GET /posts/{slug} */
|
||||
get: (slug: string, options?: { revalidate?: number | false }) =>
|
||||
client.get<Post>(`posts/${slug}`, options),
|
||||
|
||||
/** POST /posts/filter */
|
||||
filter: (body: PostFilter) =>
|
||||
client.post<Post[]>('posts/filter', body),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/methods/forms.ts
|
||||
import type { PetProjectApiClient } from '../client'
|
||||
import type { Form, FormSubmissionResult } from '../types/form'
|
||||
|
||||
export function formsMethods(client: PetProjectApiClient) {
|
||||
return {
|
||||
/** GET /forms/{id} */
|
||||
get: (id: string) => client.get<Form>(`forms/${id}`),
|
||||
|
||||
/** POST /forms/{id} — multipart/form-data */
|
||||
submit: (id: string, data: FormData) =>
|
||||
client.post<FormSubmissionResult>(`forms/${id}`, data, { type: 'formdata' }),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Правила методов
|
||||
|
||||
- **Группировка по сущности** (`pages`, `posts`, `forms`), не плоский список.
|
||||
- **Имя метода — глагол действия**: `get`, `list`, `filter`, `create`, `update`, `delete`, `submit`. Не `getPost`/`getPosts` — сущность уже в имени группы.
|
||||
- **Типы запросов и ответов — в `types/{entity}.ts`**, импортируются в файл методов. В `methods/` лежит только композиция вызовов клиента, без объявлений типов.
|
||||
- **Фабрика принимает клиент** — это даёт тестируемость (моковый клиент в юнит-тестах) и единый источник конфигурации.
|
||||
- **Никаких знаний об UI.** Клиент не знает про React, SWR, тосты — только данные и ошибки.
|
||||
|
||||
## Сборка инстанса
|
||||
|
||||
Группы методов соединяются в один объект на уровне `index.ts`. Это даёт процедурный доступ `petProjectApi.posts.get(...)`.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/index.ts
|
||||
import { PetProjectApiClient } from './client'
|
||||
import { pagesMethods } from './methods/pages'
|
||||
import { postsMethods } from './methods/posts'
|
||||
import { formsMethods } from './methods/forms'
|
||||
|
||||
const client = new PetProjectApiClient(process.env.NEXT_PUBLIC_API_URL, {
|
||||
'X-App-Key': process.env.NEXT_PUBLIC_APP_KEY,
|
||||
})
|
||||
|
||||
export const petProjectApi = {
|
||||
pages: pagesMethods(client),
|
||||
posts: postsMethods(client),
|
||||
forms: formsMethods(client),
|
||||
}
|
||||
|
||||
export { PetProjectApiError } from './errors/pet-project-api.error'
|
||||
export type { Post, PostFilter, Page, Form } from './types'
|
||||
export * from './hooks'
|
||||
```
|
||||
|
||||
## Хуки для клиентских компонентов
|
||||
|
||||
В клиентских компонентах вызовы клиента не делаются напрямую — компонент получает готовый хук, который инкапсулирует SWR + метод клиента. Хуки живут в сегменте `hooks/`, по файлу на операцию.
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/hooks/use-post-detail.hook.ts
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import { petProjectApi } from '..'
|
||||
import type { Post } from '../types/post'
|
||||
|
||||
/**
|
||||
* Получение поста по slug.
|
||||
*/
|
||||
export const usePostDetail = (
|
||||
slug: string | null,
|
||||
config?: SWRConfiguration,
|
||||
) => {
|
||||
const key = slug ? ['pet-project-api', 'post', 'detail', slug] : null
|
||||
const fetcher = () => petProjectApi.posts.get(slug!)
|
||||
|
||||
return useSWR<Post>(key, fetcher, config)
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/hooks/use-post-filter.hook.ts
|
||||
import useSWR from 'swr'
|
||||
import type { SWRConfiguration } from 'swr'
|
||||
import { petProjectApi } from '..'
|
||||
import type { Post, PostFilter } from '../types/post'
|
||||
|
||||
/**
|
||||
* Получение списка постов по фильтру.
|
||||
*/
|
||||
export const usePostFilter = (
|
||||
filter: PostFilter,
|
||||
config?: SWRConfiguration,
|
||||
) => {
|
||||
return useSWR<Post[]>(
|
||||
['pet-project-api', 'post', 'filter', filter],
|
||||
() => petProjectApi.posts.filter(filter),
|
||||
config,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/hooks/index.ts
|
||||
export { usePostDetail } from './use-post-detail.hook'
|
||||
export { usePostFilter } from './use-post-filter.hook'
|
||||
```
|
||||
|
||||
### Правила хуков
|
||||
|
||||
- **Один файл — один хук**, имя файла `use-{action}.hook.ts` ([Именование](/docs/basics/naming)).
|
||||
- **Тонкая обёртка над SWR.** Внутри — построение ключа, fetcher через метод клиента, возврат `useSWR(...)`. Никакой бизнес-логики.
|
||||
- **Ключ начинается с имени сервиса** (`['pet-project-api', ...]`) — изолирует кеш между разными API.
|
||||
- **Условный ключ для опциональных параметров:** `id ? [...key, id] : null`. `null` приостанавливает запрос, пока параметры не готовы.
|
||||
- **Параметр `config?: SWRConfiguration`** — даёт потребителю переопределить ревалидацию, `fallbackData`, `suspense` и т.п. без обёрток.
|
||||
|
||||
## Запрет прямого `fetch`
|
||||
|
||||
В коде приложения (слои выше `infrastructure`) прямые вызовы `fetch` к API запрещены. Все запросы идут через клиент.
|
||||
|
||||
Исключение допускается точечно — например, разовая отладочная проверка эндпоинта в скрипте — и требует обоснования в коде (комментарий с причиной).
|
||||
|
||||
## Использование
|
||||
|
||||
```ts
|
||||
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||
|
||||
const post = await petProjectApi.posts.get('my-post')
|
||||
const list = await petProjectApi.posts.filter({ limit: 10, categories: [1, 2] })
|
||||
const form = await petProjectApi.forms.get('contact')
|
||||
```
|
||||
|
||||
Стиль вызовов совпадает с автогенерированным клиентом — потребитель не различает, ручной API или сгенерирован.
|
||||
165
docs/docs/usage/data/rest/fetching/client.md
Normal file
165
docs/docs/usage/data/rest/fetching/client.md
Normal file
@@ -0,0 +1,165 @@
|
||||
---
|
||||
title: Клиентские компоненты
|
||||
keywords: [swr, клиентские компоненты, useSWR, хук, мутация, useSWRMutation, кеш, ревалидация]
|
||||
---
|
||||
|
||||
# Клиентские компоненты
|
||||
|
||||
В клиентских компонентах данные получаются через **готовые хуки**, которые экспортируются из модуля API. SWR инкапсулирован в хуке — компонент не знает про `useSWR`, ключи и fetcher.
|
||||
|
||||
Создание клиента и хуков — [Автоматическая](/docs/usage/data/rest/clients/auto) / [Ручная](/docs/usage/data/rest/clients/manual) генерация.
|
||||
|
||||
## Правила
|
||||
|
||||
- **Только готовые хуки.** В компоненте — `usePostDetail(slug)`, не `useSWR(['post', slug], () => api.posts.get(slug))`.
|
||||
- **`useSWR` пишется один раз — в `hooks/`** модуля API. В клиентских компонентах никогда напрямую.
|
||||
- **Прямой вызов методов клиента в `useEffect` запрещён.** Это потеря кеша, повторные запросы и гонки.
|
||||
- **Мутации — через `useSWRMutation`**, тоже инкапсулированный в хуке. В компоненте вызывается готовый `trigger`.
|
||||
|
||||
## Чтение
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { usePostDetail } from 'infrastructure/pet-project-api'
|
||||
|
||||
export function PostView({ slug }: { slug: string }) {
|
||||
const { data: post, error, isLoading } = usePostDetail(slug)
|
||||
|
||||
if (isLoading) return <Spinner />
|
||||
if (error) return <ErrorView error={error} />
|
||||
|
||||
return <article>{post?.title}</article>
|
||||
}
|
||||
```
|
||||
|
||||
В компоненте нет `useSWR`, нет ключей, нет fetcher — только готовый хук.
|
||||
|
||||
## Параметризованный запрос
|
||||
|
||||
Хук сам обрабатывает «нет параметра — нет запроса». В компоненте можно безопасно передавать `null`:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useUserDetail } from 'infrastructure/pet-project-api'
|
||||
|
||||
export function UserProfile({ userId }: { userId: string | null }) {
|
||||
const { data: user } = useUserDetail(userId)
|
||||
|
||||
if (!userId) return <EmptyState />
|
||||
return <UserCard user={user} />
|
||||
}
|
||||
```
|
||||
|
||||
Внутри `useUserDetail` ключ становится `null`, когда `userId` не задан, и SWR не делает запрос — это поведение зашито в хук, потребитель об этом не думает.
|
||||
|
||||
## Мутации
|
||||
|
||||
Мутации тоже оборачиваются в хук модуля API:
|
||||
|
||||
```ts
|
||||
// src/infrastructure/pet-project-api/hooks/use-create-user.hook.ts
|
||||
import useSWRMutation from 'swr/mutation'
|
||||
import { mutate } from 'swr'
|
||||
import { petProjectApi } from '..'
|
||||
import type { User, UserCreateInput } from '../types'
|
||||
|
||||
/**
|
||||
* Создание пользователя с инвалидацией списка.
|
||||
*/
|
||||
export const useCreateUser = () => {
|
||||
return useSWRMutation<User, Error, [string, string, string], UserCreateInput>(
|
||||
['pet-project-api', 'user', 'create'],
|
||||
(_key, { arg }) => petProjectApi.user.create(arg),
|
||||
{
|
||||
onSuccess: () => mutate(['pet-project-api', 'user', 'list']),
|
||||
},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { useCreateUser } from 'infrastructure/pet-project-api'
|
||||
|
||||
export function CreateUserForm() {
|
||||
const { trigger, isMutating } = useCreateUser()
|
||||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={(input) => trigger(input)}
|
||||
disabled={isMutating}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
В компоненте — снова только хук. Логика инвалидации кеша зашита внутрь, потребитель её не дублирует.
|
||||
|
||||
## Передача config из компонента
|
||||
|
||||
Каждый хук принимает второй (или третий) параметр `config?: SWRConfiguration` — он пробрасывается в `useSWR`. Это даёт потребителю точечно настроить ревалидацию, `fallbackData`, `suspense` и т.п.:
|
||||
|
||||
```tsx
|
||||
'use client'
|
||||
|
||||
import { usePostDetail } from 'infrastructure/pet-project-api'
|
||||
|
||||
export function PostView({ slug, initialPost }: Props) {
|
||||
const { data: post } = usePostDetail(slug, { fallbackData: initialPost })
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Начальное состояние с сервера
|
||||
|
||||
Если данные пришли из серверного компонента (см. [Серверные компоненты](/docs/usage/data/rest/fetching/server)) — передаются в `fallbackData` через `config` хука:
|
||||
|
||||
```tsx
|
||||
// page.tsx (server)
|
||||
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||
|
||||
export default async function Page({ params }: { params: { slug: string } }) {
|
||||
const initialPost = await petProjectApi.posts.get(params.slug)
|
||||
return <PostView slug={params.slug} initialPost={initialPost} />
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// post-view.tsx ('use client')
|
||||
import { usePostDetail } from 'infrastructure/pet-project-api'
|
||||
|
||||
export function PostView({ slug, initialPost }: Props) {
|
||||
const { data: post } = usePostDetail(slug, { fallbackData: initialPost })
|
||||
return <article>{post?.title}</article>
|
||||
}
|
||||
```
|
||||
|
||||
Для массового заполнения кеша на странице с несколькими хуками — используется `<SWRConfig fallback>` обёртка. Серверный компонент собирает данные и передаёт сериализованную карту ключей в провайдер; все вложенные хуки сразу видят кеш.
|
||||
|
||||
## Запрет прямых вызовов
|
||||
|
||||
```tsx
|
||||
// Плохо — прямой fetch в обход клиента
|
||||
useEffect(() => {
|
||||
fetch('/api/users').then(...)
|
||||
}, [])
|
||||
|
||||
// Плохо — клиент без SWR: нет кеша, нет дедупликации
|
||||
useEffect(() => {
|
||||
petProjectApi.user.list().then(setUsers)
|
||||
}, [])
|
||||
|
||||
// Плохо — useSWR в компоненте: SWR должен быть в хуке модуля
|
||||
const { data } = useSWR(
|
||||
['pet-project-api', 'user', 'list'],
|
||||
() => petProjectApi.user.list(),
|
||||
)
|
||||
|
||||
// Хорошо — готовый хук модуля
|
||||
const { data } = useUserList()
|
||||
```
|
||||
|
||||
Если для нужной операции хука ещё нет — он добавляется в `hooks/` модуля API, не в компонент.
|
||||
67
docs/docs/usage/data/rest/fetching/server.md
Normal file
67
docs/docs/usage/data/rest/fetching/server.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Серверные компоненты
|
||||
keywords: [server components, rsc, серверные компоненты, fetch, api, app router, прямой вызов]
|
||||
---
|
||||
|
||||
# Серверные компоненты
|
||||
|
||||
В серверных компонентах (Server Components App Router) данные получаются **прямым вызовом метода API-клиента**. SWR и хуки здесь не применяются — они для клиентского кода.
|
||||
|
||||
Создание клиента — [Автоматическая](/docs/usage/data/rest/clients/auto) / [Ручная](/docs/usage/data/rest/clients/manual) генерация.
|
||||
|
||||
## Правила
|
||||
|
||||
- **Прямой `await` метода клиента.** Никаких хуков, обёрток состояний, `useEffect` — серверный компонент не имеет жизненного цикла React-клиента.
|
||||
- **Ошибки бросаются.** Не оборачивать `try/catch` без необходимости — Next.js поднимет ближайший `error.tsx`.
|
||||
- **Параллельные запросы — через `Promise.all`.** Последовательный `await` за `await` блокирует рендер.
|
||||
|
||||
## Шаблон
|
||||
|
||||
```tsx
|
||||
// src/app/(routes)/users/page.tsx
|
||||
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||
|
||||
export default async function UsersPage() {
|
||||
const users = await petProjectApi.user.list()
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{users.map((user) => (
|
||||
<li key={user.id}>{user.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Параллельные запросы
|
||||
|
||||
```tsx
|
||||
export default async function DashboardPage() {
|
||||
const [users, orders] = await Promise.all([
|
||||
petProjectApi.user.list(),
|
||||
petProjectApi.order.list(),
|
||||
])
|
||||
|
||||
return <Dashboard users={users} orders={orders} />
|
||||
}
|
||||
```
|
||||
|
||||
## Передача данных в клиентский компонент
|
||||
|
||||
Серверный компонент получает данные и передаёт их пропсами в клиентский. На клиенте данные становятся начальным состоянием — при необходимости перезапрашиваются через SWR (см. [Клиентские компоненты](/docs/usage/data/rest/fetching/client)).
|
||||
|
||||
```tsx
|
||||
// page.tsx (server)
|
||||
import { petProjectApi } from 'infrastructure/pet-project-api'
|
||||
import { UsersList } from 'widgets/users-list'
|
||||
|
||||
export default async function UsersPage() {
|
||||
const initialUsers = await petProjectApi.user.list()
|
||||
return <UsersList initialUsers={initialUsers} />
|
||||
}
|
||||
```
|
||||
|
||||
## Запрет прямого `fetch`
|
||||
|
||||
Серверный компонент тоже использует только клиент из `infrastructure/`. Прямой `fetch` в `page.tsx` или в server-action запрещён теми же правилами, что и на клиенте.
|
||||
@@ -4,7 +4,7 @@ title: Использование
|
||||
|
||||
# Использование
|
||||
|
||||
Правила написания CSS: PostCSS Modules, форматирование, переменные. Установка и настройка процессора — [PostCSS](/docs/applied/styles/postcss).
|
||||
Правила написания CSS: PostCSS Modules, форматирование, переменные. Установка и настройка процессора — [PostCSS](/docs/setup/postcss).
|
||||
|
||||
## Общие правила
|
||||
|
||||
@@ -5,7 +5,7 @@ keywords: [svg, спрайт, sprite, иконка, icon, SvgSprite, превь
|
||||
|
||||
# Использование
|
||||
|
||||
Работа с SVG-иконками через сгенерированный компонент `<SvgSprite/>`. Установка пакета — [Установка и настройка](/docs/applied/svg-sprites/setup).
|
||||
Работа с SVG-иконками через сгенерированный компонент `<SvgSprite/>`. Установка пакета — [Установка и настройка](/docs/setup/svg-sprites).
|
||||
|
||||
## Шаги
|
||||
|
||||
@@ -28,4 +28,4 @@ title: Генерация кода
|
||||
- Повторяющаяся структура появляется больше одного раза.
|
||||
- Существующий шаблон не покрывает нужный тип модуля.
|
||||
|
||||
Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/docs/applied/templates-generation).
|
||||
Инструменты и синтаксис шаблонов — [Шаблоны и генерация кода](/docs/usage/templates-generation).
|
||||
|
||||
@@ -12,11 +12,11 @@ title: Добавление UI-модуля
|
||||
|
||||
## Порядок действий
|
||||
|
||||
1. [Сгенерировать](/docs/applied/templates-generation) модуль из соответствующего шаблона в целевой слой.
|
||||
1. [Сгенерировать](/docs/usage/templates-generation) модуль из соответствующего шаблона в целевой слой.
|
||||
2. Заполнить модуль логикой и стилями.
|
||||
|
||||
## Дочерние компоненты
|
||||
|
||||
Если модулю нужны внутренние подкомпоненты — [генерировать](/docs/applied/templates-generation) их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя.
|
||||
Если модулю нужны внутренние подкомпоненты — [генерировать](/docs/usage/templates-generation) их из шаблона `component` в папку `ui/` внутри родительского модуля. Дочерние компоненты не экспортируются через `index.ts` родителя.
|
||||
|
||||
Правила написания компонентов — [Компоненты](/docs/applied/components).
|
||||
Правила написания компонентов — [Компоненты](/docs/usage/components).
|
||||
|
||||
@@ -12,7 +12,7 @@ title: Добавление страницы
|
||||
|
||||
## Порядок действий
|
||||
|
||||
1. [Сгенерировать](/docs/applied/templates-generation) экран из шаблона `screen` в папку `src/screens/`.
|
||||
1. [Сгенерировать](/docs/usage/templates-generation) экран из шаблона `screen` в папку `src/screens/`.
|
||||
|
||||
2. Заполнить экран логикой и стилями.
|
||||
|
||||
@@ -20,8 +20,8 @@ title: Добавление страницы
|
||||
|
||||
## Правила
|
||||
|
||||
- Ручное создание файловой структуры экрана запрещено — только [генерация](/docs/applied/templates-generation) из шаблона.
|
||||
- Ручное создание файловой структуры экрана запрещено — только [генерация](/docs/usage/templates-generation) из шаблона.
|
||||
- Логика, стили и зависимости размещаются в экране, не в `page.tsx`.
|
||||
- Каждая страница содержит `metadata` с `title` и `description`.
|
||||
|
||||
Примеры `page.tsx` и `metadata` — [Page-level компоненты](/docs/applied/page-level).
|
||||
Примеры `page.tsx` и `metadata` — [Page-level компоненты](/docs/usage/page-level).
|
||||
|
||||
@@ -19,4 +19,4 @@ title: Начало работы
|
||||
|
||||
## Настройка окружения
|
||||
|
||||
Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/docs/applied/vscode).
|
||||
Открыть проект в VS Code и установить рекомендуемые расширения — редактор предложит это автоматически. Подробнее — [Настройка VS Code](/docs/setup/vscode).
|
||||
|
||||
@@ -20,4 +20,4 @@ title: Стилизация
|
||||
- **Магические значения** — произвольные цвета, отступы и скругления запрещены, использовать токены.
|
||||
- **Глобальные стили** вне `app/styles/` запрещены.
|
||||
|
||||
Правила написания CSS, вложенность, медиа-запросы и токены — [Стили: использование](/docs/applied/styles/usage).
|
||||
Правила написания CSS, вложенность, медиа-запросы и токены — [Стили: использование](/docs/usage/styles).
|
||||
|
||||
Reference in New Issue
Block a user