See the wiki for complete docs!
resource.js is a lightweight, flexible dependency loader for JavaScript. It loads "things" -- scripts, json, object references, or anything else -- in the right order. That's it!
The API was designed to be obvious, and is focused on working directly with plain JavaScript or other assets. Unlike other JavaScript module loaders, there are no constraints whatsoever about the format of files, the type of the resource, or the file layout of a project.
resource.js supports lazy loading from remote (AJAX) sources, but does not include an AJAX loader itself. resource.js supports both jQuery and axios, and will automatically detect either from a global variable. Alternatively, jQuery or axios can be injected explicitly with the resource.config.useJQuery()
or resource.config.useAxios()
methods, respectively.
resource.js is a dependency loader. You define
and require
resources; resource.js ensures they are loaded in the correct order.
resource.js is not an Asynchronous Module Definition loader; at least it does not implement the full AMD spec. Specifically, resource.js does not support path-based references, or any source code modification.
resource.js is completely decoupled from the file system. resource.js requires no bundler; and at the same time bundling simply by concatenation just works. As noted above, resource.js does not modify source files or support path-based references. This requires that all define
calls include an explicit resource id, all dependencies must be listed explicitly (they will not be hoisted CommonJS-style), and all dependency identifiers must be bare and globally unique.
Although resource.js can be used to define JavaScript modules, a resource can be any valid value referenceable from JavaScript.
resource.js defines global define
and require
methods that behave similar to standard AMD loaders such as require.js.
Use define
to give a string identifier to a resource. Resources can be anything.
define(resourceID:string, factory:Function):void
define(resourceID:string, dependencies:string[], factory:Function):void
define(resourceID:string, value:any):void
define(resourceID:string, value:Function, isLiteral:boolean):void
JavaScript modules can be defined with factory functions using the same syntax as AMD loaders, with the exception that the module id is required and named dependencies will be treated as literal identifiers, not paths:
// typical factory function definition with no dependencies
define('string-utils', function() {
return { pad: function(str, width, padChar) { ... } }
});
// typical factory function with dependencies
define('app-user-form', ['jquery', 'app-constants', 'app-form-base'], function($, constants, FormBase) {
function UserForm() { ... }
return UserForm;
});
Unlike AMD loaders, resources can be anything. No plugins or special syntax is required. A literal function can also be defined as a resource (not treated as a factory function) if no depenencies are specified, and the third argument is true
.
// defining a resource that is a literal object
define('setup-data', { ... json blob ... });
// defining a resource that is a function itself (not a factory function)
define('cool-func', function() { ... }, true);
As in AMD loaders, require
can be used to define an anonymous action to be invoked when all dependencies are resolved, or to retrieve an already-resolved resource by name
require(resourceID:string):any
require(dependency:string, action:Function):void
require(dependencies:string[], action:Function):void
The whole point of defining your resources is to be able to use them without worrying about the load order of dependencies. We do this with require
:
// An anonymous function to be invoke when all dependencies are resolved
require(['jquery', 'app-user-form'], function($, UserForm) {
new UserForm($('#user-form'));
});
// If there is only on dependency, it can be provided as a bare string
require('all-the-things', function(things) {
things.doStuff();
});
At any point in code, you can retrieve a resolved resource by calling require
with a single string argument. This does not guarantee that the resource has been defined or resolved, but inside a define
or require
call, this can be used for a CommonJS-like syntax, so you don't have to meticulously order your dependency names and injected parameters:
define(
'app-user-form',
// You still have to declare your dependencies
['jquery', 'bootstrap', 'app-constants', 'app-form-base', 'jquery-date-picker'],
function() {
// But you can retrieve them by name with require:
let $ = require('jquery');
let FormBase = require('app-form-base');
...
});
This also makes it easy to define bundle resources, while still retrieving individual resources as needed:
define('all-the-basics', ['jquery', 'bootstrap', 'app-root', 'app-constants', 'app-form-base', 'jquery-date-picker'], function() {
return require('app-root');
});
define('app-user-form', ['all-the-basics'], function() {
let $ = require('jquery');
let FormBase = require('app-form-base');
...
});
define.external allows you to register an external library as a named resource even if you're not sure when that library will be loaded. resource.js will keep checking at regular intervals until the resource is available. The interval and timeout of these checks is configurable, but defaults to every 100ms for a maximum of 10 seconds.
define.external(resourceID:string):void
define.external(resourceID:string, string:variableName):void
define.external(resourceID:string, source:Function):void
define.external(resourceID:string, source:Function, test:Function):void
In the simplest case, you know a library will register a global variable:
// This works, but requires you to know that moment is already loaded:
define('moment', moment, true);
// This will automatically keep checking for moment until it is defined:
define.external('moment');
// This will do the same for jQuery. resource.js will check for the global variable 'jQuery'
// (capital 'Q') but will register it as 'jquery' within the resource.js dependency registry
define.external('jquery', 'jQuery');
You can also provide a source function. resource.js will keep invoking the source function until it successfully returns a non-null value. Errors will be silently swallowed. Alternatively, a separate test function can be provided, and resource.js will keep checking until test
returns true
, and then will invoke source
to actually get the resource.
// I know that when jQuery datatables is loaded, then window.jQuery.fn.dataTable will exist
define.external('jquery-datatables', function() { return window.jQuery.fn.dataTable });
// When some weird side effect has occurred, I know the library is loaded, and / or
// I don't like throwing and swallowing a bunch of errors
define.external(
'ui-monitor',
function() { return window.__secretName.instance(); },
function() { return document.getElementById('__UI_MONITOR') != null; }
};
define.remote allows you to declare a url from which a named resource should be loaded when it is required. Multiple resources can be associated with the same url, accommodating bundled scripts. The default usage assumes that the target url is a script file which, when executed, will itself call define
to concretely define the applicable resources. Alternatively, you can indicate that the url represents a literal content resource (json, html, etc).
Note: the order in which define.remote
and define
is called doesn't matter. The only requirement is that define
and define.remote
can only be called once each for the same resourceID.
define.remote(resourceID:string, url:string):void
define.remote(resourceIDs:string[], url:string):void
define.remote(resourceID:string, url:string, isLiteral:boolean):void
As noted above, resource.js makes a different decision than conformant AMD module loaders when it comes to file paths and lazy loading. In AMD loaders, dependency strings are treated as paths relative to the file in which require
or define
is called. This gives you path-based lazy-loading of script files for free, but requires that all of your modules know where they are on disk relative to each other, and also precludes bundling unless you introduce a transpiling JavaScript build step.
resource.js makes no assumptions about the file structure of your modules or other resources. The cost of this is that lazy loading requires you to explicitly declare the location of resources. In practice, all of these declarations can go in a single manifest script. Further, you will generally have one version of the manifest script for dev, where most script modules are in their own files, and another for prod, where script modules are consolidated into a small number of bundle files.
// In manifest-dev.js:
define.remote('app-user-form', '/js/admin/app-user-form.js');
define.remote('app-roles-form', '/js/admin/app-roles-form.js');
define.remote('app-group-form', '/js/admin/app-group-form.js');
define.remote('app-user-audit', '/js/admin/app-user-form.js');
// In manifest-prod.js
// It's ok to use the same url for multiple resources
const adminBundleUrl = '/dist/js/adminForms.js';
define.remote('app-user-form', adminBundleUrl);
define.remote('app-roles-form', adminBundleUrl);
define.remote('app-group-form', adminBundleUrl);
define.remote('app-user-audit', adminBundleUrl);
// ... But it's more convenient to use array syntax for this
define.remote(['app-user-form', 'app-roles-form', 'app-group-form', 'app-user-audit'], adminBundleUrl);
define.remote can also be used to succinctly define content resources. This is useful for declaring expensive or dynamic data that only needs to be loaded in certain situations (such as going to a certain route in a single page application), and simplifies the logic for fetching such resources compared to more complex chains of Promises
s or other callback strategies.
// Compiled data blobs for building walkthrough
define.remote('asset-hq-building', '/models/compiledAsset?modelId=hq-building', true);
define.remote('asset-hq-hvac', '/models/compiledAsset?modelId=hq-hvac', true);
define.remote('asset-hq-wan', '/models/compiledAsset?modelId=hq-wan', true);
// If user loads the building viewer, load the assets
function openBuildingViewer(mode) {
... /* other stuff */
viewer.init();
require('asset-hq-building', function(asset) { viewer.load(asset) });
switch(mode) {
case 'hvac':
require('asset-hq-hvac', function(asset) { viewer.load(asset) });
break;
case 'wan':
require('asset-hq-wan', function(asset) { viewer.load(asset) });
break;
}
}
resource.js provides a handful of configuration options to tweak how it behaves. All properties are under resource.config
Property | Type | Default | Access | Description |
---|---|---|---|---|
debug | boolean | false | read-write | Whether to print debug messages to the console while resolving resources |
ignoreRedefine | boolean | true | read-write | Whether to ignore redefinitions of resource ids. If true, redefinition will be ignored. If false, resource.js will throw an Error |
immediateResolve | boolean | false | read-write | Whether to attempt to immediately resolve resources defined with define . If false, resources will only be resolved when they are referred to directly or indirectly via a call to require |
ajaxProvider | string | null | read only | Indicates the currently loaded ajax provider ('axios', 'jquery' or null). To set this value, use the useJQuery or useAxios methods |
external.autoResolve | boolean | true | read-write | Whether to automatically attempt to match resource IDs to global variables |
external.interval | number | 100 | read-write | Interval in milliseconds between automatic checks for resolved external resources |
external.timeout | number | 10000 | read-write | Timeout in milliseconds for automatic checking for resolved external resources |
resource.config.debug = true|false;
resource.config.ignoreRedefine = true|false;
resource.config.immediateResolve = true|false;
resource.config.external.autoResolve = true|false;
resource.config.external.interval = <number >= 10>;
resource.config.external.timeout = <number >= 10>;
resource.config.useJQuery(jQuery:Function):void;
resource.config.useAxios(axios:Function):void;
By default, resource.js will silently ignore attempts to redefine the same resource id, though it will print a warning message indicating this. If ignoreRedefine
is set to false, resource.js will instead throw an Error. Either way, resource.js will never allow a resource definition to be overwritten.
By default, resource.js will defer execution or assignment of resource definitions until the resource has been directly or indirectly referenced in a call to require
. If immediateResolve
is set to true, resource.js will instead immediately attempt to resolve any resource definitions. This is of course discouraged. If you rely on a named resource to be executed to set up certain global state or other side effects, it is recommended that instead you simply require
this resource explicitly, or call resource.resolve()
:
define('polluting-module', function() { ... });
// elsewhere
require('polluting-module', function() { /* do nothing. Just ensure polluting-module is executed */ });
// OR: Attempt to resolve a resource without require:
resource.resolve('polluting-module');
// if you REALLY want to immediately resolve all resources:
resource.config.immediateResolve = true;
By setting the resource.config.external.autoResolve
property to true
, you can instruct resource.js to attempt to automatically match referenced resources IDs against existing global variables. This will not trigger automatic rechecking as with define.external
.
resource.config.external.autoResolve = true;
define('time-utils', ['base-utils', 'tz-map', 'moment'], function() { ... });
// For each of 'base-utils', 'tz-map', and 'moment': resource.js will as always first
// check to see if the resource id is already defined. If the resource id is *not*
// defined but there *is* an exactly matching global variable, then resource.js
// will automatically define a resource with the same name, and with the current value
// of the global variable.
A safer approach is to leave external.autoResolve
set to false
and instead explicitly declare external resources:
// This is probably a better idea:
resource.config.external.autoResolve = false; // this is the default
// Explicitly declare a module called 'moment', defined by the global variable of the same name.
// If the global variable doesn't exist, keep checking until it does, per interval and timeout settings:
define.external('moment');
define('time-utils', ['base-utils', 'tz-map', 'moment', 'jquery'], function() { ... });
// As with any other definition, it doesn't matter if you call define before or after
// dependent define and require calls:
define.external('jquery', 'jQuery');
AMD loaders don't really have a concept of disposing of a module, because in a narrow understanding of resources as only JavaScript modules it doesn't make much sense. The expense is generally in the GET request to fetch the script file and then in executing the definition itself. Neither of these can be undone.
resource.js is intentionally built to manage any kind of resource, and sometimes the expense of a data resource is in the memory it occupies, and we want to release that memory when the resource is no longer needed in the lifetime of a page. resource.js provides two mechanisms to accomodate this: resource.destroy
to release a single named resource, and the concept of Context
s to manage and release whole related collections of resources.
resource.destroy is very simple: It removes the provided resource id from the resource.js registry, and will invoke a destructor method if it finds one. This does not guarantee the resource can be garbage collected, nor does it "undefine" the value of the resource already injected into other contexts. This is most useful when cleaning up strongly-named resources associated with a chunk of a single page application or other long-lived page.
resource.destroy(resourceID:string):void
// Generated on the server with an injected guid:
const widget_id = 3023;
const component_guid = '8d5b00b3db9043bd87bb952ccb2f3c29';
const history_rsrc = 'widget-history-' + component_guid;
const relations_rsrc = 'widget-relations-' + component_guid;
const graph_rsrc = 'widghet-graph-' + component_guid;
// Fetch these json blobs in parallel:
define.remote(history_rsrc, '/widget/history/' + widget_id, true);
define.remote(relations_rsrc, '/widget/relations/' + widget_id, true);
define.remote(graph_rsrc, '/widget/graph/' + widget_id, true);
require([history_rsrc, relations_rsrc, graph_rsrc, 'app-utils', 'jquery', 'app-forms', 'widget-forms'],
function(history, relations, graph) {
let $ = require('jquery');
let $form = $('#div-' + component_guid);
let widgetForm = require('app-widget-form');
new widgetForm($form, component_guid, history, relations, graph);
}
);
// In widget.dispose:
function dispose() {
resource.destroy('widget-history-' + this.guid);
resource.destroy('widget-relations-' + this.guid);
resource.destroy('widget-graph-' + this.guid);
}
When invoking destroy
on a resource, resource.js will check if the resource value has a member called ~
(tilde) that is a function. If so, the resource.js will attempt to invoke that method on the resource value without any arguments. This is the equivalent to the following:
let value = resource.get('resource-id')
if(value != null && ('function' == typeof value['~'])) {
try {
value[~]();
} catch { /* ignore */ }
}