A simple, dynamic, powerful module loader with hot swapping and optional remote loading support.
This is a simple JS loader that can dynamically load files as well as support multiple modules within a single file. It also supports hot swapping. This plus its 528-byte minified+gzipped size makes it a pretty nice solution for a simple module system if you need one. Also, it's one of the smallest module loaders I know of, yet compares feature-wise to ones over 3 times its size (1.5+ kilobytes).
- Concise syntax
- Namespaced modules with optional default exports
- Hot swapping and introspection
- Lazy, synchronous instantiation
- Optional asynchronous, dynamic remote loading support
- Node-like cyclic dependency handling
- Very small (528 bytes minified + gzipped, 340 without remote loading support)
- Fully supports both browsers and workers (and shells, but without remote loading support)
- Easy bundling with concatenation or whatever else you like
- Thoroughly tested
<!-- Include this -->
<script src="r.min.js"></script>
<script>
// Alias these utilities, calling their respective methods to detach them from
// the global object. Once they're both loaded, load the main module. Note that
// it's just another module - it doesn't magically load like an inline
// `<script>` element.
var remaining = 2
r.load("/jquery.min.js", function (err) {
if (err) return console.error(err)
r.module("jquery", $.noConflict())
if (--remaining) r.require("main")
})
r.load("/lodash.min.js", function (err) {
if (err) return console.error(err)
r.module("lodash", _.noConflict())
if (--remaining) r.require("main")
})
// Define a few modules - not loaded yet!
r.define("foo", function () { return "default export" })
r.define("bar", function (exports) { exports.named = "named export" })
r.define("assert", function () {
return function assert(condition, message) {
if (!condition) throw new Error(message)
}
})
// Define our main module
r.define("main", function () {
// Load an `assert` module
var assert = r.require("assert")
var $ = r.require("jquery")
var _ = r.require("lodash")
// And use its export
assert(r.require("foo") === "default export",
"default exports are read correctly")
assert(_.matches(r.require("bar"), {named: "named export"}),
"named exports are read correctly")
// Load a remote module and immediately use it
r.load("page/base", "/page.js", function (err, BaseComponent) {
if (err != null) return displayError(err)
renderComponent($("#body").get(0), BaseComponent)
})
// Hot-swap an existing module
r.unload("assert")
r.define("assert", function () {
return function assert() {
return true
}
})
// Hot-unload an existing module
r.unload("bad-module")
})
</script>
r.defined("module-name")
Check if "module-name"
is defined (regardless of whether it is loaded or not).
r.required("module-name")
Check if "module-name"
has been defined and loaded.
r.unload("module-name")
Unload "module-name"
if it was previously loaded. If there was no such module, this does nothing (no need to remove non-existent modules).
r.require("module-name")
Require "module-name"
CommonJS-style. This throws an error if the module is not defined.
"module-name"
is the name of the module.impl
is the function to initialize the module.
r.module("module-name", impl)
Define an already-instantiated module. It's equivalent to the following below, but much simpler under the hood:
r.define("module-name", function () { return impl })
r.require("module-name")
"module-name"
is the name of your module. Anything that can be an object key works, really. Even multi-line strings or ES6 symbols work.impl
is the actual exported value of the module, or{}
if it isnull
/undefined
.
If "module-name"
is already defined, this throws an error.
r.define("module-name", impl)
"module-name"
is the name of your module. Anything that can be an object key works, really. Even multi-line strings or ES6 symbols work.impl
is the function to initialize the module. It's called by need with one argument:exports
, which acts a lot like CommonJS's and AMD'sexports
variable.
If impl
returns anything other than null
/undefined
, that return value is used as the export.
If "module-name"
is already defined, this throws an error.
r.load("module-name", "/remote-resource", callback?) r.load("/remote-resource", callback?)
Note: This is not available in local.js
.
-
"module-name"
is the name of the module to callcallback
with -
"/remote-resource"
is the name of the remote resource to get. It is assumed to contain only JavaScript. -
callback
is a required Node-style callback, called with the following arguments:err
, the unmodified error if one occurred when either getting the module or initializing it, ornull
otherwise. In browsers, if the script failed to load, then this is the correspondingerror
event.data
, the result of requiring"module-name"
, orundefined
if no such module was defined, or if the module name itself isnull
or not given.
The callback is always called asynchronously.
There are two versions of this API (and \*.min.js
minified variants):
r.js
for browsers and web workers. Scripts are loaded viascript
elements appended to the body in the main thread, and viaimportScripts
in workers.local.js
, which sacrifices file loading support for a significant reduction in size and wider compatibility (it is pure ES5, with no native or runtime-specific dependency).
Each of these has a minified variant within this repo as well, generated via npm run minify
.
Here's a size comparison in bytes for each file (complete with license header) at the time of writing:
File | Size | Gzipped |
---|---|---|
r.js | 4066 | 1279 |
local.js | 2405 | 933 |
r.min.js | 1126 | 528 |
local.min.js | 665 | 340 |
If you found a bug, please tell me! I'd like to make sure things remain in working order.
Pull requests are always welcome. Mocha is used for tests, and Chai for assertions.
- If you haven't already, install Node and
npm
. npm test
- Lint this with ESLint and run the tests in PhantomJS. This doesn't run the worker tests, as PhantomJS doesn't support those. Also note that this runs them with thefile:
protocol.node minify
- Regenerate the minified variants with UglifyJS2.
Do note that when running the tests, the browser (or PhantomJS) will rightly complain about missing files. If it's about test/fixtures/missing.js
, that's intentional, and you don't have to worry.
Note that the two files are separately written, to minimize minified+gzipped file size.
And do check out http-server. It will make testing smaller browser things much easier. I use that to load the web pages here for testing.