init
This commit is contained in:
parent
06f61d8ce8
commit
f301cb744d
2271 changed files with 103162 additions and 0 deletions
287
02-ui/05-widgets/02-widgets-structure/article.md
Normal file
287
02-ui/05-widgets/02-widgets-structure/article.md
Normal file
|
@ -0,0 +1,287 @@
|
|||
# Графические компоненты
|
||||
|
||||
Первый и главный шаг в наведении порядка -- это оформить код в объекты, каждый из которых будет решать свою задачу.
|
||||
|
||||
Здесь мы сосредоточимся на графических компонентах, которые также называют "виджетами".
|
||||
|
||||
В браузерах есть встроенные виджеты, например `<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>
|
||||
|
||||
Далее мы продолжим работать со разметкой виджета.
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue