Моя предыдущая публикация на Хабре достигла своих целей, — множество людей узнали о существовании W3View, некоторые посетили GitHub, кому-то наверное даже понравилось.


В то-же время, последовавшие комментарии выявили потребность в более чётком описании того, какую задачу решает эта библиотека, как она это делает и для чего она может понадобиться вам (ну вдруг).


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


Шаблоны, традиции и "шаблоны"


По традиции, заведённой одним малым и innerHTML, UI в Web строится с помощью различных шаблонизаторов, за последние несколько лет шаблонизаторы прошли большой путь и развились до невозможности. Они должны были упростить создание интерфейсов, но задачи от этого проще не стали. Для решения сложных задач разработчики шаблоных движков ввели массу новых понятий и связали из косты изобрели разнообразные концепции. Некоторые концепции даже диктуют правила, по которым должна строиться Модель, что на мой взгляд совершенно противоречит здравому смыслу. Мы ведь пишем приложения не для концепций. При этом шаблонизатор не всегда избавляет нас от потребности использовать DOM API, как ни крути, а предусмотреть всё — невозможно. Однако прямое воздействи на DOM запросто может поломать любую, даже самую прекрасную концепцию…
Разработчики шаблонизаторов, дабы не допустить разрушения концепций, объявили такое воздействие анафемой и моветоном, — то есть, по нашему, анти-паттерном.


Ну и что остаётся делать разработчику, если то, что он разрабатывает не укладывается в шаблоны? — остаётся одно — искать альтернативу.


Альтернатива шаблонам


Когда-то, давным-давно, HTML, Javascript и DOM API вполне позволяли обходиться без использования шаблонов и чёрной магии. HTML описывал структуру формы, Javascript, применяя DOM API, управлял этой формой и обрабатывал её события, это было красиво, просто и удобно. Но интерфейсы становятся всё сложнее и динамичнее, а HTML продолжает оставаться языком описания данных. Выходом может стать декомпозиция — разбиение большой и сложной формы на маленькие, независимые, переиспользуемые капсулы — компоненты.


Декларация компонента может выглядеть как маленькая страничка HTML со скриптом, в котором описывается реакция на изменение данных и определяются обработчики событий. Потом из таких компонентов можно собирать более сложные.


  • Где их брать? — из фабрики.
  • Как они туда попадут? — вы их сами опишете и туда положете.
  • Как их описывать? — обыкновенно — HTML, Javascript и DOM API.
  • Где взять такую фабрику? — npm install w3view.

W3View — это фабрика для создания простых и сложных компонентов


Как это устроено и как этим пользоваться я попробую объяснить на примере небольшого приложения. Долго думал о том какой пример лучше рассмотреть, остановился на W3View • TodoMVC. Готово, работает, сделано для демонстрации, есть с чем сравнивать, не слишком большое.


Со спецификацией, макетом и прочими входящими можно ознакомиться здесь. Наверное стоит взглянуть на спецификацию и макет прежде чем читать дальше.


Как устроено приложение W3View • TodoMVC


Я опущу описания Модели и Контроллера, они ничем не примечательны. Скажу лишь, что использовал классическую триаду MVC, где активная Модель реализует свою внутреннюю логику и не делает никаких предположений о том, как её будут отображать, Представление полностью базируется на W3View, а Контроллер связывает всё вместе в конечное приложение. Приложение реализует макет TodoMVC без изменений и не добавляет никаких своих стилей.


Для упрощения, описание компонентов размещено в скрытом DIV, прямо на странице.


Подключение, инициализация и запуск приложения


Приложени подключается к странице четырьмя тегами SCRIPT:


<script src="node_modules/w3view/w3view.js"></script>
<script src="js/models/todo.js"></script>       
<script src="js/controllers/todo.js"></script>      
<script src="js/app.js"></script>

Первый — это библиотека W3View, второй и третий — Модель и Контроллер, четвёртый — скрипт запуска, главное в нём (из того, что имеет отношение к работе W3View), — это:


var appContext = new todoController(todoModel('todos-W3View'));
var w3view = new W3View(appContext);
var sources = document.getElementById('components');
w3view.register(sources.children);
document.body.removeChild(sources);
w3view.create('application').mount(document.body, 0);
// далее настраивается роутинг, но это не интересно

  • Инициализируем Контроллер и Модель.
  • Создаём экземпляр W3View и инициализируем его контроллером, можно инициализировать чем угодно.
  • Берём описания компонентов, они будут располагаться в DIV#components.
  • Регистрируем компоненты в фабрике W3View.
  • Удаляем описания компонентов из дерева DOM, — можно было бы оставить, но зачем они там? — после попадания в фабрику они больше не понадобятся.
  • Создаём экземпляр рутового компонента APPLICATION и монтируем его на странице первым элементом.
  • Voila, приложение запущено!

