JavaScript занимает все более важное место в веб, а значит все больше времени в графиках разработки. Приходится задуматься над тем, как сделать его пригодным для повторного использования и простым в поддержке. В этом нам может помочь MVC.

Термин MVC давно стал привычным в контексте разработки бэкэнда с использованием фреймворков, таких как Struts, Ruby on Rails, и CakePHP — истоки MVC лежат в области структурирования клиентских приложений. Давайте посмотрим, что такое MVC, как мы можем использовать его, и какие есть готовые MVC фреймворки.

Что такое MVC?

Если вы не знаете, что это такое, то после четырех упоминаний этого акронима вам наверняка не терпится узнать. MVC (сокращение от  Model-View-Controller) — это шаблон проектирования, который разделяет приложение на три части: данные (Model), представление этих данных пользователю (View) и действие выполняемые в ответ на активность пользователя (Controller).

В 1978 году в исследовательском центре Xerox, Trygve Reenskau сформулировал основы концепции MVC (PDF):

Объект, играющий роль модели, это компьютерное внутреннее представление этой информации. Компьютер отображает различные аспекты этой информации с помощью объекта View, несколько объектов View ассоциированных с одной и той же моделью могут быть видимы одновременно. Объект Controller транслирует команды пользователя в сообщения для объектов View и Model.

Другими словами, когда пользователь что-то делает, это «что-то» передается контроллеру, который решает, что должно произойти дальше. Контроллер запрашивает данные модели и отсылает их виду, который показывает данные пользователю.

Что же это разделение может дать веб-разработчику.

Истоки

Статический документ это основа веб-страницы. Такая страница отображает состояние информации на сервере в момент ее создания.

Раньше, чтобы модифицировать данные на сервере мы создавали форму, в которую пользователь вводил новые данные, потом эта форма отправлялась на сервер, а пользователь получал уведомление, что действие выполнено. Но постоянные перезагрузки всей страницы быстро утомляют пользователя, особенно если он допускает ошибку и нужно заново внести все изменения.

Прорыв

Но темные времена веб закончились. JavaScript и Ajax, пришли нам на помощь, теперь мы можем изменять отдельные элементы страницы и сообщать об этом серверу. Особенно важно то, что теперь мы можем реагировать на действия пользователя, не дожидаясь ответа сервера.

На современном уровне сложности JavaScript приложений мы приходим к необходимости разделять компоненты приложения в стиле MVC. Конечно, такое разделение необходимо не всегда — иногда оно делает код излишне объемным. Но когда приложение становиться достаточно сложным, требующим взаимодействия с различными частями сайта, использование паттерна MVC, позволяет создавать более модульный и пригодный к повторному использованию код.

Структурирование кода

Обычно, если нужно проверять данные формы, мы устанавливаем обработчик события отправки формы, который проверяет заданные поля и сообщает пользователю о найденный ошибках.

Выглядит это примерно так:

function validateForm(){
   var errorMessage = 'The following errors were found:<br>';
   if (document.getElementById('email').value.length == 0) {
      errorMessage += 'You must supply an email address<br>';
   }
   document.getElementById('message').innerHTML = errorMessage;
}

Этот подход работает, но не отличается гибкостью. Если понадобиться добавить поля или проверять другую форму на другой странице, то придется дублировать функциональность для каждого нового поля.

Вперед к модульности

Сделаем первый шаг к модульности и разделению, добавив дополнительную семантику в форму. Для обязательного поля содержащего адрес электронной почты код будет примерно такой:

<input type="text" class="required email">

Теперь скрипт может перебрать все поля формы и в зависимости от атрибутов, сохраненных в классе, проверить поле подходящим способом. Еще один плюс таких классов в том, что их можно использовать в CSS.

Мы внедрили метаданные, на основе которых скрипт решает, как работать с соответствующими данными. Но при таком подходе данные и метаданные сильно связаны с разметкой. К тому же сам подход несколько ограничен, трудно описать условия с помощью HTML, например, что делать, если необходимость или допустимые значения одного поля, зависят от заполненности или значения другого поля. Конечно, простейший случай можно закодировать в HTML, но это будет не очень красивое решение, а с усложнением зависимости оно будет просто ужасным:

<input type="checkbox" name="other"> Other
<textarea class="dependson-other"></textarea>

В предыдущем примере префикс dependson указывает на то, что обязательность textarea зависит от заполненности поля other. Чтобы исключить такие конструкции, давайте попробуем определить всю бизнес логику в JavaScript.

Используем JavaScript для описания сущностей

Несмотря на то, что мы можем внедрить некоторую семантику и метаданные в HTML, в конце концов, нам придется, как то представлять эту информацию на уровне JavaScript.

Например:

var fields = {
   'other': { 
       required:true 
       },
   'additional': {
       'required': {
           'other':{checked:true},
           'total':{between:[1,5]}
           },
       'only-show-if': {
           'other': {checked:true}
           }
       }
};

В данном случае поле additional зависит от двух других полей, и отображается только если пользователь активировал чекбокс other.

Несмотря на то, что мы достигли некоторого разделения, лишних зависимостей все еще много. Проверка данных все еще связана с отображением выявленных ошибок, а функция выполняющая проверку данных все еще связана с обработкой события и отвечает за то, чтобы форма не была отправлена, пока данные не введены корректно.

Давайте посмотрим, как мы можем структурировать код с помощью паттерна MVC, а потом вернемся к нашему примеру с проверкой данных формы.

Модель

Поскольку паттерн MVC состоит из трех компонент, мы должны попытаться разделить наше приложение как минимум на три главных объекта.

Выделить модель в отдельный объект достаточно просто, как мы видели в предыдущем примере, это происходит естественным образом.

Давайте посмотрим на другой пример, у нас есть календарь событий, данные каждого события сохранены в отдельном объекте. Методы объекта предоставляют абстрактный способ взаимодействия с данными. Часто эти методы называют CRUD tasks (create, read, update, delete).

var Events = {
  get: function (id) {
    return this.data[id];
  },
  del: function (id) {
    delete this.data[id];
    AjaxRequest.send('/events/delete/' + id);
  },
  data:{
   '112': { 'name': 'Party time!', 'date': '2009-10-31' },
   '113': { 'name': 'Pressies!', 'date': '2009-12-25' }
  }
  metadata: {
    'name': { 'type':'text', 'maxlength':20 },
    'date': { 'type':'date', 'between':['2008-01-01','2009-01-01'] }
  }
}

Модель содержит метаданные, определяющие допустимые значений полей события.

Кроме того CRUD методы сохраняют состояние объекта на сервере, например, функция delete удаляет запись локально и отсылает запрос на удаление записи на сервер.

Вид

В паттерне MVC, вид получает данные и определяет, как их отобразить. Для этого он может использовать существующий HTML, запрашивать HTML блок у сервера, или создавать его с помощью DOM. Вид не беспокоится о том, откуда и как получить данные, он только отображает те данные, которые ему передали.

View.EventsDialog = function(CalendarEvent) {
   var html = '<div><h2>{name}</h2>' +
              '<div class="date">{date}</div></div>';
   html = html.replace(/\{[^\}]*\}/g, function(key){
        return CalendarEvent[key.slice(1,-1)] || '';
        });
   var el = document.getElementById('eventshell');
   el.innerHTML = html;
}

Events.data = {
   '112': { 'name': 'Party time!', 'date': '2009-10-31' },
   '113': { 'name': 'Pressies!', 'date': '2009-12-25' }
  }

View.EventsDialog(Events.data['112']); // edits item 112

Чтобы контроллер мог управлять видом, не беспокоясь о его внутренней реализации, добавим методы open и close.

View.EventsDialog = function(CalendarEvent){ ... }
View.EventsDialog.prototype.open = function(){
   document.getElementById('eventshell').style.display = 'block';
}
View.EventsDialog.prototype.close = function(){ 
   document.getElementById('eventshell').style.display = 'none';
}

