MVC в JavaScript
JavaScript займає усе більше важливе місце у веб, а значить усе більше часу в графіках розробки. Доводиться задуматися над тим, як зробити його придатним для повторного використання й простим у підтримці. У цьому нам може допомогти MVC.
Термін MVC давно став звичним у контексті розробки бекенда з використанням фреймворків, таких як Struts, Ruby on Rails, і CakePHP — джерела MVC лежать в області структурування клієнтських додатків. Давайте подивимося, що таке MVC, як ми можемо використовувати його, і які є готові MVC фреймворки.
Що таке MVC?
Якщо ви не знаєте, що це таке, то після чотирьох згадувань цього акроніма вам напевно не терпиться довідатися. MVC (скорочення від Model-View-Model-View-Controller1)—це шаблон проектування, що розділяє додаток на три частини: дані (Model), подання цих даних користувачеві (View) і дія виконувані у відповідь на активність користувача (Controller).
В 1978 році в дослідницькому центрі Xerox, Trygve Reenskau сформулировал основы концепции MVC (PDF)2:
Об'єкт, що грає роль моделі, це комп'ютерне внутрішнє подання цієї інформації. Комп'ютер відображає різні аспекти цієї інформації за допомогою об'єкта 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>
Тепер ~~4584 може перебрати всі поля форми й залежно від атрибутів, збережених у класі, перевірити поле підходящим способом. Ще один плюс таких класів у тім, що їх можна використовувати в CSS.
Ми впровадили метаданні, на основі яких ~~4584 вирішує, як працювати з відповідними даними. Але при такому підході дані й метаданні сильно пов'язані з розміткою. До того ж сам підхід трохи обмежений, важко описати умови за допомогою HTML, наприклад, що робити, якщо необхідність або припустимі значення одного поля, залежать від заповнювання або значення іншого поля. Звичайно, найпростіший випадок можна закодувати в HTML, але це буде не дуже гарне рішення, а з ускладненням залежності воно буде просто жахливим:
<input type="checkbox" name="other"> Other
<textarea></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>{date}</div></div>';
html = html.replace(/\{[^\}]*\}/g, function(key){
return CalendarEvent[key.slice(1,-1)] || '';
});
var el = document.getElementById('eventshell');
el.innerHTML = html;
}
var 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 i = 0; i < data.length; i++) {
if (this.metadata[data.key].required && !data.value) {
invalidFields[invalidFields.length] = {
field: data.key,
message: data.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 у конкретному проекті. Для невеликих додатків з обмеженим функціоналом, це може бути не доцільно, але чим більше ваш додаток, тим більше переваг ви одержуєте від поділу коду на модель, вид і контролер.