MiroHibler / meteor-preloader

A Meteor "lazy-loader" for external .js and .css libraries
75 stars 3 forks source link

Meteor Preloader

A Meteor "lazy-loader" for external .js and .css libraries

Preloader is to Meteor what yepnope.js was to pre-Meteor era.

NEW VERSION - v1.2.4

Dependencies

TL;DR;

Preloader extends the iron:router with with two main functions:

What?!

Meteor gathers all JavaScript files in the application tree, with the exception of the server, public, and private subdirectories, for the client. It minifies this bundle and serves it to each new client.

That's fine for small (one-page) applications with a few (dozen) custom .js files, but when you're building a large, structured application that has many different screens, like a CMS application for example, things are getting more complicated.

The problem in large applications is that you usually don't want to serve all libraries (.js and .css files) to ALL pages. A login page or a user profile page doesn't need fairly large mapping or graph visualization libraries or extensive styling.

Also, to improve the page loading speed and responsiveness, you'd often want to load 3rd party libraries from CDN (or author's server) instead of your own server.

To load such files, a usual approach is to use AJAX loader, for example jQuery's $.ajax, or a higher-level alternatives like $.get() and $.load(). The main problem with those methods is that they work asynchronously, meaning - they will not block the page loading, which may be a problem for depending libraries and user defined methods - successful AJAX load doesn't guarantee that the library has finished self-initialization, therefore may not be available to other libraries and custom methods when they load or being invoked.

Preloader's main task is to fix that problem.

NOTE: .css files will be just appended to the <head> section which will cause their immediate loading and they are always loaded asynchronously.

KEWL!

Yeah! Now, go ahead and type:

meteor add miro:preloader

to add it to your app.

Bring it on!

Preloader adds one method parameter (preload) to any or all of the following Iron.Router objects:

To use Preloader, add these method parameters to them:

'preload': {

    /*
     | Parameters can be a string (file path) or an array of strings
     */

    // Added in v1.2.1 - this one works only in Router.Configure!
    'verbose': true,  // Show loading messages in console

    // Custom time-out to replace internal 2 seconds
    'timeOut': 5000,    // milliseconds

    // CSS style(s) to load
    'styles' : '',  // or []

    // File(s) to be loaded asynchronously (non-blocking)
    'async'  : '',  // or []

    // File(s) to be loaded synchronously (blocking)
    'sync'   : '',  // or []

    // (optional) User-defined method called BEFORE each asynchronously
    // loaded library to allow additional processing
    'onBeforeAsync': function ( fileName ) {
        // Return 'true' to continue normally, otherwise skip library
        return true;
    },

    // (optional) User-defined method called on each asynchronously
    // loaded library to check whether it finished initialization
    'onAsync': function ( error, result ) {
        // Check if library finished initialization
        // and have your way with it

        /* error:
        {
            file      : <full path of the file being loaded>,
            jqxhr     : <jqxhr object returned from AJAX call>,
            status    : <textual status returned from AJAX call>,
            exception : <exception object returned from AJAX call>,
            counter   : <current file counter>,
            totalFiles: <total number of files being loaded>
        }

        // result:
        {
            file      : <full path of the file being loaded>,
            script    : <file content returned from AJAX call>,
            status    : <textual status returned from AJAX call>,
            counter   : <current file counter>,
            totalFiles: <total number of files being loaded>
        }
        */
    },

    // (optional) User-defined method called AFTER each asynchronously
    // loaded library to allow additional processing
    'onAfterAsync': function ( fileName ) {
        // Return 'true' to continue normally,
        // otherwise don't mark library as loaded
        return true;
    },

    // (optional) User-defined method called BEFORE each synchronously
    // loaded library to allow additional processing
    'onBeforeSync': function ( fileName ) {
        // Return 'true' to continue normally, otherwise skip library
        return true;
    },

    // (optional) User-defined method called on each synchronously
    // loaded library to check whether it finished initialization
    'onSync' : function ( fileName ) {
        // Check and return `true` if `fileName` finished initialization
        return true;
    },

    // (optional) User-defined method called AFTER each synchronously
    // loaded library to allow additional processing
    'onAfterSync': function ( fileName ) {
        // Return 'true' to continue normally,
        // otherwise don't mark library as loaded
        return true;
    }
}

These options are processed by the PreloadController - a built-in route controller that handles preloading.

It's used in two ways:

HomeController = PreloadController.extend();

Router.route( '/', {
    name: 'home'
});

or

Router.route( '/', {
    controller: PreloadController   // or 'PreloadController'
});

