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
На этом всё, спасибо за внимание, ждём конструктивной критики!
Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

Комментарии 21

    +2
    Лучшим дополнением к статье и к фреймворку была бы разработка todomvc проекта.
    Вот ссылка todomvc.com/
      0
      да, спасибо
      на самом деле todomvc уже в процессе
    • НЛО прилетело и опубликовало эту надпись здесь
        +2
        Добавил ссылку, спасибо.

        this.template = "...здесь описываем разметку...";
        


        «Описываем разметку» не означает, что она должна быть обязательно текстом вставлена в js. Можно использовать jQuery-селектор:

        this.template = "#myViewTemplate";
        


        и, соответственно, в html:

        <script id='myViewTemplate' type='text/view'>
            непосредственно разметка тут
        </script>
        

        +1
        Спасибо за упоминание (я про Warp9), с момента статьи на хабре я немного изменил внутренности (переключился на явный граф зависимостей) это позволило избежать проблем, характерных для многих реактивных библиотек. Например, с помощью Knockout легко:


        Так как у вас система основана на идеях, близких knockout проверьте эти сценарии на своей библиотеке, возможно и у вас есть, что исправить :)
          +1
          К сожалению пока не впечатлило, на фоне других фреймворков. И как-то мне не нравиться объект js и его роль тут.
            0
            Да, фич не много, но ведь и не было задачи повторить angular по навороченности. Скорее наоборот — JohnSmith предоставляет некий минимум, который вполне достаточен для построения рабочего приложения.

            js просто содержит алиасы для доступа к функциям библиотеки, что именно с ним не так?
              0
              var firstName = js.bindableValue();   // создаём объект
              ...
              firstName.setValue("John");           // изменяем значение объекта
              
              

              то есть работа с моделью отличается от принятого в языке стандарта
              user.firstName = 'John';
              
                0
                то есть работа с моделью отличается от принятого в языке стандарта

                //  обычное присваивание значения переменной 
                // не вызывает побочных эффектов
                firstName = 'John';
                
                // вызов метода setValue()
                // может породить каскад обновлений
                firstName.setValue('John');
                
                  0
                  в js есть setters — а значит можно аналогично породить каскад обновлений
                    0
                    да есть (если забыть про кросс-браузерность), но firstName в данном случае — это не просто переменная, это объект и у него кроме getValue/setValue есть и другие методы. Поэтому и работать с ним нужно именно как с объектом.
                      0
                      На самом деле большинство браузеров уже поддерживают их.
                      Но претензия звучит так — представьте метод который меняет 20-30 полей в модели и будет 20-30 строк типа

                      name.setValue('new name'); 
                      logib.setValue('login');
                      ...
                      
                        0
                        Да, если у Вас 20 свойств в ViewModel, то придётся написать

                        prop1.setValue('value1');
                        prop2.setValue('value2');
                        //...
                        prop20.setValue('value20');
                        


                        но какие есть альтернативы? Обычное присваивание даст нам те же 20 строк и «спрячет» от нас то, что мы работаем с observable-объектом, а не с обычным свойством.
                          +1
                          ну да только
                          this.displayName = this.firstName+ this.lastName;
                          


                          смотрится лучше чем
                          this.displayName.setValue(this.firstName.getValue()+ this.lastName.getValue());
                          
                            0
                            Обычное присваивание даст нам те же 20 строк и «спрячет» от нас то, что мы работаем с observable-объектом, а не с обычным свойством.

                            У observable-объектов есть ещё ощутимый минус, обычно в проектах часть данных является readonly (для рендеринга), поэтому для этого используются обычные переменные, но если на середине проекта появилась необходимость их отслеживать — это проблема.
              +1
              Начну с критики, но ее следует воспринимать, как IMHO

              1. Мне кажется более естественным использовать деклоративный binding (мы все равно никуда от html не уйдем, так почему бы не указывать модель там)
              2. Может мне показалось, но в данном фреймворке достаточно трудно будет повторно использовать ViewModel
              3. Я немного укушен ember и мне тоже не нравиться объект js (как-то оно слишком сложно)
              4. Основная проблема всех шаблонизаторов, что мы не можем просто получить дочерний элемент из созданного элемента — хотелось бы видеть в библиотеке решение

              Теперь немного похвалы. На самом деле, я понимаю, насколько это огромная работа и очень мощно, что Вы за нее взялись и довели до рабочего состояния.

              Ну и в конце вопрос:
              1. Как Вы тестируете код на кроссбраузерность? У меня в аналогичном проекте проблема: хочу чтобы оно работало во всех основных браузерах, но не хочу руками тестировать этот момент — как-то оно дорого по трудозатратам.
                0
                Как Вы тестируете код на кроссбраузерность? У меня в аналогичном проекте проблема: хочу чтобы оно работало во всех основных браузерах, но не хочу руками тестировать этот момент — как-то оно дорого по трудозатратам.


                Да, у нас та же проблема. Мы используем jsTestDriver, TravisCi запускает нам тесты после каждого комита, но они прогоняются только в headless safari. Тоже хотелось бы автоматом запускать, если уж не на всех, то хотя бы на нескольких основных браузерах. Но пока не получается.
                  0
                  Понятно — буду рыть дальше) найду решение обязательно поделюсь
                    0
                    www.browserstack.com

                    Не то, что вы ищите?
                      0
                      на него и облизываюсь, но хотелось бы бесплатно)
                      + он умеет запускать только selenium тесты, а у меня все на qunit — придется их как-то дружить
              • НЛО прилетело и опубликовало эту надпись здесь

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

                Самое читаемое