Создание клиентского MVC приложения с помощью RequireJS

Как веб-разработчик, вы, наверное, часто писали код JavaScript в одном файле, и, когда количество кода становится все больше и больше, его трудно поддерживать. Для решения этой проблемы вы можете разделить свой ​​код на несколько файлов, добавить дополнительные теги script и использовать глобальные переменные для доступа к функциям, объявленным в других файлах. Но это загрязняет глобальное пространство имен и для каждого файла дополнительный запрос HTTP снижает пропускную способность, что увеличивает время загрузки страницы.

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

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

Введение


Что такое RequireJS и чем он крут

RequireJS является реализацией AMD (Asynchronous Module Definition), API для объявления модулей и их асинхронной загрузки «на лету», когда они понадобятся. Это разработка Джеймса Бёрка (James Burke) и она достигла версии 1.0 после двух лет разработки. RequireJS поможет организовать ваш код с помощью модулей и будет управлять за вас асинхронной и параллельной загрузкой ваших файлов. Так как скрипты загружаются только при необходимости и параллельно, это уменьшает время загрузки страницы, что очень здорово!

MVC на фронтенде?

MVC — это хорошо известных паттерн для организации кода на стороне сервера, делает его модульным и поддерживаемым. Что можно сказать о его использовании на фронтенде? Можем ли мы применять этот паттерн на JavaScript? Если вы используете JavaScript только для анимации, проверки нескольких форм или несколько простых методов, которые не требуют много строк кода (скажем, менее чем в 100 строк), нет необходимости структурировать файлы с использованием MVC, и, наверное, нет необходимости использовать RequireJS. Однако если вы создаете большое веб-приложение с множеством различных представлений, безусловно, да!

Создаем приложение

Чтобы показать, как организовать код MVC с помощью RequireJS, мы создадим очень простое приложение с 2 представлениями:
  • Первое представление отображения список пользователей (представленных атрибутом name),
  • Второе представление позволит вам добавить пользователя.


Вот как это будет выглядеть:



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

HTML и СSS

Это текст HTML файла, который мы будем использовать в примере:

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>A simple MVC structure</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <div id="container">
        <h1>My users</h1>
        <nav><a href="#list">List</a> - <a href="#add">Add</a></nav>
        <div id="app"></div>
    </div>
    <script data-main="js/main" src="js/require.js"></script>
</body>
</html>


Навигацией в нашем приложении будут ссылки меню nav, которое будет присутствовать на каждой странице приложения, и вся магия приложения MVC будет происходить в элементе <div id=«app»></div>. Мы также включили RequireJS (который вы можете скачать здесь) в нижней части body. Вы можете заметить специальный атрибут у тега script: data-main=«js/main». Значение, присвоенное этому атрибуту используется RequireJS как точка входа всего приложения.

Давайте также добавим немного стилей:

#container{
    font-family:Calibri, Helvetica, serif;
    color:#444;
    width:200px;
    margin:100px auto 0;
    padding:30px;
    border:1px solid #ddd;
    background:#f6f6f6;
    -webkit-border-radius:4px;
       -moz-border-radius:4px;
            border-radius:4px;
}
 
h1, nav{
    text-align:center;
    margin:0 0 20px;
}


Вспоминаем ООП: что такое модуль?

В объектно-ориентированном программировании на JavaScript, есть очень распространенный паттерн называемый Шаблон Модуля. Он используется для инкапсуляции методов и атрибутов в объектах (которые являются «модулями»), чтобы избежать загрязнения глобального пространства имен. Он также используется, для частичной имитации классов из других ООП языков, таких как Java или PHP. Пример того, как вы можете определить простой модуль MyMath в нашем файле main.js:

var MyMath = (function(){
 
    // Здесь объявляются частные переменные и функции
     
    return { // Здесь объявляются публичные методы
        add:function(a, b){
            return a + b;
        }
    };
})();
 
console.log(MyMath.add(1, 2));


Публичные методы объявляются с помощью объекта в литеральной нотации, что не очень удобно. В качестве альтернативы вы можете использовать Шаблон Открытия Модуля, который возвращает частные атрибуты и методы:

var MyMath = (function(){
 
    // С этим паттерном вы можете использовать обычное описание функций:
     
    function add(a, b){
        return a + b;
    }
     
    return {
        add:add // Но не забудьте объявить их в возвращаемом объекте!
    };
})();
 
