2 December 2013

kidomi: построение DOM-объектов «на лету»

JavaScript
Одним дождливым осенним вечером пришла мне в голову мысль о том, что никогда прежде я не писал JavaScript код следуя канонам test-driven development (TDD). Лиха беда начало! Результатом работы стала маленькая библиотека-шаблонизатор работающая по принципу «JSON на входе, HTMLElement или просто DOM объект на выходе».

Из инструментов использовались: CoffeeScript, QUnit, PhantomJS, Google Closure compiler, а собирается всё это с помощью старого доброго GNU Make. Статья для всех, кому интересна библиотека и для тех, кто поверхностно знаком с вышеперечисленными технологиями и хотел бы увидеть их в работе.


Что получилось в результате?


elem = kidomi(
     ['div#main.content',
         ['span', {style: {color: 'blue'}}, 'Select file'],
         ['form', {
             name: 'inputName',
             action: 'getform.php',
             method: 'get'},
         'Username: ',
         ['input', {'type': 'text', 'name': 'user'}],
         ['input', {'type': 'submit', 'value': 'Submit'}]]])

Где elem — это объект HTMLElement, который выглядит как:

<div id="main" class="content">
  <span style="color: blue;">Select file</span>
  <form name="inputName" action="getform.php" method="get">
    Username:
    <input type="text" name="user"></input>
    <input type="submit" value="Submit"></input>
  </form>
</div>

Ещё один пример, в котором сначала создаётся элемент , к onclick которого привязывается функция, после чего элемент добавляется в общую структуру:

button = kidomi(['a.button', {href: '#'}]);
button.onclick = function() { alert('Hello world!'); };

elem = kidomi(['div', ['span', 'Click this button:'], button]);

Бывалые люди сразу вспомнят jquery-haml, однако вдохновение для написания kidomi черпалось из ClojureScript-библиотеки dommy.

О чём же сыр-бор?


  • kidomi написана на CoffeeScript.
  • Она компилируется Google Closure в расширенном (ADVANCED_MODE) режиме.
  • Она покрыта юнит-тестами.
  • И эти тесты работают в т.ч. с помощью PhantomJS.
  • Всё это собирается и запускается с помощью make.


Тонкости CoffeeScript


Исходный код начинается следующим образом:

window['kidomi'] =
kidomi = (data) ->
...

Для тех, кто не знаком с особенностями компиляции CoffeeScript: по-умолчанию весь скомпилированный код оборачивается в функцию-обёртку и таким образом не экспортируется глобально. Кстати это поведение можно отключить флагом компилятора --bare, но разве позволительно засорять глобальное пространство имён?

(function() {
  /* ... */

  window['kidomi'] = kidomi = function(data) {
    /* ... */
  }

  /* ... */
}).call(this);


Ещё одна особенность записи:

window['kidomi'] =
# а не
window.kidomi =

Это сделано специально для компилятора Google Closure, который бы "сократил" название, при записи window.kidomi =

Далее, код пишется в похожем ключе:

kidomi.makeElementFromTagData =
makeElementFromTagData = (tagData) ->
# ...

kidomi.addAttributes =
addAttributes = (elem, data) ->
# ...

# и т.д.


Как вы могли заметить, функции объявляются как локально, так и "экспортируются" в функцию-объект kidomi. В первом случае это сделано для удобства: не нужно писать никаких префиксов (хотя в CoffeeScript достаточно написать @name, что скомпилируется в this.name). A чтобы юнит тесты могли до этой функции добраться, её можно записать в виде аттрибута глобального объекта. Что и делается через kidomi.functionName.

Тестирование


Помните свои первые шаги в TDD? Мне например стоило неимоверных усилий заставить себя сначала писать тесты, а после - код. Зато, как быстро TDD приносит дивиденды!

Как было сказано выше, для написания юнит тестов для kidomi использовалась библиотека QUnit. Один из простейших тестов выглядит следующим образом:

test('isString', ->
    ok(kidomi.isString(''))
    ok(not kidomi.isString({}))
    ok(not kidomi.isString([]))
    ok(not kidomi.isString(10)))



А вот и сама функция:

kidomi.isString =
isString = (s) ->
    typeof(s) == 'string' or s instanceof(String);


Стоит обратить внимание на то, что необходимо протестировать не только kidomi.js, но и обработанную напильником компилятором Closure kidomi.min.js. В идеале - все тесты покрывающие несжатый файл должны работать и для сжатой версии. Но тут мы натыкаемся на то, что все имена фунцкий кроме kidomi были изменены до неузнаваемости. Например, вышеприведённый код isString(s) превратился в

d.e=k=function(a){return"string"===typeof a||a instanceof String};

Чтобы с этим справиться, нужно скомпилировать библиотеку и тесты как единое целое. Также нужно указать компилятору, что qunit.js - это внешняя зависимость и соответственно такие имена фунцкий, как test, module, ok и т.д. должны остаться без изменений.

Тем не менее, тестирование, в котором библиотека и тесты слиплены в один min.js файл, всё-таки отличается от тестирования сжатой библиотеки отдельно. Один из вариантов - запустить тесты лишь для основной функции.

Таким образом полное тестирование kidomi происходит в 3 прохода:

  1. Все тесты прогоняются на несжатой kidomi.js. Тесты и библиотека в отельных файлах.
  2. Все тесты сжимаются вместе с kidomi.js. Тесты и библиотека в одном файле.
  3. Тесты для функции kidomi() прогоняются на сжатой kidomi.min.js. Тесты и библиотека в отельных файлах.


PhantomJS

Как уже рассказывалось на Хабре, PhantomJS - это WebKit работающий в консоли и управляющийся собственным JS-API. На просторах интерета был найден скрипт связывающий PhantomJS и QUnit простым и в то же время эффективным способом: он парсит страницу с результатами тестирования и завершает процесс с кодом 0 (успех) или 1 (ошибка) в зависимости от результата тестов. Кстати, все тесты можно запустить в обычном браузере.

Сборка

Для сборки можно было использовать Rake, Maven, Grunt и т.д., но к сожалению со всеми вышеперечисленными системами я на "вы" (камрады, обещаю наверстать упущенное к следующему посту на тему JavaScript). Make же, как мне кажется, справился с задачей на "Ура!".

Makefile состоит всего из трёх основных целей сборки (build targets): ${BUILD_DIR}, $(BUILD_DIR)/kidomi.js и $(BUILD_DIR)/kidomi.min.js (также дополнительные плюшки в виде целей all, clean, .PHONY и т.д.). В конце Makefile'а подключается файл Makefile.testsuite.mk содержащий цели и правила для сборки и запуска всех ранее упомянутых тестов.

Заключение

Надеюсь, статья была для вас интересной и вы узнали из неё что-то новое. Исходный код kidomi открыт для всех желающих. Архивы содержат собранную версию библиотеки. Все комментарии, советы, отзывы и критика горячо приветствуются!
Благодарю за внимание!
Tags:javascriptcoffeescriptDOMclosure compilertemplatesшаблонизатор
Hubs: JavaScript
+3
3.5k 20
Comments 6
Popular right now