var dialog = new View.EventsDialog(eventObject);
dialog.open();
dialog.close(); 

Обобщение Вида

Легко поддаться на искушение сделать вид зависимым от модели данных и способа их получения. Но, разделяя эти функции, мы делаем вид пригодным для повторного использования. Если мы разделим данные события и диалог в нашем примере, то диалог можно будет использовать для любых данных, а не только для событий.

View.Dialog = function(data) {
   var html = '<h2>' + data.name + '</h2>';
   delete data.name;
   for(var key in data) {
      html += '<div>' + data[key] + '</div>';
   }
   var el = document.getElementById('eventshell');
   el.innerHTML = html;
}

Теперь у нас есть общий способ просмотра любых элементов, а не только событий, и если в следующем проекте понадобиться диалог, можно будет использовать этот код без изменений.

Многие JavaScript фреймворки разработаны с учетом независимости от данных. YUI controls, jQuery UI, ExtJS, и Dojo Dijit создавались с минимальными предположениями о данных, с которыми им придется работать. В результате эти контрольные элементы можно легко использовать в любых приложениях.

Работа с методами вида

Главное правило: вид не должен вызывать свои методы, например, диалог не должен открывать или закрывать себя, это работа контроллера.

Когда пользователь кликает кнопку Сохранить, это событие передается методу контроллера, который должен решить, что дальше делать виду, он может сразу закрыть диалог, или сказать виду отобразить индикатор загрузки пока сохраняются данные, а когда данные сохраняться, событие завершения Ajax вызова запустит другой метод контроллера, который скажет виду что нужно скрыть индикатор и закрыть диалог.

Тем не менее, есть ситуации, когда вид должен сам обрабатывать события и вызывать свои методы. Например, если диалог содержит слайдер, не нужно перекладывать на контроллер, обработку взаимодействия пользователя со слайдером и отображение его значения.

Контроллер

Как же данные Модели попадают в Вид? Это задача Контроллера. Он активируется, каким либо событием, это может быть загрузка страницы или действие пользователя, для этого обработчик события связывается с методом Контроллера.

Controllers.EventsEdit = function(event) {
   /* здесь event это событие js, а не календарное событие */
   // event.target.id, содержит идентификатор соответствующего календарного события
   var id = event.target.id.replace(/[^d]/g, '');
   var dialog = new View.Dialog( Events.get(id) );
   dialog.open();
}

Этот паттерн особенно удобен, когда данные используются в разных контекстах. Например, мы редактируем событие в календаре, кликаем на кнопку Удалить и теперь нам нужно убрать диалог, удалить событие в календаре и на сервере.

Controller.EventsDelete = function(event) {
   var id = event.target.id.replace(/[^d]/g, '');
   View.Calendar.remove(id);
   Events.del(id);
   dialog.close();
}

Экшены контроллера получаются простыми и понятными, а это ключ к созданию удобных в поддержке приложений.

Внедряем MVC на примере проверки данных формы

Теперь, когда мы знаем как разделить код на составные части, вернемся к примеру проверки полей формы с которого начинали. Как мы можем сделать его максимально гибким с помощью паттерна MVC?

Проверка данных модели

Модель определяет, корректны данные или нет, не беспокоясь о том, как это будет представлено, ей просто нужно определить какие поля не соответствуют требованиям.

У нас уже есть переменная fields содержащая некоторые метаданные Модели. Теперь мы добавим к этому объекту метод, который может понимать и проверять передаваемые ему данные. Метод validate перебирает поля переданного ему объекта данных и проверяет соответствуют ли он требованиям определенным внутренними метаданными.

var MyModel = {
   validate: function(data) {
      var invalidFields = [];
      for (var key in data) {
         if (this.metadata[key].required && !data[key]) {
             invalidFields[invalidFields.length] = {
                field: key, 
                message: key + ' is required.'
             };
         }
      }
      return invalidFields;
   },
   metadata: {
      'other': {required: true}
   }
}

