Definer.js — простая модульная система

    Пока JavaScript не обзавёлся настоящими модулями мы продолжаем импровизировать.
    Так появилась на свет ещё одна реализация модулей — definer.

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

    Для хорошего понимания идеи, под катом примеры от простого к сложному.



    Возьмём абстрактную страницу товаров интернет-магазина, index.html:
    <html>
    <head>
        <meta charset="utf-8"/>
        <title>Каталог интернет-магазина</title>
        <script src="jquery.js"></script>
        <script src="https://rawgithub.com/tenorok/definer/master/definer.js"></script>
        <script src="modules/cart.js"></script>
        <script src="modules/list.js"></script>
    </head>
    <body>
        <ul class="list">
            <li class="item">
                <span class="name">Компьютер</span>, <span class="price">250</span>
            </li>
            <li class="item">
                <span class="name">Телевизор</span>, <span class="price">100</span>
            </li>
            <li class="item">
                <span class="name">Холодильник</span>, <span class="price">300</span>
            </li>
        </ul>
    </body>
    </html>
    


    Будем использовать jQuery для работы с DOM и definer для модулей.

    Модуль Cart, реализующий корзину интернет-магазина с возможностью добавить товар и получить суммарную стоимость добавленных товаров:
    definer('Cart', function() {
    
        function Cart() {
            this.list = [];
        }
    
        Cart.prototype = {
            add: function(target) {
                var item = $(target);
                this.list.push({
                    name: item.find('.name').text(),
                    price: +item.find('.price').text()
                });
            },
            sum: function() {
                return this.list.reduce(function(sum, item) {
                    return sum + item.price;
                }, 0);
            }
        };
    
        return Cart;
    
    });
    


    Модуль list, зависящий от Cart и реализующий взаимодействие посетителя с каталогом:
    definer('list', function(Cart) {
    
        var iCart = new Cart();
    
        $(function() {
            $('.item').on('click', function(e) {
                iCart.add(e.currentTarget);
                console.log(iCart.sum());
            });
        });
    
    });
    


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

    В данном примере, у нас осталась глобальная переменная, которую тоже можно вынести в модуль, это jQuery.

    Подключим файл modules/clean.js перед существующими модулями:
    definer.clean('$');
    


    Теперь переменной $ нет в глобальном контексте:
    console.log($); // undefined
    


    Чтобы продолжать использовать jQuery в модулях, добавим зависимость:
    definer('Cart', function($) { ... });
    definer('list', function($, Cart) { ... });
    


    Сборка


    Теперь всё готово и можно собрать исходники в единый файл.

    Устанавливаем сборщик модулей:
    npm install definer
    


    Собираем все модули из директории modules в файл index.js:
    ./node_modules/.bin/definer -d modules/ index.js
    


    Теперь в index.html достаточно подключить только jQuery и собранный файл:
    <script src="jquery.js"></script>
    <script src="index.js"></script>
    


    Сборка с помощью grunt-definer


    Для удобства разработки есть grunt-плагин. Можно установить мониторинг на изменение файлов модулей и автоматически запускать сборку.

    Устанавливаем всё, необходимое для гранта:
    npm install grunt grunt-cli grunt-contrib-watch grunt-definer
    


    Добавим в корень проекта файл Gruntfile.js:
    module.exports = function(grunt) {
    
        grunt.initConfig({
            watch: {
                scripts: {
                    files: ['modules/*.js'],
                    tasks: ['definer:all']
                },
            },
            definer: {
                all: {
                    target: 'index.js',
                    directory: 'modules/'
                }
            }
        });
    
        grunt.loadNpmTasks('grunt-contrib-watch');
        grunt.loadNpmTasks('grunt-definer');
    
    };
    


    И запустим мониторинг:
    ./node_modules/.bin/grunt watch
    


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

    Сборка сторонних файлов


    Сейчас к странице подключается два файла — jQuery и собранные модули. Можно добавить jQuery в сборку и подключать к странице всего один файл.

    Для этого достаточно добавить опцию clean в grunt-цель:
    definer: {
        all: {
            target: 'index.js',
            directory: 'modules/',
            clean: {
                $: 'jquery.js'
            }
        }
    }
    


    Теперь в index.html достаточно подключить только один собранный файл:
    <script src="index.js"></script>
    


    JSDoc


    Возможно формирование JSDoc, содержащего информацию о собранном файле.

    Для этого добавим опцию jsdoc в grunt-цель:
    jsdoc: {
        "file": "Добавление товаров в корзину интернет-магазина",
        "copyright": "2014 Artem Kurbatov, tenorok.ru",
        "license": "MIT license",
        "version": "package.json",
        "date": true
    }
    


    Возможно указание относительного пути до JSON-файла, из которого сборщик получит значение одноимённого поля.

    Положим в корень проекта файл package.json:
    {
        "version": "0.1.0"
    }
    


    Перед модулями в собранном файле появится такой JSDoc:
    /*!
     * @file Добавление товаров в корзину интернет-магазина
     * @copyright 2014 Artem Kurbatov, tenorok.ru
     * @license MIT license
     * @version 0.1.0
     * @date 17 February 2014
     */
    


    Итого



    Definer помогает:
    • разбить всё приложение на модули по зависимостям
    • избавиться от глобальных переменных
    • собирать все скрипты в один файл с помощью grunt-плагина


    Документацию по definer и grunt-definer можно найти на гитхабе.
    Поделиться публикацией

    Похожие публикации

    Комментарии 46
      +2
      А чем не устроили require.js? В чем преимущества вашего решения?
        +3
        Добавлю еще: чем ваше решение лучше browserify?
          0
          смысл использовать его только для сборки?
            0
            browserify — отлиная штука, мне она очень нравится, но для работы собранного кода она добавляет реализацию дополнительных функций вроде define, а я хочу получать чистый код
              0
              Каких функций? Там обертка не намного больше чем у вас получается.
                0
                Возьмём три простых модуля и сравним.

                Definer.

                Модули:
                definer('a', function() { return 'a'; });
                definer('b', function() { return 'b'; });
                definer('c', function(a, b) { return a + b + 'c'; });
                


                Собранный файл (206 символов):
                (function(global, undefined) {
                var a = (function () { return 'a'; }).call(global),
                b = (function () { return 'b'; }).call(global),
                c = (function (a, b) { return a + b + 'c'; }).call(global, a, b);
                })(this);
                


                Browserify.

                Модули:
                module.exports = function() { return 'a'; };
                module.exports = function() { return 'b'; };
                module.exports = function() { return require('./a') + require('./b') + 'c'; }
                


                Собранный файл (828 символов):
                require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
                module.exports = function() { return 'a'; };
                },{}],2:[function(require,module,exports){
                module.exports = function() { return 'b'; };
                },{}],"XGSqeA":[function(require,module,exports){
                module.exports = function() { return require('./a') + require('./b') + 'c'; }
                },{"./a":1,"./b":2}],"./c.js":[function(require,module,exports){
                module.exports=require('XGSqeA');
                },{}]},{},[])
                


                Очевидно, файл, собранный с помощью definer работает быстрее и его проще отлаживать.
                  +1
                  Да вы не заметите разницу на нормальном приложении, а не на синтетическом примере. То о чем вы говорите — микрооптимизация.
                    0
                    Definer предназначается в первую очередь для разработки библиотек и инструментов для разработчиков.
                    Когда вы предоставляете другому разработчику библиотеку, важно, чтобы там не было ничего лишнего, а вырезать куски кода регулярными выражениями, как это делает jQuery мне кажется дикостью.
                      0
                      > Когда вы предоставляете другому разработчику библиотеку, важно, чтобы там не было ничего лишнего

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

                      Ради примера — посмотрите на библиотеки на npm, например на React, он поставляется в ввиде набора CommonJS модулей, которые потом можно собрать в вместе с приложением с помощью browserify, webpack или чем-нибудь другим.
                        0
                        Думаю, что подавляющее большинство разработчиков не собирает библиотеки по частям, а просто скачивают собранный файл в один клик.

                        Но если хочется кастомно собрать проект из необходимых модулей, то с помощью definer можно сделать и это. А какую команду для сборки вы будете использовать, уже не так важно, хоть grunt, хоть make, или npm run-script и так далее.
                          0
                          Думаю подавляющее большинство разработчиков не будет беспокоится из-за лишних 500 байт. Но если уж кто-то и будет, то точно пойдет по пути использования модульных библиотек, где модно сэкономить гораздо больше.

                          Здесь дело не в том, grunt, make или npm run-script, а в том, какая система модулей используется (синтаксис и семантика).
                            0
                            Какая вам разница, какая система модулей используется в проекте, если вы можете собрать необходимый модуль командой make modulename?

                            Спросите у разработчиков jQuery, зачем они вырезают из собранного файла всё про RequireJS.
                              0
                              То есть для каждого модуля библиотеки мне надо будет делать make modulename? Спасибо, я лучше browserify/webmake попользуюсь.
                                0
                                Это же абстрактный пример. К тому же использование browserify сводится к тому же самому.
                                Сборка указанного модуля приведёт к получению файла, содержащего все модули, необходимые для его работы. Если надо собрать несколько модулей, то можно make module1 module2 ... moduleN.
                                  0
                                  C browserify мне надо сделать это в одном месте и место это — код, где будут эти модули использоваться. Зачем делать какие-то make module1 module2 ... при этом мне непонятно.
                                0
                                jQuery кстати есть на npm, жаль что там не модуляризованный билд… Но вот например lodash — var assign = require('lodash/compat/objects/assign') если нужна всего одна функция.
                                0
                                После gzip, разница всего в 200 байт, кстати. Но это ещё не все — у webpack overhead ещё меньше.
                                  0
                                  Смысл не в байтах, а в читаемости финального кода и максимальной приближенности к оригиналу.
                                    +1
                                    > Смысл не в байтах, а в читаемости финального кода и максимальной приближенности к оригиналу.

                                    С source maps (которые кстати генерирует browserify) вы это все получаете в полном объеме — при ошибке будут видны строки оригинального файла и при клике в devtools вам покажутся оригинальные исходники.

                                    Видимо вы не пробовали пользоваться browserify/webmake/…
                                      0
                                      У разработчика, использующего скачанную библиотеку нет оригинальных исходников.
                                        0
                                        source map обычно идет вместе с собранной и минифицированной версией библиотеки. И нормальный разработчик вообще ничего скачивать не будет руками, а возьмет bower.

                                        Суть в чем, у вас своя система модулей, несмотря на то что существуют популярные форматы вроде common.js. require.js и т.д. Причем я бы даже модулями это не назвал, есть для grunt кучи плагинов которые в нужное место подставляют запрошенный код, оверхэда вообще нету, и результат будет приблизительно таким же.
                                          +1
                                          Ещё раз, если вы используете browserify/webpack/…

                                          1. вы не скачиваете билд библиотеки, вы устанавливаете пакет из npm/bower/github/....
                                          2. в пакете — оригинальные исходники ввиде отдельных модулей
                                          3. вы собираете свое приложение, которое использует библиотеки (сборщик узнает об этом анализирую require() вызовы)
                                          4. на выходе имеет билд своего приложение + source map (внутри сам source map + оригинальные исходники, можно настроить это отдельно, чтобы исходники подгружались по необходимости)
                                            0
                                            Да, сейчас я понял чего вы хотите.
                                            Для этого действительно лучше использовать какую-то реализацию CommonJS.
                                            Definer задуман для другого применения.
                                              0
                                              NIH? :p
                                                0
                                                На самом деле забегайте в #browserify на irc.freenode.net — обсудим ваши сценарии использования и, возможно (даже вероятно), browserify вам подойдет.
                    0
                    require.js не нравится тем, что после сборки требуются дополнительные функции для работы кода, а вырезать их, как это делает jQuery — не хочется: github.com/jquery/jquery/blob/master/build/tasks/build.js
                      +1
                      Есть еще LMD, для сборки оно приятнее r.js
                        0
                        Да, я видел Мишину систему, но мне она показалась сложной и я решил ради интереса сделать совсем примитивную.
                    +3
                    Кажется, определение зависимостей не переживет сжатие кода.
                      0
                      очень похоже на то
                        0
                        В Angular же справляются как-то с этой проблемой:)
                          +1
                          Ну там у injectable должен быть прописан список того, что нужно инджектить. Он покоится в свойстве .$inject вроде как, как массив названий сервисов. При объявлении injectable можно либо самому указать, либо прописать через array notation (типа обернуть в массив, где определение нашего сервиса будет последним элементом, а до этого названия сервисов) либо ангулар сам возьмет из названий аргументов.
                          0
                          Переживёт, ведь собранный файл представляет из себя чистый код вроде такого:

                          (function(global, undefined) {
                          var a = (function () { return 'a'; }).call(global),
                              b = (function () { return 'b'; }).call(global),
                              c = (function (a, b) { return a + b + 'c'; }).call(global, a, b);
                          })(this);
                          
                          0
                          Самая простая модульная система

                          Извините, а чем она проще в использовании чем requirejs?
                            0
                            Насколько я понял, это больше замена lumbar.
                              0
                              Самое простое — это то, чего нет. В результате сборки модульная система отсутствует.
                                0
                                А вы не думали о том, чтобы просто написать новый сборщик для AMD проектов? Мне кажется, это было бы куда полезнее, так как тогда была бы возможность уже в готовых проектах использовать ваш сборщик.
                                  0
                                  Об этом я как-то не подумал.
                                  Хорошая идея, но при этом надо обдумывать детали.
                              0
                              Github-проект на русском — это очень печально.
                                0
                                Согласен, но мой английский пока не позволяет нормально писать.
                                Если пришлёте пулреквест с документацией на английском — буду сильно благодарен :)
                                  +1
                                  У сабжа есть ряд недостатков, из-за которых я не стану им пользоваться и, следовательно, участвовать.

                                  1) > Модули должны быть объявлены в правильной последовательности, иначе возникнет ошибка.
                                  То есть сабж не помогает разрешать зависимости.

                                  2) Сабж не автоматизирует подключение модулей в качестве отдельных <script>'ов в девелопменте и единственного минифицированного файла в продакшене.

                                  Честно говоря, непонятно, зачем он вообще нужен.
                                    0
                                    1) Эта ошибка описана для ясности, в жизни же не предполагается такое использование системы, а зависимости нормально разрешаются при сборке.

                                    2) В девелопменте с помощью грант-плагина можно автоматизировать сборку и добавить минификацию с помощью grunt-contrib-uglify, например.

                                    Мне definer нужен в первую очередь для того, чтобы писать инструменты с разбивкой по модулям и автоматически собирать единый файл без модулей, который легко сможет использовать кто-то другой.
                                      0
                                      Я использую Sprockets. Он работает так:

                                      Я перечисляю для Sprockets имена неминифированных файлов модулей. При этом разрешение зависимостей лежит на моих плечах: я должен позаботиться, чтобы зависимости были объявлены раньше зависимых модулей.

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

                                      Никаких сборок типа `grunt build` я при этом не запускаю.

                                      Definer мне может как-то помочь?
                                        0
                                        Нет, definer вам не подойдёт.
                                        Он всегда конкатенирует модули в один файл и не работает с HTML.
                                0
                                По моему модули, в итоге все же присутствуют. Отсутствуют файлы модулей, но сами модули, как объекты, обернутые анонимной функцией, присутствуют в конечном файле.
                                  0
                                  Да, в результирующем файле собирается содержимое модулей в нужном порядке, но отсутствует вызов методов definer и их реализация.
                                  Получается, что модулей в виде модулей нет и можно поставлять приложение из чистого кода.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое