1 of 172

Разбираемся в�Feature-Sliced Design

Моргунов Александр

HolyJS 2024 Autumn

2 of 172

План

  • Основные концепции
  • Проблемы и их решения

2

3 of 172

Что вас ждем

  • Несколько практических инструментов, которые вы можете использовать у себя
  • Примеры на React/Redux (но в целом они достаточно простые для понимания, даже если вы используете другой стек)
  • Все примеры доступны на гитхабе

3

4 of 172

Об авторе

  • В разработке 9 лет
  • Тимлид в ecom.tech (разрабатываю веб-приложение Самоката)
  • Пишу про фронтенд: t.me/amorgunov

4

5 of 172

Об авторе (что по FSD)

  • Используем на проекте уже более двух лет
  • Автор популярного проекта по методологии
  • Контрибьютил несколько примеров использования

5

6 of 172

6

Кадр с моего выступления про FSD

7 of 172

🍿🍿🍿

7

8 of 172

База про FSD 🐷🍔

9 of 172

База про FSD

  • Архитектурная методология для фронтенд проектов

9

10 of 172

База про FSD

  • Архитектурная методология для фронтенд проектов
  • Framework agnostic

10

11 of 172

База про FSD

  • Архитектурная методология для фронтенд проектов
  • Framework agnostic
  • Русскоязычное комьюнити

11

12 of 172

База про FSD

  • Архитектурная методология для фронтенд проектов
  • Framework agnostic
  • Русскоязычное комьюнити
  • Заложено несколько архитектурных концептов и паттернов + структура проекта

12

13 of 172

Основные понятия

  • Разделение кода на слои (layers)
  • Каждый слой состоит из слайсов (slices) - модулей
  • Каждый слайс состоит из сегментов (segments)

13

14 of 172

Слои (layers)

14

📂 app

📂 pages

📂 widgets

📂 features

📂 entities

📂 shared

📂 processes

15 of 172

Слои (layers)

15

📂 app // Глобальная инициализация

📂 pages

📂 widgets

📂 features

📂 entities

📂 shared

📂 processes

16 of 172

Слои (layers)

16

📂 app

📂 pages // Страницы

📂 widgets

📂 features

📂 entities

📂 shared

📂 processes

17 of 172

Слои (layers)

17

📂 app

📂 pages

📂 widgets // Бизнес модули с UI

📂 features

📂 entities

📂 shared

📂 processes

18 of 172

Слои (layers)

18

📂 app

📂 pages

📂 widgets

📂 features // Пользовательские сценарии

📂 entities

📂 shared

📂 processes

19 of 172

Слои (layers)

19

📂 app

📂 pages

📂 widgets

📂 features

📂 entities // Бизнес сущности

📂 shared

📂 processes

20 of 172

Слои (layers)

20

📂 app

📂 pages

📂 widgets

📂 features

📂 entities

📂 shared // Инфра + переиспользуемое

📂 processes

21 of 172

Слайсы (slices)

21

📂 app

📂 pages

📂 widgets

📂 CartCapsule

📂 ProductPopularList

📂 features

📂 entities

📂 shared

22 of 172

Сегменты (segments) + public API

22

📂 app

📂 pages

📂 widgets

📂 CartCapsule

📂 ProductPopularList

📂 api

📂 model

📂 ui

📄 index.ts

📂 features

📂 entities

📂 shared

23 of 172

23

24 of 172

24

// tsconfig.json

{

"compilerOptions": {

"paths": {

"@/app/*": ["./1_app/*"],

"@/pages/*": ["./2_pages/*"],

"@/widgets/*": ["./3_widgets/*"],

"@/features/*": ["./4_features/*"],

"@/entities/*": ["./5_entities/*"],

"@/shared/*": ["./6_shared/*"],

},

}

25 of 172

База по FSD (саммари)

  • Слои, слайсы и сегменты
  • Слайсы могут использовать только слайсы, которые лежат на слоях ниже
  • Многие архитектурные паттерны лежат в основе методологии

25

26 of 172

Как мы начали�разработку проекта

26

27 of 172

Что мы хотели на старте?

  • Модульность
  • Слоистая архитектура
  • Разделение бизнес логики и�инфраструктуры
  • Feature-based подход

27

28 of 172

Что мы хотели на старте?

  • Выбор был придумывать что-то с нуля или попробовать использовать FSD, в котором все было из коробки

28

29 of 172

29

30 of 172

Сущности

  • Собрали бизнес-�сущности и�построили ERM

30

31 of 172

Entity relationship model (ERM)

31

32 of 172

Сущности

  • Собрали бизнес-�сущности и�построили ERM
  • Отрисовали UI�компоненты

32

33 of 172

Фичи

  • Фича != Фича
  • Для каждой�сущности собрали�пользовательские�сценарии

33

34 of 172

Фичи

  • Предложил переименовать�слой в usecases :)

