В этой статье я хочу поделиться некоторыми советами и приемами, которые я использую при создании нашей основной библиотеки внешнего интерфейса с использованием Ember.js. Не имея контакта с ним раньше, это была отличная возможность для обучения. Надеюсь, вам понравится! Обратите внимание, что код, используемый для иллюстрации идей в статье, содержит достаточно информации, чтобы донести мысль. Он также использует некоторую терминологию Ember.js, но концепции должны быть независимыми от фреймворка.

Цели

Проще говоря, требования для сборки библиотеки следующие:

  1. Это должно быть продуктивно.
  2. Он должен быть ремонтопригодным.
  3. Он должен быть последовательным.

Подходы

Минимизируйте бизнес-логику

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

Перед реализацией какой-либо функциональности полезно обрисовать некоторые обязанности, за которые отвечает компонент.

Представьте, что мы создаем компонент-кнопку.

Я бы хотел уметь:

  • Сообщите, какой это тип кнопки - основная или обычная
  • Сообщите содержимое, отображаемое внутри кнопки (значок и текст)
  • Отключить или включить кнопку
  • Выполнять какое-то действие при нажатии

Имея этот небольшой набросок, разделите различные части, участвующие в процессе создания этого компонента. Постарайтесь определить, где можно разместить вещи.

1 - Тип и содержимое зависят от компонента, поэтому их можно поместить в файл компонента.

Поскольку тип в некоторой степени является обязательным, давайте добавим проверку на случай, если значение не было предоставлено.

const type = get(this, 'type');
const types = {
  primary: 'btn--primary',
  regular: 'btn--regular',
}
return (type) ? types[type] : types.regular;

Мне нравится отображать свойства в объекте, потому что это позволяет масштабировать объекты без особых усилий - на случай, если нам понадобится кнопка опасности или что-то в этом роде.

2 - Отключенное состояние можно найти на разных компонентах, например, на входе. Чтобы избежать повторения, это поведение можно перенести в модуль или любую общую структуру - люди называют это миксином.

3 - Действие щелчка можно найти в разных компонентах. Таким образом, он также может быть перемещен в другой файл и не должен содержать внутри себя логики - просто вызывая обратный вызов, предоставленный разработчиком.

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

Отдельное многоразовое состояние пользовательского интерфейса

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

  • Включить / отключить - например. кнопки, входы
  • Развернуть / Сжать - например. свернуть, раскрывающиеся списки
  • Показать / скрыть - Практически все

Надеюсь, эти свойства часто используются только для управления визуальным состоянием.

Поддерживайте единообразную номенклатуру различных компонентов. Все действия, связанные с визуальным состоянием, можно перенести в миксин.

