0,0
рейтинг
21 апреля 2014 в 14:39

Разработка → jWidget — объектно-ориентированный JavaScript MV* framework

Есть замечательный сайт http://todomvc.com/, на котором демонстрируется решение одной и той же задачи с помощью разных JavaScript MV* (Model-View-[Controller]) фреймворков. Сейчас там представлены десятки различных фреймворков, у каждого из которых есть свои преимущества и недостатки. Есть там и такие гиганты, как Angular, Ember и Backbone. Несмотря на высокую конкуренцию, я все равно хотел бы продемонстрировать свой MV* фреймворк — jWidget.

Я быстро просмотрел все решения, представленные на сайте TodoMVC, и не нашел ни одного фреймворка, похожего на jWidget. Дело в том, что, помимо JavaScript, я много программирую на объектно-ориентированных языках программирования, таких как Java, C#, а в прошлом и на C++. Поэтому я большой фанат объектно-ориентированного программирования, SOLID принципов и паттернов объектно-ориентированного проектирования. Мне не нужен фреймворк, который стеснял бы меня в возможности применения стандартных объектно-ориентированных решений. То, что я увидел в существующих решениях TodoMVC, не внушает доверия в этом отношении. Как правило, они предоставляют некий декларативный синтаксис и мощный шаблонный движок, но объектно-ориентированная основа всего этого, даже если она существует, скрыта от наших глаз.


Документация на английском: http://enepomnyaschih.github.io/jwidget/index.html#!/guide/home

Документация на русском: http://enepomnyaschih.github.io/jwidget/index.html#!/guide/ruhome

Проект на GitHub: https://github.com/enepomnyaschih/jwidget

Twitter: @jwidgetproject

Реализация TodoMVC на jWidget: http://enepomnyaschih.github.io/todomvc/labs/architecture-examples/jwidget/release/

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

Исходный код TodoMVC на jWidget: https://github.com/enepomnyaschih/todomvc/tree/gh-pages/labs/architecture-examples/jwidget/

Кратко перечислю основные характеристики jWidget:

1. Строгое соответствие принципам ООП. Полностью задокументированная на двух языках библиотека классов с примерами руководством.
2. Скорость работы скрипта превыше всего. Отсюда явное объявление конструктора класса и минимальное использование замыканий при объявлении классов, т.к. в Google Chrome наследование через прототипы гораздо эффективнее наследования по паттерну «Модуль» (в Firefox наоборот, но там разница не так велика).
3. Ни одна манипуляция в модели не требует полного перерендеринга представления. Каждый компонент рендерится только один раз, после чего он только обновляет свои отдельные элементы, за счет чего обеспечивается высокая скорость работы приложения.
4. Фреймворк работает на базе jQuery.
5. Имеет простейший шаблонный движок, не требующий препроцессинга перед отправлением в функцию https://api.jquery.com/jQuery.parseHTML/. Никакой магии в шаблонах, никакого inline-кода: весь Data binding осуществляется в JavaScript коде компонента. Благодаря этому, одни и те же техники Data binding'а можно применять как для связи представления с моделью, так и для связи объектов внутри модели или внутри представления, что часто оказывается полезным.
6. Все объекты после использования полностью уничтожаются. Благодаря этому обеспечивается экономный расход ресурсов клиента и отсутствие непредвиденных ошибок в консоли от «мертвых» объектов, пытающихся обработать некоторое событие. Например, вы можете использовать одну и ту же модель на протяжении работы приложения, налету меняя ее представления. Любое представление слушает события модели, но после того, как представление удаляется из DOM, оно обязано отписаться от этих событий, чтобы не тратить процессорное время на обработку этих событий, и чтобы сборщик мусора мог очистить память. Предусмотрен легкий способ уничтожения объектов — т.н. механизм агрегации объектов.
7. Собственный сборщик приложения — jWidget SDK, — упрощает разработку приложения и выполняет оптимизацию кода перед релизом на продакшен. Планируется заменить его стеком плагинов к GruntJS. Просто когда я начал разработку jWidget, GruntJS или чего-то подобного еще не существовало.

Чтобы подтвердить, что jWidget работает очень быстро, я отмерил время добавления 500 записей в TodoMVC с ожиданием в 0 миллисекунд после добавления каждой записи, чтобы дать браузеру время перерисовать представление. Также, я примерно отмерил время операций Select all и Clear completed для 500 записей. Результаты таковы:

  • Angular JS — 16847 миллисекунд. Операции Select all и Clear completed выполняются мгновенно.
  • Angular JS (performance optimized version) — 13287 миллисекунд. Операции Select all и Clear completed выполняются мгновенно.
  • Ember JS — 13095 миллисекунд. Операции Select all и Clear completed выполняются примерно 3 секунды.
  • Backbone — 9506 миллисекунд. Операции Select all и Clear completed выполняются примерно 3 секунды.
  • jWidget — 9974 миллисекунд. Операции Select all и Clear completed выполняются мгновенно.
  • YUI — больше минуты. Не дождался.


Как видите, только Backbone незначительно превзошел jWidget по скорости добавления записей, но при этом сильно отстал по скорости Select all и Clear completed. При этом учтите, что отставание Angular и Ember в 3 секунды на самом деле является значительным, поскольку кучу времени во всех случаях просто скушал 500-кратный вызов setTimeout. В общем, из 3 наиболее популярных фреймворков ни один не справился до конца с большими объемами данных, тогда как jWidget показал себя на высоте.