34

35 of 172

Виджеты

  • Самостоятельные
  • Ориентированные�на бизнес
  • Изолированные

35

36 of 172

Проблемы 🚧🛠️

37 of 172

Проблемы

  1. Кросс-импорты слайсов

37

38 of 172

1 - Кросс импорты

  • Кросс-импорт - это импорт кода из другого слайса в рамках текущего слоя.
  • Методология запрещает такие импорты
  • Но в реальных проектах избавится от кросс-импортов (взаимодействия) слайсов невозможно

38

39 of 172

1 - Кросс импорты

  • Запрет кросс импортов - это один из способов сделать модули независимые друг от друга (low coupling)

39

40 of 172

40

41 of 172

41

// @entities/cart/model/types.ts

import type { ProductId } from '@entities/product'

^^^^^^^^^^^^^^^^^^

export type Cart = {

id: CartId

productIds: ProductId[]

}

42 of 172

42

// @entities/cart/lib/mapCart.ts

import { mapProduct } from '@entities/product'

^^^^^^^^^^^^^^^^^^

export function mapCart(dto: CartDto): Cart {

return {

// ...

products: dto.products.map((p) => mapProduct(p)),

}

}

43 of 172

43

44 of 172

1 - Кросс импорты

  • Разрешить прямые кросс импорты 👎

44

45 of 172

1 - Кросс импорты

  • Разрешить прямые кросс импорты 👎
  • Выносить общее между двумя слайсами на верхлежащие слои 👍👎

45

46 of 172

1 - Кросс импорты

  • Разрешить прямые кросс импорты 👎
  • Выносить общее между двумя слайсами на верхлежащие слои (вернемся дальше) 👍👎
  • Использовать паттерн инверсии зависимостей (DIС или composition root)

46

47 of 172

47

48 of 172

48

// @app/rootStore.ts�

@Singleton()

export class RootStore {

constructor() {

this.cart = new CartStore({

getProductById: (productId) => {

return this.product.getProductById(productId)

},

// ...

})

// ...

}

}

49 of 172

49

// @app/App.ts

function App() {

return (

<DepsProvider store={{productService, cartService}}>

{children}

</DepsProvider>

)

}

// @entities/cart/ui/CartItemList.ts

function CartItemList({productIds}) {

const {productService} = useDepsContext()

productService.getByIds(productIds)

}

50 of 172

1 - Кросс импорты

  • Разрешить прямые кросс импорты 👎
  • Выносить общее между двумя слайсами на верхлежащие слои (вернемся дальше) 👍👎
  • Использовать паттерн инверсии зависимостей (DIС или composition root) 👍
  • Для вьюх использовать паттерн render slots

50

51 of 172

51

52 of 172

52

53 of 172

53

54 of 172

54

// @entities/product/ui/ProductCard

type Props = { actionSlot: ReactNode }

// Глупый UI компонент карточки товара

export function ProductCard(props: Props) {

return (

<div>

// Вся магия происходит тут

{props.actionSlot}

</div>

)

}

55 of 172

55

// @widgets/AwesomeProductSlider

{products.map((product) => (

<ProductCard

product={product}

actionSlot={

// Прокидываем снаружи через рендер проп

<AddProductToCart productId={product.id} />

}

/>

))}

56 of 172

56

const render = (

<AwesomeProductList

renderListSlot={() => {

<ProductList

itemSlot={() => {

<ProductCart

actionSlot={

isAuth

? isAdded

? <RemoveProductFromCart />

: <AddProductToCart />

: null

}

/>

}}

/>

}}

/>

)

57 of 172

57

58 of 172

1 - Кросс импорты

  • Разрешить прямые кросс импорты 👎
  • Выносить общее между двумя слайсами на верхлежащие слои (вернемся дальше) 👍👎
  • Использовать паттерн инверсии зависимостей (DIС или service locator) 👍
  • Для вьюх использовать паттерн render slot 👍
  • Явное указание прямых зависимостей между слайсами (@x)