/* UIStateMixin */
disable() {
  set(this, ‘disabled’, true);
  return this;
},
enable() {
  set(this, 'disabled', false');
  return this;
},

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

button
  .disable()
  .showLoadingIndicator();

Этот подход можно расширить. Он может принимать разные контексты и управлять внешними переменными вместо использования внутренних. Например:

_getCurrentDisabledAttr() {
  return (isPresent(get(this, 'disabled')))
    ? 'disabled'            /*  External parameter  */
    : 'isDisabled';         /*  Internal variable   */
},
enable(context) {
  set(context || this, this._getCurrentDisabledAttr(), false);
  return this;
}

Абстрагирование базовых функций

Каждый компонент содержит определенные процедуры. Эти процедуры должны выполняться независимо от назначения компонента. Например, проверка обратного вызова перед его запуском.

Эти методы по умолчанию также можно переместить в их собственные миксины, например:

/* BaseComponentMixin */
_isCallbackValid(callbackName) {
  const callback = get(this, callbackName);
  
  return !!(isPresent(callback) && typeof callback === 'function');
},
_handleCallback(callback, params) {
  if (!this._isCallbackValid(callback)) {
    throw new Error(/* message */);
  }
  this.sendAction(callback, params);
},

А потом включили в компоненты.

/* Component */
onClick(params) {
  this._handleCallback('onClick', params);
}

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

Составление компонентов

По возможности избегайте переписывания функциональности. Специализация достижима. Это можно сделать с помощью композиции и группировки. А также настраивать вместе более мелкие компоненты для создания новых.

Например:

Base components: Button, dropdown, input.
Dropdown button => button + dropdown
Autocomplete => input + dropdown
Select => input (readonly) + dropdown

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

Максимальное разделение проблем.

Разделение проблем

При составлении более сложных компонентов существует возможность разделения задач. Вы можете разделить проблемы между разными частями компонента.

Допустим, мы создаем выбранный компонент.

{{form-select binding=productId items=items}}
items = [
  { description: 'Product #1', value: 1 },
  { description: 'Product #2', value: 2 }
]

Внутри у нас есть простой компонент ввода и раскрывающийся список.

{{form-input binding=_description}}
{{ui-dropdown items=items onSelect=(action 'selectItem')}}

Наша основная задача - представить описание пользователю, но оно не имеет значения для нашего приложения - имеет значение.

При выборе параметра вы разделяете объект, отправляя описание на наш ввод через внутреннюю переменную, одновременно передавая значение контроллеру, обновляя связанную переменную.

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

Риски возрастают по мере усложнения преобразований. Из-за чрезмерной логики или необходимости поддерживать события - так что подумайте, прежде чем реализовывать этот подход.

Пресеты против новых компонентов

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

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

Сложнее всего знать, когда применять предустановки или создавать новые компоненты. Принимая это решение, я руководствуюсь следующими принципами:

Когда создавать предустановки

1 - Повторяющиеся шаблоны использования

Бывают случаи, когда конкретный компонент повторно используется в разных местах с одинаковыми параметрами. В этих случаях я предпочитаю пресеты новым компонентам, особенно когда базовый компонент имеет чрезмерное количество параметров.

/* Regular implementation */
{{form-autocomplete
    binding=productId
    url="products"            /*   URL to be fetched         */
    labelAttr="description"   /*   Attribute used as label   */
    valueAttr="id"            /*   Attribute used as value   */
    apiAttr="product"         /*   Param sent on request     */
}}
/* Presets */
{{form-autocomplete
    preset="product"
    binding=productId
}}

Значения из предустановки устанавливаются только в том случае, если параметр не был проинформирован, сохраняя его гибкость.

/* Naive implementation of the presets module */
const presets = {
  product: {
    url: ‘products’,
    labelAttr: ‘description’,
    valueAttr: ‘id’,
    apiAttr: ‘product’,
  }, 
}
const attrs = presets[get(this, ‘preset’)];
Object.keys(attrs).forEach((prop) => {
  if (!get(this, prop)) {
    set(this, prop, attrs[prop]);
  }
});

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

2 - Базовый компонент слишком сложный

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

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

Когда создавать новые компоненты

1 - Расширение функциональности

При расширении функциональности более простого компонента целесообразно создать новый компонент. Это помогает предотвратить передачу логики конкретного компонента другому компоненту. Это особенно полезно при реализации дополнительного поведения.

/* Declaration */
{{ui-button-dropdown items=items}}
/* Under the hood */
{{#ui-button onClick=(action 'toggleDropdown')}}
  {{label}} <i class="fa fa-chevron-down"></i>  
{{/ui-button}}
{{#if isExpanded}}
  {{ui-dropdown items=items}}
{{/if}}

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

2 - Параметры декорирования

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

/* Declaration */
{{form-datepicker onFocus=(action 'doSomething')}}
/* Under the hood */
{{form-input onFocus=(action '_onFocus')}}
_onFocus() {
  $(this.element)
    .find('input')
    .select();                 /* Select field value on focus */
  this._handleCallback('onFocus'); /* Triggers param callback */
}

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

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

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

В моем случае событие onBlur было заменено другим событием - onChange. Это срабатывает, когда пользователь либо заполняет поле, либо выбирает дату в календаре.

Заключение

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

Наилучший результат достигается, когда каждый в группе делает то, что лучше всего для себя и группы - Джон Нэш

Кроме того, не стесняйтесь спрашивать об обратной связи. Вы всегда найдете то, над чем можно поработать.

Чтобы еще больше отточить свои навыки разработки программного обеспечения, я рекомендую ознакомиться с серией статей Эрика Эллиота «Создание программного обеспечения». Замечательно!

Что ж, надеюсь, вам понравилась статья. Воспользуйтесь этими концепциями, воплотите в жизнь свои собственные идеи и поделитесь ими с нами!

Также не стесняйтесь обращаться ко мне в твиттере @gcolombo_! Я хотел бы услышать ваше мнение и даже поработать вместе.

Спасибо!