Website development
JavaScript
29 November 2013

JohnSmith — простой и легковесный JavaScript-фреймворк для построения UI



На сегодняшний момент существует масса JavaScript-библиотек для создания rich-client приложений. Кроме всем известных Knockout, Angular.JS и Ember есть великое множество других фреймворков, и каждый имеет свою особенность — кто-то пропагандирует минимализм, а кто-то — идеологическую чистоту и соответствие философии MVC. При всём этом многообразии, регулярно появляются всё новые и новые библиотеки. Из последнего, что упоминалось на хабре — Warp9 и Matreshka.js. В связи с этим хочется рассказать о собственной поделке, встречайте, JohnSmith — простой и легковесный JavaScript фреймворк для построения UI.




Прежде всего, хочется сказать, что JohnSmith писался не ради какого-то академического интереса и не для устранения того самого фатального недостатка. Совсем наоборот, JohnSmith зародился в реальном проекте, затем мигрировал из проекта в проект, постепенно улучшаясь и меняя свою форму. И вот теперь он материализовался как полноценная open-source библиотека.


Пример



Для демонстрации возможностей JohnSmith, напишем простейшее приложение со следующей функциональностью:

Имеется поле ввода, в которое пользователь пишет своё имя. Как только имя введено, показываем сообщение: Hello, %username%.

Кому сразу хочется увидеть результат: вот готовый User Greeter.



View Model



Начнём с создания View Model, и прежде всего, напишем «класс»:

var GreeterViewModel = function(){
}


View Model обычно «выставляет» во внешний мир объекты, изменения которых могут отслеживаться из вне. В JohnSmith эти объекты называются bindable. Добавим поле для хранения имени пользователя:

var GreeterViewModel = function(){
    this.userName = js.bindableValue();
};


Это поле (userName) будет использоваться для двунаправленного связывания в Виде. Добавим еще одно поле, которое будет формировать текст сообщения. Это поле зависит от userName, поэтому опишем его в виде dependentValue:

    var GreeterViewModel = function(){
    this.userName = js.bindableValue();

    this.greetMessage = js.dependentValue(
        this.userName,
        function(userNameValue){
            if (userNameValue) {
                return "Hello, " + userNameValue + "!";
            }

            return "Please, enter your name";
        });
};


js.dependentValue похож на computed в knockout, за исключением того, что в JohnSmith мы вручную указываем зависимости, т.к. за сценой нет никакой магии авто-трекинга.

Модель Вида готова, теперь опишем Вид.



View



Начнём с создания класса:

var GreeterView = function(){
}


Вид — это совокупность разметки и логики связи этой разметки с внешним миром. Разметка описывается в поле template, а логика — в методе init:

var GreeterView = function(){
    this.template = "...здесь описываем разметку...";
    this.init = function(){
      // здесь описываем логику
    }
};


В нашем тестовом примере разметка довольно-таки простая, поэтому запишем её прямо в поле template:

var GreeterView = function(){
    this.template =
        "<p>Enter your name: <input type='text'/></p>" +
        "<p class='message'></p>";

    this.init = function(){
        // здесь скоро будет логика
    };
};


Теперь переходим к методу init. Во-первых, JohnSmith подразумевает, что каждый Вид работает с определённой Моделью Вида, поэтому добавим параметр viewModel:

var GreeterView = function(){
    this.template =
        "<p>Enter your name: <input type='text'/></p>" +
        "<p class='message'></p>";

    this.init = function(viewModel){                        // <---
        // здесь скоро будет логика
    };
};


Дальше наша задача состоит в том, чтобы связать свойства Модели Вида с разметкой, которую «отрисует» наш Вид. JohnSmith предоставляет синтаксис для настройки этой связи непосредственно в js-коде. Для нашего случая это будет выглядеть так:

var GreeterView = function(){
    this.template =
        "<p>Enter your name: <input type='text'/></p>" +
        "<p class='message'></p>";

    this.init = function(viewModel){
        this.bind(viewModel.userName).to("input");          // <---
        this.bind(viewModel.greetMessage).to(".message");   // <---
    };
};


Теперь всё готово и нам нужно только отрисовать наш вид (подразумевается, что на странице есть элемент с id='greeter'):

