Javascript: тесты, компиляция и MVVM

Доброго времени суток.
Современный мир оставляет мало возможностей не сталкиваться с 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


Собственно, представляет интерес тут только одна строчка, а именно
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 еще хуже

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 10

    0
    Спасибо за QUnit.config, не знал про него.

    Как Вы тестируете UI?

    Selenium
    У Selenium есть и addon для Firefox и WebDriver для тестирования без браузера например под Jenkins.

      0
      Рад, что смог помочь

      По поводу Selenium — хотелось бы примерно так: в js: doUserAction({id:«btn1», action: click}) — синтаксис в данном случае не важен, не подскажите он так умеет?
        0
        Есть Soda, адаптер для Node.js.
        Он позволяет такое:
        browser
          .chain
          .session()
          .setTimeout(8000)
          .open('/')
          .waitForPageToLoad(5000)
          .clickAndWait('//input[@value="Submit"]')
        ...
        

        То есть элементы задаются при помощи XPath.
          0
          Спасибо — посмотрю, выглядит очень интересно, но хотелось бы запускать тест просто открытием страницы, как и остальные без node и silenium.
      0
      Roadmap — план развития, например.

      Technology Roadmap — краткосрочный или долгосрочный план выпуска производителем какого-либо продукта.

      В технике roadmap — перспективный план, в смысле «не очень конкретный и открытый компанией для публики».

      С одного форума,

      Идем на MERRIAM-WEBSTER ONLINE, ищем там
      Второе значение:

      www.m-w.com/cgi-bin/dictionary?book=Dictionary&va=roadmap&x=0&y=0
      2 a: a detailed plan to guide progress toward a goal b: a detailed explanation

      Т.е. roadmap это всего навсего «план», может быть «детальный план».
        0
        Наверное, это самый адекватный вариант)
        0
        как дела с modelChanged для коллекций и есть ли биндинги-аналоги forEach?
          0
          Плохо на самом деле — с коллекциями надо что-то думать, но невозможность подписаться на событие obj[index] для любого индекса это грусть. У меня есть несколько ответов в голове, надо выбрать наиболее непохожий на костыль.
            0
            Пока я склоняюсь к мысли использовать свои коллекции, которые полностью повторяют стандарт, кроме одной возможности: array[array.length + 20] = 42; То есть изменение длинны массива заданием последнего элемента.
          0
          Лучше добавить, чтобы нормально перезапускались тесты:
          require.config({ urlArgs : "date=" + (new Date()).getTime() });
          

          Only users with full accounts can post comments. Log in, please.