Головна / Статті

Дизайн з функціональними шарами для React: шари, правила імпорту та коли припинити

Дізнайтеся, як шари підходу Feature-Sliced Design, правило однонапрямкового імпорту, легкі публічні API та @x крос-імпорти допомагають розплутати кодові бази React, та коли варто використовувати лише частину цього підходу.

Видавець: REACTAPP.TOP7 хв читання1715 слів

Джерела: Feature-Sliced Design in React: How to Structure a Scalable Codebase (and When It’s Overkill)

Майже кожен проект на React, який протримується понад два роки, має папку utils/, яку ніхто не хоче відкривати. Усередині знаходяться форматувальник дат, обгортка для повторних спроб, напівмашина станів та файл helpers.ts, який імпортує дані з трьох функціональних папок, тоді як дві з цих папки імпортують його назад, тож ніхто не може сказати, що залежить від чого. Методологія Filament-Sliced Design (FSD) спрямована саме на вирішення цієї проблеми. Прочитавши цей текст, ви дізнаєтесь про її шість рівнів, єдине правило імпорту, яке виконує більшу частину роботи, як проектувати межі фрагментів для безпечного рефакторингу, а також, що найважливіше, як вирішити, чи варта повна реалізація FSD для вашого додатку, чи кращим вибором буде простіший її підмножина.

Чому папки, засновані на типах, заважають масштабуванню

Класичний формат групує файли за їхньою природою (компоненти, хуки, утиліти), а не за призначенням. Це підходить для невеликого додатку, але коли з’являються справжні бізнес-домени, все починає виглядати ось так:

src/
 components/ # 200 files, no domain grouping
 hooks/ # useAuth next to useCartTotals
 utils/ # the grab-bag
 services/ # api calls that import from utils, which imports back

У такій структурі занепадають дві речі. По-перше, на запитання „До кого належить цей файл?“ немає чіткої відповіді, тому все неоднозначне потрапляє до папки utils/. По-друге, ніщо не обмежує напрямок залежностей, що дозволяє утворюватися циклам. Уявіть собі, що сервіс з обробки замовлень використовує utils/user.ts, цей утилітний модуль залежить від services/auth.ts, а сервіс автентифікації знову імпортує саме цей сервіс з обробки замовлень. Такі цикли працюють безперешкодно. Зазвичай вони проявляються значно пізніше у вигляді значення undefined, коли модуль вперше завантажується, оскільки один із файлів у циклі було оброблено раніше, ніж завершилась обробка файлу, від якого він залежить, і до того часу структура залежностей вже настільки заплутана, що її складно зрозуміти.

FSD вирішує обидві проблеми, надаючи всьому проекту визначену структуру замість того, щоб покладатися на звички кожного розробника. На момент написання цього тексту версія специфікації — v2.1; перевіряйте офіційну документацію для отримання інформації про подальші зміни.

Шість шарів та що міститься в кожному

