Знакомство с javascript-фреймворком Backbone я, как и многие, начинал с todo-туториала, на базе которого строилось дальнейшее использование фреймворка в своих проектах.
Но туториалы заканчиваются, и начинаются рабочие будни.
Думаю, многим знаком такой участок кода (из вышеупомянутого туториала):
window.AppView = Backbone.View.extend({ // Instead of generating a new element, bind to the existing skeleton of // the App already present in the HTML. el: $("#todoapp"), // Our template for the line of statistics at the bottom of the app. statsTemplate: _.template($('#stats-template').html()), ...
Давайте разберемся подробнее:
- декларацию вида AppView необходимо производить после готовности DOM-дерева, то есть оборачивать в jQuery(function() {...}), что не всегда удобно.
- шаблон statsTemplate подгружается на этапе декларации вида, чтобы экземпляры могли использовать его без дополнительной загрузки и обработки
- шаблон находится внизу html-страницы и подгружается в DOM при загрузке страницы
Конечно, для небольших приложений вопросов нет. А если приложение большое и содержит десятки шаблонов?
Зачем загружать и инициализировать все шаблоны сразу? Нам может и не понадобиться большинство из них.
Загрузка шаблонов
Хорошо бы подгружать шаблоны только тогда, когда они нам нужны и кешировать их.
Исходя из этого, напишем загрузчик:
Core.Template = { cache: {}, // здесь хранятся загруженные шаблоны pending: {}, // очередь callback-ов, которые необходимо вызвать после загрузки шаблона load: function(url, callback) { callback = callback || function() {}; if (this.cache[url]) { //если шаблон уже загружен, просто вызовем callback callback(this.cache[url]); return; } // добавляем callback в очередь if (this.pending[url]) { this.pending[url].push(callback); return; } this.pending[url] = [callback]; jQuery.ajax({ //загружаем шаблон url : url, dataType: 'text', complete: function(resp) { var cache = this.cache[url] = _.template(resp.responseText); // парсим и заносим в кеш _.each(this.pending[url], function(cb) { // обрабатываем очередь cb(cache); }); delete this.pending[url]; // очищаем очередь }.bind(this), error: function() { throw new Error("Could not load " + url); } }); } };
Загружая шаблон по ajax, имеем дополнительную возможность кешировать его средствами браузера.
Очередь сделана для того, чтобы загрузчик не грузил шаблон для каждого вида модели из нашей коллекции, а сделал это один раз и передал готовый для работы шаблон в каждый вид.
Надеюсь, что комментариев к коду достаточно, чтобы понять, как все работает.
Далее нам необходимо научить наши views работе с шаблонами по-новому.
Базовый класс вида для работы с шаблонами
Напишем базовый класс вида, чтобы он умел работать с нашим загрузчиком шаблонов.
Core.View = Backbone.View.extend({ renderQueue: false, // очередь запросов на рендеринг initialize: function() { if (this.templateUrl) { Core.Template.load(this.templateUrl, function(data) { this.template = data; // запоминаем шаблон if (this.renderQueue !== false) { // обработаем очередь на рендер _.each(this.renderQueue, function(item) { this._render.apply(this, item); }.bind(this)); this.renderQueue = false; } }.bind(this)); } }, _render: function() {}, // эта функция будет переопределяться в видах вместо render render: function() { if (!this.template) { // если шаблон не загружен - дождемся загрузки и отрендерим после this.renderQueue = this.renderQueue || []; this.renderQueue.push(arguments); return this; } return this._render.apply(this, arguments); //вызываем метод рендеринга } });
Таким образом, если у нас в параметрах вида передается templateUrl, то загружаем шаблон, иначе работаем по обычной логике.
Единственное отличие от стандартного поведения, функция непосредственного рендеринга выносится в метод _render. В принципе, ничто не мешает переписать код, оборачивающий все в метод render, но мне так показалось удобнее и проще.
Использование
Что на практике?
Вот так будет выглядеть измененная часть кода, показанного в начале статьи:
window.AppView = Core.View.extend({ el: $("#todoapp"), // Загружаем шаблон отсюда templateUrl: '/templates/app.html', initialize: function() { AppView.__super__.initialize.apply(this); // вызываем родительский инициализатор ... }, // Переименовываем render в _render и statsTemplate в template _render: function() { this.$('#todo-stats').html(this.template({ total: Todos.length, done: Todos.done().length, remaining: Todos.remaining().length })); }, ...
Готово!
Код загрузчика доступен здесь.
Пример изменненного todo-туториала можно посмотреть здесь.
Если интересно, что поменялось в todo: diff
Спасибо за внимание!
Автор: glibin