console.log(MyMath.add(1, 2));


Я буду использовать Шаблон Открытия Модуля в остальной части статьи.

RequireJS


Описание модуля с помощью RequireJS

В предыдущем разделе мы определили модуль в переменной для его дальнейшего вызова. Это лишь один из способов объявления модуля. Сейчас мы рассмотрим другой метод, используемый RequireJS. Цель RequireJS заключается в разделении наших JavaScript файлов для более удобного сопровождения, поэтому давайте создадим файл MyMath.js, для определения нашего модуля MyMath, в той же папке, где находится main.js:

define(function(){
 
    function add(a, b){
        return a + b;
    }
     
    return {
        add:add
    };
});


Вместо того, чтобы объявить переменную, мы просто поместили модуль в качестве параметра функции define. Эта функция определена в RequireJS и она делает наш модуль доступным извне.

Загрузка модуля в основном файле

Давайте вернемся к нашему main.js файлу. RequireJS предоставляет еще одну функцию с названием require, которую мы используем, для обращения к нашему модулю MyMath. Вот так наш main.js выглядит сейчас:

require(['MyMath'], function(MyMath){
     
    console.log(MyMath.add(1, 2)); 
 
});


Обращение к модулю MyMath сейчас обернуто в функцию require, которая принимает два параметра:
  • Массив модулей, которые мы хотим загрузить, объявленных путями к ним относительно точки входа (помните атрибут data-main в HTML) и без расширения .js,
  • Функция вызываемая после того, как эти зависимости будут загружены. Модули будут переданы в качестве параметров этой функции, так что вы можете просто назвать эти параметры именами модулей.


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

Структура MVC


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

Давайте начнем с создания папок и файлов нашего проекта. Мы будем использовать модели для представления данных, бизнес-логика будет сосредоточена в контроллерах, и эти контроллеры будут вызывать определенные представления для отображения страниц. Как вы думаете? Нам нужно 3 папки: Models, Controllers и Views. Учитывая простоту нашего приложения, у нас будет 2 контроллера, 2 представления и 1 модель. Наш папка с JavaScript теперь выглядит следующим образом:

  • Controllers
    • AddController.js
    • ListController.js

  • Models
    • User.js

  • Views
    • AddView.js
    • ListView.js

  • main.js
  • require.js


Итак структура готова. Начнем с реализации простейшей части: это модель.

Модель: User.js


В этом примере user — это простой класс с одним атрибутом name:

define(function(){
     
    function User(name){
        this.name = name || 'Default name';
    }
     
    return User;
});


Если теперь вернемся к нашему файлу main.js, теперь мы можем объявить зависимость от User в методе require, и вручную создать набор пользователей для данного примера:

require(['Models/User'], function(User){
     
    var users = [new User('Barney'),
                 new User('Cartman'),
                 new User('Sheldon')];
     
    for (var i = 0, len = users.length; i < len; i++){
        console.log(users[i].name);
    }
     
    localStorage.users = JSON.stringify(users);
});


Затем мы сериализуем массив пользователей в JSON и сохраним его в локальном хранилище HTML5, чтобы сделать их доступным как в базе данных:



Примечание: для сериализации JSON методом stringify и десериализации методом parse нужна любая polyfill библиотека, что бы пример работал в IE7 и более ранних. Для этого вы можете использовать библиотеку json2.js Дугласа Крокфорда из Github репозитория.

Отображаем список пользователей

Пора показать список пользователей в интерфейсе нашего приложения. Для этого мы будем использовать ListController.js и ListView.js. Очевидно, что эти два компонента связаны между собой. Есть много способов сделать это, но что бы наш пример оставался простым я предлагаю: ListView будет иметь метод render и наш ListController будет просто получать пользователей из локального хранилища и вызвать метод render у ListView, передавая пользователей в качестве параметра. Итак, очевидно, в ListController нужна зависимость от ListView.

Так же, как в require, вы можете передать массив зависимостей в define, если модуль зависит от других модулей. Давайте также создадим метод start (или с каким-то другим именем, которое вам покажется осмысленным, например run или main), чтобы описать в нем основное поведение контроллера:

