Перейти к основному содержимому

Публичный API

Публичный API — это контракт между группой модулей, например, слайсом, и кодом, который его использует. Он также действует как ворота, разрешая доступ только к определенным объектам и только через этот публичный API.

На практике это обычно реализуется как индексный файл с реэкспортами:

pages/auth/index.js
export { LoginPage } from "./ui/LoginPage";
export { RegisterPage } from "./ui/RegisterPage";

Что делает публичный API хорошим?

Хороший публичный API делает использование и интеграцию слайса в другой код удобным и надежным. Этого можно достичь, поставив три цели:

  1. Остальная часть приложения должна быть защищена от структурных изменений в слайсе, таких как рефакторинг.
  2. Значительные изменения в поведении слайса, которые нарушают предыдущие ожидания, должны вызывать изменения в публичном API.
  3. Только необходимые части слайса должны быть доступны.

Последняя цель имеет важные практические последствия. Может возникнуть соблазн создать слепые реэкспорты всего, особенно на ранних этапах разработки слайса, потому что любые новые объекты, которые вы экспортируете из своих файлов, также автоматически экспортируются из слайса:

Плохая практика, features/comments/index.js
// ❌ НИЖЕ ПЛОХОЙ КОД, НЕ ДЕЛАЙТЕ ТАК
export * from "./ui/Comment"; // 👎 не пытайтесь повторить дома
export * from "./model/comments"; // 💩 это плохая практика

Это ухудшает понимаемость слайса беглым взглядом, потому что вы не можете легко определить, каков интерфейс этого слайса. Не зная интерфейс, вам придется глубоко погружаться в код слайса, чтобы понять, как его интегрировать. Еще одна проблема заключается в том, что вы можете случайно раскрыть внутренние модули, что усложнит рефакторинг, если кто-то начнет от них зависеть.

Публичный API для кросс-импортов

Кросс-импорты — это ситуация, когда один слайс импортирует из другого слайса на том же слое. Обычно это запрещено правилом импорта для слоёв, но часто есть реальные причины, чтоб сделать кросс-импорт. Например, в реальном мире бизнес-сущности часто ссылаются друг на друга, и лучше отразить эти отношения в коде, а не пытаться избавиться от них.

Для этой цели существует особый вид публичного API, также известный как @x-нотация. Если у вас есть сущности A и B, и сущность B должна импортировать из сущности A, то сущность A может объявить отдельный публичный API только для сущности B.

  • 📂 entities
    • 📂 A
      • 📂 @x
        • 📄 B.ts — специальный публичный API только для кода внутри entities/B/
      • 📄 index.ts — обычный публичный API

Затем код внутри entities/B/ может импортировать из entities/A/@x/B:

import type { EntityA } from "entities/A/@x/B";

Нотацию A/@x/B следует читать как "пересечение A и B".

примечание

Старайтесь свести кросс-импорты к минимуму и используйте эту нотацию только на уровне Entities, где устранение кросс-импортов часто неразумно.

Проблемы с индексными файлами

Индексные файлы, такие как index.js, также известные как barrel-файлы (файлы-бочки), являются самым распространенным способом определения публичного API. Их легко создать, но они иногда вызывают проблемы с некоторыми сборщиками и фреймворками.

Циклические импорты

Циклический импорт — это когда два или более файла импортируют друг друга по кругу.

Три файла, импортирующие друг друга по кругуТри файла, импортирующие друг друга по кругу

На изображении выше: три файла, fileA.js, fileB.js и fileC.js, импортирующие друг друга по кругу.

Эти ситуации часто трудно обрабатывать сборщикам, и в некоторых случаях они могут даже привести к ошибкам во время выполнения кода, которые может быть трудно отладить.

Циклические импорты могут возникать и без индексных файлов, но наличие индексного файла создает явную возможность случайно создать циклический импорт. Это часто происходит, когда у вас есть два объекта, доступных в публичном API слайса, например, HomePage и loadUserStatistics, и HomePage нужно получить доступ к loadUserStatistics, но он делает это следующим образом:

pages/home/ui/HomePage.jsx
import { loadUserStatistics } from "../"; // импортируем из pages/home/index.js

export function HomePage() { /* … */ }
pages/home/index.js
export { HomePage } from "./ui/HomePage";
export { loadUserStatistics } from "./api/loadUserStatistics";

Эта ситуация создает циклический импорт, потому что index.js импортирует ui/HomePage.jsx, но ui/HomePage.jsx тоже импортирует index.js.

Чтобы предотвратить эту проблему, воспользуйтесь этими принципами. Если у вас есть два файла, и один импортирует из другого:

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

Большие бандлы и неработающий tree-shaking в Shared

Некоторым сборщикам может быть трудно выполнять tree-shaking (удаление неимпортированного кода), когда у вас есть индексный файл, который реэкспортирует все.

Обычно это не проблема для публичных API, потому что содержимое модуля обычно довольно тесно связано, поэтому вам редко нужно импортировать одну вещь, но удалить другую. Однако есть два очень распространенных случая, когда обычные правила публичного API в FSD могут привести к проблемам — shared/ui и shared/lib.

Эти две папки являются коллекциями несвязанных вещей, которые часто не нужны все в одном месте. Например, в shared/ui могут быть модули для каждого компонента в библиотеке UI:

  • 📂 shared/ui/
    • 📁 button
    • 📁 text-field
    • 📁 carousel
    • 📁 accordion

Эта проблема усугубляется, когда один из этих модулей имеет тяжелую зависимость, такую как подсветка синтаксиса или библиотека drag'n'drop. Вы не хотите подтягивать их на каждую страницу, которая использует что-то из shared/ui, например, кнопку.

Если ваши бандлы нежелательно растут из-за единого публичного API в shared/ui или shared/lib, рекомендуется вместо этого иметь отдельный индексный файл для каждого компонента или библиотеки:

  • 📂 shared/ui/
    • 📂 button
      • 📄 index.js
    • 📂 text-field
      • 📄 index.js

Тогда потребители этих компонентов могут импортировать их напрямую, как показано ниже:

pages/sign-in/ui/SignInPage.jsx
import { Button } from '@/shared/ui/button';
import { TextField } from '@/shared/ui/text-field';

Нет реальной защиты от обхода публичного API

Когда вы создаете индексный файл для слайса, ничто не мешает другим не использовать его и импортировать напрямую. Это особенно заметно с автоимпортами, потому что существует несколько мест, откуда объект может быть импортирован, поэтому IDE должна решить за вас. Иногда она может выбрать прямой импорт, нарушая правило публичного API для слайсов.

Чтобы автоматически выявлять эти проблемы, мы рекомендуем использовать Steiger, архитектурный линтер с набором правил для Feature-Sliced Design.

Худшая производительность сборщиков на больших проектах

Наличие большого количества индексных файлов в проекте может замедлить работу сервера разработки, как отметил TkDodo в своей статье "Please Stop Using Barrel Files".

Есть несколько вещей, которые вы можете сделать, чтобы справиться с этой проблемой:

  1. То же самое, что и в разделе "Большие бандлы и неработающий tree-shaking в Shared" — создайте отдельные индексные файлы для каждого компонента/библиотеки в shared/ui и shared/lib вместо одного большого

  2. Избегайте наличия индексных файлов в сегментах на слоях, которые имеют слайсы.
    Например, если у вас есть индекс для фичи "comments", 📄 features/comments/index.js, нет смысла иметь еще один индекс для ui сегмента этой фичи, 📄 features/comments/ui/index.js.

  3. Если у вас очень большой проект, есть большая вероятность, что ваше приложение можно разделить на несколько больших кусков.
    Например, у Google Docs очень разные обязанности для редактора документов и для файлового браузера. Вы можете создать монорепозиторий, где каждый пакет является отдельным корнем FSD со своим набором слоев. Некоторые пакеты могут иметь только слои Shared и Entities, другие могут иметь только Pages и App, а некоторые могут включать свой небольшой Shared, но при этом использовать большой Shared из другого пакета.