js.renderView(GreeterView, new GreeterViewModel()).to("#greeter");


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



Binding



Основа связывания в JohnSmith — это observable-объекты (как в knockout). Создаются эти объекты одним из методов:

  • js.bindableValue — обычный observable объект;
  • js.dependentValue — значение, зависящее от других объектов;
  • js.bindableList — observable-коллекция, уведомляет подписчиков о добавлении/удалении элементов.


Непосредственно связывание объекта A и слушателя B настраивается кодом вида:

js.bind(A).to(B);


Например так:

var firstName = js.bindableValue();   // создаём объект
js.bind(firstName).to(".firstName");  // привязываем к jQuery-селектору
firstName.setValue("John");           // изменяем значение объекта


Внутри Вида код привязки немного меняется:

// мы внутри метода init некоторого Вида
this.bind(viewModel.firstName).to(".firstName");


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

Синтаксис js.bind(A).to(B) позволяет сочетать «декларативный» стиль с императивным и использовать jQuery-style в тех случаях, где это необходимо:

// это больше похоже на декларативный стиль:
js.bind(firstName).to(".firstName");  

js.bind(firstName).to(
    function(newValue, oldValue){  // <-- в качестве обработчика используется функция
        // здесь мы можем использовать jQuery как обычно,
        // например, скрыть/показать какую-то панель в зависимости
        // от значений newValue/oldValue, добавить класс, запустить анимацию и т.п.
    });


Если в качестве bindable-объекта передать обычное (не observable) значение, то произойдёт единовременная синхронизация с интерфейсом. Это позволяет единообразно обрабатывать как observable так и «обычные» поля View Model:

var ViewModel = function(){
    this.firstName = "John";             // static value
    this.lastName = js.bindableValue();  // observable value
};

//...
// somewhere in the View:
this.bind(viewModel.firstName).to(".firstName");  // will sync only once
this.bind(viewModel.lastName).to(".lastName");    // will sync on every change


Для отрисовки сложных объектов может использоваться дочерний Вид:

var ViewModel = function(){
    this.myAddress = js.bindableValue();

    this.initState = function(){
        this.myAddress.setValue({
            country: 'Russia',
            city: js.bindableValue();
        });
    };
};

// ...
this.bind(viewModel.myAddress).to(".address", AddressView);
// ...




Views Composition



Вид в JohnSmith — это атомарная единица для построения интерфейса. Каждый Вид является полностью независимым и обеспечивает возможность повторного использования. Интерфейс всего приложения составляется из отдельных Видов, путём построения «дерева» (composite pattern). То есть, имеется один главный Вид, у него есть дочерние Виды, у каждого из дочерних есть свои дочерние Виды и т.д. Композиция достигается несколькими способами:

  • непосредственное добавление дочернего вида:

    var ParentView = function(){
        this.init = function(){
            this.addChild(".destination", new ChildView(), new ChildViewModel()); // <--
        }
    };
    
  • использование Вида для отрисовки bindable-значения:

    var ParentView = function(){
        this.init = function(viewModel){
            this.bind(viewModel.details).to(".details", DetailsView);   // <--
        }
    };


В качестве небольшой демонстрации составного вида — файловое дерево:


Заключение



В качестве заключения, обозначим особенности JohnSmith:

  • компануемость UI позволяет с легкостью использовать JohnSmith для проектов любого размера. При этом с возрастанием сложности легко удаётся держать код под контролем. Это достигается модульностью и четким разделением ответственности между Видом и Моделью;
  • JohnSmith очень простой — всего две основные концепции (View и Bindable), да и те хорошо известны любому программисту, работавшему с UI. Никакого сдвига парадигмы и никакой магии за сценой;
  • JohnSmith оперирует обычными объектами с обычными полями и методами. Это значит, что Вам не придётся осуществлять какие-либо действия полагаясь на строчные идентификаторы (типа model.set('firstName', 'John')). Такой подход обеспечивает тесную дружбу с IDE и отлично сочетается с инструментами типа TypeScript или ScriptSharp;
  • JohnSmith манипулирует элементами DOM из JavaScript-кода, поэтому он нуждается в jQuery.


репозиторий на GitHub
На этом всё, спасибо за внимание, ждём конструктивной критики!

+24
15.1k 144
Comments 21