KnockoutJs. Разбор кода todomvc

И вот мы подобрались к следующему фреймворку, который пройдет через разбор кода приложения 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). Последнее опять же говорит в пользу того, что фреймворк этот скорее для небольших приложений.

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *