четверг, 5 декабря 2013 г.

Модули в JavaScript



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

    • Они грузились в правильном порядке и подключалось только то, что нужно.
    • Не конфликтовали между собой.
    • На продакшене всё грузились одним-двумя файлами, а не сотней.
    • Не было проблем с отладкой во время разработки.

 Давайте рассмотрим наиболее яркие решения этой проблемы...


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

#   js/common.php
    <?php return array(
        'libs/jquery.js',
        'libs/jquery-cookie.js',
    );

#   js/main.php
    <?php return array(
        include( 'common.php' ),
        'app/main.js',
    );

    При разработке каждый такой скрипт подключается отдельным тегом <script/>, а при выкладке на продакшен, исходники сливаются в один файл.

+   Плюсы
    + Предельно простая реализация.
    + Мы точно знаем что и в каком порядке грузится.
    + Нет никаких ограничений на содержимое подключаемых файлов.
    + Может работать с различными типами ресурсов (js, css, шаблоны).

−   Минусы
    − Сложно поддерживать эти списки в актуальном состоянии, что приводит к загрузке лишнего кода или наоборот недозагрузке необходимого, но редко используемого.
    − Вынуждает регистрировать каждый файл, а при переносе или переименовывании файла искать его упоминания и изменять их. Это рутина, которую разработчики стараются избегать, что приводит к излишней группировке большого числа модулей в небольшом числе файлов, а также бардаку с их расположением.
    − Нельзя просто скопировать пачку файлов из другого проекта – нужно поподключать их в  нужные места.
    − Требуется серверный сборщик на нужном языке.   