Теперь расскажу о механизме работы jWidget. Фреймворк состоит из 5 слоев:

  1. Классы и объекты. Наследование классов. Механизм агрегации объектов.
  2. События. Объявление событий. Подписка, отписка и генерация событий.
  3. Свойства и их хелперы. Создание новых свойств на базе существующих. Data binding на базе свойств.
  4. Коллекции и их синхронизаторы. Массив, словарь, множество. Создание новых коллекций на базе существующих. Data binding на базе коллекций.
  5. Компоненты. Шаблоны. Связь элементов шаблона с кодом компонента. Создание дочерних компонентов с помощью Data binding'а на базе свойств и коллекций.


1. Классы и объекты.



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

// Объявляем конструктор.
var Hand = function(side) {
    // Вызываем конструктор базового класса.
    Hand._super.call(this);
    
    // Присваиваем поле.
    this.side = side;
    
    // Присваиваем даже те поля, которые по умолчанию не установлены.
    // Опыт показал, что в некоторых браузерах это существенно ускоряет работу приложения.
    this.grabbedObject = null;
};

// Наследуем Hand от JW.Class.
JW.extend(Hand, JW.Class, {
    // Объявляем поля в комментарии, для нашего удобства.
    // String side;
    // Grabbable grabbedObject;
    
    // Объявляем метод.
    grab: function(obj) {
        this.grabbedObject = obj;
    },
    
    // Перегружаем метод уничтожения объекта.
    destroy: function() {
        console.log("Destroying " + this.side + " hand");
        
        // Тот же метод базового класса можно вызвать через _super.
        this._super();
    }
});


Одна из ключевых возможностей JW.Class — это механизм агрегации объектов, который служит для уничтожения объектов, которые находятся под контролем другого объекта. Эту идею я почерпнул из введения к книге Приёмы объектно-ориентированного проектирования. Паттерны проектирования от «банды четырех». Там рассказывается, что все указатели на объекты делятся на два типа: агрегирование и осведомленность. Осведомленность обозначает, что объект, владеющий указателем, не несет никакой ответственности за объект, на который он ссылается. Он просто имеет доступ к его публичным полям и методам, но время жизни этого объекта не под его контролем. Агрегирование же обозначает, что объект, владеющий ссылкой, несет ответственность за уничтожение объекта, на который он ссылается. Как правило, агрегируемый объект живет, пока жив объект-владелец, хотя бывают и более сложные случаи.

В jWidget агрегирование реализуется через метод own класса JW.Class. Передав объект B в метод own объекта A, вы сделали объект A владельцем объекта B. При уничтожении объекта A объект B будет уничтожен автоматически. Для удобства, метод own возвращает объект B. Ниже приведен пример кода, использующего эту возможность.

var Soldier = function() {
    Soldier._super.call(this);
    
    // Создаем две руки. Руки - неотъемлемая часть солдата,
    // поэтому агрегируем их.
    this.leftHand = this.own(new Hand("left"));
    this.rightHand = this.own(new Hand("right"));
};

JW.extend(Soldier, JW.Class, {
    // Hand leftHand;
    // Hand rightHand;
    
    destroy: function() {
        console.log("Destroying soldier");
        this._super();
    }
});


Теперь мы можем создать солдата и уничтожить его вызовом метода destroy.

var soldier = new Soldier();
soldier.destroy();


В результате чего мы увидим в консоли браузера следующие строки:

Destroying soldier
Destroying right hand
Destroying left hand


Как видите, при уничтожении солдата руки уничтожаются автоматически. Альтернативно, мы могли бы уничтожить руки явно в методе destroy класса Soldier вызовом их метода destroy. Но агрегация позволяет нам добиться этого меньшим количеством кода. Вообще, в реальном приложении метод destroy приходится перегружать очень редко. Например, в моей реализации TodoMVC этот метод не перегружается ни разу — все достигается одним механизмом агрегации объектов.

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

2. События.



События — неотъемлемая часть любого MV* фреймворка. Если представление имеет прямой доступ к модели, то модель ничего не знает о представлении. Обратная связь осуществляется никак не иначе, как через события. Здесь не имеются ввиду стандартные события пользовательского интерфейса, такие как click, mousedown или keypress, а события вроде «изменилось имя документа», «новый документ добавлен в папку», «документы в папке отсортированы по дате». Это не заложено в стандартные средства языка программирования, поэтому это задача фреймворка.

Как я писал в начале статьи, скорость работы скрипта в jWidget превыше всего. Поэтому стандартная схема подписки на события, которая предлагается, например, в jQuery, нам не подходит.

$("#document").bind("click", onClick);
$("#document").unbind("click", onClick);


Проблема здесь заключается в том, что алгоритм отписки от события имеет линейную вычислительную сложность (полный перебор). Мне же удалось реализовать схему, при которой время отписки от события равно времени удаления ключа из словаря, что существенно быстрее. Кроме того, схема событий jWidget реализована по всем принципам ООП и отлично сочетается с механизмом агрегации объектов.

var Document = function(title) {
    Document._super.call(this);
    this.title = title;
    
    // Создаем объект события.
    this.titleChangeEvent = this.own(new JW.Event());
};

JW.extend(Document, JW.Class, {
    // String title;
    // JW.Event titleChangeEvent;
    
    setTitle: function(title) {
        if (this.title === title) {
            return;
        }
        this.title = title;
        
        // Выбрасываем событие.
        this.titleChangeEvent.trigger(new JW.ValueEventParams(this, title));
    }
});

var Client = function(document) {
    Client._super.call(this);
    this.document = document;
    
    // Подписываемся на событие. Благодаря агрегации, подписка на событие будет
    // уничтожена автоматически при уничтожении клиента.
    this.own(document.titleChangeEvent.bind(this._onTitleChange, this));
};

