- PVSM.RU - https://www.pvsm.ru -

Matreshka.js — Введение

Приветствую всех читателей и писателей хабра.

Хочу познакомить вас с компактным фреймворком (4.44KB gzipped, 14.04KB uncompressed), который позволяет несколько изменить взгляд на структурирование приложений. В «Матрешку» вложено чуточку магии, раскрыть которую позволит серия статей, озаглавленных следующим образом:

  • Введение
  • Наследование
  • MK.Object
  • MK.Array

Код для привлечения внимания:

mk.on( 'change:x', function() {
	alert( 'x is changed to ' + this.x );
});
mk.x = 2; // alerts "x is changed to 2"

И это работает в… IE8.

Что такое Матрешка?

Матрешка, как фреймворк
Компактный размер и легкая в изучении архитектура даёт возможность строить крупные расширяемые приложения. Этим сегодня никого не удивишь, но я постараюсь.
Матрешка, как библиотека
Если фичи, предоставляемые Матрешкой вам понравятся, то не обязательно менять свой код. Матрешкой можно пользоваться, как набором классов с интересными методами.
Матрешка, как платформа для создания собственного фреймворка
Матрешка — расширяемый фреймворк общего назначения, который не позиционируется, как MVC, MVVM или %your_design_pattern% фреймворк, поэтому программист имеет возможность реализовать собственную архитектуру, которая будет уметь желаемый набор шаблонов проектирования.

Зачем?

Мне часом надоело думать о представлении и о том, чтоб его менять написанным мной кодом. Все костыли синхронизации данных и представления в Javascript вызывают у меня негативные чувства, и амбиционной, направленной в будущее, целью Матрешки является возможность полностью забыть о том, что у нас есть UI, оперируя только данными. Конечно же, решить эту задачу невозможно на 100%, но мы, программисты, должны выжимать максимум из данных нам инструментам, дабы сделать код чище, короче и гибче. Пора встряхнуть свой код и стать злым.

Гифка для привлечения внимания

Matreshka.js — Введение

Привязка данных

Известно, что, клиентские приложения в браузере требуют синхронизации между данными и представлением. Классическая задача: есть данные:

var o = { x: 2 };

Есть select, который должен менять эти данные:

<select>
	<option>1</option>
	<option>2</option>
	<option>3</option>
<select>

Если использовать чистый JS + jQuery код мы напишем примерно следующее:

$( 'select' ).on( 'change', function() {
	o.x = +this.value;
});

Затем, когда хотим поменять данные, пишем:

o.x = 1;
$( 'select' ).val( o.x );

Это не очень хорошо, так как заставляет устанавливать значение сразу для двух «атомов»: свойства и элемента.

Эту проблему решают по разному: Backbone (MVC) разделяет приложение на модель (Model, Collection), представление (HTML код) и контроллер (View). Knockout и Angular (MVVM) привязывают данные, используя практику, перекочевавшую из .NET в клиентский веб: часть логики, заменяющая контроллер (в данном случае ViewModel) пишется в HTML коде (прошу прощения, если описание неверно). Есть еще куча фреймворков, за которыми не угонишься.

Матрешка привязывает данные к представлению таким образом, что, во первых, программист не трогает HTML код (в отличие от MVVM), во вторых, программист перестаёт думать о событиях HTML элемента для изменения модели (в отличие от Backbone).

var mk = new Matreshka; // или new MK; помните, что MK === Matreshka, это обычное сокращение
mk.bindElement( 'x', 'select', {
	on: 'change',
	getValue: function() {
		return this.value;
	},
	setValue: function( v ) {
		this.value = v;
	}
});

Если мы хотим поменять данные, то просто пишем:

mk.x = 2;

Такой код не только присвоит иксу двойку, но и поменяет состояние селекта, установив ему .value = 2.
И это без использования методов, типа .set.
jsbin.com/isaFAZE/8/ [1]

Что здесь произошло?

mk.bindElement( 'x', 'select' ... )

Метод bindElement, как ни странно, отвечает за привязку элемента к свойству. Первый аргумент — ключ объекта, второй — селектор, в данном случае 'select'. Тип второго аргумента — любое значение, которое принимается jQuery: селектор, чистый элемент, jQuery объект, NodeList, массив элементов… То есть, 'select' можно заменить на $( 'select' ) или document.getElementsByTagName( 'select' ) или document.querySelector( 'select' ).

