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.