JW.extend(Client, JW.Class, {
    // Document document;
    
    _onTitleChange: function(params) {
        console.log("Changed title to " + params.value);
    }
});

// Немного потестируем.
var doc = new Document("apple");
var client = new Client(doc);
doc.setTitle("banana"); // Вывод: Changed title to banana
doc.setTitle("cherry"); // Вывод: Changed title to cherry

// Не забываем уничтожать все, что создаем.
client.destroy();
doc.destroy();


Событие представляется классом JW.Event. Подписка на событие возвращается методом bind в виде экземпляра класса JW.EventAttachment. Уничтожение подписки равноценно отписке от события. Когда мы выбрасываем событие методом trigger, мы передаем туда экземпляр JW.EventParams для передачи его обработчикам событий в качестве аргумента.

3. Свойства и их хелперы



Фреймворк не может называться полноценным MV* фреймворком, если он не предоставляет возможности Data binding'а. jWidget предоставляет эту возможность. Объекты следующих классов автоматически выбрасывают события о своем изменении, и, следовательно, могут быть использованы для Data binding'а:



О коллекциях (Array, Map, Set) расскажу в следующем параграфе, а сейчас я хотел бы объяснить, что такое свойство (JW.Property). Свойство — это «переменная», которая выбрасывает события об изменении своего значения. Отсюда простейший интерфейс этого класса:



Когда вы передаете значение x в метод set, свойство проверяет, не равно ли оно этому значению x. Если равно, ничего не происходит. Если не равно, свойство присваивает себя значению x и выбрасывает событие changeEvent.

Несмотря на то, что интерфейс класса прост до неузнаваемости, он предоставляет широкие возможности для Data binding'а, которые на порядок сокращают объем кода приложения. Во-первых, мы можем связать два свойства, копируя значение одного свойства в другое:

var source = new JW.Property("apple");
var target = new JW.Property();
new JW.Copier(source, {target: target});
assertEqual("apple", target.get());
source.set("banana");
assertEqual("banana", target.get());


Метод bindTo делает то же самое, что позволяет нам сделать код более понятным. Кроме того, обращаю ваше внимание на то, что свойство target в данном случае тоже выбрасывает события о своем изменении, так что можно связать сколько угодно свойств по цепочке:

var source = new JW.Property("apple");
var target1 = new JW.Property();
target1.bindTo(source);
var target2 = new JW.Property();
target2.bindTo(target1);
source.set("banana");
assertEqual("banana", target2.get());


Копирование свойств налету — это только начало. Давайте попробуем создать новое свойство на базе двух существующих свойств по формуле text = value + " " + unit:

var value = new JW.Property(1000);
var unit = new JW.Property("MW");
var functor = new JW.Functor([ value, unit ], function(value, unit) {
    return value + " " + unit;
}, this);
var target = functor.target;
assertEqual("1000 MW", target.get());
value.set(1500);
assertEqual("1500 MW", target.get());
unit.set("МВт"); // включаем русскую локализацию
assertEqual("1500 МВт", target.get());


Наконец, привяжем текст внутри какого-то элемента представления к построенному свойству:

new JW.UI.TextUpdater("#capacity", target);


Теперь при изменении value и unit у вас автоматически будет обновляться текст внутри элемента #capacity.

Полный список возможностей класса JW.Property смотрите в документации.

jWidget переносит привычный Data binding через HTML-шаблоны в JavaScript-код приложения. Это дает колоссальные возможности по оптимизации приложения и расширению его возможностей. Data binding не ограничен лишь прослойкой между моделью и представлением. Вы можете с легкостью связывать между собой свойства внутри модели и внутри представления. Алгоритм работы приложения совершенно прозрачен, и вы сами можете контролировать, что с чем связывать, исходя из конкретных сценариев использования вашего приложения. Появляется возможность повторного использования всех функций приложения. Фреймворк не выполняет никакой прекомпиляции HTML-шаблонов, чтобы вычленить оттуда формулы для Data binding'а, благодаря чему скорость работы приложения увеличивается.

Значение свойства можно заагрегировать методом ownValue.

4. Коллекции и синхронизаторы



jWidget вводит 3 собственных класса коллекций: JW.AbstractArray, JW.AbstractMap и JW.AbstractSet. Это не значит, что вам запрещено использовать нативные Array и Object — коллекции jWidget легко преобразуются в нативные и обратно. Каждая коллекция jWidget имеет две реализации — простую и оповещающую:

JW.AbstractArray: JW.Array и JW.ObservableArray
JW.AbstractMap: JW.Map и JW.ObservableMap
JW.AbstractSet: JW.Set и JW.ObservableSet

Простые коллекции работают чуть-чуть быстрее оповещающих, зато оповещающие коллекции выбрасывают события о своем изменении, благодаря чему к ним свободно применяется Data binding. Также, классы простых коллеций имеют идентичный набор статических методов, которые предназначены для выполнения таких же операций с нативными Array и Object. В качестве примера приведу операцию создания массива объектов представления по массиву объектов модели:

// @param {JW.AbstractArray} documents
function createDocumentViews(documents) {
    return documents.$map(function(document) {
        return new DocumentView(document);
    }, this);
}


При этом мы просто создали новый экземпляр JW.Array и заполнили его объектами представления. Никакой связи между массивами документов и их представлений не сохранилось, так что изменение массива documents не повлечет за собой изменение массива представлений. Чтобы связать их между собой, нужно настроить Data binding. В jWidget это делается путем создания синхронизатора. В данном случае, нужно создать Mapper:

