Цифровой элемент
10 минут на чтение
1586
Отправь статью на почту?

Работа с ES6 классами

Подписаться

В стандарте JavaScript под названием ECMAScript 6 в языке появилась возможность нативной реализации ООП парадигмы, а именно, реализация таких сущностей как классы, описывающие структуру свойств и поведения одного типа объектов.

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

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

В нашей статье мы приведём общую структуру стандартного компонента и реализацию определенных его частей.

Содержание



Привязка событий и их обработка

export default class MyClass {
  constructor() {
    this.btn = document.querySelector('#some-btn')
    this.bindEvents()
  }

  someAction() {
    // ...
  }

  handleBtnClick(e) {
    e.preventDefault()
    // в handle-методе вызываются другие методы
    this.someAction()
  }

  handleClick(e) {
    // ...
  }

  bindEvents() {
    // все обработчики событий пишутся в формате handleEventNameEventType
    this.btn.addEventListener('click', (e) => this.handleBtnClick(e))

    // или, если привязываются к document/window, в формате handleEventType
    document.addEventListener('click', (e) => this.handleClick(e))
  }
}

При инициализации класса происходит выполнение кода в конструкторе.

В данном случае вызывается метод bindEvents, отвечающий за привязку обработчиков событий. В методе-обработчике не желательно прописывать сложную логику, лучше вызывать другие методы, внутри которых описывается функционал, т. к. одно и то же действие может быть результатом нескольких типов событий (например, ‘click’ и ‘change’).

Работа с состоянием

export default class MyClass {
  constructor() {
    // все вычисляемые при инициализации параметры состояния пишутся внутри конструктора
    this.state = {
      isOpen: false,
      someResults: [2, 4 + 1, 2 ** 3]
    }
    // ...
  }

  toggle() {
    // проверка состояния по значению в state (а не по css-классу)
    this.state.isOpen ?
      this.close() :
      this.open()
  }

  open() {
    // своевременное изменение state
    this.state.isOpen = true
    // ...
  }

  close() {
    // своевременное изменение state
    this.state.isOpen = false
    // ...
  }
}

Дефолтное состояние компонента пишется вне конструктора. Вычисляемые параметры (например, зависящие от входных данных) пишутся внутри конструктора.

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

Экспорт

// является экспортируемой переменной для возможности её использования в других частях кода
export const instance = '[data-js-select]'

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

Импорт

// импорт вспомогательных функций
import { getCfg } from './utils/getCfg'
import { makeRequest } from './utils/makeRequest'

// импорт переменных из файла другого компонента
import { instance as selectInstance } from '../components/select/index'

export default class MyClass {
  // ...
  
  someMethod() {
    makeRequest({params}).then(() => {
      // ...
    })
  }
  
  // ...
}

Все утилитарные функции находятся в отдельной папке и при необходимости импортируются в компонент.

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

Статические и асинхронные методы

export default class MyClass {
  // ...

  // асинхронный метод для получения данных
  async getData() {
    // ...
  }

  // не имеет this, можно обращаться прямо: MyClass.myMethod
  static myMethod() {
    // ...
  }

  // ...
}

Статические методы не имеют this, доступны вне класса, к ним можно обращаться напрямую: MyClass.myMethod().

Наследование

export default class MyClass {
  // ...
  
  someAction() { 
    // ...
  }
  
  toggle() {
    // ...
  }
  
  close() {
    // ...
  }
  
  open() {
    // ...
  }
  
  // ...
}

export class ChildClass extends MyClass {
  constructor() {
    super()
    // внутри класса ChildClass будет доступен любой метод класса MyClass
    this.toggle()
  }
}

Внутри класса ChildClass будет доступен любой метод класса MyClass.

Пример листинга кода классового компонента с комментированием множества code-style паттернов

 
// импорт вспомогательных функций
import { getCfg } from './utils/getCfg'
import { makeRequest } from './utils/makeRequest'

// instance - селектор корневого элемента компонента
// является экспортируемой переменной для возможности её использования в других частях кода
export const instance = '[data-js-my-class]'

// bubbles - список кастомных событий в отдельной экспортируемой переменной
// может использоваться как в текущем, так и в других компонентах
export const bubbles = {
  someEvent: 'myClass:someEvent',
  someExtraEvent: 'myClass:someExtraEvent',
}

export default class MyClass {
  // все селекторы элементов компонента находятся в els
  // каждый селектор является атрибутом формата data-js-component-name-*
  els = {
    instance,
    someBtn: '[data-js-my-class-some-btn]',
    someInput: '[data-js-my-class-some-input]',
  }

  // все классы состояний находятся в stateClasses
  // каждый класс в формате isSomething: 'is-something'
  stateClasses = {
    isOpen: 'is-open',
  }