58

59 of 172

@x

  • Экспериментальная фича, которая позволяет описывать публичный API для каждого слайса, требующего кросс-импорт
  • Работает только на слое сущностей

59

60 of 172

@x

60

📂 entities

📂 cart

📄 ui/AddToCart.tsx // Хотим использовать в product

📄 index.ts

📂 product

📄 lib/mapProduct.ts // Хотим использовать в cart

📄 model/types.ts // Тоже

📄 index.ts

61 of 172

@x

61

📂 entities

📂 cart

📄 @x/product/index.ts Публичный API для слайса product

📄 ui/AddToCart.tsx

📄 index.ts

📂 product

📄 @x/cart/index.ts Публичный API для слайса cart

📄 lib/mapProduct.ts

📄 model/types.ts

📄 index.ts

62 of 172

@x

62

📂 entities

📂 cart

📄 @x/product/index.ts Публичный API для слайса product

📄 @x/user/index.ts Публичный API для слайса user

📄 @x/order/index.ts Публичный API для слайса order

📄 ui/AddToCart.tsx

📄 index.ts

📂 product

📄 @x/cart/index.ts Публичный API для слайса cart

📄 lib/mapProduct.ts

📄 model/types.ts

📄 index.ts

63 of 172

@x

63

// @entities/product/@x/cart.ts

export type { ProductDto } from '../api/types'

export type { Product, ProductId } from '../model/types'

export { mapProduct } from '../lib/mapProduct'

64 of 172

@x

64

// @entities/cart/model/types.ts:diff

- import type { ProductId } from '@entities/product'

+ import type { ProductId } from '@entities/product/@x/cart'

export type Cart = {

id: CartId

productIds: ProductId[]

}

65 of 172

@x

  • Экспериментальная фича, которая позволяет описывать публичный API для каждого слайса, требующего кросс-импорт
  • Работает только на уровне сущностей
  • Фича, которая легализует кросс-импорты

65

66 of 172

Нет кросс импортов -

нет проблем?

66

67 of 172

Пару слов про Redux

  • Стейт менеджер, который является моностором и все хранит в одном месте
  • Из любого санка (где обычно описывается бизнес логика) можно получить доступ к любой части стора

67

68 of 172

68

// @entities/product/model/actions.ts

const someAwesomeProductAction = createAsyncThunk(

'someAwesomeProductAction',

async (_, { getState }) => {

const state = getState() as AppState

const сartItems = state.cart.items

^^^^^^^^^^^^^^^^

}

)

69 of 172

А можно ли Redux использовать с FSD?

  • Можно, но нужно использовать модульный подход через заведение отдельных redux-слайсов и запрет на прямое чтение не своего стейта

69

70 of 172

70

// @shared/lib/store/types.ts

export type AppState = any

export type AppDispatch = () => any

71 of 172

71

// @entities/cart/model/slice.ts

export const cartSlice = createSlice({ /* … */ })

// Явно экспортируем селекторы

export const selectCartItems = /* … */

rootReducer.inject(cartSlice)

72 of 172

72

// @entities/product/model/actions.ts

import { selectCartItems } from '@entities/cart'

const someAwesomeProductAction = createAsyncThunk(

'someAwesomeProductAction',

async (_, { getState }) => {

const state = getState() as AppState� // И используем чужие экшены и части стейта� // через явную передачу их в селекторы

const cartItems = selectCartItems(state)

}

)

73 of 172

73

// @entities/product/model/actions.ts

import { selectCartItems } from '@entities/cart/@x/product'

const someAwesomeProductAction = createAsyncThunk(

'someAwesomeProductAction',

async (_, { getState }) => {

const state = getState() as AppState� // И используем чужие экшены и части стейта� // через явную передачу их в селекторы

const cartItems = selectCartItems(state)

}

)

74 of 172

1 - Кросс импорты (саммари)

  • Запрещаем, чтобы сделать модули более независимыми
  • Вводим абстракции путем использования DI и render slot паттернов, если модули явно не связаны друг с другом
  • Используем явную связь через свой публичный API (@x)

74

75 of 172

Проблемы

  • Кросс-импорты модулей
  • Размазывание кода по слоям

75

76 of 172

