Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Optionally call AMD define() to register module #338

Merged
merged 3 commits into from

9 participants

@jrburke

This issue has come up before, in Issue #287. This pull request is different in these two aspects:

  • The patch itself is different, doing it optionally as part of the global registration branch.
  • I can provide more context on why this is useful to do.

Patch

It is done as part of the "set global" branch, and it does not interfere with that branch, just optionally calls define if it is available. So this should not cause issues with code that still wants to use underscore as a global even when an AMD loader is on the page.

Context

In the previous pull/issue there were concerns that a global define not being part of too many useful JS runtimes.

define() is part of the AMD API which is being adopted by a few toolkits like Dojo, MooTools and EmbedJS. jQuery 1.7 will include a very similar opt-in define() call as specified in this patch. define()-based code can run in Node via the requirejs package, and define() can be used in jetpack/add-on SDK add-ons for Firefox. Firebug 1.8+ uses define().

More reasoning and context for AMD.

@taybin

I would like to see this pulled in as well. AMD is more about namespace protection than asynchronous loading.

@jashkenas
Owner

If Underscore is already being exported globally in the same conditional body ... then what use case will this patch enable, where Underscore isn't already usable?

@jakewins

To shine some light on why this would be really cool, here is how I currently use underscore in my AMD-based projects:

myfile.js:

require(['underscore'], function() {
    // Do stuff using the global "_" reference, hoping no other part of the system has defined "_" globally
});

But with this pull request, AMD loaders can use the "define" factory method to create an underscore instance that isn't globally available:

myfile.js:

require(['underscore-with-amd'], function(_) {
    // Do stuff using the function-local "_" reference
});

You can of course, like you say, inject underscore into the same conditional body using scripts, but then you'd need to include the same source in multiple places if you use underscore in different namespaces. You would also need a custom build system to assemble your application (vs. requirejs that just reads your dependency tree and can both load files individually in the browser, making debugging super-simple, as well as create concatenated single-file apps for production).

@jrburke

In reply to the question from @jashkenas about use cases:

The original patch in this pull request was to minimize the impact on behavior for underscore, but the main use case is to allow not bleeding globals and being able to build contained libraries that use underscore that have their own version of underscore that does not interfere with the global on the page.

This is possible with AMD because the requirejs optimizer can be used to rename the AMD calls to namespace-specific items, or if a single file library is being generated and even the AMD API should not be globally available since there will be no dynamic loading, the almond AMD API shim can be used to completely encapsulate the library without using an AMD loader.

Using _.noConflict does not give 100% coverage in this case -- if the library dynamically loads underscore, in IE, the script onload callback (the first opportunity for the library to call noConflict) can actually fire after other dynamic scripts load, and if one of those dynamically loaded scripts wants a different version of underscore, they could bind to the wrong version, since IE can execute a few scripts before calling the first script's onload handler.

Furthermore, by using string names for dependencies as AMD does, it allows swapping out implementations without the sub-library having to export a specific global name. This is really useful for creating mocks for testing, and in the case of libraries that use jQuery, they can swap in a lighter library that exports jQuery's API/behavior without having to code for a bunch of specific global names.

The original patch I put up required the library developer that wants this case to manually call _.noConflict() in their code if they want these use cases. Not the ideal for the library developer, and it had the noConflict edge case, but the patch caused the minimal amount of behavior change for underscore. In other libraries, having a global particularly for their unit tests was important.

However for underscore, since a new test page that uses an AMD loader is not being included, this is not an issue. I have tested it in an AMD project, and I am willing to own any issues reported to underscore with this registration.

It is actually best if underscore does not export a global if define() is available.

Therefore, I have updated this pull request to not export a global if define() is available. However, if you are uncomfortable with the latest version of the pull request, I can revert it back to the original one.

@jashkenas jashkenas merged commit ebca7f2 into from
@jashkenas
Owner

Thanks for the fix -- putting it in its own conditional makes much more sense.

@iammerrick

This particular commit breaks a work around commonly used for Backbone & AMD.

http://backbonetutorials.com/organizing-backbone-using-modules/

