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

Туториал

Часть 1. На бумаге

В этом руководстве мы рассмотрим приложение Real World App, также известное как Conduit. Conduit является упрощённым клоном Medium — он позволяет вам читать и писать статьи в блогах, а также комментировать статьи других людей.

Главная страница Conduit

Это довольно небольшое приложение, поэтому мы не станем сильно усложнять разработку излишней декомпозицией. Вероятнее всего, что всё приложение поместится в три слоя: App, Pages и Shared. Если нет, будем вводить дополнительные слои по ходу. Готовы?

Начните с перечисления страниц

Если мы посмотрим на скриншот выше, мы можем предположить, что по крайней мере, есть следующие страницы:

  • Домашняя (лента статей)
  • Войти и зарегистрироваться
  • Просмотр статей
  • Редактор статей
  • Просмотр профилей людей
  • Редактор профиля (настройки)

Каждая из этих страниц станет отдельным слайсом на слое Pages. Вспомните из обзора, что слайсы — это просто папки внутри слоев, а слои — это просто папки с заранее определенными названиями, например, pages.

Таким образом, наша папка Pages будет выглядеть так:

📂 pages/
  📁 feed/ (лента)
  📁 sign-in/ (войти/зарегистрироваться)
  📁 article-read/ (просмотр статей)
  📁 article-edit/ (редактор статей)
  📁 profile/ (профиль)
  📁 settings/ (настройки)

Ключевое отличие Feature-Sliced Design от произвольной структуры кода заключается в том, что страницы не могут зависеть друг от друга. То есть одна страница не может импортировать код с другой страницы. Это связано с правилом импорта для слоёв:

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

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

Пристальный взгляд на ленту

Перспектива анонимного посетителя

Перспектива анонимного посетителя

Перспектива авторизованного пользователя

Перспектива авторизованного пользователя

На странице ленты есть три динамических области:

  1. Ссылки для логина, показывающие статус авторизации
  2. Список тэгов, фильтрующих ленту
  3. Одна—две ленты статей, у каждой статьи кнопка лайка

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

Список тэгов

Чтобы создать список тэгов, нам нужно получить все доступные тэги, отобразить каждый тэг как чип (chip) и сохранить выбранные тэги в хранилище на стороне клиента. Эти операции относятся к категориям «взаимодействие с API», «пользовательский интерфейс» и «хранение данных». В Feature-Sliced Design код делится по назначению с помощью сегментов. Сегменты — это папки в слайсах, и они могут иметь произвольные названия, описывающие их цель, но некоторые цели настолько распространены, что существует несколько общепринятых названий:

  • 📂 api/ для взаимодействия с бэкендом
  • 📂 ui/ для кода, отвечающего за отображение и внешний вид
  • 📂 model/ для хранения данных и бизнес-логики
  • 📂 config/ для фиче-флагов, переменных окружения и других форм конфигурации

Мы поместим код, который получает тэги, в api, сам компонент тэга в ui, а взаимодействие с хранилищем в model.

Статьи

Следуя той же логике, мы можем разбить ленту статей на те же три сегмента:

  • 📂 api/: получить постраничный список статей с количеством лайков, оставить лайк
  • 📂 ui/:
    • список вкладок, который может отображать дополнительную вкладку при выборе тэга
    • отдельная статья
    • рабочая пагинация
  • 📂 model/: клиентское хранилище загруженных постов и текущей страницы (при необходимости)

Переиспользование общего кода

Страницы, как правило, очень отличаются по своей цели, но что-то остается одинаковым по всему приложению — например, UI-кит, соответствующий языку дизайна, или соглашение на бэкенде, что все делается через REST API с конкретным методом аутентификации. Поскольку слайсы должны быть изолированными, переиспользование кода происходит за счёт слоя ниже, Shared.

Shared отличается от других слоев тем, что он содержит сегменты, а не слайсы. Таким образом, слой Shared представляет собой гибрид между слоем и слайсом.

Обычно код в Shared не планируется заранее, а извлекается по ходу разработки, потому что только во время разработки становится ясно, какие части кода действительно переиспользуются. Тем не менее, полезно держать в голове, какой код имеет смысл хранить в Shared:

  • 📂 ui/ — UI-кит, только внешний вид, без бизнес-логики. Например, кнопки, диалоги, поля форм.
  • 📂 api/ — удобные обёртки вокруг запросов на бэкенд (например, обёртка над fetch() в случае веба)
  • 📂 config/ — обработка переменных окружения
  • 📂 i18n/ — конфигурация поддержки разных языков
  • 📂 router/ — примитивы и константы маршрутизации

Это лишь примеры сегментов в Shared, вы можете опустить любой из них или создать свой собственный. Единственное, что нужно помнить при создании новых сегментов — названия сегментов должны описывать цель (почему), а не суть (что). Такие названия как components , hooks или modals не стоит использовать, потому что они описывают, что содержат эти файлы по сути, а не то, с какой целью писался этот код. Как следствие таких названий, команде приходится копаться в таких папках, чтоб найти нужное. Помимо этого, несвязанный код лежит рядом, из-за чего при рефакторинге затрагивается большая часть приложения, что усложняет ревью и тестирование.

Определите строгий публичный API

В контексте Feature-Sliced Design термин публичный API означает, что слайс или сегмент объявляет, что из него могут импортировать другие модули в проекте. Например, в JavaScript это может быть файл index.js, который переэкспортирует объекты из других файлов в слайсе. Это обеспечивает свободу рефакторинга внутри слайса до тех пор, пока контракт с внешним миром (т.е. публичный API) остается неизменным.

Для слоя Shared, на котором нет слайсов, обычно удобнее определить публичный API (он же индекс) на уровне сегментов, а не один индекс на весь слой. В таком случае импорты из Shared естественным образом организуются по назначению. Для других слоев, на которых слайсы есть, верно обратное — обычно практичнее определить один индекс на слайс и позволить слайсу самому контролировать набор сегментов внутри, потому что другие слои обычно имеют гораздо меньше экспортов и чаще рефакторятся.

Наши слайсы/сегменты будут выглядеть друг для друга следующим образом:

📂 pages/
  📂 feed/
    📄 index
  📂 sign-in/
    📄 index
  📂 article-read/
    📄 index
  📁 …
📂 shared/
  📂 ui/
    📄 index
  📂 api/
    📄 index
  📁 …

Все, что находится внутри папок типа pages/feed или shared/ui , известно только этим папкам, и нет никаких гарантий по содержанию этих папок.

Крупные переиспользуемые блоки интерфейса

Ранее мы хотели отдельно вернуться к переиспользуемому заголовку приложения. Собирать его заново на каждой странице было бы непрактично, поэтому мы его переиспользуем. У нас уже есть слой Shared для переиспользования кода, однако, в случае крупных блоков интерфейса в Shared есть нюанс — слой Shared не должен знать о слоях выше.

Между слоями Shared и Pages есть три других слоя: Entities, Features и Widgets. В других проектах на этих слоях может лежать что-то, что хочется использовать в крупном переиспользуемом блоке, и тогда мы не сможем поместить этот блок в Shared, потому что тогда ему придется импортировать со слоёв выше, а это запрещено. Тут приходит на помощь слой Widgets. Он расположен выше Shared, Entities и Featuers, поэтому он может использовать их всех.

В нашем случае заголовок очень простой — это статический логотип и навигация верхнего уровня. Навигация должна спросить у API, авторизован ли сейчас пользователь, но это может быть решено простым импортом из сегмента api. Поэтому мы оставим наш заголовок в Shared.

Пристальный взгляд на страницу с формой

Давайте также рассмотрим страницу, на которой можно не только читать, но и редактировать. К примеру, редактор статей:

Редактор статей в Conduit

Она выглядит тривиально, но содержит несколько аспектов разработки приложений, которые мы еще не исследовали — валидацию форм, состояние ошибки и постоянное хранение данных.

Для создания этой страницы нам нужно несколько полей и кнопок из Shared, которые мы соберём в форму в сегменте ui этой страницы. Затем, в сегменте api мы определим изменяющий запрос, чтобы создать статью на бэкенде.

Чтобы проверить запрос перед отправкой, нам нужна схема валидации, и хорошим местом для нее является сегмент model , поскольку это модель данных. Там же мы сгенерируем сообщение об ошибке, а отобразим его с помощью ещё одного компонента в сегменте ui.

Чтобы улучшить пользовательский опыт, мы также можем сохранять введённые данные постоянно, чтобы предотвратить случайную потерю при закрытии браузера. Это тоже подходит под сегмент model.

Итоги

Мы разобрали несколько страниц и пришли к базовой структуре нашего приложения:

  1. Слой Shared
    1. ui будет содержать наш переиспользуемый UI-кит
    2. api будет содержать наши примитивы для взаимодействия с бэкендом
    3. Остальное разложим по ходу написания кода
  2. Слой Pages — для каждой страницы отдельный слайс
    1. ui будет содержать саму страницу и составляющие её блоки
    2. api будет содержать более специализированные функции получения данных, использующие shared/api
    3. model может содержать клиентское хранилище данных, которые мы будем отображать

Давайте создадим это приложение!

Продолжение следует.