2 - Размазывание кода по слоям

  • Пытаемся избежать кросс-импортов (выносим общие компоненты на более верхнеуровневые слои)
  • Следуем рекомендациям FSD что все должно быть раскидано по слоям 😎😎😎

76

77 of 172

77

78 of 172

78

79 of 172

79

📂 entities

📂 cart

📄 ui/CartItem.tsx

📄 ui/CartPrice.tsx

📂 features

📂 cart

📄 AddToCart/ui/AddToCartButton.tsx

📄 RemoveFromCart/ui/RemoveFromCartButton.tsx

📄 GoToCheckout/ui/GoToCheckoutButton.tsx

📂 widgets

📂 MiniCart

📄 ui/MiniCart.tsx

80 of 172

2 - Размазывание кода по слоям

  • Сильное размазывание кода одного модуля по проекту приводит к destructive decoupling

80

81 of 172

2 - Размазывание кода по слоям

  • Сильное размазывание кода одного модуля по проекту приводит к destructive decoupling
  • Для решения использовать local-first паттерн - все держим рядом, пока это не понадобится где-то еще

81

82 of 172

2 - Размазывание кода по слоям

  • Сильное размазывание кода одного модуля по проекту приводит к destructive decoupling
  • Для решения использовать local-first паттерн - все держим рядом, пока это не понадобится где-то еще
  • Для сущностей использовать rich domain model

82

83 of 172

83

📂 entities

📂 cart

📂 api

📂 model

📂 ui

📄 CartItem.tsx

📄 CartPrice.tsx

📄 AddToCartButton.tsx

📄 RemoveFromCartButton.tsx

📄 ToCheckoutButton.tsx

📄 MiniCart.tsx

84 of 172

84

📂 entities

📂 cart

📂 api

📂 model

📄 CartItemsList.ts (entity like)

📄 AddItem.ts (feature like)

📄 RemoveItem.ts (feature like)

📂 ui

📄 CartItemsList.tsx

📄 AddToCartButton.tsx

📄 RemoveFromCartButton.tsx

....

85 of 172

2 - Размазывание кода по слоям

  • Сильное размазывание кода одного модуля по проекту приводит к destructive decoupling
  • Для решения использовать local-first паттерн - все держим рядом, пока это не понадобится где-то еще
  • Для сущностей использовать rich domain model
  • Для остальных слоев такое же правило

85

86 of 172

86

87 of 172

87

88 of 172

88

89 of 172

2 - Размазывание кода по слоям (саммари)

  • Использовать local-first паттерн

89

90 of 172

2 - Размазывание кода по слоям (саммари)

  • Использовать local-first паттерн
  • Для сущностей придерживаться rich domain model (базовые действия и пользовательские сценарии можно класть прямо в сущность)

90

91 of 172

2 - Размазывание кода по слоям (саммари)

  • Использовать local-first паттерн
  • Для сущностей придерживаться rich domain model (базовые действия и пользовательские сценарии можно класть прямо в сущность)
  • Для остальных слоев тоже самое

91

92 of 172

Закрепим

92

93 of 172

93

94 of 172

94

95 of 172

95

📂 entities/

📂 advert/

📂 api/

📄 advertApi.ts

📄 types.ts

📂 model/

📄 advertStore.ts

📄 types.ts

📂 ui/

📄 AdvertLabel.ts

📄 AdvertModal.ts

📂 story/

96 of 172

А это точно сущность?

96

97 of 172

97

📂 entities/

📂 story/

📂 api/

📄 advertApi.ts

📄 types.ts

📂 model/

📄 advertStore.ts

📄 types.ts

📂 ui/

📄 AdvertLabel.ts

📄 AdvertModal.ts

98 of 172

А что такое сущность?

98

99 of 172

Проблемы

  • Кросс-импорты модулей
  • Размазывание кода по слоям
  • Что на какой слой раскладывать?

99

100 of 172

3 - Что на какой слой раскладывать?

  • Проблема в субъективном понимании, что является сущностью, фичей или виджетом

100

101 of 172

3 - Что на какой слой раскладывать?

  • Проблема в субъективном понимании, что является сущностью, фичей или виджетом
  • Есть два типа людей проектов - толстый и тонкий клиент

101

102 of 172

Как отличить тонкий клиент?

  • Почти вся бизнес-логика выполняется на бекенде

102

103 of 172

Как отличить тонкий клиент?

  • Почти вся бизнес-логика выполняется на бекенде
  • В качестве стейт-менеджера ваc полностью устраивает react-query + zustand (или вообще не использовать стейт-менеджер)

