Клиентская оптимизация / Оптимизируем работу с шаблонами в Backbone

в 10:48, , рубрики: backbone, javascript, оптимизация, шаблоны, метки: , , ,

Знакомство с 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


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js