The problem is Backbone.js depends on the global _. Then one would call _.noConflict(). It took me a few hours to figure out what was happening. Quite frustration honestly. What is the new recommendation to use Backbone no without wrapping it in our own define(), I felt the order plugin workaround was quite utilitarian.

The hope is to get a similar patch landed for Backbone so there are no more workarounds. You can use this branch of Backbone to get AMD registration that works well with the latest underscore. Feel free to contact the requirejs list if you would like some other ideas.

I will try and switch to that branch but I'm not sure breaking the existing work arounds and forcing a switch to another branch of Backbone in hopes of a patch landing was the best idea.

Other options: go back to a previous version of underscore, or modify your local copy to do the behavior you are expecting, and wait for a resolution on the other patch. It is difficult to time changes across libraries, each has their own needs and timelines. I can appreciate it may be a bit bumpy for a bit, but hopefully leads to the best solution for the future.

You are right, and I am all for AMD landing in Backbone & Underscore; if it is bumpy along the way its worth it. I apologize if I came off sharp earlier, was after about 3 hours of frustration wondering why the exact setup from a few days before being replicated today was throwing ambiguous unhelpful errors; and eventually dived into the source and found this commit. :-P

@jrburke thanks for pushing for this. AMD FTW. Only downside though is sometimes I still want underscore to be a global, as bad as that may be. But I agree it's better not to leak into the global namespace by default. Looking forward to backbone implementing this as well.

@iammerrick git bisect is your friend - I found this commit in less than a minute thanks to it.

@jrburke thanks for advancing AMD. Kinda wish there was still a way to load underscore with RequireJS and still have it be a global dependency available in all my modules. Maybe global! plugin or something would do the trick.

@iammerrick git bisect is your friend, I found this commit in less than a minute thanks to it.

@geddesign I would argue most people just download the underscore.js file without git, utilizing bisect useless. You are right though I could have pulled, and checked took me a while to realize my work around was failing cause of vendor code.

@geddesign: you can still export the global yourself in you main module, or just any module you know loads before the global use:

define(['underscore'], function () {
window.
= _;
});

@jrburke: I think you meant to say:

define(['underscore'], function (_) {
     window._ = _;
});

Markdown changed the underscores into italics: window. instead of window._

@jrburke yep that's what I'm doing. I'll have to figure something out though for libs that depend on the first, for example
define(['order!jquery', 'order!jqueryUI'], function () {
//jquery 1.7 AMD not global, jqueryUi breaks
});

Maybe it's time I say AMD or nothing, and modify any JS lib I use with a define wrapper.

@KidkArolis

Should

define('underscore', function() {
  return _;
});

because the location of the file doesn't match the moduleId. I use alias in require configuration right now.

Am I missing something?

instead be

define(function() {
  return _;
});

Having that "underscore" there means that I can't do stuff like

define(["vendors/underscore"], function (_) {});
@jrburke

@KidkArolis: I just created a document that explains some of the choices when upgrading an existing library for AMD, and it has a section on named vs. anonymous modules that explains why underscore does a named module call. Feel free to contact the amd-implement list if you want to talk more about the philosophy behind the choice.

@KidkArolis

Awesome. Thanks!

@dvdotsenko

@jrburke

Read the "Unserscore is so important we will use named define(" monologue at https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries

Still did not see a good-looking example of loading underscore from a file-name that is not "[root]/underscore[.js]"

Is there a good example somewhere on how to require underscore that is actually located in a file "js/libs/underscore-1.2.3.min.js" and getting a ref for it in require( callback?

do I have to double-require it?

require(["js/libs/underscore-1.2.3.min.js"], function(_){
    // Whoa! WTF? underscore is null!?
    require(['underscore','myapp'], function(_, myapp){
        // finally, do usefull things with _
    })
})

frankly, i would rather see you go back to anonymous define in unserscore and say to users:

// You want consistent, named module for 'underscore'?  insert the following into your "app_loader.js"
define(
    'underscore'
    , ["js/libs/underscore-1.2.3.min.js"]
    , function(_) {return _}
)

You get the preloading bonus and it works as expected.

Having file named with version and all is rather simple cache-busting approach. Would hate to have underscore require it to be named underscore.js and hosted at root.

@jrburke

@dvdotsenko all AMD loaders allow mapping a module ID to a partial path, usually the configuration is called 'paths', so to do what you want:

requirejs.config({
    paths:
        underscore: 'js/libs/underscore-1.2.3.min'
    }
});