103

104 of 172

Как отличить тонкий клиент?

  • Почти вся бизнес-логика выполняется на бекенде
  • В качестве стейт-менеджера ваc полностью устраивает react-query + zustand (или вообще не использовать стейт-менеджер)
  • Большинство веб-приложением представляют собой тонкий клиент

104

105 of 172

Весь FSD который мы рассматривали - для толстых клиентов

105

106 of 172

106

107 of 172

107

108 of 172

Page-sliced�методология 😮👋

108

109 of 172

«Page sliced» методология

  • Исключаем слои entities и features

109

110 of 172

«Page sliced» методология

  • Исключаем слои entities и features
  • Сначала все модули и весь код собираем на уровне слоя «pages» (страниц)

110

111 of 172

«Page sliced» методология

111

📂 pages

📂 products

📂 model

📄 productsStore.ts // entity like

📄 addProductToCart.ts // feature like

📂 ui

📄 ProductCard.tsx // entity like

📄 AwesomeProductSlider.tsx // widget like

112 of 172

«Page sliced» методология

  • Исключаем слои entities и features
  • Сначала все модули и весь код собираем на уровне слоя «pages» (страниц)
  • И только если что-то переиспользуется на разных страницах, выносим на более низкие слои (виджеты или shared)

112

113 of 172

«Page sliced» - первые впечатления?

  • Вместо модульной архитектуры все собирается в «мега» модули в рамках страницы 👎

113

114 of 172

«Page sliced» - первые впечатления?

  • Вместо модульной архитектуры все собирается в «мега» модули в рамках страницы 👎
  • Нет возможности использовать «тонкие» страницы для композиции виджетов 👎

114

115 of 172

«Page sliced» - первые впечатления?

  • Вместо модульной архитектуры все собирается в «мега» модули в рамках страницы 👎
  • Нет возможности использовать «тонкие» страницы для композиции виджетов 👎
  • Сложнее интегрироваться вместе с фреймворками, которые задают свою структуру для страниц 👎

115

116 of 172

«Page sliced» - НО

  • Я рассматривал это все с точки зрения толстого клиента, где логично выделять домен (бизнес сущности) и использовать фичи

116

117 of 172

«Page sliced» - НО

  • Я рассматривал это все с точки зрения толстого клиента, где логично выделять домен (бизнес сущности) и использовать фичи
  • Но с точки зрения тонкого клиента - это отличная идея, позволяющая использовать local-first подход и избежать преждевременной декомпозиции

117

118 of 172

Упрощенная версия

  • Исключить из проекта entities/features слои, использовать «page sliced» подход по умолчанию + виджеты в качестве модулей.

118

119 of 172

Упрощенная версия

119

📂 app📂 shared

📂 widgets -> modules

📂 pages

📂 home

📂 api

📂 model

📂 ui

📄 index.ts

120 of 172

3 - Что на какой слой (саммари)

  • Определите ваш клиент (тонкий или толстый)
  • Для тонкого клиента начинайте с упрощенной версии методологии (без сущностей/фичей и с page-sliced подхода)
  • Для толстого - проектируйте от сущностей

120

121 of 172

Проблемы

  • Кросс-импорты модулей
  • Размазывание кода по слоям
  • Что на какой слой раскладывать?
  • Работа с инфраструктурой

121

122 of 172

122

123 of 172

4 - Инфраструктура

  • Инфраструктурные сущности предоставляют техническую основу, которая поддерживает и обеспечивает выполнение бизнес логики

123

124 of 172

4 - Инфраструктура

  • Инфраструктурные сущности предоставляют техническую основу, которая поддерживает и обеспечивает выполнение бизнес логики
  • Например, система аналитики, нотификации, менеджер модалок или тостов, i18n, шина событий, RBAC, API провайдеры и хранилища, тема приложения* и т.д.

124

125 of 172

Где размещать?

  • В FSD нет явного слоя для таких сервисов, поэтому все их выносят на слой shared (или держат рядом со слайсом, где сервис используется)

125

126 of 172

Где размещать?

126

📂 shared

📂 api

📂 model

📄 themeStore.ts

📂 lib

📄 changeTheme.ts� 📄 themeProvider.ts

📂 ui

📄 ThemeToggler.tsx

127 of 172

Где размещать?

127

📂 shared

📂 ????

