Одним солнечным весенним утром, мне пришла в голову замечательная идея — заняться изучением популярной библиотеки RequireJS. Я уже давно читал много хорошего и о простоте использования и о преимуществах, которые она оказывает при использовании в проектах. Поэтому и подумать не мог, что подключение RequireJS к модульному проекту на Backbone может вызвать столько проблем. Я потратил два дня на то, что должно занять не более часа. А если у разработчика нет этих двух дней? Вот и решил поделиться с другими своим опытом, чтобы сэкономить время и нервы.
Немного теории (вместо предисловия)
Библиотека RequireJS действительно очень проста. Доступна она, как и документация, на официальном сайте разработчика. Принцип работы библиотеки заключается в разбивании джава скрипт кода на «модули» и дальнейшее их использование. Описываются модули с помощью директивы define(), а используются — с помощью директивы require(). В чем же тогда проблема? А проблема возникла при попытке подключить к проекту другие библиотеки. Вся документация, которую я смог найти в интернете, описывала только простейшие случаи — один маленький скрипт, который использует только библиотеку jQuery, и все файлы находятся просто в одной папке. Вот и пришлось мне поломать голову, чтобы подружить между собой RequireJS, jQuery и Backbone. А теперь довольно теории, переходим к практике.
Модули, модули, модули
Рано или поздно, каждый разработчик приходит к пониманию необходимости разбивания большого проекта на мелкие части (модули). Займемся этим и мы. Давайте создадим простое одностраничное приложение, для наглядной демонстрации. Чтобы не изобретать велосипед, используем готовую библиотеку Backbone, которая позволяет реализовать шаблон MVC для кода на джава скрипт. Что это значит? А значит это то, что весь наш код будет разбит на вот такие модули:
- Контроллер (он же роутер)
- модель
- представление
Кроме того, у нас будет объект Application, который отвечает за работу приложения в целом и модуль для инициализации RequireJS (он же точка входа). Для работы нам понадобятся следующие библиотеки:
- RequireJS
- Backbone
- underscore (без него Backbone не работает)
- jQuery
- jStorage ( просто плагин для демонстрации работы зависимостей в RequireJS )
- json2 ( нужен для jStorage )
Размещаем все по своим папкам и получаем вот такую структуру:
В каждом файле у нас находится один объект. Это требование RequireJS. Если вы захотите разместить в одном файле несколько объектов — нужно будет использовать оптимизатор. Давайте превратим эти объекты в модули, т.е. подключим к RequireJS.
define(['backbone'], function(Backbone){
var Controller = Backbone.Router.extend({
initialize: function (options) {
this.appModel = options.model;
},
routes: {
"": "showMainPage",
"result": "showResultPage",
"page/:page": "showPage"
},
showMainPage: function () {
this.appModel.set({ type: "mainpage", page: 0 });
},
showResultPage: function () {
this.appModel.set({ type: "resultpage", page: 0 });
},
showPage: function (pageNum) {
this.appModel.set({ type: "page", page: pageNum });
},
})
return Controller;
});
Данный блок описывает с помощью define модуль контроллера. Define принимает три параметра:
- Имя модуля (мы его опустили, потому, что использование имен для модулей требует использования оптимизатора)
- Зависимости (библиотеки или объекты, которые будут использоваться в коде блока)
- Функция (выполняется после загрузки всех зависимостей)
Как видим наш модуль зависит от библиотеки Backbone, и в функцию передается переменная Backbone — переменная, которую библиотека экспортирует в глобальную область видимости приложения (как $ для jQuery). Функция обязательно должна вернуть объект, который будет доступен для использования в дальнейшем. То же самое делаем и для других модулей.
define(['appModels/appModel', 'appViews/appView', 'appControllers/appController', 'jquery', 'backbone'], function(baseModel, View, Controller, $, Backbone){
var Application = (function() {
var appView;
var appTemplates = {
"mainpage": _.template($('#main-page').html()),
"page": _.template($('#page').html()),
};
var appController;
var appModel;
var self = null;
var module = function() {
self = this;
};
module.prototype =
{
constructor: module,
init: function() {
self.initModel();
self.initView();
self.initRouter();
},
initRouter: function() {
appController = new Controller({ model: appModel});
Backbone.history.start();
},
initView: function() {
appView = new View({ model: appModel, templates: appTemplates, el: $("#main-content")});
appModel.trigger("change");
},
initModel: function() {
appModel = new baseModel();
},
};
return module;
})();
return Application;
});
Объект Application описывается точно так же, как контроллер. Конечно здесь больше зависимостей. И это нормально, ведь Application объединяет в одно целое контроллер, модель, представления (Views) и дает нам готовый объект, который умеет самостоятельно отслеживать изменения в приложении и эффективно реагировать (перерисовывать элементы на странице). А в функцию мы как раз и передаем все составляющие нашего приложения (объекты, которые возвращаются функцией в блоке define), плюс глобальные объекты для библиотек Backbone и jQuery. Все просто, и не понятным остается только одно — что это за пути к файлам такие 'appModels/', 'appViews/ ', 'appControllers/', почему к библиотекам мы обращаемся просто 'jquery' или 'backbone’ и как Backbone работает без underscore? Поиск ответа на этот вопрос занял у меня два дня, и оказался очень простым. Не хватает конфигурации.
Конфигурация
Давайте попробуем запустить наше приложение. Для этого добавим в заголовок html страницы строку
<script data-main="js/init" src="js/library/require.js"> < / script >
Именно так подключается RequireJS к проекту. Атрибут src указывает на размещение файла библиотеки, а data-main — на размещение файла, который является «точкой входа», то есть из него будет начата работа скрипта. У нас он будет выглядеть вот так:
require(["jquery", "../application"], function ($, Application) {
$(document).ready(function() {
var myApplication = new Application();
myApplication.init();
});
});
Как видим, мы ничего не описываем (не используем define), а просто используем готовые модули, описанные в других файлах, с помощью ключевого слова require (). Первым аргументом сюда передаются зависимости (в нашем случае это библиотека jQuery и объект Application), а второй — это функция, которая выполнится, когда все зависимости будут загружены. В отличие от define () эта функция ничего не возвращает. Все готово и можно проверить работу приложения в браузере. Открываем и видим что? Правильно, ничего. Казалось бы все, что описано в документации сделали. В чем же проблема? А проблема в правильном подключении библиотек. Их файлы называются не просто jquery.js или backbone.js (как в документации) и лежат в папке library, а не рядом с другими модулями. Поэтому и для правильной работы приложения мы должны как-то указать все это, то есть создать конфигурацию для RequireJS. Добавим ее перед вызовом оператора require в файле init.js
requirejs.config({
baseUrl: "js/library",
paths: {
jquery: 'jquery.min',
backbone: 'backbone.min',
underscore: 'underscore.min',
storage: 'jstorage',
json: 'json2',
appControllers: '../Controllers',
appModels: '../Models',
appViews: '../Views'
},
shim: {
'underscore': {
exports: '_'
},
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
},
'json': {
exports: 'JSON'
},
'storage': {
deps: ['json', 'jquery'],
}
}
});
Рассмотрим конфигурацию подробнее
- baseUrl — это путь к библиотекам. Все пути в RequireJS берутся относительно этого базового пути. Если не указать baseUrl, то за базовый будет взят путь из атрибута data-main при подключении. Если и он не указан, то в качестве базового берется путь к странице, которая запустила скрипт
- paths — позволяет задать синонимы для определенных путей или файлов. Здесь мы создали синонимы для файлов библиотек, а также, для папок, в которых содержатся контроллеры, модели и представления. Как видим, для этих папок мы использовали относительный путь "../", ведь как отмечалось ранее, система считает корнем путь, который указан в baseUrl. Особенностью RequireJS является то, что нам не нужно указывать расширение .js для файлов
- shim — один из главных разделов конфигурации. Здесь подключаются библиотеки, которые имеют зависимости от других библиотек, и те, которые не поддерживают технологию AMD (Asynchronous Module Definition). Если говорить проще — то в этом блоке описываются библиотеки, в тело которых мы не можем добавить блок define, и соответственно RequireJS не будет ничего знать о зависимости этой библиотеки от других и не сможет ее загрузить. Самый простой случай — это jQuery плагины. Они не содержат define блоков и не могут быть использованы пока библиотека jQuery не будет полностью загружена
Чтобы описать библиотеку в блоке shim просто добавьте туда запись типа
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
Здесь 'backbone' — синоним модуля, который подключаем, deps — зависимости, exports — глобальная переменная, которую библиотека экспортирует в область видимости. То есть данный пример подключает библиотеку Backbone (ее псевдоним описан в path) и указывает, что она зависит от библиотек underscore и jQuery, а также в дальнейшем доступ к функциям данной библиотеки можно осуществлять через переменную Backbone. Вот еще один пример подключения плагина jQuery
'storage': {
deps: ['json', 'jquery'],
}
Как видим плагин storage зависит от jQuery и JSON2, ничего не экспортирует, ведь использование плагина происходит через идентификатор $ библиотеки jQuery.
Ура, успех! Или не совсем?
Мы хорошо поработали, и наше приложение заработало. Но зачем было столько мучиться? Неужели не проще ли просто подключить все эти библиотеки в html файле и даже не думать кто от кого зависит?
Скажу честно — для нашего небольшого примера проще. Я и сам задумался, а какой выигрыш, дает нам RequireJS, если в конечном итоге были загружены все без исключения библиотеки и все дополнительные файлы (контроллер, модель, представление, объект Application)?
Но давайте представим себе, что у нас, например 20 моделей и скажем 40 представлений (Views). Каждый модуль в отдельном файле. Кроме того, для каждой модели есть достаточно большая (требует длительного времени загрузки) вспомогательная библиотека функций. Если все это мы подключим в html файле мы получим:
- Достаточно огромный список тегов в заголовке документа, что затрудняет контроль зависимостей (как в таком списке проверить, не загружается ли один из плагинов раньше самой библиотеки)
- Поскольку отдельные скрипты могут иметь большое время загрузки, то и суммарное время загрузки приложения будет очень большим (мы загружаем все модели со вспомогательными библиотеками, хотя в процессе выполнения приложения может быть использовано лишь несколько из них)
- Обычно работа дизайнера и программиста разделена, поэтому дизайнеру необходимо заранее знать, какие инструменты будет использовать программист, чтобы включить их в код страницы. При дальнейшей поддержке такого проекта (изменении технологий, рефакторинге) изменения нужно будет проводить как в код скриптов так и в код самой страницы
Только этих трех пунктов уже достаточно, чтобы задуматься над использованием RequireJS. Ведь это:
- Единая точка входа для скриптов (дизайнер не задумывается над тем, что будет использовать программист)
- Асинхронная загрузки скриптов (они подгружаются по мере необходимости, поэтому суммарное время загрузки приложения чрезвычайно мало)
- Упрощается контроль зависимостей между модулями
Надеюсь, что моя небольшая статья станет неплохим стартом в изучении RequireJS. Исходный код примера вы можете скачать здесь.
Автор: kambur