И вот мы подобрались к следующему фреймворку, который пройдет через разбор кода приложения todomvc. Используемая версия KnockoutJs – 3.2.0.
Как обычно, код примера в github-репозитори. Рассматривается самая свежая версия примера на момент создания статьи. И всегда можно код todomvc на KnockoutJs скачать отсюда.
Здесь все просто. Есть лишь 2 основных файла
- index.html
- js/app.js
Как мы знаем из прошлого обзора, в Knockout реализован паттерн MVVM. И вот в первом файле содержится вид, а во втором модель и модель вида.
Модель у нас имеет такой вид:
var Todo = function (title, completed) {
this.title = ko.observable(title);
this.completed = ko.observable(completed);
this.editing = ko.observable(false);
};
А модель вида – это все, что содержится между этими строками:
var ViewModel = function (todos) {
.......
}
В файле вида у нас имеются следующие части.
Верхняя:
<header id="header">
<h1>todos</h1>
<input id="new-todo" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add" placeholder="What needs to be done?" autofocus>
</header>
В ней кроме заголовка есть поле для добавления нового элемента в список.
Главная (средняя) секция:
<section id="main" data-bind="visible: todos().length">
<input id="toggle-all" data-bind="checked: allCompleted" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list" data-bind="foreach: filteredTodos">
<li data-bind="css: { completed: completed, editing: editing }">
<div class="view">
<input class="toggle" data-bind="checked: completed" type="checkbox">
<label data-bind="text: title, event: { dblclick: $root.editItem }"></label>
<button class="destroy" data-bind="click: $root.remove"></button>
</div>
<input class="edit" data-bind="value: title, valueUpdate: 'afterkeydown', enterKey: $root.saveEditing, escapeKey: $root.cancelEditing, selectAndFocus: editing, event: { blur: $root.saveEditing }">
</li>
</ul>
</section>
В ней содержится сам список, элементы для его редактирования и чекбокс групповых операций.
И нижняя часть:
<footer id="footer" data-bind="visible: completedCount() || remainingCount()">
<span id="todo-count">
<strong data-bind="text: remainingCount">0</strong>
<span data-bind="text: getLabel(remainingCount)"></span> left
</span>
<ul id="filters">
<li>
<a data-bind="css: { selected: showMode() == 'all' }" href="#/all">All</a>
</li>
<li>
<a data-bind="css: { selected: showMode() == 'active' }" href="#/active">Active</a>
</li>
<li>
<a data-bind="css: { selected: showMode() == 'completed' }" href="#/completed">Completed</a>
</li>
</ul>
<button id="clear-completed" data-bind="visible: completedCount, click: removeCompleted">Clear completed</button>
</footer>
Где мы можем видеть отображение различных счетчиков и фильтры.
Вся работа приложения Knockout основана на, известной нам уже концепции, Data-binding. Например, в главной секции мы видим data-bind=”visible: todos().length”, что прописывает связь видимости элемента вида и наличия записей в списке todos. Если в списке не записей, то элемент будет полностью скрыт.
По тому же принципу работают и следующие элементы:
<input id="toggle-all" data-bind="checked: allCompleted" type="checkbox">
<footer id="footer" data-bind="visible: completedCount() || remainingCount()">
<a data-bind="css: { selected: showMode() == 'all' }" href="#/all">All</a>
<strong data-bind="text: remainingCount">0</strong>
<span data-bind="text: getLabel(remainingCount)"></span> left
Думаю, что здесь интуитивно понятно между чем сделана связь.
Так же эта концепция используется для привязки событий.
<label data-bind="text: title, event: { dblclick: $root.editItem }"></label>
<button class="destroy" data-bind="click: $root.remove"></button>
Переменная $root служит для обращения к соответствующей модели вида. editItem и remove – это методы модели вида.
Подобным образом реализуются и итераторы:
<ul id="todo-list" data-bind="foreach: filteredTodos">
<li data-bind="css: { completed: completed, editing: editing }">
<div class="view">
<input class="toggle" data-bind="checked: completed" type="checkbox">
<label data-bind="text: title, event: { dblclick: $root.editItem }"></label>
<button class="destroy" data-bind="click: $root.remove"></button>
</div>
<input class="edit" data-bind="value: title, valueUpdate: 'afterkeydown', enterKey: $root.saveEditing, escapeKey: $root.cancelEditing, selectAndFocus: editing, event: { blur: $root.saveEditing }">
</li>
</ul>
Здесь биндинг foreach использует результат полученнй от filteredTodos (описаного в модели вида) для отображения каждого пункта списка.
Похоже, что index.html практически на 100% понятен. Модель и модель вида так же не должны вызывать особых проблем при чтении кода.
Интересный код содержится над описанием модели в файле app.js:
function keyhandlerBindingFactory(keyCode) {
return {
init: function (element, valueAccessor, allBindingsAccessor, data, bindingContext) {
..........
}
};
}
// a custom binding to handle the enter key
ko.bindingHandlers.enterKey = keyhandlerBindingFactory(ENTER_KEY);
// another custom binding, this time to handle the escape key
ko.bindingHandlers.escapeKey = keyhandlerBindingFactory(ESCAPE_KEY);
// wrapper to hasFocus that also selects text and applies focus async
ko.bindingHandlers.selectAndFocus = {
init: function (element, valueAccessor, allBindingsAccessor, bindingContext) {
ko.bindingHandlers.hasFocus.init(element, valueAccessor, allBindingsAccessor, bindingContext);
ko.utils.registerEventHandler(element, 'focus', function () {
element.focus();
});
},
update: function (element, valueAccessor) {
ko.utils.unwrapObservable(valueAccessor()); // for dependency
// ensure that element is visible before trying to focus
setTimeout(function () {
ko.bindingHandlers.hasFocus.update(element, valueAccessor);
}, 0);
}
};
Это иллюстрация того, как можно создавать собственные data-binding. Это очень гибкий инструмент, который выручает нас когда встроенных биндингов не хватает. Он хорошо описан в документации по custom bindings.
В данном же приложении (как мы видим выше) описан код для событий нажатия Enter, Escape, а так же для перехода отображения элемента в статус редактирования посредством вызова selectAndFocus.
В самом низу app.js есть еще несколько строк инициализации:
// check local storage for todos
var todos = ko.utils.parseJson(localStorage.getItem('todos-knockoutjs'));
// bind a new instance of our view model to the page
var viewModel = new ViewModel(todos || []);
ko.applyBindings(viewModel);
// set up filter routing
/*jshint newcap:false */
Router({ '/:filter': viewModel.showMode }).init();
Здесь в коде все прокомментировано. Отмечу лишь, что для роутинга здесь по-видимому используется библиотека director.js, а в блоке #filters можно видеть ссылки, использующие роутинг.
Итоги. Код на KnockoutJs предполагает меньший необходимый объем знаний для написания такого приложения. Тоесть мы еще раз убедились, что порог вхождения у него ниже чем у EmberJs. Однако data-bindings и конструкции ko.observable() создают некоторый дискомфорт и могут приводить к известным проблемам с производительностью и нарушению связей между зависимыми величинами (об это писалось в предыдущей статье по Knockout). Последнее опять же говорит в пользу того, что фреймворк этот скорее для небольших приложений.