Третий аргумент — самое интересное, разберем по порядку.

on: 'change' 

Отвечает на вопрос: «Какое событие должно произойти на элементе, чтобы мы взяли значение из элемента и присвоили соответствующему свойству?»

getValue: function() {
	return this.value;
}

Отвечает на вопрос: «Каким образом извлечь значение элемента?», возвращая значение.

setValue: function( v ) {
	this.value = v;
}

Отвечает на вопрос: «Как нам установить значение для элемента?», где v — значение (в данном случае, 2).

Ссылка на доку: finom.github.io/matreshka/docs/Matreshka.html#bindElement [2]

Усложним задачу. Скажем, у нас есть слайдер из jQuery UI: api.jqueryui.com/slider/ [3] и у нас появилась задача привязать его к нашим данным. Смотрим документацию и видим событие "slide", которое срабатывает при перетаскивании ползунка.

Во-первых, перед привязкой объявим слайдер:

<div class="slider"></div>

$( ".slider" ).slider({ min: 0, max: 100 });

Во-вторых объявляем экземпляр Матрешки:

var mk = new Matreshka();

Дальше вызываем привязку:

mk.bindElement( 'x', '.slider', {
  on: 'slide', // событие, по которому из элемента извлекается значение
  getValue: function() {
    return $( this ).slider( 'option', 'value' ); // как вытащить значение из элемента (см. документацию jQuery ui.slider)? 
  },
  setValue: function( v ) {
    $( this ).slider( 'option', 'value', v ); // как установить значение для элемента (см. документацию jQuery ui.slider)? 
  }
});

Теперь, когда мы вызовем следующий код…

mk.x = 44;

… позиция слайдера изменится.

И наоборот, когда мы перетащим ручку слайдера, наш mk.x изменится.

Еще немного усложним задачу, добавив HTML элемент, выводящий значение:

<div class="output">Value is <span class="x"></span></div>

И привяжем его к 'x':

mk.bindElement( 'x', '.output .x', {
  setValue: function( v ) {
    this.innerHTML = v;
  }
});

Как видно, у опций привязки элемента отсутствует свойство "on" и метод "getValue". Это значит, что из элемента ".output .x" мы не будем извлекать значение, а только устанавливать.

Мы можем привязать к одному свойству несколько элементов. Обратное утверждение тоже верно: мы можем привязывать к одному элементу много свойств. Просто помните, что Матрешка умеет привязывать элементы по правилу «многие ко многим».

Результат: jsbin.com/isaFAZE/6/ [4] (в верхнем правом углу нажмите «Edit in JS Bin», откройте вкладку Console и попробуйте изменить x, например, напишите mk.x = 42;)

Отлично, мы привязали два элемента, но получили кучу избыточного кода (на мой взляд). Что, если у нас будет много слайдеров? Каждый раз писать…

mk.bindElement( property, element, {
  on: 'slide',
  getValue: function() {
    return $( this ).slider( 'option', 'value' );
  },
  setValue: function( v ) {
    $( this ).slider( 'option', 'value', v );
  }
});

… не очень красиво.

Что делать? Надо запомнить общие черты элементов, которые будут проверяться при привязке элементов. Было бы удобно сделать так:

mk.bindElement( 'x', '.slider' );

Как это сделать? У Матрешки есть статичное свойство MK.elementProcessors, которое является массивом функций. Функции принимают аргумент el, являющийся проверяемым элементом, и содержат условие: если элемент соответствует некоторому правилу, то возвращаем объект опций, которые в примерах .bindElement выше, являются третьим аргументом.

Для любого слайдера есть одно правило: каждый имеет класс ui-slider. Поэтому наша функция-условие будет выглядеть так:

function( el ) {
  if( $( el ).hasClass( 'ui-slider' ) ) {
    return {
      on: 'slide',
      getValue: function() {
        return $( this ).slider( 'option', 'value' );
      },
      setValue: function( v ) {
        $( this ).slider( 'option', 'value', v );
      }
    };
  }
}

Пример, надеюсь, достаточно прост: мы проверяем, является ли элемент слайдером и, если да, возвращаем опции.

Вставляем функцию в массив MK.elementProcessors:

MK.elementProcessors.push( function( el ) {
  if( $( el ).hasClass( 'ui-slider' ) ) {
    return {
      on: 'slide',
      getValue: function() {
        return $( this ).slider( 'option', 'value' );
      },
      setValue: function( v ) {
        $( this ).slider( 'option', 'value', v );
      }
    };
  }
});

Ссылка на доку: finom.github.io/matreshka/docs/Matreshka.html#elementProcessors [5]

После этого, имея хоть сотню слайдеров, в функции .bindElement мы определяем только лишь свойство и привязываемый элемент:

<div class="slider1"></div>
<div class="slider2"></div>

$( ".slider1, .slider2" ).slider({ min: 0, max: 100 });
var mk = new Matreshka();
mk.bindElement({
  x1: '.slider1',
  x2: '.slider2'
});

Результат: jsbin.com/uzINaWe/1/ [6]

Обратите внимание на то, как в примере по ссылке привязан вывод:

mk.bindElement({
  x1: '.output .x1',
  x2: '.output .x2'
}, MK.htmlp );

MK.htmlp — статичное свойство, которое всего-навсего содержит опции привязки к обычному html элементу, меняя его innerHTML.

Дока: finom.github.io/matreshka/docs/Matreshka.html#htmlp [7]

Почему бы не создать правило, которое меняет .innerHTML для любого элемента, не соответствующего ни одному другому правилу? Одним словом: удобство. Иногда есть нужда привязывать элементы, для которых не требуется установка значения, но об этом в следующей статье.

По умолчанию MK.elementProcessors содержит одну функцию, проверяющую элемент на соответствие тривиальным правилам, давая возможность использовать простые элементы (select, textarea, input[type="text"], input[type="checkbox"], input[type="radio"]).
Например,

<select class="my-select">
   ...
</select>

Используя такой код:

mk.bindElement( 'x', '.my-select' );

… Матрешка сама узнает, когда, как добыть, как установить значение элемента, так как используется самый обычный select.

Пример: jsbin.com/ocOmolA/2/ [8] (нажимайте, изменяйте текст инпутов и взгляните на исходник)

Крититка подхода с селектором по классу

Два дня назад я запостил анонс [9] Матрешки на форум javascript.ru. Пользователь nerv_ обоснованно описал критику подхода с селекторами, приведя код на AngularJS.

Получается, нужно следить как минимум за служебными селекторами.
И получается что все «биндинги» необходимо писать в js коде. А их, как правило, много, вместо того, чтобы задать декларативно — дешево и сердито.

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

<form>
	<select data-key="a"></select>
	<select data-key="b"></select>
</form>

Деревянный способ:

$( '[data-key]' ).each( function() {
	this.bindElement( this.getAttribute( 'data-key' ), this );
});

Правильный способ (подробнее о методе .each в статье об MK.Object):

this.each( function( v, key ) {
	this.bindElement( key, this.$( '[data-key=' + key + ']' ) );
});

Эти два способа можно назвать первыми шагами к паттерну MVVM, который можно реализовать на базе Матрешки. Готовится пруф в виде плагина и статьи о нем.

Я предпочитаю разграничивать «комплексные» элементы приложения таким образом, чтоб каждый элемент являлся элементарным классом (что я имею в виду, будет понятнее в статье о наследовании), привязок в котором не так много. Возможно, мой подход сильно деформирован убеждением о том, что JS код и HTML должны быть максимально разграничены: верстальщик верстает, а программист, если и вносит, то только минимальные и необходимые изменения.

События

Матрешка содержит в себе генератор событий, который работает так же, как и в Backbone.js. Изначально, фреймворк писался под собственные нужды, поэтому часть кода, отвечающая за события, была позаимствована как раз из этого фреймворка. Но код этих методов оказался очень стабильным, поэтому было решено его оставить.

var mk = new Matreshka();
mk.on( 'someevent', function( a, b ) {
  alert( a + ', ' + b );
});
mk.trigger( 'Hello', 'World!' ); // alerts "Hello, World!"

Знакомо?

Ссылка на документацию: finom.github.io/matreshka/docs/Matreshka.html#on [10]

Одной из вкусных возможностей, указанных в начале поста является отслеживание события изменения свойства:

var mk = new Matreshka();
mk.on( 'change:x', function() {
  alert( 'New x value is ' + this.x );
});
mk.x = 5; //alerts "New x value is 5"

Мы так же можем привязать свойство к какому-нибудь элементу и отслеживать изменение его значения:

<select class="my-select">
    <option>1</option>
    <option>2</option>
    <option>3</option>
</select>  