📂 theme

📂 model

📄 themeStore.ts

📄 changeTheme.ts� 📄 themeProvider.ts

📂 ui

📄 ThemeToggler.tsx

128 of 172

Где размещать?

  • В FSD нет явного слоя для таких сервисов, поэтому обычно их выносят на слой shared
  • Мы выделили кастомный подслой shared/services, где и размещаем такие модули

128

129 of 172

shared/services

129

📂 shared

📂 services

📂 accidentMode

📂 analyticsBus

📂 featureConfig

📂 featureFlags

📂 geoService

📂 ...

📂 toastManager

130 of 172

Проблемы

  • Кросс-импорты модулей
  • Размазывание кода по слоям
  • Что на какой слой раскладывать?
  • Работа с инфраструктурой
  • Расширение методологии

130

131 of 172

5 - Расширение методологии

  • Не хватает дополнительных слоев из-за длинных цепочек зависимостей между модулями или из-за специфики проекта

131

132 of 172

Дополнительные слои?

  • page-modals
  • page-layouts
  • abstract-widgets
  • shared-services / infrastructure-entities
  • feature-aggregates
  • ...

132

133 of 172

Абстрактные виджеты

133

📂 widgets

📂 _AbstractProductSlider

📂 api

📂 lib

📂 ui

📂 ProductsSliderAmazingPrices

📂 ProductsSliderRecentlyPurchased

📂 ProductsSliderRecommended

📂 ProductsSliderUpsell

134 of 172

Виджеты внутри страниц

134

📂 pages

📂 home

📂 widgets

📂 AwesomeProductSlider

📂 SearchContainer

📂 ...

📂 ShowcaseUserBanners

📄 index.ts

135 of 172

Фабрики для сущностей

135

📂 entities

📂 _AbstractEntity1Factory

📂 _AbstractEntity2Factory

📂 api

📂 lib

📂 model

📂 ui

📂 Product

📂 Category

📂 User

136 of 172

Слой для дополнительной навигации

136

📂 page-modals

📂 cart

📂 checkout

📂 product-[id]

📂 profile

📂 profile-settings

137 of 172

shared/services

137

📂 shared

📂 services

📂 accidentMode

📂 analyticsBus

📂 featureConfig

📂 featureFlags

📂 geoService

📂 ...

📂 toastManager

138 of 172

5 - Расширение методологии

  • Не хватает дополнительных слоев из-за длинных цепочек зависимостей между модулями
  • FSD на верхнем уровне делит код не вертикально (на домены), а горизонтально на технические слои

138

139 of 172

Кастомная модификация с доменами

139

📂 app

📂 shared

📂 domains

📂 cart

📂 entities

📂 features

📂 widgets

📂 page

📂 product

📂 reports

140 of 172

Монорепозиторий

140

📂 src

📂 packages

📂 lib-shared

📂 domain-cart // внутри уже деление по FSD

📂 domain-product

📂 domain-reports

141 of 172

5 - Расширение методологии (саммари)

  • Использовать FSD как точку старта
  • Не стоит бояться расширять методологию дополнительными кастомными слоями, если они необходимы

141

142 of 172

Проблемы

  • Кросс-импорты модулей
  • Размазывание кода по слоям
  • Что на какой слой раскладывать?
  • Работа с инфраструктурой
  • Расширение методологии

142

143 of 172

Какая проблема�главная?

143

144 of 172

144

145 of 172

Главная проблема

  • Отсутствие примеров и туториалов, очень верхнеуровневая документация

145

146 of 172

roke-to/roketo-business-ui

146

// apps/near-dapp/src/shared/hooks/useToken.ts

import {useStore} from 'effector-react';

import {$listedTokens} from '~/entities/wallet';

import {env} from '~/shared/config/env';

export function useToken(tokenAccountId: string) {

const tokens = useStore($listedTokens);

// ...

}

147 of 172

roke-to/roketo-business-ui

147

// apps/near-dapp/src/entities/employee/model/employee-model.ts

import {format, parseISO} from 'date-fns';

import {createForm} from 'effector-forms';

import {t} from 'i18next';

import {isAccountExistFx} from '~/entities/account-exist-effect';

import {$currentDaoId} from '~/entities/wallet';

148 of 172

amorgunov/nukeapp

148

📂 src

📂 features

📂 cart

📂 add-to-cart

📂 product

📂 sort-by

📂 session