Для проверки мы передаем массив пар ключ/значение, где ключ это имя поля, а значение то, что пользователь ввел в поле.

var data = { 'other': false };

var invalid = MyModel.validate(data);

Теперь переменная invalid содержит список полей, которые не прошли проверку, эти данные можно передать в вид для отображения сообщения об ошибках.

Отображение полей с ошибками

Теперь нам нужно отобразить ошибки. Отображение это работа для Вида, а данные об ошибках он должен получить от Контроллера.

View.Message = function(messageData, type){
    var el = document.getElementById('message');
    el.className = type;
    var message = '<h2>We have something to bring to your attention</h2>' +
    			  '<ul>';
    for (var i=0; i < messageData.length; i++) {
       message += '<li>' + messageData[i] + '</li>';
    }
    message += '</ul>';
    el.innerHTML = message;
}
View.Message.prototype.show() {
	/* provide a slide-in animation */
}

Дополнительный параметр type позволяет указать класс элемента, чтобы применить к нему нужные стили.

Связываем все вместе с помощью контроллера

У нас есть модель, которая сохраняет данные и проверяет их на корректность, и вид, который может отображать сообщения об ошибке или успешном выполнении операции, теперь нам нужно связать их ,чтобы проверка данных выполнялась, когда пользователь пытается отправить форму.

addEvent(document.getElementById('myform'), 'submit', MyController.validateForm);

Метод контроллера, получает данные, проверяет их корректность и отображает ошибки.

MyController.validateForm = function(event){
    var data = [];
    data['other'] = document.getElementById('other').checked;
    var invalidFields = MyModel.validate(data);

    if (invalid.length) {
       event.preventDefault();
       // создает вид и отображает сообщение
       var message = new View.Message(invalidFields, 'error');
       message.show();
    }
}

Массив данных содержит значения полей. Модель проверяет их корректность и возвращает список полей содержащих ошибки. Если одно из полей содержит ошибку, сохранение данных отменяется, и сообщение об ошибке передается Виду, после чего он его отображает.

Готово! Теперь у нас есть пригодные к повторному использованию Вид и методы проверки данных Модели.

Прогрессивное улучшение

В нашем примере, MVC отлично сочетается с прогрессивным улучшением. JavaScript просто дополняет страницу. Благодаря разделению, меньшему числу компонентов нужно понимать, что происходит на странице, а это упрощает использование прогрессивного улучшения.

Часто в таких приложениях сначала загружается веб страница, а потом отдельным Ajax запросом отображаемые на ней данные. Это может создать у пользователя впечатление медленного интерфейса, поскольку прежде чем он сможет работать со страницей должны выполниться два запроса.

Чтобы избежать задержек, сразу отображайте все данные статически, это должно быть начальное состояние. Данные представленные в начальном состоянии, можно продублировать в виде JavaScript внизу страницы и как только страница загрузиться JavaScript часть вашего приложения будет готова к работе.

Можно предварительно загрузить и некоторые дополнительные данные. Например, в нашем примере изначально отображаются только события текущего месяца, но JavaScript данные могут содержать еще и события предыдущего и следующего месяца, на времени загрузки это скажется незначительно, зато пользователь сможет перейти к следующему и предыдущему месяцу, не запрашивая данные с сервера.

Фреймворки

Многие JavaScript MVC фреймворки предоставляют намного более структурированный и мощный чем представленный в статье подход к MVC разработке, но понимание паттерна MVC и того как он может быть применен, важно и в случае разработки своего и в случае использования существующего фреймворка.

Вот несколько примеров таких фреймворков:

Нужно ли использовать фреймворк в конкретном случае или нет, зависит от сложности приложения, если приложение очень простое, то фреймворк будет только лишней нагрузкой.

В заключение

Как и всегда в разработке, вам нужно решать стоит ли использовать MVC в конкретном проекте. Для небольших приложений с ограниченным функционалом, это может быть не целесообразно, но чем больше ваше приложение, тем больше преимуществ вы получаете от разделения кода на модель, вид и контроллер.