Обычно я обхожусь меньшим количеством строк, но здесь решил для красоты почистить дерево DOM, что поделать — перфекционизм. При использовании предварительно подготовленного бандла, эти шесть строк превращаются в две:


var appContext = new todoController(todoModel('todos-W3View'));
w3view(appContext).create('application').mount(document.body, 0);

Понятное дело, бандл гораздо лучше для продакшена, но при разработке и для наглядности я предпочитаю подключать исходники.


Подключили, инициализировали, запустили, а что? Да..., надо же было создать компоненты!


Создаём компоненты


Как я уже говорил, декларация компонента — это HTML элемент, поэтому достаточно просто распилить существующий макет на кусочки, описать реакцию на изменение данных и обработчики событий, а затем скомпоновать всё это обратно.


В TodoMVC несложно было выделить динамические части, и рутовый компонент APPLICATION получился такой:


<section as="application" class="todoapp">
    <header class="header">
        <h1>todos</h1>
        <input ref="newTodo" class="new-todo" 
            placeholder="What needs to be done?" 
            autofocus>
    </header>
    <main ref="main"></main>
    <totals ref="totals"></totals>
    <script type="javascript">
        // здесь будет скрипт, отвечающий за 
        // обновление данных и обработку событий
    </script>
</section>

Как видите — просто HTML, с парой нестандартных атрибутов, двумя нестандартными тегами и неправильным типом тега SCRIPT.


  • Нестандартные атрибуты as и ref, соответственно имя объявляемого компонента и метка для ссылки на его внутренний элемент. Эти атрибуты будут интерпретированы так:

<section as="application" ...>...
из тега SECTION создаём новый компонент 
с именем APPLICATION,

<input ref="newTodo" ...>
внутри содержится элемент INPUT, 
который будет доступен из скрипта как "this.ref.newTodo"

Сам экземпляр компонента будет доступен из его собственного скрипта как this, кстати экземпляр компонента это просто узел DOM.

  • Нестандартные теги MAIN и TOTALS, — это (как вы уже догадались) те самые динамические части, они выделены в отдельные компоненты и вставлены в APPLICATION как теги. Их мы рассмотрим позже.


  • Неправильный тип тега SCRIPT приходится использовать, когда нужно предотвратить его автоматическое выполнение. Здесь ровно такой случай, потому что мы разместили описания прямо на странице. Тело скрипта в описании компонента — это тело функции-конструктора, она выполняется для каждого экземпляра после его создания. Выглядит эта функция так:

function (appContext, factory){
    // тот самый скрипт
}

  • appContext — тот самый appContext, которым мы инициализировали фабрику W3View,
  • factory — экземпляр фабрики, которая создала экземпляр компонента.

Содержимое тега SCRIPT и жизненный цикл экземпляров компонента


Итак SCRIPT. Мы должны указать Контроллеру о нашем Представлении, сделаем это при размещении приложения на странице, для этого определим в скрипте хэндлер onMount:


this.onMount = function (){
    appContext.setView(this);
    console.log("application section created");
}

Этот хэндлер вызывается сразу после вставки (монтирования) компонента в дерево DOM. Можно определить парный ему onUnmount, который вызывается непосредственно перед демонтированием компонента. Для того, чтобы они отрабатывали нужно монтаж и демонтаж производить специальными методами компонента — mount(targetElement, index) и unmount(). Если компонент демонтирован из DOM, его можно опять вмонтировать, при необходимости. Есть ещё onCreate и onDestroy, соответственно вызываются при создании и уничтожении компонента (уничтожать нужно методом destroy). При вызове destroy на экземпляре компонента, он сначала демонтируется (unmount()), а затем всё его поддерево тоже рекурсивно разрушается в хлам. После вызова destroy элемент уже нельзя использовать.


Хэндлеры onCreate и onMount следует определять, если вам необходимо разместить ссылки на компонент в других объектах. Код, удаляющий эти ссылки, должен располагаться в хэндлерах onDestroy и onUnmount, это желательно делать симметрично.


// этот пример не из TodoMVC
// в контексте компонента определена функция,
function someFunc(e){...}
// при передаче этой функции куда-либо,
// вместе с ней туда передаётся и контекст 
// в котором она была определена
this.onMount(){
    window.addEventListener('mousemove', someFunc);
}
// поэтому следует удалить её из внешнего объекта?
// чтобы и на неё и на этот контекст не оставалось ссылок
this.onUnmount(){
    window.removeEventListener('mousemove', someFunc);
}

Просто нужно мыть руки, и тогда память не будет утекать.


Содержимое тега SCRIPT, обновление данных


Так, с жизненным циклом разобрались, разберёмся теперь с изменением данных:


<section as="application" class="todoapp">
    <header class="header">
        <h1>todos</h1>
        <input ref="newTodo" class="new-todo" 
            placeholder="What needs to be done?" 
            autofocus>
    </header>
    <main ref="main"></main>
    <totals ref="totals"></totals>
    <script type="javascript">

    this.onSetData = function (input){
        this.ref.main.setData(input);
        this.ref.totals.setData(input.total);
    };

    this.onMount = function (){...};
    </script>
</section>

Здесь всё просто — при получении новых данных раздаём их элементам в своём поддереве. Извне (здесь из Контроллера) данные устанавливаются методом setData, он уже присутствует во всех создаваемых экземплярах W3View компонентов и просто вызывает хэндлер onSetData, который нужно определить. Метод setData и хэндлер onSetData могут принимать до трёх аргументов:


  • data — собственно данные для отрисовки — основной аргумент, обычно хватает только его, но иногда могут пригодиться и другие,
  • opts — в сложных случаях, например при работе со списками, требуется передать некоторый дополнительный контекст, общий для всех элементов списка,
  • additional — дополнительный атрибут, в случае со списками тут может оказаться номер элемента в списке.

А в прочем, можно применять все три аргумента на своё усмотрение, передавайте туда всё, что посчитаете нужным, это зависит только от решаемой задачи и реализации хэндлера onSetData.


this.ref.main и this.ref.totals — это ссылки на элементы в поддереве нашего компонента, помеченные атрибутом refref="main" и ref="totals" соответственно, я об этом кажется уже упоминал.


Содержимое тега SCRIPT — как обработать события


В компоненте APPLICATION есть ещё один элемент, на нём стоит метка ref="newTodo", — это поле ввода. По спецификации в него можно вводить что-попало, но при нажатии на клавишу ENTER, это что-попало должно попасть в список тудушек. Естесственно, чтобы такое случилось, нужно обработать нажатие на ENTER.


События в W3View обрабатываются совсем стандартным способом — навешиванием совсем стандартных обработчиков событий на обыкновенные элементы. В данном случае — onkeydown на элементе, на который мы поставили атрибут ref="newTodo".


<section as="application" class="todoapp">
    <header class="header">
        <h1>todos</h1>
        <input ref="newTodo" class="new-todo" 
            placeholder="What needs to be done?" 
            autofocus>
    </header>
    <main ref="main"></main>
    <totals ref="totals"></totals>
    <script type="javascript">

    this.ref.newTodo.onkeydown = function (e){
        var ENTER_KEY = 13;
        if(e.which !== ENTER_KEY) return;
        appContext.addNewTodo(this.value);
        this.value='';
    };

    this.onSetData = function (input){...};

    this.onMount = function (){...};
    </script>
</section>

Всё совсем обыкновенно: взяли элемент, навесили событие, которое, если нажата клавиша ENTER, вызывает метод на Контроллере (Контроллер здесь называется appContext — помните, мы его передавали в new W3View — это он).


Готово, рутовый компонент приложения закончился, перейдём к следующему. Следующим будет MAIN, потому что компонент TOTALS тривиальный — не стану про него ничего писать. Просто такая-же установка атрибутов и свойств элементам DOM, ничего нового. А вот на компоненте MAIN стоит остановится поподробнее.


Встроенный компонент ARRAY-ITERATOR


Самое интересное в MAIN — использование встроенного ARRAY-ITERATOR:


...
<array-iterator ref="list" usetag="ul" class="todo-list">
    <listItem></listItem>
</array-iterator>
...

ARRAY-ITERATOR уже присутствует в каждом экземпляре W3View, этот компонент служит для вывода массивов. Он настраивается компонентом, который будет отображать элементы. В setData ARRAY-ITERATOR принимает два аргумента:


  • data — массив данных,
  • opts — что-то общее для всех элементов массива.

Компонент, отображающий элемент массива получит три аргумента:


  • item — элемент иассива,
  • opts — то самое, общее для всех, что мы передали в ARRAY-ITERATOR,
  • index — индекс элемента в массиве.

Если вы укажете несколько компонентов для отображения элементов массива, то их экземпляры будут чередоваться, например:


...
<array-iterator ref="list" usetag="ul" class="todo-list">
    <listItem class="even"></listItem>
    <listItem class="odd"></listItem>
</array-iterator>
...

В таком случае — все нечётные элементы будут иметь класс "even", а все чётные — класс "odd".


ARRAY-ITERATOR сделан на основе DIV, а здесь нам нужен UL, поэтому мы модифицируем его с помощью атрибута usetag="ul". Любой кмпонент W3View может получать такой атрибут для подмены тега, на основе которого он был объявлен.


Если вам понадобится при объявлении компонентов использовать теги TBODY, TR или TD вне контекста TABLE, вы можете использовать атрибут tagname для любого элемента, кроме компонентов W3View.


