Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Javascript - Delaying module return/declaration in Require.js?

I have a module that returns an array comprised of JSON data and image objects. Since both loading the JSON (from other files) and the image objects takes time, I need my module to only return the array once both are completed.

Currently, the module is always returning 'undefined' in other modules, and I believe it is because the module is not waiting to return like I expect it to (but I'm not sure). Or, because the other module using this Atlas module is declaring it as a variable before it has returned anything.

Editted to show how I am defining/requiring modules *Editted again to show more code*

The live code can be seen here.

Here is my tile-atlas module:

define( function() {

var tilesheetPaths = [
            "tilesheets/ground.json",
    "tilesheets/ground-collision.json",
    "tilesheets/objects-collision.json"
];

var tileAtlas = [ ];


function loadAtlasJSON() {  
    for (var i = 0; i < tilesheetPaths.length; i++) {
        loadJSON( 
            {
                fileName: tilesheetPaths[ i ],
                success: function( atlas ) {            
                    addToTileAtlas( atlas );
                }
            } 
        );
    }
};


function addToTileAtlas( atlas ) {
    atlas.loaded = false;
    var img = new Image();

    img.onload = function() {
        atlas.loaded = true;
    };

    img.src = atlas.src;

    // Store the image object as an "object" property
    atlas.object = img;
    tileAtlas[ atlas.id ] = atlas;
}


// Returns tileAtlas[ ] once everything is loaded and ready
function tileAtlasReady() {
    if ( allJSONloaded() && allImagesLoaded() ) {
        console.log("TileAtlas ready"); 
        return tileAtlas;
    }
    console.log("TileAtlas not ready");
    setTimeout(tileAtlasReady, 10);
};


// Checks to make sure all XMLHttpRequests are finished and response is added to tileAtlas
function allJSONloaded() {
    // If the tilesheet count in tileAtlas !== the total amount of tilesheets
    if ( Object.size(tileAtlas) !== tilesheetPaths.length ) {
        // All tilesheets have not been loaded into tileAtlas
        console.log("JSON still loading");
        return false;
    } 
    console.log("All JSON loaded");
    return true;
};


// Checks that all img objects have been loaded for the tilesheets
function allImagesLoaded() {
    for ( var tilesheet in tileAtlas ) {
        if (tileAtlas[tilesheet].loaded !== true) {
            console.log("Images still loading");
            return false;
        }
    }
    console.log("All images loaded");
    return true;
};


// Loads the JSON/images
loadAtlasJSON();

// Module should only return when tileAtlasReady() returns
return tileAtlasReady();

} );

And this is my loadJSON function from my lib:

window.loadJSON = function( args ) {
    var xhr = new XMLHttpRequest();
    xhr.overrideMimeType( "application/json" );
    xhr.open( "GET", args.fileName, true );

    xhr.onreadystatechange = function () {
        if ( xhr.readyState == 4 ) {        

            if ( xhr.status == "200" ) {
                // Check that response is valid JSON
                try {
                    JSON.parse( xhr.responseText );
                } catch ( e ) {
                    console.log( args.fileName + ": " + e );
                    return false;
                }
                args.success( JSON.parse(xhr.responseText) );
            // xhr.status === "404", file not found
            } else {
                console.log("File: " + args.fileName + " was not found!");
            }
        }
    }
    xhr.send();
}   

And the module loading my tile-atlas module:

define( ['data/tile-atlas'], function( tileAtlas ) {

function displayImages() {
    // Code
};

// Returns undefined
console.log(tileAtlas);

displayKey();

} );

And here is what the output looks like:

[12:14:45.407] "JSON still loading"
[12:14:45.407] "TileAtlas not ready"
[12:14:45.408] undefined 

[12:14:45.428] "JSON still loading"
[12:14:45.428] "TileAtlas not ready"

[12:14:45.469] "All JSON loaded"
[12:14:45.470] "Images still loading"
[12:14:45.470] "TileAtlas not ready"

[12:14:45.481] "All JSON loaded"
[12:14:45.481] "Images still loading"
[12:14:45.481] "TileAtlas not ready"

[12:14:45.492] "All JSON loaded"
[12:14:45.492] "All images loaded"
[12:14:45.492] "TileAtlas ready"

The 'undefined' is from when I console.log my Atlas module from a different module, that depends on the Atlas module.

I'm not sure if it is the Atlas module returning something before it should, or the other modules declaring the Atlas module as a variable before it has returned something.

But if it is the latter, is there a way to make it so that modules will not run until their dependencies are finished returning something?

I am completely new to Require.js and AMD: Is this approach inherently flawed? I would think that using AMD with loading-sensitive modules is common.

Thanks for your help.

EDIT Looking at the source code of another game, I realized that I can just load my jSON files with the require.js text plugin, instead of implementing an XHR in my module. This is much simpler, as I won't need to deal with the complexity of waiting for XHRs within a module. Here is how BrowserQuest does so.

like image 471
user2736286 Avatar asked Sep 11 '13 19:09

user2736286


1 Answers

Ok I checked your code out and have a couple of observations.

  • Unless you know exactly what you are doing (or are doing this to learn about XHR), I wouldn't use XMLHttpRequest directly. There are several cross-browser gotchas with this object and so I would use a JS library to help out, jQuery being one of the more obvious ones.

  • I would also try and use a deferred/promise approach rather than callbacks. Again, a library will help you out here. jQuery has these for ajax, as do other libraries like when.js.

So with this in mind, here is some jsfiddle code you might like to look at, which might give you some ideas.

Firstly, there is an ajax abstraction in a new module, which uses when.js deferreds. The module will resolve or reject the deferred depending on whether the XHR was successful or not. Note I have left in the direct XHR use, but I would recommend using a JS library instead.

// --------------------------------------------------
// New module
// Using https://github.com/cujojs/when/wiki/Examples
// --------------------------------------------------

define("my-ajax", ["when"], function (when) {
    // TODO - Fake id only for testing
    var id = 0;

    function makeFakeJSFiddleRequestData(fileName) {
        var data = {
            fileName: fileName
        };
        data = "json=" + encodeURI(JSON.stringify(data));
        var delayInSeconds = Math.floor(8 * Math.random());
        data += "&delay=" + delayInSeconds;
        return data;
    }

    return function loadJSON(args) {
        // Create the deferred response
        var deferred = when.defer();

        var xhr = new XMLHttpRequest();
        xhr.overrideMimeType("application/json");

        var url = args.fileName;

        // TODO - Override URL only for testing
        url = "/echo/json/";

        // TODO - Provide request data and timings only for testing
        var start = +new Date();
        var data = makeFakeJSFiddleRequestData(args.fileName);

        // TODO - POST for testing. jsfiddle expects POST.
        xhr.open("POST", url, true);

        xhr.onreadystatechange = function () {
            // TODO - duration only for testing.
            var duration = +new Date() - start + "ms";
            if (xhr.readyState == 4) {
                if (xhr.status === 200) {
                    // Check that response is valid JSON
                    var json;
                    try {
                        json = JSON.parse(xhr.responseText);
                    } catch (e) {
                        console.log("rejected", args, duration);
                        deferred.reject(e);
                        return;
                    }
                    console.log("resolved", args, duration);
                    // TODO - Fake id only for testing
                    json.id = ("id" + id++);
                    deferred.resolve(json);
                } else {
                    console.log("rejected", args, duration);
                    deferred.reject([xhr.status, args.fileName]);
                }
            }
        }

        // TODO - Provide request data only for testing
        xhr.send(data);

        // return the deferred's promise.
        // This promise will only be resolved or rejected when the XHR is complete.
        return deferred.promise;
    };
});

Now your atlas module looks a bit like this (with Image code removed for clarity):

// --------------------------------------------------
// Your module
// Image stuff removed for clarity.
// --------------------------------------------------

define("tile-atlas", ["my-ajax", "when"], function (myAjax, when) {

    var tilesheetPaths = [
        "tilesheets/ground.json",
        "tilesheets/ground-collision.json",
        "tilesheets/objects-collision.json"];

    // TODO - Changed to {} for Object keys
    var tileAtlas = {};

    function loadAtlasJSON() {
        var deferreds = [];
        // Save up all the AJAX calls as deferreds
        for (var i = 0; i < tilesheetPaths.length; i++) {
            deferreds.push(myAjax({
                fileName: tilesheetPaths[i]
            }));
        }
        // Return a new deferred that only resolves
        // when all the ajax requests have come back.
        return when.all(deferreds);
    };

    function addToTileAtlas(atlas) {
        console.log("addToTileAtlas", atlas);
        tileAtlas[atlas.id] = atlas;
    }

    function tileAtlasesReady() {
        console.log("tileAtlasesReady", arguments);
        var ajaxResponses = arguments[0];
        for (var i = 0; i < ajaxResponses.length; i++) {
            addToTileAtlas(ajaxResponses[i]);
        }
        return tileAtlas;
    };

    function loadAtlases() {
        // When loadAtlasJSON has completed, call tileAtlasesReady.
        // This also has the effect of resolving the value that tileAtlasesReady returns.
        return when(loadAtlasJSON(), tileAtlasesReady);
    }

    // Return an object containing a function that can load the atlases
    return {
        loadAtlases: loadAtlases
    };
});

And your app (or my fake demo) can use this code in the following way:

// --------------------------------------------------
// App code
// --------------------------------------------------

require(["tile-atlas"], function (atlas) {
    console.log(atlas);

    // The then() callback will only fire when loadAtlases is complete
    atlas.loadAtlases().then(function (atlases) {
        console.log("atlases loaded");
        for (var id in atlases) {
            console.log("atlas " + id, atlases[id]);
        }
    });
});

And dumps out the following sort of info to the console:

Object {loadAtlases: function}
resolved Object {fileName: "tilesheets/ground-collision.json"} 3186ms
resolved Object {fileName: "tilesheets/ground.json"} 5159ms
resolved Object {fileName: "tilesheets/objects-collision.json"} 6221ms
tileAtlasesReady     [Array[3]]
addToTileAtlas Object {fileName: "tilesheets/ground.json", id: "id1"}
addToTileAtlas Object {fileName: "tilesheets/ground-collision.json", id: "id0"}
addToTileAtlas Object {fileName: "tilesheets/objects-collision.json", id: "id2"}
atlases loaded
atlas id1 Object {fileName: "tilesheets/ground.json", id: "id1"}
atlas id0 Object {fileName: "tilesheets/ground-collision.json", id: "id0"}
atlas id2 Object {fileName: "tilesheets/objects-collision.json", id: "id2"}
like image 126
Paul Grime Avatar answered Oct 12 '22 23:10

Paul Grime