  // конфигурация компонента по умолчанию
  defaultCfg = {
    url: './json/example.json',
    method: 'get',
    type: 'json',
  }

  constructor() {
    this.instance = document.querySelector(instance)

    // проверка наличия DOM-элемента инстанса на странице
    // если не найден, то прекращаем дальнейшую работу компонента
    if (!this.instance) return

    // поиск внутренних элементов компонента внутри инстанса
    this.someBtn = this.instance.querySelector(this.els.someBtn)
    this.someInput = this.instance.querySelector(this.els.someInput)

    // getCfg - вспомогательная функция, которая принимает на вход DOM-элемент,
    // data-атрибут в формате строки '[data-js-my-class]' и объект дефолтных параметров.
    // Функция парсит значение атрибута, которое представляет собой строку в JSON формате,
    // затем объединяет с defaultCfg и возвращает объект с итоговой конфигурацией компонента
    this.cfg = getCfg(this.instance, this.els.instance, this.defaultCfg)

    // все параметры состояния находятся в state
    this.state = {
      isOpen: false,
      someResults: []
    }

    // вызов метода инициализации
    this.init()

    // вызов метода привязки событий
    this.bindEvents()
  }

  someAction() {
    // ...
  }

  toggle() {
    // проверка состояния по значению в state (а не по css-классу)
    this.state.isOpen ?
      this.close() :
      this.open()
  }

  open() {
    // своевременное изменение state
    this.state.isOpen = true
    this.instance.classList.add(this.stateClasses.isOpen)
    // ...
  }

  close() {
    // своевременное изменение state
    this.state.isOpen = false
    this.instance.classList.remove(this.stateClasses.isOpen)
    // ...
  }

  // асинхронный метод для получения данных
  async getData() {
    // деструктуризация везде, где это удобно
    const {url, method, type} = this.cfg

    return makeRequest({
      url,
      method,
      type
    })
  }

  // не имеет this, можно обращаться прямо: MyClass.myMethod()
  static myMethod() {
    // ...
  }

  // остальные методы
  // ...

  handleSomeBtnClick(e) {
    e.preventDefault()
    // в handle-методе вызываются другие методы
    this.someAction()
    this.toggle()
  }

  handleSomeInputChange() {
    // ...
  }

  init() {
    // ...
  }

  bindEvents() {
    // все обработчики событий пишутся в формате handleElementNameEventType
    this.someBtn.addEventListener('click', (e) => this.handleSomeBtnClick(e))
    this.someInput.addEventListener('change', (e) => this.handleSomeInputChange(e))
  }
}

export class ChildClass extends MyClass {
  constructor() {
    super()
    // внутри класса ChildClass будет доступен любой метод класса MyClass
    this.toggle()
  }
}

Входная точка bundle.js

// импорты компонентов
// ...

// Глобальный объект App предназначен для хранения сервисной информации
// и дальнейшей привязки компонентов
window.App = {
  lang: getCurrentLang(),
  scrollableBody: 'body',
  stateClasses: {
    domReady: 'dom-is-ready',
    pageLoaded: 'page-is-loaded',
    touchscreen: 'is-touchscreen',
    mobileDevice: 'is-mobile-device'
  },
  ...parseJSON(document.body.getAttribute('data-js-app-globals')),
}

document.addEventListener('DOMContentLoaded', () => {
  // standalone components
  new SvgUse()
  new Btn()
  new Modals()
  new Forms()
  new Gallery()

  // app components
  App.SlidersCollection = new SlidersCollection()
  App.GoogleCaptchaCollection = new GoogleCaptchaCollection()
  App.AdaptiveTables = new AdaptiveTables()
  App.FileAttachCollection = new FileAttachCollection()
  App.SelectCollection = new SelectCollection()
  App.AccordionCollection = new AccordionCollection()
  App.TabsCollection = new TabsCollection()
  App.PasswordCollection = new PasswordCollection()
})

Для управления классовыми компонентами и для доступа к ним из любой строки кода приложения используется глобальная переменная window.App.

Внутри App.js классовые компоненты могут вызываться как с привязкой к window.App, так и без.

Коллекция компонентов

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

В дальнейшем этот класс будет являться родителем каждого компонента, у которого больше одного инстанса на странице.

Конфигурация класса коллекции:

export const defaultCfg = {
  excludedSelector: '[data-js-observer-exclude]',
  observer: {
    attributes: false,
    childList: true,
    subtree: false
  },
  callbacks: {
    add: () => {
    },
    remove: () => {
    }
  }
}

Конфигурация коллекции вынесена в отдельную экспортируемую переменную. Содержит в себе параметры observer-слушателя, селектор исключения из слушателя, а также опциональные функции-коллбеки.

Начало класса Collection:

