Модель-Представление-Контроллер (MVC) с помощью JavaScript

Цель этой статьи - продемонстрировать, как реализовать схему MVC при разработке простого компонента JavaScript. Компонент, который мы будем разрабатывать, это своего рода элемент управления ListBox для HTML (тег <select>) с редактируемым списком элементов: пользователь сможет выбирать или удалять элементы, а также добавлять в список новые элементы. Компонент будет состоять из трех классов, что соответствует схеме проектирования Модель-Представление-Контроллер.

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

Схема MVC JavaScript нуждается в некотором пояснении. Шаблоны проектирования называются в соответствии с их составляющими: Модель, которая хранит модель данных приложения; Представление, которое отображает Модель в соответствующем виде; и Контроллер, который обновляет Модель.

Википедия дает следующее определение стандартных компонентов архитектуры Модель-Представление-Контроллер:

  • Модель - представление информации, на которой работает приложение, для конкретного домена. Модель - это другое название для слоя домена. Он реализует логику для манипуляции с данными (например, расчет общей стоимости налогов и транспортных расходов для товара в корзине);
  • Представление - визуализирует модель в форме, подходящей для взаимодействия, обычно в виде элемента пользовательского интерфейса. MVC часто используется в веб-приложениях, где представление - это HTML-страница и код, который собирает динамические данные для страницы;
  • Контроллер - обрабатывает и реагирует на события (действия пользователя), и вносит изменения в модель и в представление.

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

Вот ее код:

/ **
 * Модель. Модель сохраняет элементы и уведомляет
 * наблюдателей об изменениях.
 * /
function ListModel(items) {
    this._items = items;
    this._selectedIndex = -1;
    this.itemAdded = new Event(this);
    this.itemRemoved = new Event(this);
    this.selectedIndexChanged = new Event(this);
}
ListModel.prototype = {
    getItems : function () {
        return [].concat(this._items);
    },
    addItem : function (item) {
        this._items.push(item);
        this.itemAdded.notify({ item : item });
    },
    removeItemAt : function (index) {
        var item;
        item = this._items[index];
        this._items.splice(index, 1);
        this.itemRemoved.notify({ item : item });
        if (index === this._selectedIndex) {
            this.setSelectedIndex(-1);
        }
    },
    getSelectedIndex : function () {
        return this._selectedIndex;
    },
    setSelectedIndex : function (index) {
        var previousIndex;
        previousIndex = this._selectedIndex;
        this._selectedIndex = index;
        this.selectedIndexChanged.notify({ previous : previousIndex });
    }
};

Событие - это простой класс для реализации MVC паттерна Observer (Наблюдатель):

function Event(sender) {
    this._sender = sender;
    this._listeners = [];
}
Event.prototype = {
    attach : function (listener) {
        this._listeners.push(listener);
    },
    notify : function (args) {
        var index;
        for (index = 0; index < this._listeners.length; index += 1) {
            this._listeners[index](this._sender, args);
        }
    }
};

Класс Представления требует определения элемента управления для взаимодействия с ним. Для этой задачи существуют многочисленные варианты интерфейса, но я предпочитаю наиболее простые из них. Я хочу, чтобы у меня был ListBox и две кнопки под ним: кнопка "плюс" для добавления элементов и кнопка "минус" для удаления выбранного элемента.

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

Ниже приводятся классы Представления и Контроллера для модели MVC:

/**
 * Представление. Представление отображает модель и предоставляет
 * элементы пользовательского интерфейса. Контроллер используется для обработки взаимодействий пользователя.
 */
function ListView(model, elements) {
    this._model = model;
    this._elements = elements;
    this.listModified = new Event(this);
    this.addButtonClicked = new Event(this);
    this.delButtonClicked = new Event(this);
    var _this = this;
    // присоединяем слушатели модели
    this._model.itemAdded.attach(function () {
        _this.rebuildList();
    });
    this._model.itemRemoved.attach(function () {
        _this.rebuildList();
    });
    // присоединяем слушатели к HTML-элементам управления
    this._elements.list.change(function (e) {
        _this.listModified.notify({ index : e.target.selectedIndex });
    });
    this._elements.addButton.click(function () {
        _this.addButtonClicked.notify();
    });
    this._elements.delButton.click(function () {
        _this.delButtonClicked.notify();
    });
}
ListView.prototype = {
    show : function () {
        this.rebuildList();
    },
    rebuildList : function () {
        var list, items, key;
        list = this._elements.list;
        list.html('');
        items = this._model.getItems();
        for (key in items) {
            if (items.hasOwnProperty(key)) {
                list.append($('<option>' + items[key] + '</option>'));
            }
        }
        this._model.setSelectedIndex(-1);
    }
};
/**
 * Контроллер. Контроллер реагирует на действия пользователя и
 * вызывает изменения в модели.
 */
function ListController(model, view) {
    this._model = model;
    this._view = view;
    var _this = this;
    this._view.listModified.attach(function (sender, args) {
        _this.updateSelected(args.index);
    });
    this._view.addButtonClicked.attach(function () {
        _this.addItem();
    });
    this._view.delButtonClicked.attach(function () {
        _this.delItem();
    });
}
ListController.prototype = {
    addItem : function () {
        var item = window.prompt('Add item:', '');
        if (item) {
            this._model.addItem(item);
        }
    },
    delItem : function () {
        var index;
        index = this._model.getSelectedIndex();
        if (index !== -1) {
            this._model.removeItemAt(this._model.getSelectedIndex());
        }
    },
    updateSelected : function (index) {
        this._model.setSelectedIndex(index);
    }
};

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

$(function () {
    var model = new ListModel(['PHP', 'JavaScript']),
        view = new ListView(model, {
            'list' : $('#list'), 
            'addButton' : $('#plusBtn'), 
            'delButton' : $('#minusBtn')
        }),
        controller = new ListController(model, view);

    view.show();
});
<select id="list" size="10"></select>
<button id="plusBtn">  +  </button>
<button id="minusBtn">  -  </button>

Пример реализации можете посмотреть здесь.

И UML-схема для тех, кто знаком с ними:

Вадим Дворниковавтор-переводчик статьи «Model-View-Controller (MVC) with JavaScript»