вторник, 9 апреля 2013 г.

AMD и RequireJS

Модульный подход к разработке web-приложений с использованием JavaScript: AMD и RequireJS

http://habrahabr.ru/post/152833/
http://sly-and-fluffy.blogspot.ru/2013/02/amd-requirejs-javascript.html

При разработке приложений с модульной структурой на JavaScript возникает две проблемы:
  • описание и удовлетворение зависимостей различных частей приложения, необходимость организации подключения зависимостей на серверной стороне;
  • экспорт переменных в глобальную область видимости и их коллизия.

Обе эти задачи решаются при использовании подхода Asynchronous Module Definition. Он сводится к описанию модулей функцией define и подключению их с помощью require. На данный момент есть несколько инструментов, реализующих AMD. Я начал своё знакомство с ними с RequireJS и был удивлён, насколько удобно и просто можно описывать зависимости модулей. Расскажу, как это работает, на простом примере.

Подключение загрузчика

Имеем следующую структуру каталогов:
siteroot/
  js/
    app.js
    require.js
    jquery.js
    mymodule.js
  index.html

Для начала, подключим в index.html загрузчик. Будем использовать RequireJS:
<script data-main="/js/app" src="/js/require.js"></script>

Отлично, это единственный тег script, который нам нужен. Остальную работу по подключению JS сделает загрузчик. Указанный в data-атрибуте файл (расширение .js для краткости в RequireJS всегда опускается) будет своеобразной точкой входа нашего приложения. В нём мы сможем подключить необходимые модули с помощью require и совершить задуманные действия.

Описание модуля

Опишем наш модуль в /js/module.js с помощью define:
define(
    'mymodule',
    ['jquery'],
    function( $ ){
        return {
            foo : 'bar'
        };
    }
);

Первый аргумент — строка, название модуля, не обязателен. Вторым аргументом передаются зависимости в виде массива строк, также опционально. Третий аргумент — функция-фабрика, которая выполняется только после удовлетворения всех зависимостей (загрузки перечисленных файлов). В неё в качестве аргументов передаются экспортируемые зависимостями переменные. А возвращать она должна сам модуль. В данном случае это объект с одним полем.

Использование

В /js/app.js подключим нужные модули с помощью JS и выполним свой код:
require(
    ['mymodule', 'jquery'],
    function( Module, $ ){
        $('body').append( Module.foo );
    }
);

Module при этом не будет доступна в глобальной области видимости, как и другие переменные, экспортируемые библиотеками из зависимостей. Не смотря на то, что библиотека jQuery с версии 1.7 поддерживает AMD-архитектуру, она является исключением: экспортирует свой доллар в глобальную область видимости. Скорее всего, это сделано для сохранения совместимости с армией плагинов, написанных за многие годы.

Конфигурация

RequireJS обладает рядом параметров, которые можно передавать перед использованием. Для этого служит объект require.config.

Что делать, если вам необходимо подключить модуль, которая не оформлен в виде AMD и экспортирует переменную в глобальную область видимости? Можно, конечно, модифицировать его исходный код, но это плохая практика. Для описания таких модулей служит параметр shim. Можно вручную указать его зависимости и экспортируемую переменную, и он станет частью нашего приложения наравне с другими AMD-парнями:
require.config = {
    shim: {
        'oldmodule' : {
            deps: [],
            exports: 'OldModule'
        }
    }
};

Теперь можно указывать его в качестве зависимости:
require(
    ['mymodule', 'jquery', 'oldmodule'],
    function(){}
);

Помимо shim есть ещё много параметров: корневая директория подключения файлов baseUrl, псевдонимы для более удобного подключения paths, и т.д.

Заключение

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

На прощание, приведу несколько ссылок, которые помогут продолжить изучение вопроса:

Исходный код из статьи доступен в репозитории на GitHub.
Happy hacking!

Философия AMD, RequireJs и модульная разработка веб-приложений на JavaScript

 
Разделяй и влавствуй - подход на все времена. Концепция модульного программирования не нова, и хорошо себя зарекомендовала. В мире разработки web-приложений на JavaScript существует подход Asynchronous Module Definition или AMD, который
specifies a mechanism for defining modules such that the module and its dependencies can be asynchronously loaded [1]
т.е. по сути определяет API, следуя которому можно реализовать асинхронную загрузку самих модулей и их зависимостей. Следуя этому подходу, непроизвольно создаешь более элегантные решения, но это всего лишь бонусный побочный эффект. Главное - уменьшается хаос js-кода, сложность разработки и поддержки приложения. Каждый модуль явно указывает свои зависимости и получает только их, работая изолированно, что означает минимум мусора глобальной области видимости.
Всё, что нужно, чтобы начать использовать AMD подход в проекте, - это загрузчик, который знает, что такое AMD-модуль, и умеет подгружать зависимости.  Итак,

Require.js