!!! CommonJS


    Подход [CommonJS\http://www.commonjs.org/specs/] предполагает объявление зависимостей в самих модулях и отсутствие захламления глобальной области видимости. Каждый модуль исполняется в своей песочнице, где есть несколько предопределённых модулей: exports, require, module.

#   js/libs/jquery.cjs.js
    // jQuery content
    exports.jQuery= jQuery

#   js/libs/jquery-cookie.cjs.js
    var $= require( '../libs/jquery.cjs' ).jQuery
    // jQuery-cookie content

#   js/modules/user.cjs.js
    var $= require( '../libs/jquery.cjs' ).jQuery
    exports.User= function( ){
        // User content
    }

#   js/app/main.cjs.js
    var $= require( '../libs/jquery.cjs' ).jQuery
    var User= require( '../modules/user.cjs' ).User
    require( '../libs/jquery-cookie.cjs' )
    alert( new User( $.cookie.get( 'session-id' ) ) )

    Как видно, если подключается библиотека, не рассчитанная на подключение через CommonJS API, то её код приходится модифицировать. Заметьте, тут jQuery-cookie ничего не экспортирует, а патчит jQuery (от которого зависит), поэтому в приложении мы вызываем require, чтобы патчинг состоялся.

+   Плюсы
    + Не засоряется глобальная область видимости.
    + Зависимости всегда грузятся до того, как они понадобятся.

−   Минусы
    − Прежде чем воспользоваться каким-либо модулем, его нужно сначала импортировать с помощью require. А когда он больше не используется – нужно не забыть и удалить соответствующий require. Это рутина и потенциальное место ошибок загрузки (лишней или недостающей).
    − Чтобы воспользоваться модулями в консоли, их опять же нужно проимпортировать, что нивелирует основное преимущество консоли – быстрая отладка без лишних телодвижений.
    − Необходимо вносить изменения в код библиотек, не совместимых с CommonJS.
    − Скрипты должны исполняться в отдельных «песочницах», что усложняет отладку и даёт пенальти в производительности.
    − Статический анализ кода для поиска зависимостей либо очень сложный, либо не надёжный.
    − Не умеет подключать необходимые для скриптов стили и шаблоны.
    − Много инфраструктурного кода (подключение моделей, задание их имён).

±   Спорные моменты
    ± В разных скриптах один и тот же модуль может называться по разному. Это приводит к путанице при разработке (тут одно имя, там другое и боже упаси перепутать) и усложнению рефакторинга (перенос кода между скриптами, переименовывание модуля). А если ввести соглашение «везде называть одинаково», то это будет ничем не лучше глобальных переменных (разумеется с использованием пространств имён, как например $.cookie), а только хуже (из-за рутины с импортами).
    ± Удобно динамически синхронно подгружать скрипты. Но это приводит к тормозам: при разработке из-за большого числа модулей (загрузка которых идёт последовательно), а на продакшене из-за долгой загрузки по сети собранного пакета.

!!! AMD


    [Asynchronous Module Definition\https://github.com/amdjs/amdjs-api/wiki/AMD] не требует изоляции модулей, но тем не менее поощряет её. Определение модуля заключается в вызове функции define или require с передачей им опционального списка зависимостей и коллбэк-функции, которая будет вызвана когда зависимости будут подгружены. В качестве зависимостей могут быть любые скрипты, в том числе даже и jsonp.

#   js/libs/jquery.js
    // jQuery content

#   js/libs/jquery-cookie.js
    // jQuery-cookie content

#   js/modules/user.amd.js
    define( [ "../libs/jquery" ], function( ){
        // User content
    })

#   js/app/main.amd.js
    require([ "../modules/user.amd", "../libs/jquery", "../libs/jquery-cookie" ],
    function( User                                                         ){
        alert( new User( $.cookie.get( 'session-id' ) ) )
    })

+   Плюсы
    + Благодаря асинхронной архитектуре зависимости грузятся параллельно.
    + Модуль инициализируется строго после загрузки зависимостей.

−   Минусы
    − Опять же, необходимость вручную следить за всеми зависимостями, со всеми вытекающими отсюда последствиями.
    − Опять же, неудобства с консолью.
    − Опять же, статический анализ зависимостей сложен или ненадёжен. Разве что используя тяжёлую артиллерию – один из серверных JS движков.
    − Опять же, работает исключительно с JS и знать не знает про сопутствующие файлы.
    − Очень громоздкое и вариативное определение модуля. Особенно это чувствуется, когда есть много зависимостей (параметры коллбэка сопоставляются с зависимостями по порядковому номеру).

±   Спорные моменты
    ± Опять же, возможность локального переименовывания модуля – фича скорее вредная, чем полезная.


!!! JAM


    Javascript Autoloadable Modules – конвенция в рамках архитектуры [PMS\https://github.com/nin-jin/hyoo.ru/] (Package/Module/Source), идея которой заключается в том, чтобы организовывать исходники следующим образом:
    • Все файлы (не только скрипты, но и сопутствующие им стили, картинки, шаблоны и прочее) лежат на одном уровне иерархии и объединены в модули.
    • Имена файлов произвольные. Файлов одного типа может быть произвольное количество.
    • Модули объединены в пакеты. На продакшене, все модули грузятся объединёнными попакетно. При разработке все файлы подключаются по отдельности.
    • Зависимости между модулями определяются статическим анализом содержащихся в них исходников, основываясь на расширении файлов.
    • Все файлы подключаемого модуля входят в сборку пакета, даже если сам модуль из другого пакета.

    Конкретно JAM соглашение заключается в следующем:
    • Все модули регистрируются глобально через добавление поля в this. При этом имя задаётся вида «$foo_bar».
    • Если в jam-файле встречается комбинация вида «$foo_bar», то считается, что его модуль зависит от модуля «bar» в пакете «foo».
    • JAM-файлы должны иметь расширение «jam.js», чтобы не путать их с обычными JS-файлами, зависимости внутри которых не анализируются.
    • Все модули пакета зависят от главного модуля пакета, который имеет то же имя, что и сам пакет.

    Правила достаточно тривиальны. Проще понять их на простом примере:
   
#   jq/jq/jq.js
    // jQuery content

#   jq/jq/jq.jam.js
    this.$jq= jQuery

#   jq/cookie/jq_cookie.js
    // jQuery-cookie content

#   jq/cookie/jq_cookie.jam.js
    this.$jq_cookie= jQuery.cookie

#   myApp/myApp/myApp.jam.js
    this.$myApp= {}

#   myApp/User/myApp_User.jam.js
    $myApp.User= function(){
        // User content
    }

#   myApp/startup/myApp_startup.jam.js
    alert( new $myApp.User( $jq_cookie.get( 'session-id' ) ) )

    Дополнительные зависимости, которые автоматика (пока;) не в состоянии распознать, можно указывать в отдельном файле в формате, meta-tree:

#   jq/cookie-plus/jquery_cookie-plus.js
    // jQuery-cookie-plus content

#   jq/cookie-plus/jquery_cookie-plus.meta.tree
    include module =jq/cookie

+   Плюсы
    + Нет никаких ограничений на содержимое подключаемых js-файлов (только на jam).
    + Зависимости в основном определяются автоматически. Дополнительно вручную задаются в отдельном файле для модуля целиком, а не для каждого файла по отдельности.
    + Отсутствие какой-либо рантайм библиотеки (все зависимости подключаются статически).
    + При разработке все файлы подключаются отдельно и грузятся параллельно – это быстро и удобно в отладке.
    + В ссылки на файлы добавлется метка времени модификации, что избавляет от необходимости чистить кэш браузера и от лишних http-запросов.
    + Системное решение. Вместе со скриптами подгружаются и стили, и шаблоны. А вместе с шаблонами, необходимые скрипты и стили (в том числе скрипты, стили и подшаблоны из других модулей).
    + Единое именование модулей везде и отсутствие необходимости каждый файл где-либо прописывать позволяет легко рефакторить код и в том числе переносить между проектами.

−   Минусы
    − Теоретически возможно неправильное распознание зависимости в JAM-файлах (например, в закоментированном коде или строковой константе), но на практике, зная правила их поиска, допустить эти ошибки сложно.
    − В JAM нельзя напрямую использовать сторонний код, апи которого использует знак доллара. В примере выше, эта проблема обходится созданием алиаса, удовлетворяющим принципам JAM. Ну либо отказом от JAM в пользу «meta.tree».

±   Спорные моменты
    ± Требуется серверный сборщик. Впрочем, запускается он как утилита, которая формирует все необходимые файлы в модуле «-mix», что позволяет легко интегрировать его в любую систему.
    ± Все модули доступны глобально, что потенциально конфликтноопасно. Однако, для встраивания на сторонние сайты, собирается специальная изолированная версия – «library.js».

!!! Попробуйте PMS


    Чтобы было легко поиграться, я написал простенький тестовый [пример\http://nin-jin.github.com/pms-demo/] ([исходники\https://github.com/nin-jin/PMS-demo]). Скрипт «index.php» сначала запускает компиляцию пакета «demo», а потом выдаёт браузеру две статические хтмл-ки, демонстрирующие подключение ресурсов при разработке и на продакшене.

Источник -
http://hyoo.ru/?article=%D0%9C%D0%BE%D0%B4%D1%83%D0%BB%D0%B8+%D0%B2+JavaScript;author=Nin+Jin

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

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