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

287 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Графические компоненты
Первый и главный шаг в наведении порядка -- это оформить код в объекты, каждый из которых будет решать свою задачу.
Здесь мы сосредоточимся на графических компонентах, которые также называют "виджетами".
В браузерах есть встроенные виджеты, например `<select>`, `<input>` и другие элементы, о которых мы даже и не думаем, как они работают. Просто работают: принимают значение, вызывают события...
Наша задача -- сделать то же самое на уровне выше. Мы будем создавать объекты, которые генерируют меню, диалог или другие компоненты интерфейса, и дают возможность удобно работать с ними.
## Виджет Menu
Мы начнём работу с виджета, который предусматривает уже готовую разметку.
То есть, в нужном месте HTML находится DOM-структура для меню -- заголовок и список опций:
```html
<div class="menu" id="sweets-menu">
<span class="title">Сладости</span>
<ul>
<li>Торт</li>
<li>Пончик</li>
<li>...</li>
</ul>
</div>
```
Далее она может дополняться, изменяться, но в начале -- она такая.
Обратим внимание на важные соглашения:
<dl>
<dt>Вся разметка заключена в корневой элемент `<div class="menu" id="sweeties-menu">`.</dt>
<dd>Это очень удобно: вынул этот элемент из DOM -- нет меню, вставил в другое место -- переместил меню. Кроме того, можно удобно искать подэлементы.</dd>
<dt>В разметке -- только классы.</dt>
<dd>Документ вполне может содержать много различных меню. Они не должны конфликтовать между собой, поэтому для разметки везде используются классы.
Исключение -- корневой элемент. В данном случае мы предполагаем, что данное конкретное "меню сладостей" в документе только одно, поэтому даём ему `id`.</dd>
</dl>
Для работы с разметкой будем создавать объект `new Menu` и передавать ему корневой элемент. В конструкторе он поставит необходимые обработчики.
```js
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"]
Это, конечно, только первый шаг, но уже здесь видны некоторые важные соглашения в коде.
<dl>
<dt>У конструктора только один аргумент -- объект `options`.</dt>
<dd>Это удобно, так как у графических компонентов обычно много настроек, большинство из которых имеют разумные значения "по умолчанию". Если передавать аргументы через запятую -- их будет слишком много.</dd>
<dt>Обработчики назначаются через делегирование.</dt>
<dd>Вместо того, чтобы найти элемент и поставить обработчик на него:
```js
var titleElem = elem.find('.title');
titleElem.on('click', function() {
elem.toggleClass('open');
}
```
...Мы пишем так:
```js
elem.on('click', '.title', function() {
elem.toggleClass('open');
});
```
Это ускоряет инициализацию, так как не надо искать элементы, и даёт возможность в любой момент менять DOM внутри, в том числе через `innerHTML`, без необходимости переставлять обработчика.
</dd>
</dl>
## Публичные методы
Уважающий себя компонент обычно имеет публичные методы, которые позволяют управлять им снаружи.
Рассмотрим повнимательнее этот фрагмент:
```js
elem.on('click', '.title', function() {
elem.toggleClass('open');
});
```
Здесь в обработчике события сразу код работы с элементами. Пока одна строка -- всё понятно, но если их будет много, то при чтении понадобится долго и упорно вникать: "А что же, всё-таки, такое делается при клике?"
Для улучшения читаемости выделим обработчик в отдельную функцию `toggle`, которая к тому же станет полезным публичным методом:
```js
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`.
Наши бонусы:
<ol>
<li>Во-первых, стало проще найти и расширить обработчик события в коде -- имя `onTitleClick` найти и запомнить.</li>
<li>Во-вторых, код стал лучше читаться.</li>
<li>Во-третьих, `toggle` теперь -- отдельная функция, доступная извне.</li>
</ol>
Пример использования публичного метода:
```js
var menu = new Menu(...);
menu.toggle();
```
## Генерация DOM-дерева
До этого момента меню "оживляло" уже существующий HTML. Но в более сложном интерфейсе нужно уметь сгенерировать меню "на лету", по данным.
Для этого добавим меню три метода:
<ul>
<li>`render()` -- генерирует корневой DOM-элемент и заголовок меню, приватный.</li>
<li>`renderItems()` -- генерирует DOM для списка опций (`<li>`), приватный.</li>
<li>`getElem()` -- возвращает DOM-элемент меню, при необходимости запуская генерацию, публичный.</li>
</ul>
Функция генерации корневого элемента с заголовком `render` отделена от генерации списка `renderItems`. Почему -- будет видно чуть далее.
Новый способ использования меню:
```js
*!*
// создать объект меню с данным заголовком и опциями
*/!*
var menu = new Menu({
title: "Сладости",
items: [
"Торт",
"Пончик",
"Пирожное",
"Шоколадка",
"Мороженое"
]
});
*!*
// получить DOM-элемент меню
*/!*
var elem = menu.getElem();
*!*
// вставить меню в нужное место страницы
*/!*
$('#sweets-menu-holder').append( elem );
```
Код `Menu` с новыми методами:
```js
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:
```js
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"]
## Итого
Мы начали создавать компонент "с чистого листа", пока без дополнительных библиотек, но они скоро понадобятся.
Основные принципы:
<ul>
<li>В конструктор передаётся объект аргументов `options`, а не список аргументов -- для удобства дополнения и расширения виджета.</li>
<li>Обработчики назначаются через делегирование -- для производительности и упрощения виджета.</li>
<li>Не экономим буквы ценой понятности -- действие и/или обработчик заслуживают быть отдельными функциями.</li>
<li>Будем ленивыми -- если существенный участок работы можно отложить до реального задействования виджета -- откладываем его.</li>
</ul>
Далее мы продолжим работать со разметкой виджета.