Qoopido.demand
Promise like module loader with automatic resolution of nested dependencies using XHR requests and localStorage caching to dynamically load modules, legacy JavaScript, CSS, text and bundles. Supports custom handler and plugins as well.
Install / Use
/learn @dlueth/Qoopido.demandREADME
Qoopido.demand
Qoopido.demand is a modular, flexible and 100% async JavaScript module loader with a promise like interface that utilizes localStorage as a caching layer. It comes in a rather tiny package of ~7kB minified and gzipped.
Qoopido.demand originated from my daily use of require.js for the initial development of my Qoopido.nucleus library which is strictly atomic by nature, unbundled.
Key features in detail
- promise like interface (no native promise support required)
- localStorage caching for blazingly fast performance
- dependency resolution for modules
- automatic cache invalidation by version or lifetime
- per module/path/subpath setting of cache parameters
- relative and absolute module path resolution
- fallback URLs for any dependency
- support for handling modules, legacy scripts, bundles (concatenated scripts like from jsdelivr), text, CSS and JSON included
- plugins for cookie support, lzstring compression and SRI included
- support for custom handlers & plugins built in
- optional support for auto-bundles via
genieincluded
Compatibility
Qoopido.demand is developed for Chrome, Firefox, Safari, Edge, Opera and IE11+.
Active Support for IE9 and IE10 has been removed in Qoopido.demand 6.0.0 due to upcoming refactorings/cleanups and the lack of market share of these browsers today.
Support for IE8 has been actively removed in Qoopido.demand 4.0.0 due to the lack of justifiable polyfills for parts of some underlying pattern.
I do test on MacOS Sierra where Qoopido.demand is fully working on Chrome, Firefox, Safari and Opera. IE9, 10, 11 as well as Edge are testet on the official Microsoft VMs via VirtualBox.
Requirements
Due to modules getting loaded via XHR a remote server has to have CORS enabled. Be assured that most of the usual CDNs have CORS enabled by default.
External dependencies
None!
Availability
Qoopido.demand is available on GitHub as well as jsdelivr, npm and bower at the moment.
Loading demand
Use the following minified code snippet in a standalone script tag before the closing body tag to include demand:
(function(url, main, settings) {
!function(e,n,t,r,s){r=n.getElementsByTagName(t)[0],s=n.createElement(t),e.demand={url:url,main:main,settings:settings},s.async=1,s.src=url,r.parentNode.insertBefore(s,r)}(window,document,"script");
}('../dist/demand.js', './app/main', { base: './', version: '1.0.0', cache: true }));
The snippet is very similar to Google Analytics. The outer function allows you to specify an URL from which to load demand itself as well as a path to the main module and configuration object for demand. The script tag that actually loads Qoopido.demand will be injected with its async attribute set.
As an alternative to the above snippet Qoopido.demand can now also be loaded with an alternative snippet that uses an iframe. The async method above, while not blocking rendering, seems to delay the onload event at least on some browsers. The iframe method solves this minor annoyance.
(function(url, main, settings) {
!function(e,t,n,o,i,d,a){e.demand={url:url,main:main,settings:settings},o=t.getElementsByTagName(n)[0],i=t.createElement("iframe"),i.src="javascript:void(0)",i.name="demand-loader",i.title="",i.role="presentation",(i.frameElement||i).style.cssText="display:none;width:0;height:0;border:0;",o.parentNode.insertBefore(i,o);try{i=i.contentWindow.document}catch(e){d=t.domain,i.src='javascript:var d=document.open();d.domain="'+d+'";void(0);',i=i.contentWindow.document}i.open()._=function(){d&&(this.domain=d),a=this.createElement(n),a.src=url,this.body.appendChild(a)},i.write('<body onload="document._();" />'),i.close()}(this,document,"script");
}('../dist/demand.js', './app/main', { base: './', version: '1.0.0', cache: true }));
Remember to adjust parameters according to the async method.
Configuration
The last parameter of the above code snippet is a configuration object. It just contains base and version as these are the properties you will most likely set. There are some more, less frequently used, options that can be either specified here or as part of a demand.configure call in your main module (being described in the next section):
{
// enables or disables caching in general (when true/false)
// optional, defaults to "true"
cache: true,
// fine grained cache control (when object)
// any path or part of a path can be set to true to
// activate caching or false to disable it.
// The longest matching path wins over others.
cache: {
'/demand/': true,
'/app/': true,
'/app/nocache': false
},
// cache will be validated against version
// optional, defaults to "undefined"
version: '1.0.0',
// cache will be validated against lifetime, if > 0
// optional, defaults to "0"
// unit: seconds
lifetime: 60,
// sets the timeout for XHR requests
// optional, defaults to "8" (limited to "2" up to "20")
// unit: seconds
timeout: 8,
// base path from where your relative
// dependencies get loaded
// optional, defaults to "/"
base: '[path/url to your scripts]',
// optional
pattern: {
'/nucleus': ['[path/url to Qoopido.nucleus]', '[fallback path/url to Qoopido.nucleus]'],
'/app': '[path/url to your modules]',
// just an example, loading jQuery + bundle
// will not work due to the nature of jQuery
'/jquery': '//cdn.jsdelivr.net/jquery/2.1.4/jquery.min',
'/jquery+ui': '//cdn.jsdelivr.net/g/jquery@2.1.4,jquery.ui@1.11.4'
},
// per module configuration (if applicable)
modules: {
// configure the legacy handler
'/demand/handler/legacy': {
'/jquery': {
probe: function() { return global.jQuery; }
},
'/jquery/ui': {
probe: function() { return global.jQuery.ui; },
dependencies: [ 'legacy!/jquery' ]
},
'/velocity': {
probe: function() { return global.Velocity || (global.jQuery && global.jQuery.fn.velocity); }
},
'/leaflet': {
probe: function() { return global.L; }
}
}
// configure the bundle handler
'/demand/handler/bundle': {
// declare which modules are included in the bundle
// order is important
'/jquery+ui': [ '/jquery', '/jquery/ui' ]
},
// configure genie plugin
'/demand/plugin/genie': {
// handle creation of auto-bundle URL for Qoopido.nucleus from jsdelivr
'/nucleus/': function(dependencies) {
var fragments = [],
i = 0, dependency;
for(; (dependency = dependencies[i]); i++) {
fragments.push(dependency.id.replace(/^\/nucleus\//, '') + '.js');
}
return '//cdn.jsdelivr.net/g/qoopido.nucleus@2.0.1(' + fragments.join('+') + ')';
},
// handle creation of auto-bundle URL for your modules from your server
'/app/': function(dependencies) {
var fragments = [],
i = 0, dependency;
for(; (dependency = dependencies[i]); i++) {
fragments.push(dependency.id.replace(/^\/js\//, '') + '.js');
}
return '/genie/?module[]=' + fragments.join('&module[]=');
}
}
}
Usage
The demanded main module from the above script might look like the following example:
(function(global) {
'use strict';
function definition(demand, provide) {
demand
.configure({
// any option from the previous section
// most likely something like:
pattern: {
},
modules: {
}
});
return true; // just return true if there really is nothing to return
}
provide([ 'demand', 'provide' ], definition);
}(this));
Qoopido.demand consists of two components demand and provide just like require.js require and define.
Once demand is loaded anything that is either explicitly requested via demand or as a dependency of a provide call will be loaded via XHR as well as modified and injected into the DOM with the help of a handler. The result will be cached in localStorage (if caching is enabled and localStorage is available) that will get validated against an optional version and lifetime set via demand.configure or the modules path declaration (more on that later).
As main itself is also loaded as a module it will get cached in localStorage as well.
Controlling the cache
If caching is enabled, localStorage available and its quota not exceeded chances are good you will never have to manually deal with the cache.
By default demand will invalidate a modules cache under the following conditions:
- module's
versionchanged - module's
lifetimeis exceeded
Demand will, in addition, do its best to keep leftover garbage to a minimum. It does so by starting an automatic garbage collection for expired caches on load. In addition it will also clear a specific cache if it gets requested and is found to be invalid for any reason.
When localStorage quota is exceeded while trying to cache yet another module Qoopido.demand will load a special module /demand/cache/dispose and will try to free the required space by clearing existing caches in orde
