I wrote a basic JavaScript plugin that allows you to specify what specific JavaScript files and CSS files to be loaded in, and in addition specify attributes for each file.
To specify where to load in scripts, you just have to add a data-script-loader
attribute to an element, and all scripts will be inserted after that element. To specify where to load in link elements, you just have to add a data-css-loader
attribute to an element, and all link elements will be inserted after that element.
The following function is used to load scripts/css files:
ScriptLoader.load({
scripts:{
files:['bower_components/jquery/dist/jquery.min.js', 'dist/test-2.js'],
options: {
'jquery': {
async:true
},
'test-2': {
defer:true
}
}
},
css: {
files:['dist/style.css'],
options: {
'style': {
hreflang:'en'
}
}
},
callbacks: {
'jquery':jqueryCallback,
'style':function(){
console.log("stylesheet loaded");
}
}
});
This all works fine, and all elements are loaded in with their specified attributes.
This is the code for the plugin:
window.ScriptLoader = (function(window, document, undefined){
var loadedScripts = [];
var loadedStyleSheets = [];
//script loader
var ScriptLoader = {
load: function (){
var options = {
scripts:{
files:[],
options:{}
},
css:{
files:[],
options: {}
},
callbacks:{}
}
if(arguments[0] && typeof arguments[0] === "object") {
if(!arguments[0].scripts && !arguments[0].css){
throw new Error("Error: Failed to supply required arguments (CSS or JS array is required)");
} else {
options = setOptions(options, arguments[0]);
if(arguments[0].scripts)
loadScripts(options);
if(arguments[0].css)
loadCss(options);
}
} else {
throw new Error("Error: No arguments given.");
}
},
showJS: function(){
//returns array with script sources
return loadedScripts;
},
showCSS: function(){
//returns array with css sources
return loadedStyleSheets;
}
}
//load scripts
function loadScripts(options){
var loc = document.querySelector('[data-script-loader]');
var scriptOptions = options.scripts.options;
var asyncOpt = scriptOptions.defer, deferOpt = scriptOptions.defer;
for(var i = 0; i < options.scripts.files.length; i++){
var prettyName = prettySource(options.scripts.files[i]);
var script = document.createElement('script'),
scriptSrc = options.scripts.files[i];
script.src = scriptSrc;
//default
script.type = "text/javascript";
//custom options
for(var opt in scriptOptions[prettyName])
script[opt] = scriptOptions[prettyName][opt];
//callback
if(options.callbacks[prettyName])
script.onload = options.callbacks[prettyName];
loc.parentNode.insertBefore(script, loc.nextSibling);
//push to list of loaded scripts
loadedScripts.push(scriptSrc);
}
}
//load css
function loadCss(options){
var loc = document.querySelector('[data-css-loader]');;
var cssOptions = options.css.options;
for(var i = 0; i < options.css.files.length; i++){
var prettyName = prettySource(options.css.files[i]);
var link = document.createElement('link'),
linkHref = options.css.files[i];
link.href = linkHref;
//defaults
link.type = "text/css";
link.rel = "stylesheet";
//custom options
for(var opt in cssOptions[prettyName])
link[opt] = cssOptions[prettyName][opt];
//callback
if(options.callbacks[prettyName])
link.onload = options.callbacks[prettyName];
//insert after
loc.parentNode.insertBefore(link, loc.nextSibling);
//push to list of loaded style sheets
loadedStyleSheets.push(linkHref);
}
}
//make sources pretty
function prettySource(src){
var s = src.split('/'), len = s.length - 1;
var pretty = s[len].split('.')[0];
return pretty;
}
//utility function to set options
function setOptions(src, props){
var prop;
//js files
if(props.scripts){
//scripts
for(var js in props.scripts.files){
src.scripts.files[js] = props.scripts.files[js];
}
//javascript options
for(var jsopt in props.scripts.options){
src.scripts.options[jsopt] = props.scripts.options[jsopt];
}
}
//css files
if(props.css){
for(var css in props.css.files){
src.css.files[css] = props.css.files[css];
}
//css options
for(var cssopt in props.css.options){
src.css.options[cssopt] = props.css.options[cssopt];
}
}
if(props.callbacks){
//callback options
for(cb in props.callbacks){
src.callbacks[cb] = props.callbacks[cb];
}
}
return src;
}
return ScriptLoader;
})(window, document);
How can I improve this ? Are there any browser compatibility issues I need to consider ? Can my code be improved ?
1 Answer 1
<link>
elements have very fuzzy load
and error
. You cannot trust them fully. The other way of loading them is to use AJAX to fetch the contents and put them into a <style>
element.
files:['bower_components/jquery/dist/jquery.min.js', 'dist/test-2.js'],
options: {
'jquery': {
async:true
},
'test-2': {
defer:true
}
}
This is a really odd structure to work with. Your files are in a separate collection from their options. This makes it hard to manage, especially when you want to remove or modify one. I suggest you take on something like:
scripts: [{
src: 'path/to/script'
async: ...,
defer: ...,
onload: function(){...},
onerror: function(){...},
},{
src: 'path/to/script'
async: ...,
defer: ...,
onload: function(){...},
onerror: function(){...},
}]
That way, its easy to manage as the file is in one entry. Removing simply means removing the entire entry. Properties are in the same place and so on.
An easier way to manage loading is to use promises. It's been there for a while, and for browsers that don't support it, there a polyfill. That way, you avoid having to create your own callback mechanism. Use the Promise
constructor to wrap your dynamic elements, calling resolve
when they load and reject
when they fail. To listen for multiple promises, you use Promise.all
.
This way, your "library" becomes as simple as 2 functions, createScript
and createStyles
.
ScriptLoader.createScript = function(options){
// We return a promise, a listenable object
return new Promise(function(resolve, reject){
// create script, attach options, attach handlers, additional logic
script.onload = resolve;
script.onerror = reject;
});
}
ScriptLoader.createStyles= function(options){
// We return a promise, a listenable object
return new Promise(function(resolve, reject){
// create link, attach options, attach handlers, additional logic
link.onload = resolve;
link.onerror = reject;
});
}
// Creating a single script
ScriptLoader.createScript({...}).then(function(){
// Script loaded
}, function(){
// Script failed
});
// Using Promise.all to listen for multiple promises, script or styles
Promise.all([
ScriptLoader.createScript({...}).then(function(){ /* this loaded */ }),
ScriptLoader.createStyles({...}).then(function(){ /* this loaded */ }),
ScriptLoader.createScript({...}).then(function(){ /* this loaded */ }),
]).then(function(values){
// Everyone loaded!
// values is an array of resolutions in the order they're added
// to Promise.all
});
You can take this further by automating this process. Have your plugin accept an array of resources like the one suggested above, then process it into an array of promises which you can feed to Promise.all
.