Shim Twitter Bootstrap for RequireJS

2019-01-12 23:54发布

问题:

The RequireJS docs say that to support older versions of IE, you need to configure enforceDefine: true.

So if you want to support Internet Explorer, catch load errors, and have modular code either through direct define() calls or shim config, always set enforceDefine to be true. See the next section for an example.

NOTE: If you do set enforceDefine: true, and you use data-main="" to load your main JS module, then that main JS module must call define() instead of require() to load the code it needs. The main JS module can still call require/requirejs to set config values, but for loading modules it should use define().

Since Twitter Bootstrap isn't an AMD module, I need to shim it for it to work. This is how I configure it;

<script type="text/javascript">
    var require = {
        paths: {
            "bootstrap": "../bootstrap",
            "jquery": "../jquery-1.8.2"
        },
        shim: {
            "bootstrap": ["jquery"]
        },
        enforceDefine: true
    };
</script>

Later when my module wants bootstrap as a dependency, I still end up with an error message;

Error: No define call for bootstrap

http://requirejs.org/docs/errors.html#nodefine

If I've understood the docs correctly, enforceDefineshould ignore shims but it's not.

What am I doing wrong here?

回答1:

According to the docs that error is thrown if "Script was part of a shim config that specified a global string property that can be checked for loading, and that check failed."

To fix this you need to add exports value in the shim config so that RequireJS can check whether the script was succesfully loaded. In case of Bootstrap that's slightly tricky as Bootstrap doesn't 'export' a propert global variable only a bunch of jquery plugins, but you can use any of those plugins as an export value e.g. $.fn.popover:

{
    paths: {
        "bootstrap": "../bootstrap",
        "jquery": "../jquery-1.8.2"
    },
    shim: {
        "bootstrap": {
          deps: ["jquery"],
          exports: "$.fn.popover"
        }
    },
    enforceDefine: true
}


回答2:

Instead of doing the magic with shim, I converted the bootstrap JS into a module:

define([ "jquery" ], function($) {
  // bootstrap JS code
});

Everything else I found in the forums and on stackoverflow did not work for me because I get jQuery from the CDN. I assume because I hit the issue as described in the requireJS doc on http://requirejs.org/docs/api.html

Do not mix CDN loading with shim config in a build. Example scenario: you load jQuery from the CDN but use the shim config to load something like the stock version of Backbone that depends on jQuery. When you do the build, be sure to inline jQuery in the built file and do not load it from the CDN. Otherwise, Backbone will be inlined in the built file and it will execute before the CDN-loaded jQuery will load. This is because the shim config just delays loading of the files until dependencies are loaded, but does not do any auto-wrapping of define. After a build, the dependencies are already inlined, the shim config cannot delay execution of the non-define()'d code until later. define()'d modules do work with CDN loaded code after a build because they properly wrap their source in define factory function that will not execute until dependencies are loaded. So the lesson: shim config is a stop-gap measure for for non-modular code, legacy code. define()'d modules are better.

Converting the bootstrap into a ordinary AMD module and removing the shim config solved it for me. Only drawback: you cannot retrieve bootstrap from the bootstrap CDN.



回答3:

I use this configuration inside my project:

startup.js

require.config({
    paths: {
        /* other paths are omitted */
        'bootstrap': '../libs/bootstrap'
    },
    shim: {
        'bootstrap/bootstrap-slider': { deps: ['jquery'], exports: '$.fn.slider' }, 
        'bootstrap/bootstrap-affix': { deps: ['jquery'], exports: '$.fn.affix' },
        'bootstrap/bootstrap-alert': { deps: ['jquery'], exports: '$.fn.alert' },
        'bootstrap/bootstrap-button': { deps: ['jquery'], exports: '$.fn.button' },
        'bootstrap/bootstrap-carousel': { deps: ['jquery'], exports: '$.fn.carousel' },
        'bootstrap/bootstrap-collapse': { deps: ['jquery'], exports: '$.fn.collapse' },
        'bootstrap/bootstrap-dropdown': { deps: ['jquery'], exports: '$.fn.dropdown' },
        'bootstrap/bootstrap-modal': { deps: ['jquery'], exports: '$.fn.modal' },
        'bootstrap/bootstrap-popover': { deps: ['jquery'], exports: '$.fn.popover' },
        'bootstrap/bootstrap-scrollspy': { deps: ['jquery'], exports: '$.fn.scrollspy'        },
        'bootstrap/bootstrap-tab': { deps: ['jquery'], exports: '$.fn.tab' },
        'bootstrap/bootstrap-tooltip': { deps: ['jquery'], exports: '$.fn.tooltip' },
        'bootstrap/bootstrap-transition': { deps: ['jquery'], exports: '$.support.transition' },
        'bootstrap/bootstrap-typeahead': { deps: ['jquery'], exports: '$.fn.typeahead'  },
    }
});

require(['domReady', 'app'], function(domReady, app) {
    domReady(function() {
        app.init();
    });
});

then in my code I use this:

define(['jquery', 'underscore', 'backbone', 'text!templates/photos-list.html'], function($, _, Backbone, html) {
    var PhotosListView = Backbone.View.extend({
        viewImageFullscreen: function(e) {
            e.preventDefault();

            require(['bootstrap/bootstrap-modal', 'text!templates/photo-modal.html'], function(modal, htmlModal) {
                 var modalTemplate = _.template(htmlModal, options);
                 $('body').append(modalTemplate);

                 // setup
                 $(selector + '_modal').modal({
                     backdrop: true,
                     keyboard: true,
                     show: false
                 }).css({
                     'width': function() { return ($(document).width() * 0.55) + 'px'; },
                     'margin-left': function() { return -($(this).width() * 0.5); }
                 });

                 // trigger `modal` 
                 $(selector + '_modal').modal('show');
             }); // require() call
         // ...


回答4:

@lexeme & @benjaminbenben how about wrapping this concept in a RequireJS plugin which creates the shim, requires jQuery and also returns jQuery so you wouldn't need to include this manually?

To use a bootstrap component you would simply use:

define(['bootstrap!tooltip'], function($){
  $('[data-toggle="tooltip"]').tooltip();
});

And you would use this require-bootstrap-plugin to make it work.