JavaScript был разработан как язык сценариев, который легко встраивать в большую хост-систему и предназначен для манипулирования объектами хост-системы. С развитием HTML5 ранее статичные веб-страницы превращались в сложные веб-приложения. Теперь мы ожидаем, что код JavaScript будет масштабируемым и модульным. Но как, если в JavaScript нет встроенных средств для объединения различных сценариев?
Конечно, вы можете вставить элемент сценария в DOM и, следовательно, динамически загружать внешние сценарии. Затем вам придется иметь дело с асинхронно загруженными сценариями и разрешить все необходимые зависимости перед запуском любого зависимого кода. На самом деле существует множество библиотек, которые могут сделать это за вас (http://wiki.commonjs.org/wiki/Implementations). Переходя к AMD, вы быстро поймете, что каждый модуль делает отдельный HTTP-запрос, который плохо влияет на производительность приложения. Ну, вы можете использовать такие инструменты, как r.jsобъединить модули в одну сборку. Тем не менее он приносит целую библиотеку в полученный код. Скомпилированные модули нам вряд ли понадобятся так много дополнительной сложности. После того, как компилятор заключает модули в уникальные области, нам нужна только функция для управления доступом к этим контекстам. Давайте возьмем формат модуля CommonJS. Мне очень нравится, как это реализовано в NodeJS — довольно лаконично, но невероятно эффективно. Он представляет объект модуля, представляющий модуль и доступный в каждой области видимости модуля. Таким образом, область видимости модуля может выглядеть так:
require.def( "module-id", function( require, exports, module ){ // Module constructor var moduleObj = {}; module.exports = moduleObj; return module; });
Нам нужна функция require, чтобы предоставить доступ от модуля к модулю. После NodeJS, когда require вызывается при первом построении модуля. Каждый второй вызов извлекает экспортированный объект из кэша. Это может быть реализовано с помощью следующего кода:
/** Define scope for `require` */ var require = (function(){ var /** * Store modules (types assigned to module.exports) * @type {module[]} */ imports = [], /** * Store the code that constructs a module (and assigns to exports) * @type {*[]} */ factories = [], /** * @type {module} */ module = {}, /** * Implement CommonJS `require` * @param {string} filename * @returns {*} */ _require = function( filename ) { if ( typeof imports[ filename ] !== "undefined" ) { return imports[ filename ].exports; } module = { id: filename, filename: filename, parent: module, children: [], exports: {}, loaded: false }; // Called first time, so let's run code constructing (exporting) the module imports[ filename ] = factories[ filename ]( require, module.exports, module ); imports[ filename ].loaded = true; if ( imports[ filename ].parent.children ) { imports[ filename ].parent.children.push( imports[ filename ] ); } return imports[ filename ].exports; }; /** * Register module * @param {string} filename * @param {function(module, *)} moduleFactory */ _require.def = function( filename, moduleFactory ) { factories[ filename ] = moduleFactory; }; return _require; }());
Этот дизайн предполагает, что компилятор заменит идентификаторы, указанные в вызовах require, на полностью разрешенные имена файлов. Таким образом, имена файлов становятся уникальными идентификаторами для модулей. Кроме того, компилятор должен обнаружить комбинации требуемых вызовов, вызывающие бесконечные циклы.
Использование RequireJS Compiler
Я выпустил компилятор на GitHub https://github.com/dsheiko/cjsc. Это пакет NodeJS, который можно использовать так же просто, как и он:
./cjsc main-module.js build.js
Давайте напишем несколько модулей, чтобы увидеть, что он делает.
./main.js
onsole.log( "main.js running..." ); console.log( "Imported name in main.js is `%s`", require( "./lib/dep1" ).name ); console.log( "Getting imported object from the cache:" ); console.log( " imported name in main.js is still `%s`", require( "./lib/dep1" ).name );
./lib/dep1.js
console.log( "dep1.js running..." ); console.log( "Imported name in dep1.js is `%s`", require( "./dep2" ).name ); module.exports.name = "dep1";
./lib/dep2.js
console.log( "dep2.js running..." ); module.exports.name = "dep2";
После того, как мы скомпилируем модуль main.js и запустим производную сборку в браузере, мы получим следующий вывод:
main.js running... dep1.js running... dep2.js running... Imported name in dep1.js is `dep2` Imported name in main.js is `dep1` Getting imported object from the cache: imported name in main.js is still `dep1`
Что ж, зависимости, разрешенные данными идентификаторами, основанные на относительных путях, конструкторы модулей запускались, когда требовалось и только один раз — все прошло как в NodeJS.
Поддержка модулей RequireJS
Что если мы призовем модуль UMD? Давай попробуем:
./main.js
console.log( "%s is running...", module.id ); console.log( "%s imports %s", module.id, require( "./umd/module1.js" ).id );
./umd/module1.js
// UMD boilerplate according to https://github.com/umdjs/umd if ( typeof module === "object" && typeof define !== "function" ) { /** * Override AMD `define` function for RequireJS * @param {function( function, Object, Object )} factory */ var define = function ( factory ) { module.exports = factory( require, exports, module ); }; } define(function( require, exports, module ) { console.log( "%s is running...", module.id ); return { id: module.id }; });
Результат сборки:
./umd.js is running... ./umd/module1.js is running... ./umd.js imports ./umd/module1.js
Все отлично.
Автоматизация процесса сборки
В среде разработки у нас может быть дурацкая конфигурация Grunt:
Gruntfile.js
module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-cjsc'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.initConfig({ cjsc: { development: { options: { minify: true }, files: { "js/build.js" : "./js/modules/main.js" } } }, watch: { options: { livereload: false }, build: { files: ['./js/modules/**/**/*.js'], tasks: ['cjsc'] } } }); grunt.registerTask( "default", [ "cjsc" ]); };
И зависимости развития в package.json
"devDependencies": { //.. "grunt-contrib-watch": "~0.4.4", "grunt-contrib-cjsc": "*" }
Теперь мы можем заставить Grunt автоматически компилировать build.js при каждом изменении любого из модулей:
grunt watch
Итак, как вы видите, вы можете писать модули CommonJS и запускать их в браузере без дополнительной библиотеки. Если вы спросите меня о Browserify — это выглядит потрясающе. Честно говоря, я просто не знал об этом решении при создании компилятора RequireJS.