var mk = new Matreshka();
mk.bindElement( 'x', '.my-select' );
mk.on( 'change:x', function() {
  alert( 'x is now ' + this.x );
});

Попробуйте сами: jsbin.com/ibEQiT/ [11]

Использование метода .set

Метод .set просто присваивает значение заданному свойству. Он используется для двух целей:
1. Передача свойств в объект события change:*key* (например флага "silent").

mk.on( 'change:x', function( evtOpts ) {
  alert( evtOpts.myFlag );
});
mk.set( 'x', 5, { myFlag: 'blah' } ); // устанавливает свойство и вызывает событие "change:x"
mk.set( 'x', 42, { silent: true } ); // устанавливает свойство и не вызывает никаких событий

Обратите внимание, что даже если передан флаг silent: true, значение привязанного элемента всё равно изменится.
2. Сокращение кода. В метод .set можно передать объект со свойствами.

mk.set( { x1: 1, x2: 2 } );
mk.set( { x1: 3, x2: 4 }, { silent: true } );

Где взять?

Репозиторий на github: github.com/finom/matreshka [12] (код фреймворка находится в папке build/)
Матрешка требует наличия jQuery, хотя планируется убрать зависимость от неё.

В завершение

Я познакомил вас с основными фишками, которые несет в себе Матрешка. Остальные функции вы можете найти в документации [13]. Там же вы найдете другие, не менее важные методы:
.off [14], отключающий заданные события
.remove [15], удаляющий свойство
.define [16], навешивающий кастомные акцессоры
.defineGetter [17], навешивающий геттер на элемент
.$el [18], возвращающий элемент, привязанный к свойству
.unbindElement [19], разрывающий связь между свойством и элементом
… и другие.

Почему версия 0.0.X?

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

Что дальше?

В следующей статье я расскажу, как написать своё первое приложение, используя Матрешку не просто как набор удобных функций, а как полноценный фреймворк, основанный на классах, и расскажу, почему именно классы (я знаком со здешним негодованием по поводу сего вопроса).
Дальше будет рассказано о классах MK.Array и MK.Object, отвечающих за данные.
После этого, если читателю интересно, я расскажу, как же работает магия, и как мне удалось реализовать поддержку Object.defineProperty в IE8.

Огромное спасибо всем, кто смог прочесть статью до конца. Желаю удачи и успешного кодинга.

Автор: Finom

Источник [20]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/46911

Ссылки в тексте:

[1] jsbin.com/isaFAZE/8/: http://jsbin.com/isaFAZE/8/

[2] finom.github.io/matreshka/docs/Matreshka.html#bindElement: http://finom.github.io/matreshka/docs/Matreshka.html#bindElement

[3] api.jqueryui.com/slider/: http://api.jqueryui.com/slider/

[4] jsbin.com/isaFAZE/6/: http://jsbin.com/isaFAZE/6/

[5] finom.github.io/matreshka/docs/Matreshka.html#elementProcessors: http://finom.github.io/matreshka/docs/Matreshka.html#elementProcessors

[6] jsbin.com/uzINaWe/1/: http://jsbin.com/uzINaWe/1/

[7] finom.github.io/matreshka/docs/Matreshka.html#htmlp: http://finom.github.io/matreshka/docs/Matreshka.html#htmlp

[8] jsbin.com/ocOmolA/2/: http://jsbin.com/ocOmolA/2/

[9] запостил анонс: http://javascript.ru/forum/project/42461-frejjmvork-matreshka-js.html

[10] finom.github.io/matreshka/docs/Matreshka.html#on: http://finom.github.io/matreshka/docs/Matreshka.html#on

[11] jsbin.com/ibEQiT/: http://jsbin.com/ibEQiT/

[12] github.com/finom/matreshka: https://github.com/finom/matreshka

[13] документации: http://finom.github.io/matreshka/docs/Matreshka.html

[14] .off: http://finom.github.io/matreshka/docs/Matreshka.html#off

[15] .remove: http://finom.github.io/matreshka/docs/Matreshka.html#remove

[16] .define: http://finom.github.io/matreshka/docs/Matreshka.html#define

[17] .defineGetter: http://finom.github.io/matreshka/docs/Matreshka.html#defineGetter

[18] .$el: http://finom.github.io/matreshka/docs/Matreshka.html#$el

[19] .unbindElement: http://finom.github.io/matreshka/docs/Matreshka.html#unbindElement

[20] Источник: http://habrahabr.ru/post/196146/