define(['Views/ListView'], function(ListView){
     
    function start(){
        var users = JSON.parse(localStorage.users);
        ListView.render({users:users});
    }
     
    return {
        start:start
    };
});


Здесь мы десериализуем список пользователей из локального хранилища и передаем его в метод render в виде объекта. Теперь, все, что нам нужно сделать, это реализовать метод render в ListView.js:

define(function(){
    function render(parameters){
        var appDiv = document.getElementById('app');
        var users = parameters.users;
        var html = '<ul>';

        for (var i = 0, len = users.length; i < len; i++){
            html += '<li>' + users[i].name + '</li>';
        }
        html += '</ul>';
        appDiv.innerHTML = html;
    }
 
    return {
        render:render
    };
});


Этот метод просто перебирает в цикле пользователей и объединяет их имена в HTML строку, которую мы вставляем в элемент .

Важно: Использование HTML в файле JavaScript, как в этом примере не лучшее решение, потому что это очень трудно поддерживать. Вместо этого вам следует присмотреться к шаблонам. Шаблоны — это элегантный способ вставки данных в HTML. В интернете доступно очень много хороших шаблонизаторов. Например, можно использовать JQuery-Tmpl или Mustache.js. Но это выходит за рамки данной статьи, и добавило бы сложности к текущей архитектуре, я предпочел сделать ее как можно проще.

Теперь, все что нам нужно сделать, это «запустить» наш модуль ListController. Для этого давайте объявим его как зависимость в нашем файле main.js, и вызовем метод ListController.start():

require(['Models/User', 'Controllers/ListController'], function(User, ListController){
     
    var users = [new User('Barney'),
                 new User('Cartman'),
                 new User('Sheldon')];
     
    localStorage.users = JSON.stringify(users);
     
    ListController.start();
});


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



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

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

Добавляем пользователя

Теперь нам нужна возможность добавлять пользователей в список. Мы отобразим простое поле ввода и кнопку, с обработчиком события при нажатии на кнопку, в котором будем добавлять пользователя в локальном хранилище. Давайте начнем с AddController, как в предыдущем разделе. Этот файл будет очень простым, так как у нас нет параметров для передачи их в представление. Это AddController.js:

define(['Views/AddView'], function(AddView){
 
    function start(){
        AddView.render();
    }
 
    return {
        start:start
    };
});


И соответствующее ему представление:

define(function(){
     
    function render(parameters){
        var appDiv = document.getElementById('app');
        appDiv.innerHTML = '<input id="user-name" /><button id="add">Add this user</button>';
    }
     
    return {
        render:render
    };
});


Теперь вы можете объявить AddController как зависимость в основном файле и вызвать его метод start, чтобы увидеть ожидаемое представление:



Но так как у нас пока нет привязки к событиям кнопки, это представление не очень полезно… Давайте поработаем над этим. У меня есть к вам вопрос: Где мы должны разместить логику для обработки этого события? В представлении или в контроллере? Если мы разместим в представлении, то это будет правильное место для добавления подписки на событие, но размещение бизнес-логики в представлении будет очень плохой практикой. Разместив логику в контроллере, кажется, будет лучшей идеей, даже если она не идеальна, потому что мы не хотим видеть здесь какое-либо упоминание html элементов, которые относятся к представлению.

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

Как я уже сказал, давайте разместим всю логику события в контроллере. Нам нужно создать функцию bindEvents в AddController и вызывать ее после того, как представление завершило отображение HTML:

define(['Views/AddView', 'Models/User'], function(AddView, User){
 
    function start(){
        AddView.render();
        bindEvents();   	
    }
     
    function bindEvents(){
        document.getElementById('add').addEventListener('click', function(){
            var users = JSON.parse(localStorage.users);
            var userName = document.getElementById('user-name').value;
            users.push(new User(userName));
            localStorage.users = JSON.stringify(users);
            require(['Controllers/ListController'], function(ListController){
                ListController.start();
            });
        }, false);
    }
 
    return {
        start:start
    };
});


В bindEvents, мы просто добавляем слушатель события клика по кнопке #add (не стесняйтесь использовать свою собственную функцию, если работаете с attachEvent в IE — или просто использовать JQuery). Когда кнопка нажата, мы получаем строку пользователей из локального хранилища, десериализуем ее, чтобы получить массив, добавляем нового пользователя с именем, содержащимся в поле ввода #user-name, и помещаем обновленный массив пользователей в локальное хранилище. После этого мы, наконец, загружаем ListController для выполнения его метода start, и мы можем увидеть результат:



Замечательно! Настало время вам немного отдохнуть, вы сделали хорошую работу, если вы выполняли пример вместе со мной.

Навигация между представлениями с помощью маршрутов


Наше небольшое приложение крутое, но на самом деле плохо, что мы до сих пор не можем перемещаться между представлениями, чтобы добавлять новых пользователей. Не хватает системы маршрутизации. Если вы раньше работали с серверными MVC фреймворками, вы, наверное, знакомы с такой системой. Каждый URL ведет к своему представлению. Однако сейчас мы на стороне клиента, и ситуация немного отличается. Одностраничные приложения на JavaScript, подобные этому используют хеш для навигации между различными частями приложения. В нашем случае, мы хотим отображать два различных представления, когда открываются эти URL-адреса:


Это даст возможность добавлять в закладки каждую страницу приложения.

Примечание: В Firefox, Chrome и Opera есть хорошая поддержка HTML5 history management (PushState, popState, replaceState), что позволяет избежать работы с хэшами.

Функциональность и совместимость с браузерами

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


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

Примечание: Также доступен плагин jQuery, для управления этим процессом.

Маршруты и основной цикл маршрутизации

Давайте создадим Router.js файл рядом с main.js для управления логикой маршрутизации. В начале этого файла нам нужно объявить наши маршруты и задать маршрут по умолчанию, если он не указан в URL. Мы можем, например, использовать простой массив объектов маршрутов, которые содержат хэши и соответствующие им контроллеры, которые нужно загрузить. Нам также необходим defaultRoute если в URL не присутствует хэш:

define(function(){
 
    var routes = [{hash:'#list', controller:'ListController'},
                  {hash:'#add',  controller:'AddController'}];
    var defaultRoute = '#list';
    var currentHash = '';
     
    function startRouting(){
        window.location.hash = window.location.hash || defaultRoute;
        setInterval(hashCheck, 100);
    }
     
    return {
        startRouting:startRouting
    };
});


Когда будет вызван метод startRouting, он установит в URL значение хеша по умолчанию, и начнет повторный вызов метода hashCheck, который мы еще не реализовали. Переменная currentHash будет использоваться для хранения текущего значения хэша если было обнаружено изменение.

Проверка измеения хеша

Это функция hashCheck, которая вызывается каждые 100 миллисекунд:

function hashCheck(){
    if (window.location.hash != currentHash){
        for (var i = 0, currentRoute; currentRoute = routes[i++];){
            if (window.location.hash == currentRoute.hash)
                loadController(currentRoute.controller);
        }
        currentHash = window.location.hash;
    }
}


hashCheck просто проверяет, изменился ли хэш по сравнению с currentHash, и если он совпадает с одним из маршрутов, то вызывает loadController с соответствующим именем контроллера.

Загрузка нужного контроллера

Наконец, loadController просто выполняет вызов require, чтобы загрузить модуль контроллера и вызывает его функцию start:

function loadController(controllerName){
    require(['Controllers/' + controllerName], function(controller){
        controller.start();
    });
}


Итак, окончательный вариант файла Router.js выглядит следующим образом:

define(function(){
     
    var routes = [{hash:'#list', controller:'ListController'},
                  {hash:'#add',  controller:'AddController'}];
    var defaultRoute = '#list';
    var currentHash = '';
     
    function startRouting(){
        window.location.hash = window.location.hash || defaultRoute;
        setInterval(hashCheck, 100);
    }
     
    function hashCheck(){
        if (window.location.hash != currentHash){
            for (var i = 0, currentRoute; currentRoute = routes[i++];){
                if (window.location.hash == currentRoute.hash)
                    loadController(currentRoute.controller);
            }
            currentHash = window.location.hash;
        }
    }
     
    function loadController(controllerName){
        require(['Controllers/' + controllerName], function(controller){
            controller.start();
        });
    }
     
    return {
        startRouting:startRouting
    };
});


Использование новой системы маршрутизации

Все что нам осталось сделать, это загрузить этот модуль в нашем основном файле и вызвать метод startRouting:

require(['Models/User', 'Router'], function(User, Router){
     
    var users = [new User('Barney'),
                 new User('Cartman'),
                 new User('Sheldon')];
     
    localStorage.users = JSON.stringify(users);
 
    Router.startRouting(); 
});


Если мы хотим, перемещаться в нашем приложении от одного контроллера к другому, мы можем просто заменить текущий window.hash на хэш маршрута другого контроллера. В нашем случае мы по-прежнему вручную загружаем ListController из AddController вместо использования нашей новой системы маршрутизации:

require(['Controllers/ListController'], function(ListController){
    ListController.start();
});


Давайте заменим эти три строки на простое обновление хэш:

window.location.hash = '#list';


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

Вот ссылка на онлайн демо этого приложения.

Выводы


Вы можете гордиться собой, вы написали MVC приложение без использования каких-либо фреймворков! Мы использовали только RequireJS, что бы объединить наши файлы, это единственный обязательный инструмент, чтобы построить модульную структуру. Итак, каковы дальнейшие действия? Если вам понравился минималистичный подход в стиле DIY, как в этом примере, вы можете обогатить наш маленький фреймворк новыми функциями, по мере роста вашего приложения и появления новых технических потребностей. Вот некоторые варианты будущих шагов:

  • Интегрировать систему шаблонизации,
  • Создать маленькую внешнюю библиотеку, чтобы поместить в нее все, что не связано напрямую c вашим приложением (например, система маршрутизации), чтобы иметь возможность использовать это в других проектах,
  • Описать в виде объектов модель, вид и представление в этой библиотеке,
  • Создать новый уровень абстракции для получением данных из различных источников (RESTful API, LocalStorage, IndexedDB ...).


Этот пример отлично подходит для обучения, но наш фреймворк на самом деле не готов для использования в реальных проектах. Если вам лень добавлять функционал, перечисленный выше (а этот список не является исчерпывающим!), вы можете начать изучение, одного из существующих MVC фреймворков. Вот некоторые из самых популярных:



Оригинал статьи на английском.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 13

    +8
    Конечно, спасибо за старания, но статья то написана в ноябре 2011 года. Старовата.
    Ни слова про конфиг и shim, да и про билд с помощью r.js или того же grunt-contrib-requirejs было бы тоже приятно прочитать.
      0
      Статья хоть и не первой свежести, но актуальности не потеряла и рассчитана на тех, кто только начинает знакомиться с MVC. Я думаю новичку этого будет достаточно, что бы заинтересовать его и помочь понять принцип такой архитектуры. Что качается shim, r.js и grunt-contrib-requirejs, если у Вас есть достаточно материала на статью, будем рады почитать )
        +3
        Да Вы сами хоть смотрели сколько на Хабре уже написано и разжевано про MVC для новичков, не считая всего интернета?
        На том же ГитХабе в поиске %lang%-mvc-example полно примеров
          +2
          Я же не указывают вам, что про css анимации и создание виджетов для убунту полно материала. Если вам есть чем поделиться по теме, напишите об этом, я думаю многие почитали бы с удовольствием.
      0
      Я же не указывают вам, что про css анимации и создание виджетов для убунту полно материала. Если вам есть чем поделиться по теме, напишите об этом, я думаю многие почитали бы с удовольствием.
        +1
        Для создания заглушек под юнит-тесты для Require.js есть отличная библиотека Squire.js.
          –1
          Спасибо, полезная вещь, возьму на заметку.
          +1
          Вот я где-то год назад делал доклад Require.js in details www.slideshare.net/arttrityak/requirejs-in-details-v2
            –1
            Отлично! Все комментарии были бы такими полезными )
              0
              а звук доклада есть?
              –1
              Автору огромное спассибо за єтот перевод!
              Несмотря на то что мне пришлось зделать простенький апп на MeteorJS я не знал паттерна с роутером и не понял как его прикрутить на свой проект. Хотя и видел что такое есть и «нодо б выучить». Но вот именно эта статья повернула мое надобы в зделано.
              Качество перевода супер ;)
              А то что есть 100500 таких же это не саоме главное.
                0
                Спасибо за статью, буду рекомендовать её другим людям.
                  0
                  Прелестно! Просто прелестно!
                  image

                  Only users with full accounts can post comments. Log in, please.