function createDocumentViews(documents) {
    return documents.createMapper({
        createItem: function(document) {
            return new DocumentView(document);
        },
        destroyItem: function(documentView) {
            documentView.destroy();
        },
        scope: this
    }).target;
}


Как видите, вместо одного коллбека мы теперь передаем два. Второй коллбек нужен для того, чтобы Mapper смог уничтожить представление документа, если его удалили из массива documents. Mapper формирует массив target и держит его в полном соответствии с исходным массивом. При уничтожении Mapper'а он уничтожит все оставшиеся в target представления… Кстати, мы забыли уничтожить Mapper. Воспользуемся агрегированием:

var DocumentList = function(documents) {
    DocumentList._super.call(this);
    this.documentViews = this.createDocumentViews(documents);
};

JW.extend(DocumentList, JW.Class, {
    createDocumentViews: function(documents) {
        return this.own(documents.createMapper({
            createItem: function(document) {
                return new DocumentView(document);
            },
            destroyItem: function(documentView) {
                documentView.destroy();
            },
            scope: this
        })).target;
    }
});


Обратите внимание, как стоят круглые скобочки. Мы агрегируем именно Mapper, а возвращаем именно его target.

Метод createMapper работает как для JW.Array, так и для JW.ObservableArray. Только в первом случае он не сможет осуществлять постоянный Data binding, поскольку JW.Array не выбрасывает никаких событий. Зато вы можете разрабатывать абсолютно полиморфное решение с возможностью в любой момент при необходимости заменить JW.Array на JW.ObservableArray.

jWidget предоставляет широкий набор синхронизаторов. Полный список смотрите в документации.

Элементы коллекции можно заагрегировать методом ownItems.

5. Компоненты



Наконец, добрались до представления. jWidget предоставляет класс JW.UI.Component в качестве базового класса для всех компонентов представления. Каждый класс компонента имеет свой шаблон, который наследуется вместе с этим классом. Шаблон — это обычный HTML, в котором добавлены 2 новых атрибута: jwclass и jwid. Шаблон привязывается к классу компонента методом JW.UI.template.

var MyComponent = function(message, link) {
    MyComponent._super.call(this);
    this.message = message;
    this.link = link;
};

JW.extend(MyComponent, JW.UI.Component, {
    // String message;
    // String link;

    renderComponent: function() {
        this._super();
        this.getElement("hello-message").text(this.message);
        this.getElement("link").attr("href", this.link);
    }
});

JW.UI.template(MyComponent, {
    main:
        '<div jwclass="my-component">' +
            '<div jwid="hello-message" />' +
            '<a href="#" jwid="link">Click me!</a>' +
        '</div>'
});


Атрибут jwclass задается только для корневого элемента компонента, и он является приставкой к CSS классам элементов. Атрибут jwid является суффиксом к CSS классу данного элемента. Например, приведенный выше шаблон раскроется в следующий HTML:

<div class="my-component">
    <div class="my-component-hello-message" />
    <a href="#" class="my-component-link">Click me!</a>
</div>


Чтобы отрендерить компонент в DOM, можно воспользоваться следующей инструкцией:

var component = new MyComponent("Hello, Wanderer!", "http://google.com");
component.renderTo("body");


В коде компонента видно, что с помощью метода getElement можно получить jQuery-обертку элемента по его jwid.

Метод renderComponent является методом жизненного цикла компонента. Перегрузив его, вы можете манипулировать элементами компонента и создавать дочерние компоненты.

Дочерние компоненты бывают трех типов:

  1. Именованные дочерние компоненты
  2. Легко заменяемые дочерние компоненты
  3. Массивы дочерних компонентов


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

var Application = function() {
    Application._super.call(this);
};

JW.extend(Application, JW.UI.Component, {
    renderComponent: function() {
        this._super();
        this.children.set(this.own(new Header()), "header");
        this.children.set(this.own(new Content()), "content");
    }
});

JW.UI.template(Application, {
    main:
        '<div jwclass="application">' +
            '<div jwid="header" />' +
            '<div jwid="content" />' +
        '</div>'
});


Заголовок и содержимое будут отрендерены и встанут на место элементов «header» и «content». Вы можете налету добавлять и удалять компоненты из словаря, тем самым манипулируя содержимым компонента.

Легко заменяемые дочерние компоненты похожи на именованные, но работают на базе JW.Property. Они добавляются методом addReplaceable. Такие компоненты удобно рендерить с помощью JW.Mapper:

var Application = function(selectedDocument) {
    Application._super.call(this);
    this.selectedDocument = selectedDocument;
};

JW.extend(Application, JW.UI.Component, {
    // JW.Property selectedDocument;
    
    renderComponent: function() {
        this._super();
        var documentView = this.own(new JW.Mapper([ this.selectedDocument ], {
            createValue: function(document) {
                return new DocumentView(document);
            },
            destroyValue: function(documentView) {
                documentView.destroy();
            },
            scope: this
        })).target;
        this.addReplaceable(documentView, "document");
    }
});

JW.UI.template(Application, {
    main:
        '<div jwclass="application">' +
            '<div jwid="document" />' +
        '</div>'
});


Таким образом, мы осуществили Data binding к свойству selectedDocument. При изменении значения этого свойства представление старого документа будет автоматически уничтожено, а представление нового документа будет создано и займет место старого.

Массивы дочерних компонентов работают на базе JW.AbstractArray. Они добавляются методом addArray. Если массив является JW.ObservableArray, то метод обеспечит непрерывную синхронизацию представления с этим массивом. Массивы дочерних компонентов удобно рендерить через метод createMapper:

var Application = function(documents) {
    Application._super.call(this);
    this.documents = documents;
};

JW.extend(Application, JW.UI.Component, {
    // JW.AbstractArray documents;
    
    renderComponent: function() {
        this._super();
        var documentViews = this.own(this.documents.createMapper({
            createItem: function(document) {
                return new DocumentView(document);
            },
            destroyItem: function(documentView) {
                documentView.destroy();
            },
            scope: this
        })).target;
        this.addArray(documentViews, "documents");
    }
});

JW.UI.template(Application, {
    main:
        '<div jwclass="application">' +
            '<div jwid="documents" />' +
        '</div>'
});


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

Для удобства jWidget позволяет определить метод renderChildId, где ChildId — это jwid элемента, записанный в CamelCase с большой буквы. Метод принимает на вход элемент шаблона. Ниже представлены разные возможности этого метода:

var Application = function(title, documents, selectedDocument) {
    Application._super.call(this);
    this.title = title;
    this.documents = documents;
    this.selectedDocument = selectedDocument;
};

JW.extend(Application, JW.UI.Component, {
    // JW.Property<String> title;
    // JW.AbstractArray<Document> documents;
    // JW.Property<Document> selectedDocument;
    
    // Если метод ничего не возвращает, то после его вызова ничего не произойдет.
    renderTitle: function(el) {
        this.own(new JW.UI.TextUpdater(el, this.title));
    },
    
    // Если метод возвращает JW.UI.Component, то это именованный дочерний компонент.
    renderHeader: function() {
        return this.own(new Header());
    },
    
    // Если метод возвращает JW.AbstractArray, то это массив дочерних компонентов.
    renderDocumentList: function() {
        return this.own(this.documents.createMapper({
            createItem: function(document) {
                return new DocumentListItem(document);
            },
            destroyItem: JW.destroy, // сокращение
            scope: this
        })).target;
    },
    
    // Если метод возвращает JW.Property, то это легко заменяемый дочерний компонент.
    renderSelectedDocument: function() {
        return this.own(new JW.Mapper([ this.selectedDocument ], {
            createValue: function(document) {
                return new DocumentPanel(document);
            },
            destroyValue: JW.destroy, // сокращение
            scope: this
        })).target;
    },
    
    // Если метод возвращает false, то этот элемент будет удален из DOM
    renderHappyNewYear: function() {
        return new Date().getMonth() === 0;
    }
});

JW.UI.template(Application, {
    main:
        '<div jwclass="application">' +
            '<div jwid="title" />' +
            '<div jwid="header" />' +
            '<div jwid="document-list" />' +
            '<div jwid="selected-document" />' +
            '<div jwid="happy-new-year">Happy new year!</div>' +
        '</div>'
});


HTML шаблон компонента можно вынести в отдельный HTML файл с помощью jWidget SDK. Подробнее об этом читайте в разделе руководства Инфраструктура проекта. Если фреймворк найдет успех, я планирую создать плагин к GruntJS, который заменил бы собой jWidget SDK. О jWidget SDK я писал ранее, но он не нашел большой поддержки. А теперь появился эквивалентный проект GruntJS, который сразу же нашел себе громадную поддержку и сформировал сообщество. Так что разработку jWidget SDK я сворачиваю.

