Доброго времени суток.
Современный мир оставляет мало возможностей не сталкиваться с javascript. Nodejs стал для меня последней каплей и, разочарованный в RoR (слишком много магии и генераторов — никаких холиваров, рубисты!), я снова поддался безумию: один язык на клиенте и сервере. Хоть javascript и прекрасен как язык, фреймворков, которые реализуют MVVM или хотя бы MVC и которые бы мне понравились, нет. Они все тяжеловесны и требуют написания лишнего (мусорного) кода. Поэтому я бы хотел представить на суд мое видение MVVM и получить от сообщества пинков в нужном направлении. Лучшим направлением было бы: «Вы пропустили библиотеку, посмотрите %library_name%», ибо все, что на поверхности (angularjs, knockoutjs, etc.) я посмотрел. Ну а так как фреймворк сырой и вряд ли принесет сейчас кому-то пользу, в обмен на долгожданные пинки я попытаюсь кратко сформулировать свой опыт, полученный при его написании.
Точно так же, как порядочный хоббит не отправится в путешествие без носового платка, жить без тестирования в современном мире можно, но грустно. Думаю, ни для кого не секрет, что браузеры слегка по разному воспринимают javascript, отсюда возникает естественное желание быстро и безболезненно находить проблемы, а не искать их огромным усилием воли, полагаясь на чутье.
В качестве фреймворка для тестирования я выбрал QUnit. Мне очень нравится jquery, и тот факт, что он от jquery foundation стало последней гирькой на чаше весов. Я ни разу не пожалел о выборе, хотя должен сознаться — первое столкновение с асинхронными тестами вызвало у меня легкий шок своим неочевидным поведением, но после внимательного чтения документации все встало на свои места.
Как бы странно это ни звучало, но для старта тестов нам понадобится html документ.
Посмотреть как оно работает можно склонировав репозиторий или тут
Осталось написать свои тесты. Мне очень понравилась идея с группировкой тестов в модуле, метод module. Используем имя файла в качестве имени модуля — и, вуаля, это позволяет достаточно быстро найти отваливающиеся части.
Ну а сам тест пишется с помощью функции test. В официальной документации он достаточно хорошо описан.
Есть у QUnit и приятности, которые греют душу любому программисту. Сконфигурировать что-то под себя всегда приятно, особенно когда хочется быть уверенным, что компилятор послушно выполняет свою роль, а не пытается стать соавтором, поправляя логику, с которой он не согласен.
Все описание, опять же, доступны на сайте проекта тут, но для тех кому лень кликать:
Добавляем checkbox в меню qunit, и если он включен, то в url мы увидим min=true
Отключаем автоматический старт тестов и, в конце концов, появляется замечательная библиотека ruquirejs, с чьей помощью мы загружаем либо скомпилированный код, либо код для разработчиков.
Я не буду приводить тут описание методов ok, equal или deepEqual — на мой взгляд это излишне, если есть полное и понятное их описание тут. Поэтому про тестирование и qunit, наверное, все.
Возможно борьба за несколько килобайт в современном мире кажется немного странной, но мы живем в мире, где увеличение времени загрузки сайта на доли секунды снижает количество покупателей на 10-20%, поэтому будем относиться к этому философски, тем более, что это не так сложно.
В качестве компилятора я выбрал Google Closure и получился следующий скрипт:
Собственно, представляет интерес тут только одна строчка, а именно
java -jar libs/compiler.jar --js out/binding.js --js_output_file out/binding-min.js --compilation_level SIMPLE_OPTIMIZATIONS
js — имя компилируемого файла
js_output_file — имя файла, куда положить скомпилированный код
compilation_level — уровень компиляции
Если я пропустил что-то важное для Вас более подробную информацию можно получить выполнив
Я бы не стал критиковать в глаза Торина и, конечно, не буду комментировать успешные и безусловно замечательные существующие фреймворки, без них жить было бы гораздо сложней. Поэтому я предлагаю следующий формат — я буду просто фантазировать о том, как должен выглядеть идеальный (IMHO) фреймворк для биндинга данных.
Скачать о чем пойдет далее речь можно тут
А все примеры я буду приводить из проекта XO, поиграть можно тут
Далее по тексту реверансов «IMHO» не будет, но читая дальше Вы должны понимать, что я пишу субъективное мнение, которое может оскорбить Ваше религиозное или любое другое чувство, и если Вы с чем-то не согласны, я с удовольствием прочитаю Ваш аргументированный комментарий и аргументированно на него отвечу. Все мы тут взрослые люди, ага.
В итоге у нас получается святая троица: html верстка, view, model. Связывать данные и представление через код, когда у нас есть декларативное представление интерфейса мне кажется несколько излишним, поэтому я бы хотел указывать представление и данные в верстке, благо html это позволяет. В итоге у меня получился следующий синтаксис:
bind-data говорит откуда взять данные, а bind-handler как их показать пользователю. Немного подумав про удобство, я ввел еще bind-form, который указывает контекст для дочерних элементов. С атрибутами это все, за исключением того, что bind-handler можно и не указывать, тогда bind-handler будет найден в Binding.DefaultHandlers по имени тэга.
Давайте внимательно посмотрим на ButtonBinding — у него 2 метода: init, который вызывается при инициализации связи модели и представления, и modelChanged, который вызывается при изменении модели. В данном примере это не используется, но у modelChanged есть два дополнительных аргумента: model — это изменившийся объект, и event — содержит описание события.
Тут стоит пояснить, что связь выстраивается не с самим экземпляром объекта.
В xo у нас есть $data.Game.board — это объект который содержит описание игрового поля. Когда мы нажимаем на кнопку «New game», вызывается метод из $data.Game
Эта функция создает новый экземпляр, но мы увидим, что был вызван modelChanged соответствующего view, и в качестве объекта, инициирующего событие, будет передан $data.Game, а в event — описание события.
На практике это значит примерно следующее: мы можем быть уверены, что view оперирует данными, указанными в bind-data, а не неким призрачным объектом, который заслуживает только одного внимания, а именно — сборщика мусора.
Сейчас это пре-пре-пре-альфа и может измениться все. Цель данной статьи — получить от Вас отклик, удобен будет подобный синтаксис в Ваших проектах или нет, чего не хватает? Библиотека проверена только в chrome.
Roadmap
Напоследок я бы хотел задать несколько вопросов коллективному разуму:
Современный мир оставляет мало возможностей не сталкиваться с javascript. Nodejs стал для меня последней каплей и, разочарованный в RoR (слишком много магии и генераторов — никаких холиваров, рубисты!), я снова поддался безумию: один язык на клиенте и сервере. Хоть javascript и прекрасен как язык, фреймворков, которые реализуют MVVM или хотя бы MVC и которые бы мне понравились, нет. Они все тяжеловесны и требуют написания лишнего (мусорного) кода. Поэтому я бы хотел представить на суд мое видение MVVM и получить от сообщества пинков в нужном направлении. Лучшим направлением было бы: «Вы пропустили библиотеку, посмотрите %library_name%», ибо все, что на поверхности (angularjs, knockoutjs, etc.) я посмотрел. Ну а так как фреймворк сырой и вряд ли принесет сейчас кому-то пользу, в обмен на долгожданные пинки я попытаюсь кратко сформулировать свой опыт, полученный при его написании.
Тестирование
Он привез Бильбо уйму носовых платков, любимую трубку и табаку.
Точно так же, как порядочный хоббит не отправится в путешествие без носового платка, жить без тестирования в современном мире можно, но грустно. Думаю, ни для кого не секрет, что браузеры слегка по разному воспринимают javascript, отсюда возникает естественное желание быстро и безболезненно находить проблемы, а не искать их огромным усилием воли, полагаясь на чутье.
В качестве фреймворка для тестирования я выбрал QUnit. Мне очень нравится jquery, и тот факт, что он от jquery foundation стало последней гирькой на чаше весов. Я ни разу не пожалел о выборе, хотя должен сознаться — первое столкновение с асинхронными тестами вызвало у меня легкий шок своим неочевидным поведением, но после внимательного чтения документации все встало на свои места.
Как бы странно это ни звучало, но для старта тестов нам понадобится html документ.
У меня он выглядит так
<!DOCTYPE html>
<html>
<head>
<title>Binding tests</title>
<link rel="stylesheet" href="css/qunit.css" type="text/css" media="screen">
<script type="text/javascript" src="js/libs/require.js"></script>
<script type="text/javascript" src="js/libs/qunit.js"></script>
<script type="text/javascript" src="js/binding.js"></script>
</head>
<body>
<h1 id="qunit-header">Tests</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="trash"></div>
</body>
</html>
Посмотреть как оно работает можно склонировав репозиторий или тут
Осталось написать свои тесты. Мне очень понравилась идея с группировкой тестов в модуле, метод module. Используем имя файла в качестве имени модуля — и, вуаля, это позволяет достаточно быстро найти отваливающиеся части.
Ну а сам тест пишется с помощью функции test. В официальной документации он достаточно хорошо описан.
Тут опять же пример
test("Hash tests", function() {
var obj = {};
ok(HashUtils.get(0) == HashUtils.get(0), "Equal objects, one hash (Number)");
ok(HashUtils.get(obj) == HashUtils.get(obj), "Equal objects, one hash (Object)");
ok(HashUtils.get({}) != HashUtils.get({}), "Different objects, different hashes");
});
Есть у QUnit и приятности, которые греют душу любому программисту. Сконфигурировать что-то под себя всегда приятно, особенно когда хочется быть уверенным, что компилятор послушно выполняет свою роль, а не пытается стать соавтором, поправляя логику, с которой он не согласен.
QUnit.config.urlConfig.push({
id: "min",
label: "Minified source",
tooltip: "Load minified source files instead of the regular unminified ones."
});
QUnit.config.autostart = false;
...
require(
tests.concat(document.location.href.indexOf("min=true") > 0 ? productionRequire : developmentRequire),
function() { QUnit.start(); }
);
Все описание, опять же, доступны на сайте проекта тут, но для тех кому лень кликать:
Добавляем checkbox в меню qunit, и если он включен, то в url мы увидим min=true
Отключаем автоматический старт тестов и, в конце концов, появляется замечательная библиотека ruquirejs, с чьей помощью мы загружаем либо скомпилированный код, либо код для разработчиков.
Я не буду приводить тут описание методов ok, equal или deepEqual — на мой взгляд это излишне, если есть полное и понятное их описание тут. Поэтому про тестирование и qunit, наверное, все.
Компиляция кода
Я не знаю. Саруман верит, что только лишь великая сила способна обуздать зло, но мне открылось иное. Я понял, что разные мелочи, житейские деяния простого люда помогают сдерживать тьму. Обыкновенные любовь и доброта. Почему Бильбо Бэггинс? Наверное, потому что мне страшно и он придаёт мне смелости.
Возможно борьба за несколько килобайт в современном мире кажется немного странной, но мы живем в мире, где увеличение времени загрузки сайта на доли секунды снижает количество покупателей на 10-20%, поэтому будем относиться к этому философски, тем более, что это не так сложно.
В качестве компилятора я выбрал Google Closure и получился следующий скрипт:
build.sh
rm -r -f out
mkdir out
cat js/binding/utils.js js/binding/dom.js js/binding/model.js js/binding/events.js js/binding/binding.js js/binding/templates.js > out/binding.js
java -jar libs/compiler.jar --js out/binding.js --js_output_file out/binding-min.js --compilation_level SIMPLE_OPTIMIZATIONS
cat out/binding.js js/ui/button.js js/ui/input.js > out/binding-ui.js
java -jar libs/compiler.jar --js out/binding-ui.js --js_output_file out/binding-ui-min.js --compilation_level SIMPLE_OPTIMIZATIONS
mkdir out
cat js/binding/utils.js js/binding/dom.js js/binding/model.js js/binding/events.js js/binding/binding.js js/binding/templates.js > out/binding.js
java -jar libs/compiler.jar --js out/binding.js --js_output_file out/binding-min.js --compilation_level SIMPLE_OPTIMIZATIONS
cat out/binding.js js/ui/button.js js/ui/input.js > out/binding-ui.js
java -jar libs/compiler.jar --js out/binding-ui.js --js_output_file out/binding-ui-min.js --compilation_level SIMPLE_OPTIMIZATIONS
Собственно, представляет интерес тут только одна строчка, а именно
java -jar libs/compiler.jar --js out/binding.js --js_output_file out/binding-min.js --compilation_level SIMPLE_OPTIMIZATIONS
js — имя компилируемого файла
js_output_file — имя файла, куда положить скомпилированный код
compilation_level — уровень компиляции
Если я пропустил что-то важное для Вас более подробную информацию можно получить выполнив
java -jar compiler.jar --help
BindItJS
Таков был стиль Торина. Он ведь был важной персоной. Если его не остановить, он продолжал бы в том же духе без конца, пока бы совсем не запыхался, но так бы и не сообщил обществу ничего нового.
Я бы не стал критиковать в глаза Торина и, конечно, не буду комментировать успешные и безусловно замечательные существующие фреймворки, без них жить было бы гораздо сложней. Поэтому я предлагаю следующий формат — я буду просто фантазировать о том, как должен выглядеть идеальный (IMHO) фреймворк для биндинга данных.
Скачать о чем пойдет далее речь можно тут
А все примеры я буду приводить из проекта XO, поиграть можно тут
Далее по тексту реверансов «IMHO» не будет, но читая дальше Вы должны понимать, что я пишу субъективное мнение, которое может оскорбить Ваше религиозное или любое другое чувство, и если Вы с чем-то не согласны, я с удовольствием прочитаю Ваш аргументированный комментарий и аргументированно на него отвечу. Все мы тут взрослые люди, ага.
- Код модели и представления должен быть разнесен (SRP)
- Написанный однажды код view должно быть просто использовать повторно (DRY)
- Представление не должно диктовать принцип формирования слоя модели
- Одни и те же данные из модели могут иметь разное представление на странице, без написания дополнительного кода
- Изменение модели должно автоматически дергать соответствующий метод представления
В итоге у нас получается святая троица: html верстка, view, model. Связывать данные и представление через код, когда у нас есть декларативное представление интерфейса мне кажется несколько излишним, поэтому я бы хотел указывать представление и данные в верстке, благо html это позволяет. В итоге у меня получился следующий синтаксис:
<div class="pull-left" bind-data="board" bind-handler="BoardBinding"></div>
bind-data говорит откуда взять данные, а bind-handler как их показать пользователю. Немного подумав про удобство, я ввел еще bind-form, который указывает контекст для дочерних элементов. С атрибутами это все, за исключением того, что bind-handler можно и не указывать, тогда bind-handler будет найден в Binding.DefaultHandlers по имени тэга.
var ButtonBinding = {
init : function(binding) { binding.element.onclick = function() { binding.callBindingFunction(); } },
modelChanged : function(binding) {
if (ObjectUtils.getObjectType(binding.getModel()) != ObjectUtils.TYPE_FUNCTION) {
binding.element.setAttribute("disabled", "disabled");
return;
}
binding.element.removeAttribute("disabled");
}
};
Binding.DefaultHandlers["BUTTON"] = ButtonBinding;
<button class="btn btn-success" bind-data="newGame">New game</button>
Давайте внимательно посмотрим на ButtonBinding — у него 2 метода: init, который вызывается при инициализации связи модели и представления, и modelChanged, который вызывается при изменении модели. В данном примере это не используется, но у modelChanged есть два дополнительных аргумента: model — это изменившийся объект, и event — содержит описание события.
Тут стоит пояснить, что связь выстраивается не с самим экземпляром объекта.
В xo у нас есть $data.Game.board — это объект который содержит описание игрового поля. Когда мы нажимаем на кнопку «New game», вызывается метод из $data.Game
newGame : function() {
this.board = new Board(DEFAULT_SIZE);
this.state = { player : 0, winner : null };
}
Эта функция создает новый экземпляр, но мы увидим, что был вызван modelChanged соответствующего view, и в качестве объекта, инициирующего событие, будет передан $data.Game, а в event — описание события.
На практике это значит примерно следующее: мы можем быть уверены, что view оперирует данными, указанными в bind-data, а не неким призрачным объектом, который заслуживает только одного внимания, а именно — сборщика мусора.
Текущее состояние и Roadmap
Сейчас это пре-пре-пре-альфа и может измениться все. Цель данной статьи — получить от Вас отклик, удобен будет подобный синтаксис в Ваших проектах или нет, чего не хватает? Библиотека проверена только в chrome.
Roadmap
- Косметический рефакторинг (привести в порядок имена классов — все перенести в bindit)
- Стабилизировать для основных браузеров
- Дополнительная сборка UI (view для основных тэгов input, button etc)
- ...
- PROFIT
Напоследок я бы хотел задать несколько вопросов коллективному разуму:
- Как Вы тестируете UI? Мне очень пригодилась бы библиотека, имитирующая поведение пользователя: клики, ввод текста etc
- Как перевести на русский Roadmap? Меня коробит от «представление» вместо view, но это стандарт де-факто, а русские аналоги roadmap еще хуже