I decided to take ljfranklin's advice, and do away with RequireJS completely. I personally think AMD is doing it all wrong, and CommonJS (with it's synchronous behaviour) is the way to go; but that's for another discussion.
One thing I looked at is moving to Browserify, but in development each compilation (as it scans all your files and hunts down require()
calls) took far too long for me to deem acceptable.
In the end, I rolled out my own bespoke solution. It's basically Browserify, but instead it requires you to specify all your dependencies, rather than having Browserify figure it out itself. It means compilation is just a few seconds rather than 30 seconds.
That's the TL;DR. Below, I go into detail as to how I did it. Sorry for the length. Hope this helps someone... or at least gives someone some inspiration!
Firstly, I have my JavaScript files. They are written à la CommonJS, with the limitation that exports
isn't available as a "global" variable (you have to use module.exports
instead). e.g:
var anotherModule = require('./another-module');
module.exports.foo = function () {
console.log(anotherModule.saySomething());
};
Then, I specify the in-order list of dependencies in a config file (note js/support.js
, it saves the day later):
{
"js": [
"js/support.js",
"js/jquery.js",
"js/jquery-ui.js",
"js/handlebars.js",
// ...
"js/editor/manager.js",
"js/editor.js"
]
}
Then, in the compilation process, I map all of my JavaScript files (in the js/
directory) to the form;
define('/path/to/js_file.js', function (require, module) {
// The contents of the JavaScript file
});
This is completely transparent to the original JavaScript file though; below we provide all the support for define
, require
and module
etc, such that, to the original JavaScript file it just works.
I do the mapping using grunt; first to copy the files into a build
directory (so I don't mess with the originals) and then to rewrite the file.
// files were previous in public/js/*, move to build/js/*
grunt.initConfig({
copy: {
dist: {
files: [{
expand: true,
cwd: 'public',
src: '**/*',
dest: 'build/'
}]
}
}
});
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('buildjs', function () {
var path = require('path');
grunt.file.expand('build/**/*.js').forEach(function (file) {
grunt.file.copy(file, file, {
process: function (contents, folder) {
return 'define(\'' + folder + '\', function (require, module) {\n' + contents + '\n});'
},
noProcess: 'build/js/support.js'
});
});
});
I have a file /js/support.js
, which defines the define()
function I wrap each file with; here's where the magic happens, as it adds support for module.exports
and require()
in less than 40 lines!
(function () {
var cache = {};
this.define = function (path, func) {
func(function (module) {
var other = module.split('/');
var curr = path.split('/');
var target;
other.push(other.pop() + '.js');
curr.pop();
while (other.length) {
var next = other.shift();
switch (next) {
case '.':
break;
case '..':
curr.pop();
break;
default:
curr.push(next);
}
}
target = curr.join('/');
if (!cache[target]) {
throw new Error(target + ' required by ' + path + ' before it is defined.');
} else {
return cache[target].exports;
}
}, cache[path] = {
exports: {}
});
};
}.call(this));
Then, in development, I literally iterate over each file in the config file and output it as a separate <script />
tag; everything synchronous, nothing minified, everything quick.
{{#iter scripts}}<script src="{{this}}"></script>
{{/iter}}
This gives me;
<script src="js/support.js"></script>
<script src="js/jquery.js"></script>
<script src="js/jquery-ui.js"></script>
<script src="js/handlebars.js"></script>
<!-- ... -->
<script src="js/editor/manager.js"></script>
<script src="js/editor.js"></script>
In production, I minify and combine the JS files using UglifyJs. Well, technically I use a wrapper around UglifyJs; mini-fier.
grunt.registerTask('compilejs', function () {
var minifier = require('mini-fier').create();
if (config.production) {
var async = this.async();
var files = bundles.js || [];
minifier.js({
srcPath: __dirname + '/build/',
filesIn: files,
destination: __dirname + '/build/js/all.js'
}).on('error', function () {
console.log(arguments);
async(false);
}).on('complete', function () {
async();
});
}
});
... then in the application code, I change scripts
(the variable I use to house the scripts to output in the view), to just be ['/build/js/all.js']
, rather than the array of actual files. That gives me a single
<script src="/js/all.js"></script>
... output. Synchronous, minified, reasonably quick.