export default class Collection {
  /**
   * Сама коллекция экземпляров
   * @type {Array}
   * @private
   */
  _collection = []

  /**
   * MutationObserver для отслеживания и удаления из коллекции динамически удаляемых экземпляров
   * @type{MutationObserver}
   */
  collectionObserver

  /**
   * Конфигурация для MutationObserver
   * @type {MutationObserverInit}
   */
  collectionObserverConfig = {
    attributes: false,
    childList: true,
    subtree: false
  }

  /**
   * Исключаемый из наблюдаения элемент
   * @type{String}
   */
  collectionObserverExcludedSelector = '[data-js-observer-exclude]'

  /**
   * Наблюдаемый элемент
   * @type{Element}
   */
  collectionObserverTarget = document.body

  /**
   * Селектор по которому отслеживается динамически изменяемый контент
   * @type{String}
   */
  collectionObserverInstance

  /**
   * Класс, экземпляр которого будет создан при появлении в DOM-дереве
   * @type{Class}
   */
  collectionObserverClass
  
  // ...
}

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

Конструктор, геттеры и сеттеры:

export default class Collection {
  // ...

  /**
   *
   * @param instance{String} - см. collectionObserverInstance
   * @param _class{Class} - см. collectionObserverClass
   */
  constructor(instance, _class) {
    this.collectionObserverInstance = instance
    this.collectionObserverClass = _class
    this.collectionObserve()
  }

  /**
   * Добавляет экземпляр в коллекцию. По-умолчанию проверяет, существует ли экземпляр с таким instance.
   * Если существует, то добавления не происходит (возможно, стоит что-то делать в таком случае)
   * @param newCollectionItem{Class}
   */
  set collection(newCollectionItem) {
    const itemInCollection = this.getByDOMElement(newCollectionItem.instance)
    if (!itemInCollection) {
      this._collection = [...this._collection, newCollectionItem]
    }
  }

  /**
   * Публичная коллекция
   * @return {Array}
   */
  get collection() {
    return this._collection
  }

  /**
   * Получает конфигурацию коллекции
   * @return {Object}
   */
  get config() {
    return defaultCfg
  }

  // ...
}

Конструктор коллекции принимает строку в качестве инстанса и сам класс.

Когда мы наследуемся от класса коллекции через extends, то в конструкторе вызываем super(instance, MyClass), что и передается в итоге в конструктор коллекции.

После присвоения этих переменных коллекция запускает метод collecbionObserve, который будет инициализирует MutationObserver и запускает наблюдение за DOM-узлами.

Сеттер collection необходим для удобного добавление экземпляра в коллекцию. Геттер collection нужен для получения публичной коллекции экземпляров. Геттер конфигурации необходим для получения объекта с параметрами, объявленными ранее в экспортируемой переменной defaultCfg.

Методы класса коллекции, часть 1:

export default class Collection {
  // ...

  /**
   * Проверяет DOM-исключения для callback-наблюдателя
   * @return {Boolean}
   */
  isExcludedMutationRecord(mutationList) {
    return mutationList.some(({target}) => target.nodeType === 1 && target.closest(this.config.excludedSelector))
  }

  /**
   * Ищет внутри коллекции по DOM-элементу. У экзепляров класса должен быть параметр instance, по нему идет проверка
   * @param DOMElement{Element}
   * @returns {Class}
   */
  getByDOMElement(DOMElement) {
    return this.collection.find(item => item.instance === DOMElement)
  }

  /**
   * Удаление из коллекции по экземпляру класса
   * @param collectionItem{Class}
   * @param callback{function}
   */
  removeFromCollection(collectionItem, callback = this.config.callbacks.remove) {
    const collectionItemIndex = this.collection.indexOf(collectionItem)
    this._collection.splice(collectionItemIndex, 1)
    if (typeof callback === 'function') {
      callback(collectionItem)
    }
  }

  /**
   * Удаление из коллекции по DOMElement'у
   * @param DOMElement{Element}
   * @param callback{function}
   */
  removeFromCollectionByDOMElement(DOMElement, callback = this.config.callbacks.remove) {
    const collectionItemIndex = this.collection.findIndex(collectionItem => collectionItem.instance.isEqualNode(DOMElement))
    this._collection.splice(collectionItemIndex, 1)
    if (typeof callback === 'function') {
      callback(DOMElement)
    }
  }

  /**
   * Инициализириует MutationObserver и запускает наблюдение
   */
  collectionObserve() {
    this.collectionObserver = new MutationObserver(this.collectionObserveCallback.bind(this))
    this.collectionObserver.observe(this.collectionObserverTarget, this.collectionObserverConfig)
  }
  
  // ...
}

Методы класса коллекции, часть 2:

export default class Collection {
  // ...

