Search code examples
javascriptrequirejsgrunt-contrib-requirejs

Module injection problems when combining requirejs bundles and requirejs optimizer


I am currently trying to increase the performance of a rather big Angular JS application, by moving from one big chunk of javascript file, to having 3 - 4 smaller ones, that could potentially even be lazy loaded.

I am using the following npm packages and versions:

grunt-contrib-requirejs: 1.0.0
grunt: 0.4.5
requirejs: 2.3.0

Initially, there was one grunt task, which took ALL the required javascript files, included them all into one file using the requirejs optimizer, for that there was one main config file main.js where all the paths and shims for all the specific packages were defined (I can post sample configs etc if required).

Now the idea is to move from having this one big file, to modularize the application into 3 - 4 smaller javascript files, to shorten load time (because loading 4 small files parallel is faster than loading 1 big file).

For that we are using the modules option in grunt-requirejs and the bundles option to define all the modules and their contents.

The project structure is as follows:

/src/
  ./Gruntfile.js
  ./scripts/
    ./app.js
    ./vendor.js
    ./app_mini.js
    ./features/
      ./test.service.js
/dist/

The grunt task is defined like this:

requirejs: {
    bundle: {
      options: {
        baseUrl: '/src',
        mainConfigFile: '/src/scripts/app.js',
        bundlesConfigOutFile: 'scripts/app.js',
        findNestedDependencies: true,
        dir: '/dist',
        modules: [
          {
            name: 'scripts/vendor',
            exclude: [],
            override: {
              {
                paths: {
                  'angular': 'bower/angular/angular',
                  'jquery': 'bower/jquery/dist/jquery'
                },
                bundles: {
                } 
              },
            }
          },

          {
            name: 'scripts/app',
            exclude: ['scripts/vendor'],
            override: {
              {
                paths: {
                  'vendor': 'scripts/vendor',
                  'app_mini': 'scripts/app_mini',
                  'testservice': 'scripts/features/test.service'
                },
                bundles: {
                  'vendor': ['angular', 'jquery']
                }
              } 
           }
        ]
      }
    }
  } 

The dist folder after execution of requirejs:bundle looks like this:

/dist/
  ./scripts/
    ./app.js
    ./vendor.js

So, app_mini and testservice are included into app.js, and angular and jquery are included into vendor.js.

My app.js in the src folder looks more or less like this:

define('app',
  [
    'angular', 
    'app_mini',
    'testservice'
   ],
   function(
     angular,
     app_mini,
     testservice) {}
);

require([
  'angular',
  'app_mini',
  'testservice'
], function (
  angular,
  app_mini,
  testservice
) {
  // entry to app


}, function(err) {
  console.log('error loading module: ' + err.requireModules);
};

I am sorry for the long file contents, but it is impossible to describe the problem without it :) So, the problem is, I would like to have a central place to put the whole Requirejs config for the optimizer and for the app itself. However, obviously the paths for the optimizer are relative to the gruntfile path, and the paths for the running app, are relative to the corresponding file.

As the bundle configuration is automatically generated by requirejs (because of bundlesConfigOutFile), I cannot really do a lot about the names used there, as they get pulled from the modules option.

The bundles config which gets generated and included into app.js looks like this:

require.config({
  bundles: {
    'scripts/vendor': [
      'angular',
      'jquery'
    ],
    'scripts/app': [
      'testservice',
      'app_mini',
      'app'
    ]
  }
});

Now first, I had the problem that RequireJs wanted to load vendor.js by path scripts/scripts/vendor.js

To fix that, I included a baseUrl = '/' into the require.config of app

But now I have a problem, that testservice is not able to inject app_mini and angular, so it is throwing an error there, because both of these modules are undefined.

The optimized app.js in the dist folder looks like this, maybe you can spot the problem:

(function(){


define(
    'testservice',[
        'angular',
        'app_mini'
    ], function(
        angular,
        app_mini
    ) {
        'use strict';
 
        app_mini.factory('testservice', TestService);
 
        function TestService() {
            var service = {
                test: test
            };
            return service;
        };
 
        var test = function() {
            console.log("testservice::test()");
        };
});
 
define('app_mini',
    [
        'angular',
        'testservice'
    ],
 
    function (
        angular,
        testservice
    ) {
        'use strict';
 
 
        var module = angular.module(‘some.project’, []);
 
        module.run(AppRun);
 
        AppRun.$inject = [
            '$rootScope',
            'testservice'
        ];
        function AppRun($rootScope, testservice) {
            console.log(“yay, it works”);
 
            testservice.test();
        }
 
        return module;
    }
);
 
require.config({
    bundles: {
        'scripts/vendor.bundle': [
            'angular',
            'jquery'
        ],
        'scripts/app.bundle': [
            'testservice',
            'app_mini',
            'app'
        ]
    }
});
 
require.config({
    baseUrl: '/'
});
 
 define(‘app’,
    [
        'angular',
        'app_mini',
        'testservice'
    ], function(
        angular,
        app_mini,
        testservice
){});
 
require([
        'angular',
        'app_mini',
        'testservice'
    ], function (
        angular,
        app_mini,
        testservice
    ) {
 
    angular.element().ready(function () {
        var $html = angular.element(document.getElementsByTagName('html')[0]);
        angular.bootstrap($html, [app_mini['name']]);
    });
 
}, function (err) {
    'use strict';
 
    console.log('Error loading requirejs-module: ' + err.requireModules);
});
 
define("scripts/app.bundle", function(){});
 
})();

Solution

  • Alright, I figured that probably noone will read through all this, and I just noticed that I could have pointed out the real problem much better. (It was that app_mini and angular were undefined by the time they got injected into testservice)

    So the problem was just that I completely missed to pass in the shim config, which allows require js to wrap angular and jquery into AMD modules. Because that was missing, angular could never be found, which broke app_mini as well as testservice and therefore also the whole app.

    So I edited the vendor module config accordingly, by adding the shim:

    modules: [
          {
            name: 'scripts/vendor',
            exclude: [],
            override: {
              {
                paths: {
                  'angular': 'bower/angular/angular',
                  'jquery': 'bower/jquery/dist/jquery'
                },
                shim: {
                  angular: {
                    exports: 'angular',
                    deps: ['jquery']
                  }
                },
                bundles: {
                } 
              },
            }
          },