Легко избегайте каталогов компонентов внешнего интерфейса с высокой когнитивной нагрузкой
Следуя давным-давно советам в Интернете, я принял определенную структуру компонентов, которая «просто работает».
Сценарий
Давайте сначала представим упрощенную структуру каталогов внешнего интерфейса, вдохновленную Next.js.
public/ some-image.jpg pages/ index.tsx components/ Heading.tsx Logo.tsx Layout.tsx BoxContainer.tsx Footer.tsx
Проблема
Приведенная выше простая структура приложения дает мало информации о том, как взаимодействуют эти компоненты.
Например, вы можете предположить, что Layout.tsx
импортирует Footer.tsx
и Header.tsx
, которые, в свою очередь, могут импортировать BoxContainer.tsx
. Но это не ясно только из файловой структуры.
Что еще хуже, по мере роста вашего приложения становится все труднее понять, как они связаны, по списку компонентов.
Наивный подход: структура плоских компонентов
Обычной первой мыслью может быть организация компонентов в семантически правильных каталогах.
Вот типичный результат такого подхода:
public/ some-image.jpg pages/ index.tsx components/ layout/ Layout.tsx Heading.tsx Footer.tsx common/ Heading.tsx BoxContainer.tsx
Проблема №1: хорошие имена сложно масштабировать
Называть вещи трудно. Как разработчик, вы пытаетесь создать хорошие имена и классификации для каждого каталога, например containers
, headings
и так далее.
Проблема в том, что вам нужно придумать еще больше классификаций для каталогов, а не только имена компонентов.
У вас часто будет возникать соблазн сказать: «Знаете что, я просто перемещу это в каталог common
». Наличие common
каталогов — это антипаттерн к тому, чего вы пытаетесь достичь, но с такой структурой в нее просто слишком легко втянуться.
И когда ваше приложение станет достаточно большим, вам, возможно, придется подумать о создании еще одного уровня каталогов, чтобы все было организовано.
Это требует еще большего создания имен, увеличивая когнитивную нагрузку на пользователей репозитория. В конечном счете, этот подход плохо масштабируется.
Проблема №2: повышенная когнитивная нагрузка на имена каталогов
Раньше те, кто пытался ориентироваться в репозитории, сначала пытались понять, что делает каждый компонент, по его имени и как они связаны друг с другом.
Теперь они также должны понимать созданные вами имена каталогов, которые потенциально могут еще больше запутать их, если имена семантически не складываются в единое целое.
Лучший подход: шаблон деревьев компонентов
При таком подходе вместо того, чтобы изо всех сил классифицировать группы компонентов с разными именами, вы сосредоточитесь на том, чтобы иметь хорошо названные компоненты, которые неявно объясняют, из чего они состоят.
Правила импорта компонентов
- Может импортировать вверх, кроме своего родителя
- Можно импортировать братьев и сестер
- Невозможно импортировать компоненты одного уровня
- Не удается импортировать родительский элемент
public/ some-image.jpg pages/ index.tsx components/ Layout/ components/ Heading/ components/ Logo.tsx Menu.tsx Heading.tsx CopyrightIcon.tsx Footer.tsx Layout.tsx BoxContainer.tsx
Давайте покажем содержимое Footer.tsx
и используем его в качестве примера, используя правила, которые я перечислил выше:
// components/Layout/components/Footer.tsx // Can import upwards, except its own parent import { BoxContainer } from '../../BoxContainer.tsx'; // Can import siblings import { CopyrightIcon } from './CopyrightIcon.tsx'; // WRONG: Cannot import sibling's components // import { Menu } from './Heading/components/Menu.tsx'; // WRONG: Cannot import its parent // import { Layout } from '../Layout.tsx'; export const Footer = () => ( <BoxContainer> <CopyrightIcon /> <p>All rights reserved, etc.</p> </BoxContainer> )
Преимущество № 1: очевидные отношения дочерних компонентов
Шаблон деревьев компонентов устраняет догадки; взаимосвязь между компонентами сразу становится очевидной. Например, Menu.tsx
аккуратно вложена как внутренняя зависимость Heading.tsx
.
Также ясно, что Menu.tsx
не используется ничем другим, что помогает вам отбросить его на ранней стадии, просматривая код во время ваших ежедневных задач разработки.
Преимущество № 2: определение повторного использования более тонкое
При наивном подходе компоненты были либо «обычными», либо «необычными». Имея в виду возможность повторного использования, деревья компонентов помогают избежать такого непродуктивного бинарного мышления.
components/ Layout/ components/ Heading/ components/ - Logo.tsx Menu.tsx Heading.tsx + Logo.tsx CopyrightIcon.tsx Footer.tsx Layout.tsx BoxContainer.tsx
В приведенном выше примере, если Logo.tsx
становится необходимым для большего количества компонентов, чем просто Menu.tsx
, мы можем просто переместить его на один уровень выше. Он может быть недостаточно пригодным для повторного использования (или «распространенным») для использования BoxContainer.tsx
, но он достаточно пригоден для повторного использования в контексте компонента Layout.tsx
.
Преимущество № 3: свести к минимуму необходимость называть вещи
Поскольку у вас есть деревья компонентов, нет необходимости умно классифицировать имена каталогов поверх имен компонентов. Имена компонентов являются классификациями, и когда вы увидите, из каких внутренних компонентов состоит ваш компонент, вам также будет легче определить хорошие имена для ваших компонентов.
Бонус: извлечение кода из компонентов в отдельные файлы, не задумываясь об именах.
Теперь давайте рассмотрим ситуацию, когда вы хотите извлечь некоторые служебные функции из Footer.tsx
, потому что файл становится немного большим, и вы полагаете, что можете вырвать из него часть логики, а не разбивать еще пользовательский интерфейс.
Хотя вы можете создать каталог utils/
, это заставит вас выбрать имя для любого файла, в который вы хотите поместить свои служебные функции.
Вместо этого выберите использование суффиксов файлов, таких как Footer.utils.tsx
или Footer.test.tsx
.
components/ Layout/ components/ Heading/ components/ Logo.tsx Menu.tsx Heading.tsx CopyrightIcon.tsx + Footer.utils.tsx Footer.tsx Layout.tsx BoxContainer.tsx
Таким образом, вам не придется придумывать умное имя, например emailFormatters.ts
, или что-то очень расплывчатое, например helpers.ts
. Избегайте когнитивной нагрузки, связанной с именованием — эти утилиты принадлежат Footer.tsx
и могут использоваться Footer.tsx
и его внутренними компонентами (опять же, импорт вверх).
Контраргументы деревьям компонентов
«Это много каталогов компонентов»
Глядя на эту структуру в первый раз, у большинства людей возникает рефлекторная реакция.
Да, есть много каталогов «компонентов». Но когда я работаю с командами над определением структуры проекта, я всегда подчеркиваю важность ясности, а не элегантности.
Одним из способов, которым я определяю успех в репозитории, является то, как старшие и младшие разработчики видят ясность, и я обнаружил, что деревья компонентов всегда способствуют достижению этой цели.
«Тьфу: импортировать… из ./MyComponent/MyComponent.tsx?»
Хотя import … from ./MyComponent/MyComponent.tsx
может выглядеть некрасиво, более важна ясность, которую он приносит, прямо указывая, откуда берется компонент.
Что касается строк импорта, это примеры того, что создает когнитивную нагрузку для разработчиков.
- Наличие псевдонимов импорта, таких как
import ... from 'common/components'
, утомительно для разработчиков. - Повсюду иметь
index.ts
файлов, чтобы можно было просто написатьimport ... from './MyComponent'
. Поиск нужного файла, скорее всего, займет больше времени у разработчиков, которые выполняют поиск по файлу.
Окончательное сравнение: сложный сценарий
Благодаря таким инструментам, как ChatGPT, довольно легко тестировать подобные шаблоны для более сложных сценариев в удобочитаемой форме.
После объяснения структур я попросил ChatGPT сгенерировать «плоскую» структуру каталогов в левом столбце и то, что я назвал «деревьями компонентов» в правом.
Flat Structure | Component Trees ------------------------------------+--------------------------------------------------- pages/ | pages/ index.tsx | index.tsx shop.tsx | shop.tsx product/ | product/ [slug].tsx | [slug].tsx cart.tsx | cart.tsx checkout.tsx | checkout.tsx about.tsx | about.tsx contact.tsx | contact.tsx login.tsx | login.tsx register.tsx | register.tsx user/ | user/ dashboard.tsx | dashboard.tsx orders.tsx | orders.tsx settings.tsx | settings.tsx | components/ | components/ layout/ | Layout/ Layout.tsx | components/ Header.tsx | Header/ Footer.tsx | components/ Sidebar.tsx | Logo.tsx Breadcrumb.tsx | NavigationMenu.tsx common/ | SearchBar.tsx Button.tsx | UserIcon.tsx Input.tsx | CartIcon.tsx Modal.tsx | Header.tsx Spinner.tsx | Footer/ Alert.tsx | components/ product/ | SocialMediaIcons.tsx ProductCard.tsx | CopyrightInfo.tsx ProductDetails.tsx | Footer.tsx ProductImage.tsx | Layout.tsx ProductTitle.tsx | BoxContainer.tsx ProductPrice.tsx | Button.tsx AddToCartButton.tsx | Input.tsx filters/ | Modal.tsx SearchFilter.tsx | Spinner.tsx SortFilter.tsx | Alert.tsx cart/ | ProductCard/ Cart.tsx | components/ CartItem.tsx | ProductImage.tsx CartSummary.tsx | ProductTitle.tsx checkout/ | ProductPrice.tsx CheckoutForm.tsx | AddToCartButton.tsx PaymentOptions.tsx | ProductCard.tsx OrderSummary.tsx | ProductDetails/ user/ | components/ UserProfile.tsx | ProductSpecifications.tsx UserOrders.tsx | ProductReviews.tsx LoginBox.tsx | ProductReviewForm.tsx RegisterBox.tsx | ProductDetails.tsx about/ | SearchFilter.tsx AboutContent.tsx | SortFilter.tsx contact/ | Cart/ ContactForm.tsx | components/ review/ | CartItemList.tsx ProductReview.tsx | CartItem.tsx ProductReviewForm.tsx | CartSummary.tsx address/ | Cart.tsx ShippingAddress.tsx | CheckoutForm/ BillingAddress.tsx | components/ productInfo/ | PaymentDetails.tsx ProductSpecifications.tsx | BillingAddress.tsx cartInfo/ | ShippingAddress.tsx CartItemList.tsx | CheckoutForm.tsx userDetail/ | PaymentOptions.tsx UserSettings.tsx | OrderSummary.tsx icons/ | UserProfile/ Logo.tsx | components/ SocialMediaIcons.tsx | UserOrders.tsx CartIcon.tsx | UserSettings.tsx UserIcon.tsx | UserProfile.tsx | LoginBox.tsx | RegisterBox.tsx | AboutContent.tsx | ContactForm.tsx
Теперь имейте в виду, что это пример без каких-либо тестовых файлов, служебных файлов или чего-либо подобного.
Для древовидной структуры компонентов вы можете добавить служебные или тестовые файлы с суффиксами в каталогах компонентов.
Что касается плоской структуры, вам, вероятно, придется создать отдельный каталог utils
, чтобы разобраться с тем, что и без того является довольно тяжелой когнитивной нагрузкой.
Не используете файловую маршрутизацию? Даже лучше!
До работы с Next.js я выступал за эту структуру, но с усовершенствованием: организация компонентов также по представлениям.
public/ some-image.jpg components/ BoxContainer.tsx views/ components/ Layout/ components/ [...] Layout.tsx about-us/ components/ EmailForm.tsx about-us.tsx index.tsx (<- the start page in this example)
Положительным моментом этого является то, что теперь вы видите не только зависимости между компонентами, но и то, какие компоненты используются страницами (или представлениями).
Используете Next.js?
Вы все еще можете добиться этого с помощью Next.js несколькими различными способами, с компромиссами.
Вариант 1. Использование pageExtensions
в next.config.js
public/ some-image.jpg components/ BoxContainer.tsx pages/ components/ Layout/ components/ [...] Layout.tsx about-us/ components/ EmailForm.tsx index.page.tsx index.page.tsx (<- the start page in this example)
Как: Используйте pageExtensions
(ссылка на документы) и добавляйте ко всем файлам страниц что-то вроде page.tsx
. Это позволяет вам иметь файлы, которые не являются страницами в вашем каталоге pages/
.
Потенциальный компромисс: поскольку Next.js не позволит вам создать страницу, подобную about-us/about-us.page.tsx
(что создаст URL-адрес, подобный /about-us/about-us
), вам придется иметь много файлов index.page.tsx
, которые создают когнитивную нагрузку для разработчиков, определяющих, какой индекс файл какой.
Вариант 2. Использование компонентов «уровня страницы» для ваших страниц
public/ some-image.jpg components/ Layout/ components/ [...] Layout.tsx AboutUsPage/ components/ EmailForm.tsx AboutUsPage.tsx pages/ about-us.tsx index.tsx
Как: Для каждой страницы, которую вы создаете, вы также создаете компонент для этой страницы в вашем каталоге components/
, который будет единственным компонентом, отображаемым на вашей странице.
Потенциальный компромисс: наличие практически пустых файлов pages
(за исключением getStaticProps
и других хуков) может рассматриваться как раздражающий дополнительный шаг всякий раз, когда вы собираетесь работать на какой-либо странице, и вам нужно немедленно перейти к компоненту страницы.
«Мне это нравится, но как мне реализовать подобную структуру?»
Многие разработчики, в том числе и я, используют наш редактор кода или IDE для обнаружения и автоматического импорта модулей для нас.
Это может привести к тому, что разработчики случайно нарушат любое из правил, которые мы перечислили ранее.
Именно здесь вступает в игру eslint
, по-настоящему недооцененный инструмент для создания собственных стандартов для вашего репозитория.
ESLint — это настраиваемая утилита линтинга JavaScript с открытым исходным кодом, которая позволяет разработчикам обнаруживать и исправлять проблемы с их кодом JavaScript без его выполнения. Это помогает обеспечить соблюдение стандартов кодирования и рекомендаций по стилю, улучшая согласованность кода и потенциально предотвращая ошибки.
Будучи интегрированным в ваш процесс разработки, ESLint может обеспечить раннюю обратную связь по вашему коду, позволяя вам выявлять ошибки и неверные шаблоны непосредственно в текстовом редакторе, прежде чем они станут более серьезными проблемами. Инструмент полностью подключаемый, что дает вам возможность гибко настраивать стандарты кодирования в зависимости от требований вашего конкретного проекта или команды.
Одна из замечательных особенностей ESLint — его расширяемость. Вы можете настроить правила, которые хотите применить к своей кодовой базе, или даже создать свои собственные. Это делает ESLint не только инструментом для обнаружения потенциальных ошибок, но и для обеспечения согласованного стиля и структуры кода. Например, вы можете настроить правила, которые запрещают импорт модулей таким образом, который противоречит структуре каталогов вашего проекта, как мы сейчас увидим.
Поскольку создание собственного плагина ESLint может быть немного привередливым, лучше использовать существующий. Я поделюсь двумя правилами, которые я создал с помощью eslint-plugin-import
.
Создание правил lint для принудительного применения деревьев компонентов
yarn install eslint-plugin-import
- Измените файл
.eslintrc.json
, чтобы он содержал следующее:
// .eslintrc.json { "plugins": [ // [existing plugins] "eslint-plugin-import" ], "rules": { // [existing rules] "no-restricted-imports": [ "error", { "patterns": [ { "group": ["../**/components/**/*"], "message": "Do not import from a higher level component's internal components, move that component further up the directory tree instead." }, { "group": ["**/components/**/components/**/*"], "message": "Do not import internal components used by a sub-component, move that component further up the directory tree instead." } ] } ] } }
Если у вас возникнут проблемы совместимости с вашим репозиторием, вы можете убедиться, что эти правила применяются только к определенным каталогам, например приведенным ниже:
// .eslintrc.json { "plugins": [ // [existing plugins] "eslint-plugin-import" ], "rules": { // [existing rules] }, "overrides": [ { "files": ["components/**/*.{tsx,jsx,ts,js}"], "rules": { "no-restricted-imports": [ "error", { "patterns": [ { "group": ["../**/components/**/*"], "message": "Do not import from a higher level component's internal components, move that component further up the directory tree instead." }, { "group": ["**/components/**/components/**/*"], "message": "Do not import internal components used by a sub-component, move that component further up the directory tree instead." } ] } ] } } ] }
Нижняя граница
Пожалуйста, попробуйте эту структуру компонентов. Я искренне надеюсь, что вы, как и я, найдете его настолько интуитивно понятным и эффективным, что не будет возврата к другим, более запутанным структурам, которые не упрощают управление компонентами должным образом.
Я также хотел бы поблагодарить вас за совет, который я нашел в Интернете много лет назад и который привел меня к принятию этого шаблона.
Спасибо за прочтение.