3
\$\begingroup\$

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 ?

\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

<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.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.