Оригинальный адрес:Front end component design principles
Оригинальный автор:Andrew Dinihan
Пример кода в тексте:портал
Ограничено личными возможностями, если есть какие-либо ошибки или упущения, пожалуйста, не стесняйтесь, дайте мне знать.
предисловие
Я начал разработку с Vue на своей последней работе, но у меня более трех лет опыта разработки React в моей последней компании. Хотя переключение между двумя разными интерфейсными фреймворками требует большого обучения, между ними существует множество базовых концепций и дизайнерских идей. Одним из них является проектирование компонентов, включая проектирование иерархии компонентов и разделение обязанностей между компонентами.
Компоненты — это одна из фундаментальных концепций большинства современных интерфейсных фреймворков, встречающаяся в таких фреймворках, как React и Vue, а также Ember и Mithril. Компонент обычно представляет собой набор языка разметки, логики и стилей. Они были созданы как повторно используемые модули для создания наших приложений.
Подобно дизайну классов в традиционных ООП-языках, при проектировании компонентов необходимо учитывать многие аспекты, чтобы их можно было повторно использовать, комбинировать, разделять и обеспечивать низкую связанность, но при этом функции можно было реализовать стабильно, даже если они выходят за рамки практический В случае объема тестового примера. О таком дизайне легче сказать, чем сделать, потому что на самом деле у нас часто не хватает времени, чтобы сделать его оптимально.
метод
В этой статье я хотел бы представить некоторые концепции дизайна, связанные с компонентами, которые следует учитывать при разработке внешнего интерфейса. Я думаю, что лучший способ — дать каждому понятию краткое и лаконичное имя, а затем объяснить, что представляет собой каждое понятие и почему оно важно, и привести несколько примеров для более абстрактных понятий, которые помогут понять.
Следующий список не является ни исчерпывающим, ни полным, но я заметил только 8 вещей, о которых стоит упомянуть тем, кто уже может писать базовые компоненты, но хочет улучшить свои навыки технического проектирования. Итак, вот список: В приведенном ниже списке всего 8 аспектов, которые я заметил, конечно, есть и другие аспекты проектирования компонентов. Здесь я просто перечисляю те, которые я считаю достойными упоминания.
Разработчикам, освоившим базовый компонентный дизайн и желающим улучшить свои возможности компонентного дизайна, я думаю, стоит обратить внимание на следующие 8 пунктов, конечно, это не весь компонентный дизайн.
- Иерархия и диаграммы классов UML
- Плоское, ориентированное на данные состояние/реквизит
- Более чистое изменение состояния
- низкое сцепление
- Разделение вспомогательного кода
- Изысканная сущность
- Модульность во времени
- Централизованное/единое государственное управление
Обратите внимание, что примеры кода могут содержать ошибки или быть немного надуманными. Но они не сложные, просто хочу использовать эти примеры, чтобы лучше понять концепции.
Иерархия и диаграммы классов
Компоненты в приложении вместе образуют дерево компонентов, и визуализация дерева компонентов в процессе проектирования может помочь вам полностью понять макет приложения. Хороший способ показать это — диаграмма компонентов.
В UML есть тип, который часто используется при проектировании классов ООП, называемый диаграммой классов UML. Диаграммы классов показывают атрибуты классов, методы, модификаторы доступа, отношения между классами и другими классами и т. д. Хотя дизайн ООП-класса и дизайн внешнего компонента сильно различаются, стоит упомянуть метод помощи в проектировании с помощью диаграмм. Для интерфейсных компонентов диаграмма может отображать:
- State
- Props
- Methods
- Связь с другими компонентами
Итак, давайте взглянем на следующую диаграмму иерархии компонентов для компонента базовой таблицы, чьим объектом рендеринга является массив. Функциональность этого компонента включает отображение общего количества строк, строки заголовка и некоторых строк данных, а также сортировку столбца при нажатии на ячейку заголовка ячейки. В его свойствах будет передан список столбцов (с именем свойства и удобочитаемой версией этого свойства), за которым следует массив данных. Мы можем добавить дополнительную функцию «щелчок по строке» для тестирования.
Хотя такая вещь может показаться слишком большой, она имеет много преимуществ и необходима при проектировании разработки больших приложений. Важная проблема, которую это принесет, заключается в том, что вам потребуется рассмотреть реализацию конкретных деталей перед началом кодирования, например, какой тип данных требуется каждому компоненту, какие методы необходимо реализовать, требуемые свойства состояния и т. д.
Когда у вас есть общее представление о том, как построить компонент (или группу компонентов) в целом, легко подумать, что когда вы на самом деле начнете кодировать реализацию, это будет сделано шаг за шагом, как вы ожидаете, но в на самом деле часто возникают некоторые неожиданные вещи, конечно, вы, конечно, не хотите рефакторить некоторые части предыдущего или страдать от недостатков исходной идеи и тем самым нарушать свое кодовое мышление. Следующие преимущества этих диаграмм классов могут помочь вам эффективно избежать вышеперечисленных проблем.
- Простое для понимания представление состава компонентов и ассоциаций
- Простой для понимания обзор иерархии пользовательского интерфейса приложения.
- Представление об иерархии структурированных данных и о том, как она течет
- Снимок функциональных обязанностей компонента
- Легко создать с помощью программного обеспечения для построения диаграмм
Кстати, приведенная выше диаграмма не основана на каком-то официальном стандарте, таком как диаграмма классов UML, это набор правил выражений, которые я в основном создал. Например, объявления определения типа данных в реквизитах, параметры метода и возвращаемые значения основаны на синтаксисе Typescript. Я не нашел официального стандарта для написания диаграмм классов интерфейсных компонентов, вероятно, из-за относительно новой разработки интерфейсного Javascript и несовершенной экосистемы, но если кто-нибудь знает основной стандарт, пожалуйста, дайте мне знать в ответ!
Плоское, ориентированное на данные состояние/реквизит
В случае, когда состояние и реквизиты часто отслеживаются и обновляются, если вы используете вложенные данные, ваша производительность может пострадать, особенно в следующих сценариях, таких как некоторые повторные рендеры, вызванные мелкими. В библиотеках неизменяемости, таких как React, вам необходимо создать копии состояния вместо того, чтобы изменять их напрямую, как в Vue, и это с вложенными данными может создать громоздкий, уродливый код.
Даже с оператором распространения это недостаточно элегантно. Плоские свойства также отлично подходят для очистки значений данных, которые использует компонент. Если вы передаете объект компоненту, но не знаете точно, какие значения свойств находятся внутри объекта, поэтому узнайтенедвижимостьЗначения данных, которые поступают из значений специфичных компонентов, являются дополнительной работой. Но если реквизиты достаточно пломы, по крайней мере, будет легче использовать и поддерживать.
state/props также должны содержать только те данные, которые компонент должен отображать. Вы не должны хранить целые компоненты в state/props и рендерить прямо оттуда.
(Кроме того, для приложений с большим объемом данных нормализация данных может быть огромным преимуществом, и вы можете рассмотреть некоторые другие оптимизации помимо выравнивания).
Более чистое изменение состояния
Изменения состояния обычно должны реагировать на какое-либо событие, например, нажатие пользователем кнопки или ответ от API. Также они не должны реагировать на изменения в другом состоянии, так как эта связь между состояниями может привести к поведению компонента, которое трудно понять и поддерживать. Изменения состояния не должны иметь побочных эффектов.
если ты злоупотребляешьwatchВместо того, чтобы рассматривать вышеуказанные принципы в ограниченном виде, могут возникнуть проблемы, вызванные этим при использовании Vue. Давайте посмотрим на базовый пример Vue. Я работаю над компонентом, который извлекает некоторые данные из API и отображает их в таблице, где сортировка, фильтрация и т. д. выполняются бэкэндом, поэтому все, что нужно сделать внешнему интерфейсу, — это просмотреть все параметры поиска и вызвать Вызов API при его изменении. Одним из значений, требующих наблюдения, является «зона», которая является фильтром. При изменении мы хотим обновить данные на стороне сервера, используя отфильтрованное значение. Наблюдатель выглядит следующим образом:
Вы найдете некоторые странные вещи. Если они выходят за пределы первой страницы результатов, мы сбрасываем номера страниц и заканчиваем? Это кажется неправильным, если они не на первой странице, мы должны сбросить нумерацию страниц и запустить вызов API, верно? Почему мы обновляем данные только на странице 1? На самом деле причина вот в чем, давайте посмотрим на полные часы:
При изменении разбивки на страницы приложение сначала извлекает данные через обработчик разбиения на страницы. Поэтому, если мы изменим пагинацию, нам не нужно обращать внимание на логику обновления данных.
Давайте рассмотрим следующую последовательность действий: если текущая страница находится за пределами страницы 1 и зона изменена, это изменение запускает другое изменение состояния (разбивку на страницы), что, в свою очередь, заставляет наблюдателей разбиения на страницы повторно запрашивать данные. Это не ожидаемое поведение, и результирующий код не интуитивно понятен.
Решение состоит в том, что обработчик события поведения изменения номера страницы (не наблюдатель, фактический обработчик, где пользователь меняет страницу) должен изменить значение страницыа такжеИнициировать вызовы API для запроса данных. Это также устранит необходимость в наблюдателях. При такой настройке изменение состояния разбивки на страницы напрямую из другого места также не будет иметь побочного эффекта повторной выборки данных.
Хотя этот примерОченьПросто, но нетрудно заметить, что объединение более сложных изменений состояния дает непонятный код, который не только не масштабируется и является кошмаром для отладки.
Слабая связь
Основная идея компонентов заключается в том, что их можно использовать повторно, для чего они должны быть функциональными и полными. «Связанный» — это термин, который относится к сущностям, которые зависят друг от друга. Слабосвязанные объекты должны иметь возможность работать независимо, без зависимости от других модулей. Что касается внешних компонентов, основная часть связи заключается в том, насколько функциональность компонента зависит от его родителя и переданных ему реквизитов, а также от дочерних компонентов, используемых внутри (и, конечно, ссылочные части, такие как третьи- сторонние модули или пользовательские скрипты).
Тесно связанные компоненты часто труднее повторно использовать, и когда они являются дочерними элементами определенного родительского компонента, трудно работать должным образом, когда дочерний компонент или ряд дочерних компонентов родительского компонента могут правильно функционировать только в родительском компоненте. Когда это вступает в игру, это делает код избыточным. Потому что родительские и дочерние компоненты не слишком связаны.
При проектировании компонентов следует учитывать более общие сценарии использования, а не только удовлетворение потребностей конкретного сценария в первую очередь. Хотя обычно компоненты изначально разрабатываются для определенной цели, это нормально, многие компоненты лучше подходят, если они разработаны с более высокой точки зрения.
Давайте рассмотрим простой пример React, где вы хотите написать список ссылок с логотипом, которые ведут на определенный веб-сайт. Первоначальный дизайн не может быть разумно отделен от содержания. Вот оригинальная версия:
Хотя это удовлетворяет предполагаемому варианту использования, его трудно использовать повторно. Что делать, если вы хотите изменить адрес ссылки? Вы должны сделать новую копию того же кода и вручную заменить адрес ссылки. Кроме того, если вы собираетесь реализовать функцию, в которой пользователь может изменить соединение, это означает, что невозможно написать код «мертвым», и вы не можете ожидать, что пользователь вручную изменит код, поэтому давайте посмотрим, что более повторно используемый компонент должен быть Как спроектировать:
Здесь мы видим, что, хотя его исходная ссылка и логотип имеют значения по умолчанию, мы можем переопределить значения по умолчанию, передав значение реквизита. Давайте посмотрим на это в действии:
Нет необходимости переписывать новые компоненты! Если мы решим сценарий использования, в котором пользователи могут настраивать ссылку выше, мы можем рассмотреть возможность динамического построения массива ссылок. Кроме того, хотя это и не рассматривается в этом конкретном примере, мы все же можем заметить, что этот компонент не тесно связан с каким-либо конкретным родительским/дочерним компонентом. Его можно рендерить везде, где это необходимо. Улучшенные компоненты значительно более пригодны для повторного использования, чем исходная версия.
Если речь не идет о разработке компонента, который должен обслуживать конкретный одноразовый сценарий, то конечной целью разработки компонента является сделать его слабо связанным с родительским компонентом, демонстрируя лучшую возможность повторного использования, а не ограничиваясь конкретным контекстом. .
Разделение вспомогательного кода
Это может быть не так теоретически, но я все еще думаю, что это важно. Работа с вашей кодовой базой является частью разработки программного обеспечения, и иногда некоторые базовые принципы организации могут упростить работу. Со временем даже небольшое изменение привычки может иметь большое значение. Один из эффективных принципов — выделить вспомогательный код и разместить его в определенном месте, чтобы не думать о нем при работе с компонентами. Вот некоторые аспекты:
- код конфигурации
- поддельные данные
- Обширная нетехническая документация
Потому что, пытаясь работать над основным кодом компонента, вы не хотите видеть инструкции, которые технически не связаны (из-за нескольких лишних прокруток колесика мыши или даже прерывания мышления). Имея дело с компонентами, вы хотите, чтобы они были как можно более универсальными и пригодными для повторного использования. Просмотр конкретной информации, связанной с текущим контекстом компонента, может затруднить разработку компонентов, которые нелегко отделить от конкретного бизнеса.
Изысканная сущность
Хотя это может быть сложно сделать, хороший способ разработки компонентов состоит в том, чтобы они содержали минимальный Javascript, необходимый для их рендеринга. Некоторые тривиальные вещи, такие как выборка данных, обработка данных или логика обработки событий, в идеале должны перемещать общие части во внешние js или или в общего предка.
Только из части «представления» раздела компонента это то, что вы видите (html и стили). Javascript в нем используется только для визуализации представления,возможноСуществует также некоторая специфичная для компонента логика (например, при использовании в другом месте). Все, кроме этого, например вызовы API, форматирование значений (например, валюты или времени) или повторное использование данных между компонентами, можно перенести во внешние файлы js. Давайте рассмотрим простой пример в Vue с использованием компонентов вложенного списка. Давайте взглянем на проблемную версию ниже.
Это первый уровень:
Вот компонент вложенного списка:
Здесь мы видим, что оба уровня этого списка имеют внешние зависимости, верхний уровень импортирует функции из внешних файлов js и данные из файлов JSON, вложенные компоненты подключаются к хранилищу Vuex и отправляют запросы с помощью axios. У них также есть возможности встраивания, которые применяются только к текущей сцене (специфические возможности ответа для обработки исходных данных в верхнем слое и умеренное время щелчка для вложенных списков).
Хотя здесь используются некоторые хорошие общие методы проектирования, такие как перемещение общих методов обработки данных во внешние сценарии вместо прямого жесткого кодирования функций, это все еще не очень удобно для повторного использования. Что, если мы получим данные из ответа API, но данные не той структуры или типа, которые мы ожидаем? Или мы ожидаем другого поведения при нажатии на вложенные элементы? В сценарии, где встречаются эти требования, другие компоненты не могут напрямую ссылаться на этот компонент и изменяют свои характеристики в соответствии с фактическими требованиями.
Давайте посмотрим, сможем ли мы исправить это, подняв данные и передав обработку событий в качестве свойств, чтобы компонент мог просто отображать данные без инкапсуляции какой-либо дополнительной логики.
Вот улучшенный первый уровень:
И новый второй уровень:
Используя этот новый список, мы можем получить нужные данные и определить обработчик onClick для вложенного списка, который будет передавать любые действия, которые мы хотим, в родительском элементе, которые мы затем передаем в качестве свойств компоненту верхнего уровня. Таким образом, мы можем оставить импорт и логику одному корневому компоненту, поэтому нам не нужно повторно реализовывать аналогичный компонент, чтобы его можно было использовать в новых сценариях.
Небольшую статью на эту тему можно найти наздесьоказаться. Он был написан автором Redux Дэном Абрамовым, хотя и с использованием React в качестве примера. Но идея компонентного дизайна общая.
Модульность во времени
Когда мы фактически выполняем работу по извлечению компонентов, нам нужно подумать о том, чтобы не чрезмерно разбивать их на компоненты.Хорошей практикой является превращение больших блоков кода в слабосвязанные и пригодные для использования части, но не все структуры страниц (части HTML). извлекаются в компоненты, и не все логические части необходимо извлекать из компонентов.
Вот несколько вещей, которые следует учитывать при решении того, отделить ли ваш код, будь то логика JavaScript или выделяется на новые компоненты. Опять же, этот список не завершен, просто чтобы дать вам представление о различных вещах. (Помните, только потому, что это не удовлетворяет одно условие, не означает, что он не удовлетворяет остальным, поэтому рассмотрите все условия, прежде чем принимать решение):
- Достаточно ли структуры/логики страницы, чтобы гарантировать это?Если это всего несколько строк кода, вы можете в конечном итоге создать больше кода, чтобы отделить его, чем просто поместить код в него.
- Дублирование кода (или возможное дублирование)?Если что-то используется только один раз и служит конкретному варианту использования, который вряд ли будет использоваться где-либо еще, может быть лучше встроить это. Вы всегда можете отделить его, если вам нужно (но не используйте это как предлог для лени, когда вам нужно сделать работу).
- Сократит ли это шаблоны, которые необходимо написать?Например, предположим, вам нужна структура атрибутов DIV с определенным стилем и некоторыми компонентами статического содержимого/функции, где внутри вложено некоторое переменное содержимое. Вы можете уменьшить код шаблона при создании нескольких экземпляров этих компонентов, потому что вам не нужно переписывать внешний код упаковки при создании нескольких экземпляров этих компонентов.
- Будет ли затронута производительность?Изменение состояния/реквизитов приведет к повторному рендерингу, и когда это произойдет, все, что вам нужно, — это повторно рендерить соответствующие узлы элементов после diff. В более крупных, тесно связанных компонентах вы можете обнаружить, что изменения состояния вызывают повторную визуализацию во многих местах, где это не требуется, и в этот момент производительность вашего приложения может начать страдать.
- У вас возникли проблемы с тестированием всех частей вашего кода?Мы всегда хотим иметь возможность провести адекватное тестирование, например, для компонента, мы ожидаем, что он будет работать без конкретного варианта использования (контекста), и вся логика Javascript работает должным образом. Это затрудняет оправдание наших ожиданий, когда элементы имеют определенный предполагаемый контекст или каждый из них включает целую кучу логики в одну функцию. Тестирование рендеринга компонентов также может быть затруднено, если тестируемый компонент является одним гигантским компонентом с относительно большими шаблонами и стилями.
- У вас есть четкая причина?Разбивая свой код, вы должны учитывать, чего именно он достигает. Означает ли это более слабую связь? Я нарушаю логически значимую отдельную сущность? Действительно ли возможно, что этот код может быть повторно использован в другом месте? Если вы не можете четко ответить на этот вопрос, лучше пока не заниматься извлечением компонентов. Потому что это может привести к некоторым проблемам (таким как демонтаж некоторых потенциальных взаимосвязей).
- Преимущества перевешивают затраты?Разделение кода неизбежно требует времени и усилий, количество которых варьируется в каждом конкретном случае, и есть ряд факторов (например, некоторые из этого списка), которые влияют на принятие этого решения в конце. В целом, проведение некоторых исследований затрат и выгод абстракции может помочь принять более быстрые и точные решения о необходимости компонентизации. Наконец, я упоминаю об этом, потому что, если мы слишком сосредоточимся на сильных сторонах, легко забыть об усилиях, необходимых для их достижения, поэтому перед принятием решения возникает компромисс между ними.
Централизованное/единое государственное управление
Многие крупные приложения используют инструменты управления состоянием, такие как Redux или Vuex (или имеют настройку совместного использования состояния, например Context API в React). Это означает, что они получают реквизиты из хранилища, а не через родителя. При рассмотрении возможности повторного использования компонентов вы должны учитывать не только реквизиты, переданные от непосредственного родителя, но и реквизиты, извлеченные из хранилища. Если вы используете компонент в другом проекте, вам нужно будет использовать эти значения в магазине. Возможно, другие проекты вообще не используют централизованное хранилище, и вам нужно преобразовать его в форму реквизита, передаваемого от родителя.
Поскольку подключение компонентов к хранилищу (или контексту) выполняется легко и независимо от иерархического положения компонента, легко быстро создать тесную связь между компонентами хранилища и веб-приложением (неважно, на каком уровне находится компонент). компоненты находятся внутри) ). Связывание компонента с хранилищем обычно занимает всего несколько строк кода. Но учтите, что хоть это соединение (сопряжение) и более удобное, но смысл его не отличается, а также нужно учитывать максимальное совпадение точек, как при использовании метода воспитания.
наконец
Я хочу напомнить всем, что вы должны уделять больше внимания вышеизложенным принципам проектирования компонентов и применению некоторых из лучших практик, которые вы знаете. Хотя вы должны сделать все возможное, чтобы сохранить хороший дизайн, не нарушайте целостность кода, чтобы обернуть билет JIRA или запрос на отмену, и люди, которые всегда ставят теорию выше реальных результатов, также, как правило, подвергают свою работу риску. Крупные программные проекты имеют много движущихся частей, и есть много аспектов разработки программного обеспечения, которые не связаны конкретно с кодированием, но все же являются неотъемлемой частью, например, соблюдение сроков и работа с нетехническими ожиданиями.
Хотя адекватная подготовка важна и должна быть частью любой профессиональной разработки программного обеспечения, в реальном мире важны ощутимые результаты. Когда вас нанимают, чтобы действительно что-то создать, если все, что у вас есть до дедлайна, — это потрясающая идея о том, как создать идеальный продукт.строить планы, но никаких реальных результатов, ваш работодатель может быть не слишком доволен? Кроме того, в программной инженерии дела редко идут точно по плану, поэтому чрезмерно конкретное планирование имеет тенденцию быть контрпродуктивным с точки зрения использования времени.
Кроме того, концепции планирования и проектирования компонентов также применимы к рефакторингу компонентов. Хотя было бы неплохо потратить 50 лет на планирование всех мучительных деталей, а затем написать их безупречно с самого начала, в реальном мире мы склонны сталкиваться с ситуациями, когда пытаемся опередить график. код идеален, как и ожидалось. Однако, как только у нас появится свободное время, рекомендуется вернуться назад и провести рефакторинг далеко не идеального кода ранее, чтобы он мог служить прочной основой для нашего движения вперед.
В конце концов, хотя вашей непосредственной обязанностью может быть «написание кода», вы не должны упускать из виду свою конечную цель — создать что-то. Создайте продукт. Чтобы производить что-то, чем можно гордиться и помогать другим, даже если это технически несовершенно, всегда помните о балансе. К сожалению, глядя на код перед собой по 8 часов в день в течение недели, ваше видение и перспектива станут более «узкими», и в это время вам нужно сделать шаг назад и убедиться, что вы не прогадали. весь лес на одно дерево. .