Auth
Во всех приложениях так или иначе есть бизнес-логика, завязанная на текущем авторизованном пользователе.
Обычно такая сущность называется
Viewer
/Principle
/Session
- но в рамках статьи, остановимся именно наviewer
, но все зависит от вашего проекта и команды
Также, это один из показательных примеров, когда бизнес-сущность порождает за собой бизнес-фичи, затем страницы, и даже бизнес-процессы
Рассмотрим их подробнее ниже с примерами
note
Названия директорий внутри сегментов (ui, model) могут отличаться от проекта к проекту
Методология пока никак не регламентирует этот уровень вложенности
Стоит также понимать, что приведенные ниже примеры - абстрактны и синтетичны, и сформированы для демонстрации только концепций методологии
FeatureSliced не регламентирует в себе бест-практисы конкретного дата-фетчера или стейт-менеджера
Entities
Бизнес-сущность пользователя
- Представляет собой наиболее атомарную абстракцию для проектирования
- Здесь формируется контекст авторизации, на который потом обычно полагаются все вышележащие слои приложения
info
Стоит понимать, что нередко в приложении есть публичный "внешний" пользователь (user), а есть авторизованный "внутренний" пользователь (viewer)
Не забывайте учитывать эту разницу при проектировании архитектуры и логики
Примеры
# Сегменты могут быть как файлами, так и директориями
|
├── entities/viewer # Layer: Бизнес-сущности
| | # Slice: Текущий пользователь
| ├── ui/ # Segment: UI-логика (компоненты)
| ├── lib/ # Segment: Инфраструктурная-логика (helpers/utils)
| ├── model/ # Segment: Бизнес-логика
| └── index.ts # [Декларация Public API]
| ...
entities/viewer
- сущность текущего пользователя (Session / Principle)entities/user
- сущность публичного пользователя (не обязательно связанная с текущим)- В зависимости от сложности приложения - можно использовать и
user
для нейминга текущего пользователя - Но это может вызвать серьезные проблемы, когда/если придется разделять логику обычного пользователя и текущего, который зашел в систему
- В зависимости от сложности приложения - можно использовать и
index.ts
Обычный Public API модуля
Во многом повторяет декларацию API и для описанных ниже слоев
export { ViewerCard } from "./card";
export { ViewerAvatar } from "./avatar";
...
- Redux
- Effector
В редаксе общепринят подход redux-ducks, когда его юниты (selectors/actions/...) лежат рядом и явно выделены
Но явное выделение далеко не всегда обязательно
export * as selectors from "./selectors";
export * as events from "./events";
export * as stores from "./stores";
...
Модель эффектора чаще всего будет состоять из одного файла - т.к. там принято все юниты хранить рядом
Если же в модели юниты можно семантично разделить на несколько сабмоделей, то можно явно это обозначить в Public API
export * as submodel1 from "./submodel1"
export * as submodel2 from "./submodel2"
...
export * from "./ui"
export * as viewerModel from "./model";
ui
Здесь могут содержаться компоненты, относящиеся не к конкретной странице/фиче, а напрямую к сущности пользователя
import { Card } from "shared/ui/card";
// Считается хорошей практикой - не связывать напрямую с моделью ui-компоненты из entitites
// Чтобы можно было использовать не только для текущей модели,
// Но и для поступивших извне пропсов
export type UserCardProps = {
data: User;
className?: string;
// И прочие card-пропсы
};
export const UserCard = ({ data, ... }: UserCardProps) => {
return (
<Card
title={data.fullName}
description={data.bio}
...
/>
)
}
model
На этом уровне обычно создается сущность текущего пользователя, с реэкспортом хуков/контрактов/селекторов для использования вышележащими слоями
- Redux
- Effector
// entities/viewer/model/selectors.ts
export const useViewer = () => {
return useSelector((store) => store.entities.userSlice);
}
export const useAuth = () => {
const viewer = useViewer();
return !!viewer
}
// entities/viewer/model/store.ts
export const userSlice = createSlice(...)
// entities/viewer/model/index.ts
export const $viewer = createStore(...);
export const $isAuth = $viewer.map((viewer) => !!viewer);
// **/**/ui.tsx
const viewer = useStore($viewer);
Также тут может быть реализована и другая логика
updateUserDetails
logoutUser
- ...
Features
Фичи, завязанные на текущем пользователе
- Использует в реализации бизнес-сущности (зачастую -
entities/viewer
) и shared ресурсы - Фичи могут не быть напрямую связаны с вьювером, но при этом могут использовать его контекст при реализации логики
Примеры
# Сегменты могут быть как файлами, так и директориями
|
├── features/auth # Layer: Бизнес-фичи
| | # Slice Group: Структурная группа "Авторизация пользователя"
| ├── by-phone/ # Slice: Фича "Авторизация по телефону"
| | ├── ui/ # Segment: UI-логика (компоненты)
| | ├── lib/ # Segment: Инфраструктурная-логика (helpers/utils)
| | ├── model/ # Segment: Бизнес-логика
| | └── index.ts # [Декларация Public API]
| |
| ├── by-oauth/ # Slice: Фича "Авторизация по внешнему ресурсу"
| ...
features/auth/{by-phone, by-oauth, logout ...}
- структурная группа фич авторизации (по телефону, по внешнему ресурсу, выход из системы, ...)features/wallet/{add-funds, ...}
- структурная группа фич по работе со внутренним счетом пользователя (пополнение счета, ...)
ui
- Авторизация по внешнему ресурсу
import { viewerModel } from "entities/viewer";
export const AuthByOAuth = () => {
return (
<OAuth
domain={...}
scope={...}
...
// для redux - дополнительно нужен dispatch
onSuccess=((user) => viewerModel.setUser(user))
/>
)
}
- Использование контекста пользователя в фичах
import { viewerModel } from "entities/viewer";
export const Wallet = () => {
const viewer = viewerModel.useViewer();
const { moneyCount } = viewer;
...
}
- Использование компонентов вьювера
import { ViewerAvatar } from "entities/viewer";
...
export const Header = () => {
...
return (
<Layout.Header>
...
<ViewerAvatar
onClick={...}
onLogout={...}
...
/>
</Layout.Header>
)
}
Pages
Страницы, так или иначе связанные с текущим пользователем
- Могут как напрямую затрагивать функциональность вьювера
- Так и использовать его косвенно (в том числе - и его контекст / фичи)
Примеры
# Сегменты могут быть как файлами, так и директориями
|
├── pages/viewer # Layer: Страницы приложения
| | # Slice Group: Структурная группа "Текущий пользователь"
| ├── profile/ # Slice: Страница профиля вьювера
| | ├── ui.tsx # Segment: UI-логика (компоненты)
| | ├── lib.ts # Segment: Инфраструктурная-логика (helpers/utils)
| | ├── model.ts # Segment: Бизнес-логика
| | └── index.ts # [Декларация Public API]
| |
| ├── settings/ # Slice: Страница настроек аккаунта вьювера
| ...
pages/viewer/profile
- страница ЛК пользователяpages/viewer/settings
- страница настроек аккаунта пользователяpages/user
- страница пользователя (не обязательно текущего)pages/auth/{sign-in, sign-up, reset}
- структурная группа страниц авторизации (вход в систему / регистрация / восстановление пароля)
ui
- Использование компонентов вьювера и viewer-based фич на страницах
import { Wallet } from "features/wallet";
import { ViewerCard } from "entities/viewer";
...
export const UserPage = () => {
...
return (
<Layout>
<Header
extra={<Wallet.AddFunds />}
/>
...
<ViewerCard />
</Layout>
)
}
- Использование модели вьювера
import { viewerModel } from "entities/viewer";
...
export const SomePage = () => {
...
return (
<Layout>
...
<Settings onSave={(payload) => viewerModel.saveChanges(payload)} />
</Layout>
)
}
Processes
Бизнес-процессы, затрагивающие текущего пользователя
- Затрагивает юзкейсы, пронизывающие страницы системы
- Слой процессов - опционален, и обычно используется только когда логика разрастается в страницах и нужно отдельное управление контекстом на сразу нескольких страницах
Примеры
# Сегменты могут быть как файлами, так и директориями
|
├── processes # Layer: Бизнес процессы
| ├── auth/ # Slice: Процесс авторизации пользователя
| | ├── lib.ts # Segment: Инфраструктурная-логика (helpers/utils)
| | ├── model.ts # Segment: Бизнес-логика
| | └── index.ts # [Декларация Public API]
| |
| ├── quick-tour/ # Slice: Процесс онбординга нового пользователя
| ...
processes/auth
- бизнес-процесс авторизации пользователяprocesses/quick-tour
- бизнес-процесс для ознакомления пользователя с системой (~ UserOnboard)
App
Инициализация данных по учетной записи пользователя
- Как правило, здесь проходит проверка на то, был ли уже авторизован пользователь до того, как зашел в сервис
- Если да - провайдер пополняет контекст пользователя, для дальнейшего использования в системе
- Если нет - запускается процесс авторизации или меняется контекст вьювера, чтобы страница авторизации предприняла нужные действия
Примеры
# Структура `app` уникальна для каждого проекта и не регламентируется методологией
|
├── app/providers # Layer: Инициализация приложения (HOCs провайдеры)
| ├── withAuth.tsx # HOC: Инициализация контекста авторизации
| | ... #
| ...
app/providers/withAuth
- HOC для авторизации пользователя- Используется только на верхнем уровне, как провайдер с инициализацией логики, к которому имеет доступ только
app
-слой - Не путать с хуком
useViewer
, к которому идет обращение всех остальных слоев (processes / pages / features)
- Используется только на верхнем уровне, как провайдер с инициализацией логики, к которому имеет доступ только
Выводы
Как мы видим на примерах выше - вся бизнес-логика начинает строится от одной сущности, и может распространится до самого верхнего слоя.
Поэтому нужно уметь грамотно выделять скоуп влияния модуля, и в зависимости от этого выбирать подходящий слой для расположения логики.
Таким образом - мы получим наибоолее поддерживаемый, читаемый и переиспользуемый код.