Надеюсь, эта статья была вам интересна. Я совершенно уверен, что среди JavaScript программистов найдутся такие, которые так же, как и я, фанатеют от настоящего объектно-ориентированного программирования и ценят высокую скорость выполнения кода. Если это вы, попробуйте jWidget в вашей работе, и не останетесь разочарованными. Даже при наличии огромного количества MV* фреймворков я все равно предпочитаю jWidget. Я трачу много сил на поддержание документации, руководства для начинающих и плотного покрытия юнит-тестами. Если вы хотите, чтобы проект и дальше развивался и рос, не поленитесь поставить звездочку на GitHub и зафолловить меня в Twitter @jwidgetproject. Также, я ценю конструктивную критику и хорошие предложения. Спасибо.
Егор Непомнящих @enepomnyaschih
карма
13,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • +12
    И опять, и снова. Боевым гопаком по граблям.

    «Я совершенно уверен, что языку JavaScript не хватает трех вещей:
    1) Нового фреймворка
    2) Новой библиотеки, реализующей „классовую“ ООП
    3) Библиотеки UI-виджетов»

    P.S. Если серьезно, то у меня не хватает моральных сил даже начать тезисное описание откуда и докуда мне не нравится jWidget.
    P.P.S. Angular.js, или там, Meteor.js, мне нравятся не намного больше. Но все же больше.
  • +4
    Вообще, конечно, здорово, мы все любим (кто знает) SOLID и паттерны, и большая работа проделана, и классический ООП-подход прост для понимания, но…

    Чем вам так декларативность не угодила, интересно, ведь ее достоинства хорошо известны? Вот вы от нее отказываетесь, и получается куча тупого императивного кода:
    var value = new JW.Property(1000);
    var unit = new JW.Property("MW");
    var functor = new JW.Functor([ value, unit ], function(value, unit) {
        return value + " " + unit;
    }, this);
    var target = functor.target;

    Серьезно, 4 переменных?

    И это вот
    я много программирую на строго-типизированных языках программирования <...> Поэтому я большой фанат объектно-ориентированного программирования
    Что ж, по-вашему, Haskell не строго типизированный?
    • +1
      Я не программирую на Haskell. К чему вопрос?

      3 переменных всяко нужны: 2 исходных свойства и одно целевое. В AngularJS такие переменные кладутся в $scope. В EmberJS такие же переменные создаются неявно, и вы имеете к ним доступ через методы get и set. Четвертая переменная — functor — введена для читаемости кода. Как вы видите, в следующих примерах для функторов/мапперов переменные не создаются — они агрегируются, а используется только их target.

      Я ничего не имею против декларативности. Если вам нравится программировать через шаблоны — пожалуйста. А кому-то другому больше нравится писать императивный код. В частности, императивный код хорош своим однообразием, подконтрольностью и расширяемостью.
      • +1
        Я не программирую на Haskell. К чему вопрос?
        Не путайте парадигму (ООП) и типизацию, вот к чему.

        А что такое «программировать через шаблоны»? Если вы про Angular-овские директивы в HTML — так это не единственный метод декларативно объявить биндинги. Да и незачем засорять код тривиальными деталями, в какой инпут какое поле модели пишется. Прекрасный пример декларативности в общем смысле — Backbone.View#delegateEvents.
        • +2
          Да, я неверно назвал объекно-ориентированность строгой типизацией. Спасибо.

          В данном примере delegateEvents — это своего рода сахар, который избавляет вас от необходимости для каждого элемента писать this.$el.on("dblclick", this.open.bind(this)). Это нельзя назвать ключевой возможностью фреймворка. Я старался не перегружать свой фреймворк сахаром, т.к. это его усложнило бы. И для привязки обработчиков событий к элементам, и для изменения атрибутов, и для всех остальных операций с элементами предлагается использовать jQuery API, получая элементы через getElement или render[ChildId]. Напишите свою утилитарную функцию delegateEvents, если вам нравится объявлять события, как в Backbone — это несложно.

          А вот шаблоны — это ключевая особенность фреймворков AngularJS и EmberJS. Весь функционал крутится вокруг них.

          И то, что в Backbone при изменении модели весь View перерендерится с нуля — это тоже ключевой момент фреймворка. Прежде всего, именно по этой причине я в прошлом отказался использовать Backbone.
          • +1
            delegateEvents — это своего рода сахар
            любое декларативное программирование — это своего рода сахар, за которым стоят простыни императивного кода, который все эти красивые объявления наделяет жизнью. Только вот в 2014 году мне уже лень писать свой сахар, если давно есть готовый — проверенный, покрытый тестами, с продуманным API.

            в Backbone при изменении модели весь View перерендерится с нуля

            Это уж как сделаете:) Прикрутить биндинг к бэкбоновским вьюхам, хоть через HTML-ные атрибуты, хоть через словарь в прототипе вьюхи — не rocket science, таких решений уже с десяток, наверное…
            • +1
              То, что я посчитал нужным, я сократил и вынес в методы или классы. delegateEvents я выносить нужным не посчитал. Конечно, в примере Backbone все красиво — аж целых 6 строчек удалось вынести из метода render, чтобы сделать их короче аж на 20 символов. Но в реальности обычно каждый компонент слушает 1-2 события. Взгляните на TodoMVC: в поле ввода слушаем keydown по Enter или сабмит формы (как удобнее), в TodoView слушаем dblclick, а все остальное вяжется со свойствами напрямую — в jWidget через Listener'ы, а в Angular и Ember через волшебные HTML-атрибуты.

              Если вы начнете хитро биндиться с данными в Backbone, то вы пойдете в обход фреймворка. Это называется костылями. В документации четко написано, что когда модель выбрасывает событие change, представление перерендерится. Даже TodoMVC, где демонстрируются наилучшие (!) практики работы с фреймворком, это работает именно так. Оттого Select all и тормозит.
              • 0
                Но в реальности обычно каждый компонент слушает 1-2 события.
                Да, часто так, но TodoMVC — это вырожденный случай, это демо.

                вы пойдете в обход фреймворка. Это называется костылями.
                не согласен. Благодаря OOP-подходу в Backbone можно многое переопределить, это очень удобно и документация поощряет это делать.

                В документации четко написано, что когда модель выбрасывает событие change, представление перерендерится.
                А в следующем параграфе написано, что Backbone — это попытка понять, сколько сущностей минимально необходимо, чтобы строить современные приложения. И нигде не запрещено добавлять сущности, если надо.
                • 0
                  Минимальность не обозначает тормознутость. Да и мне в 2014 году уже лень что-то переопределять в фреймворке, чтобы исправить его ошибки.
  • –2
    добавления 500 записей в TodoMVC...

    jWidget — 9974 миллисекунд...

    image
    Так говоришь показал себя на высоте? Роман Дворнов — Быстро о быстром
    • +2
      Попробуйте сами.

      todomvc.local/architecture-examples/backbone/

      Вот код.

      function testSpeed(eventType) {
          var time = new Date().getTime();
          var i = 0;
      
          function tick() {
              var el = document.getElementById("new-todo");
              el.value = "a";
              var e = document.createEvent("Event");
              e.initEvent(eventType, true, true);
              e.keyCode = 13;
              el.dispatchEvent(e);
              if (++i < 500) {
                  setTimeout(tick);
              } else {
                  console.log((new Date().getTime() - time) + " milliseconds");
              }
          }
      
          tick();
      }
      testSpeed("keypress");
      


      У меня в районе 9500 миллисекунд.

      jWidget (http://enepomnyaschih.github.io/todomvc/labs/architecture-examples/jwidget/release/) то же самое, только «keydown». 10000 миллисекунд.

      Я и не говорил, что в Backbone записи добавляются медленно. Я говорил, что Select all потом отрабатывает 3 секунды, а в jWidget — мгновенно.
      • +1
        Уточнение: запускается в консольке Google Chrome.
      • 0
        Честно говоря, не понятна методика измерения. То есть какой use case рассматривается, что пользователь будет собственноручно вбивать элементы по одному? Я сомневаюсь, что это разумно.
        Куда интереснее сколько времени будет генерироваться представление, когда в него добавят сразу 500-1000 записей. Ведь это обычный кейс, списки приходят от сервера (из localStorage) и мы их добавляем в представление.
        Так же если приводите результаты измерений, то лучше выводить их табличкой. Да и избегать оценок «мгновенно» и т.п. — нужны цифры. Тем более их можно получить тем же способом, что вы использовали для основного теста.
    • 0
      Про basis.js ничего сказать не могу. В списке на сайте его нет todomvc.com/
      • 0
        Можете добавиться в местный «пузомерный» хабра-тест
        • 0
          Спасибо за ваш комментарий. Он послужил поводом выполнить некоторые оптимизации. Мне удалось обойти Knockout, Angular и atom, но необходимо выполнить ряд тестов перед выкаткой в релиз. Это входило в мои ожидания, что хабра-сообщество укажет мне на ошибки фреймворка. В следующей версии фреймворка все будет исправлено.

          Решения basis.js, jQuery, al-fast-list и оба JS нельзя ставить в один ряд с Angular, Knockout, atom и jWidget, поскольку там идут прямые манипуляции с DOM, что противоречит архитектуре MV*. Методы fill, update и clear должны работать только с моделью.
        • 0
          Если интересно, вот промежуточная версия. Пока надежно работает только в Google Chrome http://plnkr.co/edit/6WhCanCz8hnG6a57VLHc
        • 0
          Вот финальная версия: plnkr.co/edit/6WhCanCz8hnG6a57VLHc

          Есть ли способ в Plunker'е примержить этот код к вашей версии? Я не нашел сходу.

          image
          • 0
            Ох, как лихо вы разделили ;)
            Knockout не framework, angular не MV*. Хотя, конечно, зависит от того, что вы в это вкладываете. Если любая обвязка над значением и биндинги — это MV* фреймворк, то ок.

            Решения basis.js, jQuery, al-fast-list и оба JS нельзя ставить в один ряд с Angular, Knockout, atom и jWidget, поскольку там идут прямые манипуляции с DOM, что противоречит архитектуре MV*.

            Расскажите, где вы увидили прямые манипуляции с DOM в решение basis.js?
            basis является каким то там MV* (не заморачиваюсь на эту тему) — есть и model, и своего рода view и controller.

            Методы fill, update и clear должны работать только с моделью.

            Модель не является обязательной сущностью. Эту задачу успешно может выполнять и контролер, и представление.
            В любом случае «пузкомерка» не для того, чтобы разделить решения на категории и не было особых ограничений. Есть конкретная задача, нужно ее решить.
            Конечному пользователю все равно на чем написано приложение и какие патерны используются, для него важно «работает» и «не тормозит».
            • 0
              Я не говорил, что Knockout и Angular — не MV* фреймворки. У меня к тому, что обведено рамкой, претензий нет.

              Расскажите, где вы увидили прямые манипуляции с DOM в решение basis.js?


              Объявляем basis.ui.Node и в методах fill, update, clear напрямую вызываем его методы setChildNodes, childNodes.forEach, updateBind, clear, которые выполняют DOM-манипуляции. Это точно представление, а не модель. Насколько мне известно, MV* архитектура предполагает, что вы меняете только модель, а представление само при этом понимает, когда и что нужно перерендерить.

              Модель не является обязательной сущностью. Эту задачу успешно может выполнять и контролер, и представление.
              В любом случае «пузкомерка» не для того, чтобы разделить решения на категории и не было особых ограничений. Есть конкретная задача, нужно ее решить.


              Если бы решения Angular, Knockout и jWidget тоже работали напрямую с представлением, то, естественно, они работали бы гораздо быстрее. Я профилировал решение jWidget и обнаружил, что 50% времени съедают накладные расходы на работу с событиями модели.

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


              А еще ему важно, чтобы приложение было легко расширяемым и код был понятным. Именно для этого придумали паттерн MVC. Если везде работать напрямую с DOM, то вообще никакие фреймворки не нужны, но код быстро станет слишком запутанным.
              • 0
                У меня к тому, что обведено рамкой, претензий нет.

                Значит ли это, что к basis.js претензии есть? ;)

                Объявляем basis.ui.Node и в методах fill, update, clear напрямую вызываем его методы setChildNodes, childNodes.forEach, updateBind, clear, которые выполняют DOM-манипуляции. Это точно представление, а не модель.

                Это в большей степени контролер, который абстрагирован от DOM, и ему совсем не важно что там в шаблоне. Он поставляет значения экземпляру шаблона, а шаблон (который может считаться настоящим представлением) уже трансформирует значения в DOM операции.
                Так то все приводит к DOM-манипуляциям. В angular делается вызов scope.$scan, в knockout это обновление observables. Не понятно почему в их случае все ок, а в решении basis.js что-то не так.

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

                У MV* достаточно много трактовок, и каждый понимает по своему.
                Да и представление в basis.js (шаблон) тоже само определяет, что, где и как нужно обновить. Что касается basis.ui.Node – то это абстракция, для организации интерфейса, и имеет косвенное отношение к нативному DOM. Чтобы не придумывать новое за основу была взята модель DOM.
                На самом деле basis.ui.Node может являться и контролером, и моделью, и даже представлением при желании. Все зависит от того, что нужно сделать и чего добиться.

                Если бы решения Angular, Knockout и jWidget тоже работали напрямую с представлением, то, естественно, они работали бы гораздо быстрее.

                Это не так. В том же Angular нет моделей и нет оверхеда по их созданию. Зато есть оверхед на dirty check – и это основной тормоз. В knockout тоже нет моделей и очень дорогая организация структуры ovservables, которая является основной проблемой – а еще неэффективная работа с DOM (с которым как вам кажется он не работает). То есть это все в большей степени из-за архитектурные проблемы.

                Я профилировал решение jWidget и обнаружил, что 50% времени съедают накладные расходы на работу с событиями модели.

                Работа с моделями не должна съедать столько времени. Посмотрите мой доклад про данные – www.slideshare.net/basisjs/ss-32305540
                В дополнение – вот вам решение на basis.js «через модели» и даже с коллекцией. Закономерно, что время увеличилось, но не в разы (на 20-25%): plnkr.co/edit/ZUWORGrGbtOsPJBgTik3?p=preview

                А еще ему важно, чтобы приложение было легко расширяемым и код был понятным. Именно для этого придумали паттерн MVC. Если везде работать напрямую с DOM, то вообще никакие фреймворки не нужны, но код быстро станет слишком запутанным.

                В вашем случае как раз есть проблема с понятностью, много кода и он запутан. MVC не серебренная пуля. А про DOM я уже написал, кажется вы не до конца разобрались ;)
                • 0
                  К basis.js претензий нет :) Есть претензии к решению данной задачи на basis.js. Оно не соответствует архитектуре MV* в привычном смысле.

                  Один вызов $scan в конце обработчика события допустим, это не сложно. А вот явно создавать Nodes довольно тяжело. А если у вашей модели 10 представлений одновременно, все вручную обновлять будете?

                  Я ценю ваши достижения в области генерации DOM, но, пожалуйста, не подменяйте понятия. Во-первых, большинство фреймворков не разделяют понятия DOM и представления. Во-вторых, по архитектуре MV* обработка действий пользователя всегда осуществляется через модель, без обращения к представлению. В AngularJS это $scope, в ExtJS это Ext.data.Store, в Knockout это observables, в Backbone это Backbone.Model и Backbone.Collection. Ключевое преимущество, которое дает пользователю архитектура MV* — это возможность заводить несколько представлений для одной модели, возможность подменять представления и возможность менять данные налету, не задумываясь, какие у вас в данный момент существуют представления и в каком они состоянии. Пожалуйста, не путайте людей. Ваш фреймворк basis.js клевый, но это не MV* фреймворк в том виде, в котором он описан в вашей статье и продемонстрирован в «пузомерке». Еще я хотел бы посмотреть реализацию TodoMVC на basis.js.

                  MV*, конечно, не серебрянная пуля. Вы получаете лучшее качество кода, но теряете производительность. Принимать решение об использовании MV* подхода надо исходя из требований к динамичности приложения и максимальным нагрузкам.
                  • 0
                    По-моему вы не разобрались, получили неверное представление и делаете поспешные выводы. Но дело ваше.
                    Реализацию TodoMVC можно найти в demo: github.com/basisjs/basisjs/tree/master/demo/apps/todomvc/basis
    • +1
      Уважаемый, bakhirev.
      Это, конечно, здорово, что вы взяли табличку из моей презентации. Но она не для того, чтобы гнуть пальцы или принижать чьи-то других, а для того, чтобы показать как быстро справляются с задачей те или иные решения. Методика измерений совершенно разная, и в данном случае нельзя сопоставлять цифры.
      Судя по написанному в статье, работа проделана не малая. Уважайте других, пинать и критиковать проще, чем сделать что-то полезное.
  • +3
    Честно говоря, всё время, пока я читал вашу статью и смотрел примеры кода — я страдал.
    Обилие кода для описания простых вещей мне почему-то до боли напомнило YUI3.
    Лично моё мнение — написание большого приложения на jWidget принесло бы мне еще больше боли и страдания :)
  • 0
    Направление мыслей хорошее. Но получается слишком много кода и далеко не js-way. Особенно много по созданию классов и настройке рендеринга.
    2. Скорость работы скрипта превыше всего.

    4. Фреймворк работает на базе jQuery.

    Тут определенно что-то лишнее. Если цель добиться высокой производительности, нужно забыть про jQuery, селекторы, html и render. Посмотри в сторону dom-based шаблонизаторов. Сейчас этой дорогой идут basis.js, react.js, ractive и даже meteor. В ember тоже идут в эту сторону, создавая HTMLBars. Вот пара моих докладов по этому поводу: раз и два.

    Вообще советую посмотреть basis.js, в нем найдешь много знакомого. Например, те же именованные дочерние представления (сателлиты), или некоторые вещи из работы с данными и др. Только это все более развито…
    Если не читал, вот первая часть руководства по фреймворку.
    • 0
      Но получается слишком много кода и далеко не js-way.

      Согласен, на типичный JS не похоже. Но объектно-ориентированный подход легко можно будет перенести, скажем, в Dart. Это входит в мои планы.
      Если цель добиться высокой производительности, нужно забыть про jQuery, селекторы, html и render.

      Тоже согласен. jQuery тормозной. Чтобы фреймворк работал быстро, я использую нативные браузерные манипуляции с DOM в ядре фреймворка, где это возможно. jQuery-обертки над элементами создаются только на финальном этапе, перед тем, как отдать их пользователю фреймворка. Все-таки jQuery на данный момент является самой известной и широко используемой библиотекой для работы с DOM.

      Спасибо за ссылки, почитаю.

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