require(['underscore'], function () {});

Since underscore is used by other higher-level modules, like backbone, a common dependency name needs to be used to communicate a common dependency on underscore, and it makes sense to call that dependency 'underscore'. The paths config gives a way to do the mapping to a specific URL you want to use for that dependency.

@dvdotsenko

@jrburke

Sorry to bother. Never mind the comment above. Switched to curl.js and underscore.js just started working with custom paths in config. All good on underscore.js front.

@dvdotsenko

@jrburke

Yes, indeed. path helps. My, albeit subjective, two issues were:
1. I was trying to pass config to inline require deeper in the code (not in index.html) and, somehow, that config did not stick for require( nested deeper in that tree. Deeper calls resolved 'jquery' to a [root]/jquery[.js]. Learned my lesson - need to use global config.
2. Another edge issue - I wanted to have 'jquery' and 'jquery_other' (for funky CDN > local failover at first, then, to play with different versions of jQuery because some of the plugins i use were born before the .args .prop split)

I admit, my use-cases are not mainstream, but was seriously disenchanted with how brittle RequireJS was with modules that define themselved named inside the module. I am happy to inline my own named define(, but, obviously, with hardcoded names in modules that is not going anywhere unless i change the module (not a proposition for CDN-hosted common modules).

@jrburke

@dvdotsenko If a config value is not applied until after the target resource has been initially mentioned I can see there being problems. For loading multiple versions of jQuery in a page, requirejs has multiversion support. Although it sounds like you wanted different versions for timeout recovery, so you may be interested in this recent thread on timeout recovery.

If you are interested in talking through it or working out the use case a bit more, please feel free to start a thread on the requirejs list since we are getting outside the concerns of underscore.

@dvdotsenko

@jrburke

Indeed conversation digressed towards peculiarities of RequireJS, but, the issue of hard-coding a name INTO an AMD module is still nagging me in respect to Unserscore. Where can read up on definitive explanation for why Unserscore MUST be named AMD module, so I would stop nagging you?

@jrburke

@dvdotsenko The updating existing libraries page, anon section attempts to give the reasoning for the named module guidance for libraries like jQuery and underscore.

If you would prefer to discuss the relative merits of this guidance, it would be great if you can start a thread on the amd-implement list. This guidance is not specific to underscore, and we will have the opportunity to engage people who are very motivated to get this right in general for AMD. AMD loader implementers, like John Hann who develops the curl loader, is also on that list.

We can post back any summary that changes the guidance to this bug, or better yet if it results in a possible code change, just follow it up with a targeted pull request citing the new common understanding worked out in the other group.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 17, 2011
  1. @jrburke

    Opt in to AMD registration.

    jrburke authored
Commits on Oct 18, 2011
  1. @jrburke
Commits on Oct 19, 2011
  1. @jrburke

    If register as an AMD module, do not create a global, since the goal …

    jrburke authored
    …is to not leak out globals in that case.
This page is out of date. Refresh to see the latest.
Showing with 8 additions and 0 deletions.
  1. +8 −0 underscore.js
View
8 underscore.js
@@ -56,6 +56,14 @@
exports = module.exports = _;
}
exports._ = _;
+ } else if (typeof define === 'function' && define.amd) {
+ // Register as a module with AMD. Use a named module since underscore
+ // can be used in optimization schemes that do not understand anonymous
+ // modules, and the if it is used on a page with an AMD loader, a load
+ // error could occur.
+ define('underscore', function() {
+ return _;
+ });
} else {
// Exported as a string, for Closure Compiler "advanced" mode.
root['_'] = _;
Something went wrong with that request. Please try again.