Ну вот, теперь вы всё знаете о том, как создавать компоненты, как их регистрировать в фабрике и как применять. Осталось только рассказать о способе удержать ваши компоненты в руках.


Библиотеки компонентов — модули


TodoMVC — очень маленькое и простое приложение — всего четыре компонента понадобилось для описания его интерфейса, поэтому была возможность описать всё это прямо в странице. В реальной жизни компонентов может потребоваться очень много. Одних только кнопок, инпутов и прочих слайдеров случаются десятки, что уж говорить об экранчиках, панельках, формочках, всплывающих попупчиках и прочем всём остальном, за что так любят UI.


Конечно описывать всё это на странице не удобно, нужно выделять в отдельные файлы и желательно, чтобы это были не просто файлы, а модули с библиотеками компонентов. W3View позволяет загружать модули и разрешать зависимости между ними на лету. Для этого нужно использовать moduleLoader, использовать его нужно так:


<script src="../w3view.js"></script>
<script src="moduleLoader.js"></script>
<script src="httpreader.js"></script>
<script>
    var appContext = {};
    moduleLoader(appContext, 
        '../examples/modules/window.w3v.html', 
        reader, 
        function(factory){
            factory.create('app').mount(document.body);
        });
</script>

Это пример из папки loader пакета w3view.


moduleLoader принимает четыре аргумента:


  • appContext — контекст приложения,
  • path — путь к рутовому модулю,
  • reader — читатель HTTP (XHR) и
  • callback — функция, которая стартанёт приложение, когда всё загрузится. Она принимает в аргумент получившийся, заполненный компонентами экземпляр W3View.

Зависимости подключаются с помощью тега IMPORT, если к одной библиотеке нужно подключить другую, то в этой одной нужно написать:


<import src="./realative/path/to/module" as="namespace" type="html"></import>

<div as="my-component"> ......

Имя, указанное в атрибуте as можно далее использовать в качестве неймспейса, примерно так:


<import 
    src="./realative/path/to/module" 
    as="namespace" type="html"></import>

<div as="my-component">
    <namespace:another-component>
    </namespace:another-component>
</div>

Таким образом становятся доступны все компоненты из подключаемой библиотеки. Сколько бы раз мы ни импортировали один модуль, он будет загружен только один раз, допускаются также рекурсивные зависимости.


Для сокращения времени загрузки и разбора HTML описаний компонентов можно использовать билдер. Он находится в папке builder пакета w3view, там в файле build.js написано как им пользоваться. Результатом сборки является один js файл, включающий все импортированные библиотеки.


Наконец закончил, спасибо что дочитали


Вот собственно и всё, что следует знать о W3View, чтобы начать пользоваться. Сама библиотека гораздо меньше чем это описание, не составит большого труда разобраться в её устройстве просто заглянув в исходники, я старался сделать её как можно более простой и прозрачной. Библиотека не навязывает никаких правил, так что можно программировать так, как вы считаете нужным для решения ваших задач.


Желаю удачи.

Комментарии (8)


  1. PaulMaly
    25.02.2018 22:14

    Вообще main это довольно стандартный тег и уже давно…


    1. justboris
      25.02.2018 23:58

      Более того, по нынешней спецификации все имена тегов без дефисов — стандартные. Кастомным веб-элементам разрешены только имена с дефисом.


      1. PaulMaly
        26.02.2018 08:02

        В данном контексте это не более чем игра слов и понятий. Важно другое — автор предлагает переопределять поведение общепринятого тега.

        Не говоря уже от том, что другие разработчики могут быть не в курсе наличия подобных переопределений.


      1. OldVitus Автор
        26.02.2018 09:48

        Это не кастомные элементы, имена определяемых тегов актуальны только в пределах одного модуля, за его пределы не выходят. W3View предоставляет вам любую свободу, даже позволяет в своём контексте переопределять любые имена тегов, хоть DIV, — всё для вас и всё на ваше усмотрение.


        1. justboris
          26.02.2018 10:50

          А поведение <input/> или <video/> тоже переопределить можно?


          1. OldVitus Автор
            26.02.2018 11:12

            В пределах модуля, если хотите или считаете нужным для решения вашей задачи


            1. justboris
              26.02.2018 11:17

              и как это будет работать? Браузер не даст задать этим элементом кастомный innerHTML.


              1. OldVitus Автор
                27.02.2018 15:59

                Специально для Вас придумал кейс, Вы мне теперь должны что-нибудь :)
                если очень, прямо вот именно так надо — хоть дерись :


                <div as="video">
                    Кина не будет!
                </div>
                
                <div as="app">
                    <video></video>
                    <video></video>
                </div>
                

                В результате как-то так


                <div>
                    <div>
                        Кина не будет!
                    </div>
                    <div>
                        Кина не будет!
                    </div>
                </div>