На сегодняшний момент существует масса 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.
На этом всё, спасибо за внимание, ждём конструктивной критики!
Автор: admax