Организация зависимостей RequireJS

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

Упорядочение зависимостей

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

define(['jQuery','Underscore','Backbone'], function($, _, Backbone){
    ...
}

В примере выше jQuery, Underscope и Backbone определены как зависимости модуля в виде массива названий зависимостей первым аргументом в функции define(). Как только все зависимости загрузятся, вызовется определяющая модуль функция. В качестве аргументов ей будут переданы зависимости в том же порядке, в котором были переданы их названия в первом аргументе. Так работает RequireJS.

С точек зрения дизайна и дальнейшей реализации и поддержки проекта, вполне очевидно, что эта взаимосвязь «один-в-один» между порядком зависимостей и порядком аргументов определяющей модуль функции приведёт к большой путанице и станет потенциальным источником ошибок. На модулях с небольшим количеством зависимостей этой проблемы нет. Вернее, её просто пока не видно. Путаница и неразбериха появляются при увеличении зависимостей модуля, когда его функционал расширяется и он становится громоздким.

Добавление зависимостей

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

define(['jQuery','Underscore','Backbone','SomeModel','SomeCollection'], 
    function($, _, Backbone, SomeModel, SomeCollection){
        ...
    }
);

Теперь, если мы вдруг решили добавить в этот модуль ещё зависимость, скажем, Handlebars, мы могли бы просто добавить новую зависимость в конец списка зависимостей, а также в конец аргументов фабрики модуля:

define(['jQuery','Underscore','Backbone','SomeModel','SomeCollection','Handlebars'], 
    function($, _, Backbone, SomeModel, SomeCollection, Handlebars){
        ...
    }
);

Конечно, так сделать можно, и это будет работать. Но думать о своей карме web-разработчика нужно смолоду!) Такой способ добавления зависимостей ухудшает читаемость кода, т.к. Handlebars отнесли к группе зависимостей, специфичных для приложения, таких как сервисы, модели, а не к модульным зависимостям фрэймворка, как это должно было бы быть. Это может показаться незначительной мелочью, однако, учитывая, что код обычно читается гораздо чаще, чем меняется, имеет смысл организовать зависимости так, чтобы нам было удобнее смотреть список зависимостей в будущем.

Учитывая этот факт, мы можем сделать следующее:

define(['jQuery','Underscore','Backbone','Handlebars','SomeModel','SomeCollection'], 
    function($, _, Backbone, Handlebars, SomeModel, SomeCollection){
        ...
    }
);

Организация зависимостей

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

К нашей всеобщей радости, RequireJS подслащает нашу жизнь front-end разработчиков синтаксическим сахаром в виде реализации упрощённой обёртки над CommonJS(так называемый формат Simplified CommonJS, который для RequireJS родным не является, но разработчики были вынуждены его сделать), которая может быть использована для решения проблем, о которых мы дискутировали выше. Этот синтаксис, который разработчикам NodeJS кажется абсолютно привычным, позволяет нам упростить определение фабрики модуля до простого вызова define, и указанием require в качестве единственного аргумента. Вот пример такого упрощения:

define(function(require){
    ...
});

Даже самый заядлый любитель поспорить тут должен согласиться, что объявление модуля стало гораздо проще. А получать зависимости с таким паттерном мы можем вот так:

define(function(require) {
    var $ = require('jQuery'), 
        _ = require('Underscore'),  
        Backbone = require('Backbone'),  
        Handlebars = require('Handlebars'),  
        SomeModel = require('SomeModel'),  
        SomeCollection = require('SomeCollection');
     ...
});

С таким подходом получения зависимостей гораздо проще добавлять и удалять зависимости по мере необходимости. Вдобавок к этому, чтение зависимостей стало гораздо прозрачнее. Также этот подход выглядит знакомым за счёт схожести с директивами импорта в других языках.

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

var require = reqJS;
reqJS("myModule");
require("superModule//");
require("super module");
require("super" + "Module");

Заключение

Управление зависимостями модулей в RequireJS выглядит достаточно просто, а, если воспользоваться силой синтаксического сахара, который мы рассмотрели выше, становится ещё проще! Когда пользуешься этим подходом, важно держать в уме, что этот синтаксис основан на методе Function.prototype.toString(), у которого на сегодняшний день хорошая совместимость с большинством современных браузеров, но может себя непредсказуемо вести в некоторых браузерах постарше. Однако, документация утверждает, что использование RequireJS оптимизатора для нормализации зависимостей поможет обеспечить полную кроссбраузерность.

Я себе завёл за правило использовать такой способ получения зависимостей, когда у модуля появляется более 4-5 зависимостей. Для меня это удобно. Попробуйте и вы! Не забывайте оставлять комментарии, задавать вопросы и делиться ссылкой с друзьями и коллегами! :)

Увидимся в следующей статье!

  • Александр

    Только начал работать с RequireJS. Это реально удобно. Но только мне не понятно, что делать дальше с подобной конструкцией: define(function(require) {
    var $ = require(‘jQuery’),
    bowser = require(‘Bowser’),
    modernizr = require(‘Modernizr’)
    });
    А именно, мне нужно далее выполнить проверку версии браузеров, например. Как далее использовать переменную var bowser, допустим, или modernizr? Ступор какой-то…

    • Александр, а ты уверен, что тебе с 3 зависимостями нужен выше описанный способ? Для малого количества зависимостей (если ясно, что сильно оно не вырастет), компактнее использовать стандартный метод, на мой взгляд.

      После того, как зависимости внедрены, их можно использовать точно так же, как и без requirejs — как если бы эти объекты находились в глобальной области видимости.

      Какие ошибки в консоли появляются? В requirejs.config пути до модулей jQuery, Bowser и Modernizr?