Ключевым объектом в Require.js, как и в AMD-философии в целом, является модуль. Опеределяется модуль с помощью функции define
define("<module_name>",
        ["dependency1", "dependency2],
        function(dependency1, dependency2) {            
       }
    );
Метод, который идёт последний аргументом в списке,  - это фабрика, которая создает и возвращает сам модуль. Фабрика выполняется только после загрузки всех зависимостей, указанных во втором аргументе. Модули, указанные как зависимости, ищутся в той же папке, где находится и сам Require.Js (подробнее на теме загрузки модулей я останавлюсь ниже).  Ппервый аргумент - это имя модуля. Всё вроде просто. Значит, надо создать свой модуль в AMD-стиле! Выходя за рамки учебных простых примеров, создадим что-то более близкое к задачам из реальной жизни. Например, мне нужен объект, который умеет строить календарь на указанный месяц и год и отмечать в нём даты.
//zeitproject.calendar.js
define(
    'calendar',
    ['jquery'],
    function( $ ){      
        function Calendar()
        {
            var self = this;
            self.builCalendar = function(month, year)
            {
               var body = "";
               //неинтересные детали
               return body;
            }

            self.markDays = function(days_array, css_class, calendar)
            {               
               //неинтересные детали
            }           
        };
        return new Calendar();
    }
);

Из зависимостей - только jquery (ну куда без него!). Знак $ в параметрах функции-фабрики - конечно же, ссылка на jquery. Фабрика возвращает экземпляр класса Calendar, объявленного в внутри самой фабрики, что ограничивает область видимости класса. А куда и кому она возвращает объект? Туда, откуда кем-то была вызвана. А кем и когда она была вызвана? А вызвана она могла быть, когда модуль попал в чей-то список зависимостей, и Require.Js пытается подгрузить модуль. Продолжая пример, Calendar.js является одной из зависимостей в app.js:
//app.js
require(["jquery", "calendar", "tooltip"], function($, calendar, tooltip) {  
    //init vars
    //...//
              
    calendar.builCalendar(month, year, $('.calendar'));
    calendar.markDays(days[i][0], days[i][1], $('.calendar'));
    tooltip.init();
});

app.js - это точка входа в наше приложение. В нём конфигурируется Require.Js и инициализируется логика приложения. Это тот единственнй javascript-файл, который подгружается через Require.Js в index.html. Путь к нему указывается в атрибуте data-main:
<script data-main="/js/app" src="/js/require-jquery.js"></script>
Для краткости расширение .js можно не указывать.
Конечно, тут есть маленькая хитрость - в примере подгружается Require.Js сразу в связке с jquery, поэтому не нужно отдельно подгружать jquery.

Ручная настройка

Require.Js - гибкая легконастраиваемая штука. Например, нужно настроить загрузку AMD-модулей для такой структуры каталогов:
Для этого в app.js перед вызовом require() нужно сконфигурировать Require.Js.
require.config({
    baseUrl: "/js",
    paths: {

        "calendar" : "zeitproject.calendar",
        "jquery"   : "require-jquery",

        "formatter": "/utils/formatter",
        "tooltip"  : "/controls/zeitproject.tooltip",
        "tiptip"   : "/controls/jquery.tiptip.min"
    }

  });
Всё довольно интуитивно: baseUrl - корневая папка, относительно которой указываются все последующие пути. Дальше указывается имя AMD-модуля (имя мы указывали в методе define() первым аргументом во время определения модуля). А что, если нужно подгрузить не AMD-модуль, который добавляет в глобальное пространство имён переменные? По сути, таким модулем может быть любой jquery-плагин. Или, даже, фреймворк: тот же Backbone.js является AMD-несовместимым. Не переписывать же его под AMD-сигнатуру?! Для этого в конфиге есть специальный раздел shim (наверно, имелось в виду "shame, не AMD-модуль!"):
require.config({
    baseUrl: "/js",
    paths: {
/*...*/
        "tiptip"  : "
/controls/jquery.tiptip.min",
        "tooltip" : "
/controls/zeitproject.tooltip"
    },
    shim: {
        'tiptip' : {
            deps: ['jquery'],
            exports: 'tiptip'
        }
    }
  });

В shim-секции определяется, какие зависимости есть у не AMD-модуля и псевдоним, под которым его можно указывать в списке зависимостей AMD-модулей.
//zeitproject.tooltip.js
define("tooltip", ["jquery", "tiptip"], function($){
    function Tooltip()
    {

/*..*/
}); 
Вот и всё. Модули изолированы друг от друга, и знают лишь о существовании тех, без которых не могут работать. Это позволяет держать под контролем зависимости и облегчает жизнь. Надеюсь, вы прониклись Require.js и AMD-подходом к разработке веб-приложений.

Ссылки

http://requirejs.org официальный сайт require.js, отличная документация и примеры
AMD - описание API

Комментариев нет:

Отправить комментарий