  /**
   * Callback-наблюдатель
   * @param mutationsList{MutationRecord}
   * @param observer{MutationObserver}
   */
  collectionObserveCallback(mutationsList, observer) {
    if (!this.isExcludedMutationRecord(mutationsList)) {
      this.collectionObserveRemoving()
      this.collectionObserveAdding()
    }
  }

  /**
   * Метод проверяет присутствует ли DOMElement на странице после изменений
   * и в случае отсутствия удаляет его из коллекции
   */
  collectionObserveRemoving() {
    this.collection.forEach(collectionItem => {
      if (!this.collectionObserverTarget.contains(collectionItem.instance)) {
        this.removeFromCollection(collectionItem)
      }
    })
  }

  /**
   * Добавляет элемент в коллекцию
   * @param instance(Object}
   * @param callback{function}
   */
  addToCollection(instance, callback = this.config.callbacks.add) {
    const itemInCollection = this.getByDOMElement(instance)
    if (!itemInCollection && this.collectionObserverClass) {
      this.collection = new this.collectionObserverClass(instance)
      if (typeof callback === 'function') {
        callback(instance)
      }
    }
  }

  /**
   * Метод проверяет появился ли DOMElement на странице после изменений
   * и в случае его появления добавляет его в коллекцию
   */
  collectionObserveAdding() {
    if (this.collectionObserverInstance) {
      const instances = this.collectionObserverTarget.querySelectorAll(this.collectionObserverInstance)
      for (let instance of instances) {
        this.addToCollection(instance)
      }
    }
  }
}

Управление классом без коллекции

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

Управление классом с коллекцией

Управление классом с коллекцией
Управление классом с коллекцией

Если компонент неоднократно повторяется на странице, например, кастомный select, то компонент можно реализовать через коллекцию SelectCollection, наследуемый от класса коллекции Collection. Каждый экземпляр коллекции будет являться отдельным классом Select со своей изолированной от других экземпляров коллекции логикой.

Класс, реализованный через коллекцию:

export const instance = '[data-js-my-class]'

export class MyClass {
  els = {
    instance,
  }

  constructor(instance) {
    this.instance = instance
    // ...
  }

  // ...
}

export class MyClassCollection extends Collection {
  constructor() {
    super(instance, MyClass)
    this.init()
  }

  init(context = document) {
    context.querySelectorAll(instance).forEach((el) => {
      this.collection = new MyClass(el)
    })
  }
}

При реализации компонента через коллекцию в конструктор класса MyClass попадает DOM-элемент.

Обращение к экземпляру коллекции и выполнение его метода возможно через: App.MyClassCollection.getByDOMElement(document.querySelector('#id')).someMethod()

Заключение:

Написание кода в виде классовых компонентов позволяет пользоваться всеми преимуществами ООП:

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

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

Мне не нравится
Россия, Челябинская область, Челябинск, ул. Энтузиастов, 2, оф. 200 Телефон: +7 (351) 220-45-35

Читайте в нашем блоге

Все статьи
Что будет, если не продлить лицензию Битрикс24?

Что будет, если не продлить лицензию Битрикс24?

Битрикс24 – это платформа, которая помогает организовать и автоматизировать бизнес-процессы компании, обеспечивая удобство использ...

04.03.2024
152
Как настроить редирект через .htaccess

Как настроить редирект через .htaccess

Редирект можно настроить разными способами: в панели управления хостингом, через код HTML, через PHP, с помощью web.config, через .htaccess, а ...

04.03.2024
122
Как восстановить доступ в панель администрирования сайта на 1С-Битрикс?

Как восстановить доступ в панель администрирования сайта на 1С-Битрикс?

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

21.12.2023
727
Как создать аккаунт разработчика в App Store, Google Play, AppGallery

Как создать аккаунт разработчика в App Store, Google Play, AppGallery

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

08.12.2023
1844
Файлы-куки: как правильно информировать пользователей и избежать штрафов

Файлы-куки: как правильно информировать пользователей и избежать штрафов

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

06.12.2023
393
Безопасность сайта: поиск вирусов и троянов

Безопасность сайта: поиск вирусов и троянов

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

20.11.2023
803
«Цифровой Элемент» принял участие в Русском Экономическом Форуме

«Цифровой Элемент» принял участие в Русском Экономическом Форуме

Форум посвящен масштабным вопросам развития суверенной экономики России в XXI веке. Среди основных тем: импортозамещение, технологическое разви...

13.11.2023
356
Новый закон о запрете регистрации на российских сайтах с помощью иностранных электронных почтовых сервисов

Новый закон о запрете регистрации на российских сайтах с помощью иностранных электронных почтовых сервисов

Давайте разберемся, что это значит для владельцев сайтов и пользователей.

Что именно предписывает закон

С 1 декабря ...

09.11.2023
1087