en.javascript.info/02-ui/05-widgets/02-widgets-structure/article.md
Ilya Kantor f301cb744d init
2014-10-26 22:10:13 +03:00

13 KiB
Raw Blame History

Графические компоненты

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

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

В браузерах есть встроенные виджеты, например <select>, <input> и другие элементы, о которых мы даже и не думаем, как они работают. Просто работают: принимают значение, вызывают события...

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

Виджет Menu

Мы начнём работу с виджета, который предусматривает уже готовую разметку.

То есть, в нужном месте HTML находится DOM-структура для меню -- заголовок и список опций:

<div class="menu" id="sweets-menu">
  <span class="title">Сладости</span>
  <ul>
    <li>Торт</li>
    <li>Пончик</li>
    <li>...</li>
  </ul>
</div>

Далее она может дополняться, изменяться, но в начале -- она такая.

Обратим внимание на важные соглашения:

Вся разметка заключена в корневой элемент `
`.
Это очень удобно: вынул этот элемент из DOM -- нет меню, вставил в другое место -- переместил меню. Кроме того, можно удобно искать подэлементы.
В разметке -- только классы.
Документ вполне может содержать много различных меню. Они не должны конфликтовать между собой, поэтому для разметки везде используются классы.

Исключение -- корневой элемент. В данном случае мы предполагаем, что данное конкретное "меню сладостей" в документе только одно, поэтому даём ему id.

Для работы с разметкой будем создавать объект new Menu и передавать ему корневой элемент. В конструкторе он поставит необходимые обработчики.

function Menu(options) {
  var elem = options.elem;

  elem.on('mousedown selectstart', false);

  elem.on('click', '.title', function() {
    elem.toggleClass('open');
  });
}

// использование
var menu = new Menu({
  elem: $('#sweets-menu')
});

Меню: [example src="menu-1"]

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

У конструктора только один аргумент -- объект `options`.
Это удобно, так как у графических компонентов обычно много настроек, большинство из которых имеют разумные значения "по умолчанию". Если передавать аргументы через запятую -- их будет слишком много.
Обработчики назначаются через делегирование.
Вместо того, чтобы найти элемент и поставить обработчик на него:
var titleElem = elem.find('.title');

titleElem.on('click', function() {
  elem.toggleClass('open');
}

...Мы пишем так:

elem.on('click', '.title', function() {
  elem.toggleClass('open');
});

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

Публичные методы

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

Рассмотрим повнимательнее этот фрагмент:

elem.on('click', '.title', function() {
  elem.toggleClass('open');
});

Здесь в обработчике события сразу код работы с элементами. Пока одна строка -- всё понятно, но если их будет много, то при чтении понадобится долго и упорно вникать: "А что же, всё-таки, такое делается при клике?"

Для улучшения читаемости выделим обработчик в отдельную функцию toggle, которая к тому же станет полезным публичным методом:

function Menu(options) {
  var elem = options.elem;

  elem.on('mousedown selectstart', false);

*!*
  elem.on('click', '.title', onTitleClick);

  function onTitleClick(e) {
   toggle();
  }

  function toggle() {
    elem.toggleClass('open');
  };
*/!*

  this.toggle = toggle;
}

Здесь и сам обработчик события тоже вынесен в отдельную функцию onTitleClick.

Наши бонусы:

  1. Во-первых, стало проще найти и расширить обработчик события в коде -- имя `onTitleClick` найти и запомнить.
  2. Во-вторых, код стал лучше читаться.
  3. Во-третьих, `toggle` теперь -- отдельная функция, доступная извне.

Пример использования публичного метода:

var menu = new Menu(...);
menu.toggle();

Генерация DOM-дерева

До этого момента меню "оживляло" уже существующий HTML. Но в более сложном интерфейсе нужно уметь сгенерировать меню "на лету", по данным.

Для этого добавим меню три метода:

  • `render()` -- генерирует корневой DOM-элемент и заголовок меню, приватный.
  • `renderItems()` -- генерирует DOM для списка опций (`
  • `), приватный.
  • `getElem()` -- возвращает DOM-элемент меню, при необходимости запуская генерацию, публичный.

Функция генерации корневого элемента с заголовком render отделена от генерации списка renderItems. Почему -- будет видно чуть далее.

Новый способ использования меню:

*!*
// создать объект меню с данным заголовком и опциями
*/!*
var menu = new Menu({ 
  title: "Сладости",
  items: [
    "Торт",
    "Пончик",
    "Пирожное",
    "Шоколадка",
    "Мороженое"
  ]
});

*!*
// получить DOM-элемент меню
*/!*
var elem = menu.getElem();

*!*
// вставить меню в нужное место страницы
*/!*
$('#sweets-menu-holder').append( elem );

Код Menu с новыми методами:

function Menu(options) {
  var elem;

  function getElem() {
    if (!elem) render();
    return elem;
  }

  function render() {
    elem = $('<div class="menu"></div>');
    elem.append( $('<span/>', { class: "title", text: options.title }))

    elem.on('mousedown selectstart', false);

    elem.on('click', '.title', onTitleClick);
  }

  function renderItems() {
    var items = options.items || [];
    var list = $('<ul/>');
    $.each(items, function(i, item) {
      list.append( $('<li>').text(item) );
    })
    list.appendTo(elem);
  }

  // ... 
}

Важнейший принцип, который здесь использован -- ленивость.

Мы стараемся откладывать работу до момента, когда она реально нужна. Например, когда new Menu создаётся, то переменная elem лишь объявляется. DOM-дерево будет сгенерировано только при вызове getElem().

Более того! Пока меню закрыто -- достаточно заголовка. Кроме того, возможно, посетитель вообще никогда не раскроет это меню, так зачем генерировать список раньше времени?

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

Если изначально подходить к оптимизации на этой фазе "спустя рукава", то потом поправить может быть сложно. Всё-таки, инициализация -- это фундамент, начало работы виджета. Конечно, лучше без фанатизма. Бывают ситуации, когда по коду гораздо удобнее что-то сделать сразу, поэтому нужен взвешенный подход. Чем крупнее участок работы и чем больше шансов его вообще избежать -- тем больше доводов его отложить.

Ниже -- код меню с методами open, close и toggle, которые подразумевают ленивую генерацию DOM:

function Menu(options) {
  var elem;

  function getElem() { /* см выше */ }

  function render() { /* см выше */  }

  function renderItems() { /* см выше */ }

  function onTitleClick(e) { /* см выше */ }

*!*
  function open() {
    if (!elem.find('ul').length) {
      renderItems();
    }
    elem.addClass('open');
  };

  function close() {
    elem.removeClass('open');
  };

  function toggle() {
    if (elem.hasClass('open')) close();
    else open();
  };
*/!*

  this.getElem = getElem;
  this.toggle = toggle;
  this.close = close;
  this.open = open;
}

Основные изменения -- теперь метод toggle не просто меняет класс. Этого недостаточно, ведь, чтобы открыть меню, нужно для начала отрендерить его опции. Поэтому добавлено два метода open и close, которые также полезны и для внешнего интерфейса.

В действии: [example src="menu-3-elem" height="200"]

Итого

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

Основные принципы:

  • В конструктор передаётся объект аргументов `options`, а не список аргументов -- для удобства дополнения и расширения виджета.
  • Обработчики назначаются через делегирование -- для производительности и упрощения виджета.
  • Не экономим буквы ценой понятности -- действие и/или обработчик заслуживают быть отдельными функциями.
  • Будем ленивыми -- если существенный участок работы можно отложить до реального задействования виджета -- откладываем его.

Далее мы продолжим работать со разметкой виджета.