📂 login

📂 logout

📂 theme

📂 change-theme

149 of 172

Документация

  • Появляются новые туториалы на сайте (как работать с лайаутами, авторизацией, типами, миграция на fsd)

149

150 of 172

Документация

  • Появляются новые туториалы на сайте (как работать с лайаутами, авторизацией, типами, миграция на fsd)
  • Монорепа с различными примерами и�кейсами использования https://github.com/noveogroup-amorgunov/�fsd-community

150

151 of 172

Документация

  • Появляются новые туториалы на сайте (как работать с лайаутами, авторизацией, типами, миграция на fsd)
  • Монорепа с различными примерами и�кейсами использования https://github.com/noveogroup-amorgunov/�fsd-community
  • Планируется выпуск версии 2.1

151

152 of 172

Уже 🥳🥳

152

153 of 172

steiger@0.5.0

  • Новый линтер архитектуры FSD, который умеет работать с новыми фичами: @x публичным API и преждевременной декомпозицией (и еще +18 других правил)

153

154 of 172

Итого

155 of 172

Итого

  • FSD построен на основе архитектурных паттернов
  • Полной независимости модулей не бывает
  • Преждевременная декомпозиция очень вредна
  • Два подхода к разработке
  • Каноничный FSD не работает
  • Главная проблема - документация

155

156 of 172

Итого (что запомнить)

  • Кросс-импорты решать через @x и инверсию зависимостей (рендер слоты)
  • local-first стратегия
  • page-sliced методология для тонких клиентов

156

157 of 172

Ссылки

157

158 of 172

158

159 of 172

Допы для самостоятельного�изучения

159

160 of 172

1 - как развязать shared

и бизнес-логику

160

161 of 172

Взаимодействие с другими слоями

  • Как получить доступ из слоя shared к верхнеуровневым слоям?

161

162 of 172

162

// @shared/api/baseQueryWithReauth.ts

import { logout } from '@entities/session'

export async function baseQueryWithReauth(/* ... */) {

const result = await baseQuery(/* ... */)

if (isApiTokenBroken(/* ... */)) {

api.dispatch(logout())

}

return result

}

163 of 172

Взаимодействие с другими слоями

  • Как развязать модули между собой (например, находятся на разных слоях)?
  • Использовать событийную модель pub-sub (например, через шину событий)

163

164 of 172

164

// @shared/api/baseQueryWithReauth.ts

import { apiAccessTokenIsBrokenEvent } from './events'

export async function baseQueryWithReauth(/* ... */) {

const result = await baseQuery(/* ... */)

if (isApiTokenBroken(/* ... */)) {

api.dispatch(apiAccessTokenIsBrokenEvent())

}

return result

}

165 of 172

165

// @entities/session/model/logout.ts

import { apiAccessTokenIsBrokenEvent } from '@shared/api'

export const logoutMiddleware = createListenerMiddleware()

logoutMiddleware.startListening({

actionCreator: apiAccessTokenIsBrokenEvent,

effect: async (_, api) => {

api.dispatch(logoutThunk())

},

})

166 of 172

2 - пример как обходят кросс-импорты

166

167 of 172

Вынести типы в shared

167

// @shared/model/types.ts

import type { ProductId, Product } from '@entities/product'

import type { CartId, Cart } from '@entities/cart'

export {

ProductId,

Product,

CartId,

Cart,

// ...

}

168 of 172

Задекларировать типы в shared

168

// @shared/model/app.d.ts

declare global {

declare type ProductId = import('@entities/product').ProductId

declare type Product = import('@entities/product').Product

declare type CartId = import('@entities/cart').CartId

declare type Cart = import('@entities/cart').Cart

}

export {}

169 of 172

Мапперы и хелперы тоже в shared

169

// @shared/api/mapProductDto.ts

import type { Product, ProductId } from '@shared/model/types'

import type { ProductDto } from './types.ts'

export function mapProductDto(dto: ProductDto): Product {

return {

id: dto.id as ProductId,

// ...

}

}

170 of 172

3 - дебаг меню для виджетов

170

171 of 172

171

172 of 172

172

// src/shared/ui/global.css

body.debug [data-fsd] {

// рамка для слайса

outline: 2px solid var(--fsd-color);

// плашка с названием слайса

&::after {

content: attr(data-fsd);

pointer-events: none;

position: absolute;

background: var(--fsd-color);

}

}