We might have some options defined globally with Router.configure, some options defined on the Route and some options defined on the RouteController. Preloader looks up options in this order:

  1. Route
  2. RouteController
  3. Router

And now, something completely different!

(Drumroll...) Example time!

var routePath = 'http://js.arcgis.com/3.12/',
    routeLoaded = false,
    loadHandler = function () {
        routeLoaded = true;
    };

Router.configure({
    layoutTemplate : 'layout',
    loadingTemplate: 'loading',

    /*
     | Options declared on the extended PreloadController and route
     | will override these default Router options!
     */
    'preload': {
        // Added in v1.2.1 - this one works only in Router.Configure!
        'verbose': true,  // Show loading messages in console

        'timeOut': 5000,  // wait 5s for our humongous library to finish loading
        'styles' : '/library/icons/fontawesome/assets/css/font-awesome.css',
        'async'  : '/large/files/to/async/preload/humongous.js',
        'sync'   : [
            routePath,
            '/library/modernizr/modernizr.js'
        ],
        'onAsync': function ( error, result ) {
            if ( error ) {
                console.log( 'Some other time... :(' );
            } else {
                console.log( 'Finally! :)' );
            }
        },
        'onBeforeSync': function ( fileName ) {
            if ( fileName === routePath ) {
                // Our ArcGis library requires special treatment:
                var script    = document.createElement( 'script' );

                script.rel    = 'preload javascript';
                script.type   = 'text/javascript';
                script.src    = routePath;
                script.onload = loadHandler;

                document.body.appendChild( script );

                // No need to continue normally...
                return false;
            }
        },
        'onSync' : function ( filePath ) {
            if ( routeLoaded && filePath === routePath ) {
                // Check for Dojo
                return !!require && !!define;
            }
            // Else...
            var fileName = filePath.replace( /\?.*$/,"" ).replace( /.*\//,"" );

            switch ( fileName ) {
                case 'modernizr.js':
                        try {
                            return !!Modernizr;
                        } catch ( error ) {
                            return false;
                        }
                    break;
                default:
                    return true;
            }
        },
        'onAfterSync': function ( fileName ) {
            // We'll probably want to reload the main
            // library, so don't mark it cached
            return false;
        }
    }
});

AppRouteController = PreloadController.extend({
    /*
     | Options declared on the route will override these options!
     */
    'preload': {
        'async': '/plugins/main_badass.js'
    },

    onBeforeAction: function () {
        var self = this,
            routeName = self.route.getName();

        switch ( routeName ) {
            case 'badAss':
                self.preload.sync = '/plugins/even_more_badass.js';
                self.preload.onSync = function ( filePath ) {
                    var file = filePath.replace( /\?.*$/,"" ).replace( /.*\//,"" );

                    switch ( file ) {
                        case 'even_more_badass.js':
                                try {
                                    return !!BADASS;
                                } catch ( error ) {
                                    return false;
                                }
                            break;
                        default:
                            return true;
                    }
                };
            default:
                // Whatever goes on with other routes
        }
    }
});

Router.route( '/', {
    name          : 'home',
    template      : 'main',
    yieldTemplates: {
        'news': {
            to: 'mainContent'
        }
    },

    /*
     | If no extended controller has been provided for a particular route,
     | a PreloadController itself should be assigned to the route
     */
    controller: AppRouteController,

    /*
     | Options declared on the route will override
     | extended PreloadController and global options!
     */
    'preload': {
        'sync'  : '/plugins/yet_another_fancy_schmancy.js',
        'onSync': function ( filePath ) {
            var file = filePath.replace( /\?.*$/,"" ).replace( /.*\//,"" );

            switch ( file ) {
                case 'yet_another_fancy_schmancy.js':
                        try {
                            return !!YAFS;
                        } catch ( error ) {
                            return false;
                        }
                    break;
                default:
                    return true;
            }
        }
    }
});

Oh, no, not again!

Oh yes - you can speed up loading by caching AJAX loading (globally) with:

$.ajaxSetup({
    cache: true
});

Changelog

v1.2.4

v1.2.3

v1.2.2

v1.2.1

v1.2.0

v1.1.0

v1.0.3

v1.0.2

v1.0.1

v1.0.0

v0.4.0

v0.3.3

v0.3.2

v0.3.1

v0.3.0

v0.2.2

v0.2.1

v0.2.0

v0.1.3

v0.1.2

v0.1.1

v0.1.0

Acknowledgements

Inspired by wait-on-lib

Copyright and license

Copyright © 2014-2015 Miroslav Hibler

miro:preloader is licensed under the MIT license.