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

Работа с 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

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

Все статьи
11 лучших платформ для онлайн-совещаний

11 лучших платформ для онлайн-совещаний

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

24.12.2024
5
Лучшие бесплатные и условно-бесплатные аналоги Figma в 2024 году

Лучшие бесплатные и условно-бесплатные аналоги Figma в 2024 году

Figma — один из наиболее востребованных инструментов для создания интерфейсов и прототипов в сфере веб-дизайна и разработки мобильных приложени...

10.12.2024
339
Интеграция Битрикс24 и Asterisk

Интеграция Битрикс24 и Asterisk

Современные компании всё чаще сталкиваются с необходимостью интеграции IP-телефонии и CRM-систем для улучшения управления клиентскими коммуника...

29.11.2024
397
Хостинг. Лучшие хостинг-провайдеры в России

Хостинг. Лучшие хостинг-провайдеры в России

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

19.11.2024
538
Региональное продвижение сайта: поддомены или подпапки

Региональное продвижение сайта: поддомены или подпапки

Запросы пользователей в поисковых системах делятся на две категории: геозависимые и геонезависимые. Геозависимые запросы — это...

29.10.2024
1337
Битрикс24 vs Microsoft SharePoint: Как выбрать оптимальную платформу для вашего бизнеса?

Битрикс24 vs Microsoft SharePoint: Как выбрать оптимальную платформу для вашего бизнеса?

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

10.09.2024
1690
Внедрение системы управления персоналом и автоматизация: обзор HRM-систем

Внедрение системы управления персоналом и автоматизация: обзор HRM-систем

В современных условиях успешное управление персоналом невозможно без эффективных цифровых инструментов. HRM-системы играют ключевую роль в орга...

06.09.2024
1769
ELMA365 CRM – система автоматизации и управления бизнес процессами

ELMA365 CRM – система автоматизации и управления бизнес процессами

ELMA365 CRM – это мощная и гибкая CRM-BPM система, разработанная российской компанией ELMA, специально для удовлетворения потре...

02.09.2024
1563