FSD організовує код у шари з фіксованим порядком, зверху вниз: App, Pages, Widgets, Features, Entities, Shared. Також існує старіший шар Processes, але він застарів і може бути проігнорований. Цей порядок не є суто декоративним; він визначає, хто може залежати від кого.

  • App: маршрутизація, точки входу, глобальні стилі та постачальники.
  • Pages: повні сторінки або великі секції інтерфейсу, пов’язані з певним маршрутом.
  • Widgets: великі, самодостатні блоки інтерфейсу, які самостійно виконують певну функцію.
  • Функції: реалізації функціональності продукту, які використовуються у кількох місцях.
  • Ентитети: бізнес-концепції, з якими працює проект, такі як користувач або продукт.
  • Спільний код: код, який можна використовувати знову без необхідності знань бізнес-логіки, включаючи систему дизайну та універсальні допоміжні функції.
  • Чотири середні шари (від Pages до Entities) поділяються на сегменти — по одному на кожну сферу бізнесу, наприклад entities/user або features/auth. Шари App та Shared не мають сегментів. Кожен сегмент потім ділиться на частини залежно від технічної ролі: ui — для компонентів, api — для запитів та типів відповідей сервера, model — для схем та баз даних, lib — для допоміжних функцій, які використовуються лише всередині цього сегмента, та config.

    Правило імпорту, яке унеможливлює цикли

    Правило, яке лежить в основі всієї системи, є простим: модуль може імпортувати лише з шарів, які знаходяться строго нижче його власного. Сторінка може використовувати певну функцію, але функція ніколи не може використовувати сторінку. Крім того, елементи на одному шарі зовсім не можуть імпортувати один одного. У межах одного елемента файли можуть вільно посилатися один на одного; обмеження діє також між межами елементів та шарів.

    Застосуймо це правило до циклу, про який йшлося раніше. Після перерозподілу елементів логіка порядку знаходиться у entities/order, логіка користувача — у entities/user, а процес автентифікації — у features/auth. Дозволені залежності стають наступними:

    features/auth → can import entities, shared
    entities/order → can import shared only
    entities/user → can import shared only
    

    Якщо тепер entities/order намагається імпортувати дані з features/auth, це є структурним порушенням архітектури, а не питанням стилю. Створити цикл неможливо, оскільки кожна дуга в графі залежностей спрямована вниз. Це також змінює спосіб мислення про код: замість того, щоб аналізувати імпорти по окремих файлах, потрібно мислити у термінах шести шарів, що є моделлю, яку людина може справді запам’ятати.

    Публічні API зберігають внутрішні деталі сегментів у приватному форматі

    Кожен сегмент надає публічний API, зазвичай файл index.ts, який переекспортує те, що можуть використовувати ззовні. У документації FSD чітко сформульовано мету: відкривати лише те, що необхідно, а деталі реалізації залишати прихованими.

    Дуже поширена версія цього файлу суперечить самій меті:

    // features/auth/index.ts (the anti-pattern)
    export * from "./ui";
    export * from "./model";
    

    Вайлдкарти експортують усе, включаючи model/tokenStore.ts, який мав залишатися приватним. Рано чи пізно якась сторінка буде імпортувати цей токен-сховище безпосередньо, і після цього ви не зможете його реструктурувати, не пошкодивши якийсь інший компонент, про існування якого не знали. Крім того, вайлдкарти можуть ускладнити процес оптимізації коду за допомогою інструментів типу bundlers, тож невикористаний код може потрапити до користувачів.

    Альтернативний підхід — прямий перелік кожного експорту:

    // features/auth/index.ts (the boundary)
    export { LoginForm } from "./ui/LoginForm";
    export { useSession } from "./model/useSession";
    // tokenStore stays internal. Nobody outside auth can touch it.
    

    Тепер цей елемент має рівно два публічні імені. Усе інше всередині нього можна перейменувати, розділити чи переписати, не впливаючи на жодних користувачів цього елемента. Саме поєднання читабельної діаграми залежностей та свободи зміни внутрішньої структури є основною перевагою, яку обіцяє FSD.

    У документації FSD є одне застереження: файли index типу barrel можуть спричинити циклічні імпорти, проблеми з оптимізацією коду у директоріях shared/ui та shared/lib, а також уповільнити роботу інструментів збирання проектів великого розміру. Практичним рішенням є зберігання публічних API ясними та мінімальними, а також особлива обережність у розділі Shared, де багато невеликих, незалежних модулів краще імпортувати безпосередньо, ніж через один великий файл типу barrel.

    Міжроздільні імпорти, надмірна сегментація та використання інструментів перевірки коду

    Позначення @x для елементів, які посилаються один на одного

    Деякі ентитети закономірно залежать один від одного; наприклад, замовлення потребує типу User. Оскільки імпорти в межах одного рівня заборонені, версія v2.1 стандартизувала позначення @x саме для такої ситуації. Слайс user публікує спеціальну точку входу, призначену для слайса order, а order імпортує дані з неї:

    // entities/user/@x/order.ts
    export type { User } from "../model/types";
    
    // in entities/order/model/types.ts
    import type { User } from "entities/user/@x/order";
    

    Назва файлу чітко визначає цей зв’язок та дозволяє його пошук: entities/user/@x/order.ts — це та частина user, яку order може бачити. У документації це розглядається як виняток. Обмежуйте крос-імпорти до мінімуму та використовуйте @x лише на рівні ентитетів. Якщо вам доводиться використовувати його по всьому кодбазі, це ознака того, що ваші слайси створені неправильно.

    Флаги Steiger вказують як на порушення, так і на надмірну складність

    Правила допомагають лише тоді, коли їх дотримуються. Steiger — інструмент перевірки архітектури для FSD (його правила постачаються як @feature-sliced/steiger-plugin), працює без налаштувань та на момент написання цього тексту все ще перебував у бета-версії. Окрім перевірки правил імпорту, він запобігає надмірній складності архітектури: правило insignificant-slice активується, коли функція чи ентитет використовується лише на одній сторінці, а правило excessive-slicing вказує на те, що проект розділений на занадто багато дрібних частин. Якщо Steiger повідомляє про незначну частину коду, прийміть це як підказку: ви створили папку, де б вистачило лише локальної функції.

    Коли повна версія FSD є зайвою

    Назви папок є найменш важливою частиною FSD. Копіювання всіх шести рівнів у невеликий додаток створює своєрідний безлад: більше файлів, ніж логіки, а кожна функція розкидана по чотирьох каталогах, через що проста зміна перетворюється на пошук.

    У специфікації це враховано, тому версія v2.1 рекомендує підхід „спочатку сторінки“ для впровадження. Почніть лише з App, Pages, Widgets та Shared і подумайте про те, щоб на цьому зупинитися. Для багатьох додатків це вже є повною та зрозумілою структурою.

    Переміщуйте код у розділи Features та Entities лише тоді, коли є конкретна причина. Корисною гіпотезою є очікування, поки одна й та сама логіка не стане потрібною для трьох або більше сторінок чи віджетів, перш ніж надавати їй окремий розділ. До того часу правильним вибором є процес оформлення замовлення, який повністю знаходиться всередині pages/checkout, а не якийсь швидкий спосіб. Одноразове використання може бути випадковістю; три випадки свідчать про справжню закономірність, і саме для представлення таких повторно використовуваних закономірностей існує шар Features.

    Якщо ви також порівнюєте FSD із простішими структурами з папками функцій чи на основі доменів, наше порівняння шаблонів структури папок у React детально розглядає всі альтернативи.

    Короткий чек-лист для впровадження

    • Повністю впровадьте FSD, коли команда складається з більш ніж кількох інженерів, кількість бізнес-доменів продовжує зростати, а папка utils/ вже перетворилася на смітник.
    • Для невеликого продукту з одним чітким власником залиште правило імпорту даних назад та спростіть публічні API, уникаючи зайвих шарів.
    • Кожен файл index.ts слід писати у вигляді чіткого списку експорту; уникайте використання export *.
    • Запрягайте суфікс @x лише для посилань від однієї ентитетності до іншої, а часте його використання розглядайте як проблему меж.
    • Використовуйте інструменти типу Steiger для автоматичного застосування правил та попереджень щодо зайвих фрагментів коду.

    Підсумок

    Шість папок ніколи не були метою. Те, що насправді пропонує FSD, — це граф залежностей, який має однобічний напрямок потоку, та шари, внутрішність яких прихована за невеликими контрактами, що разом забезпечують зрозумілість кодової бази навіть через рік. Більшу частину цієї користі можна отримати лише за допомогою правил імпорту та публічних API, додавая додаткові шари лише тоді, коли це справді необхідно для повторного використання.