2011-12-24

AMD modules with named defines. So much pain for what gain?


Per James Burke's suggestion, starting here a conversation regarding *named* (where module's name is hardcoded in a define within the module) AMD modules.

Found a number of minor issues while using named modules (jQuery, underscore). Went looking for definitive, authoritative explanation for why hard-coding a name into a module *must* be done and found rather little. Hence, bringing this up here as suggested.

The issues (I personally bumped into) with named AMD modules:

1. Precludes legitimate use of multiple references to "same" module:
  a) separate aliases for CDN-to-local failover scenarios: "jquery_from_cdn" and "jquery_local"
  b) separate loading of separate (in)compatible version of the lib: "jquery" vs. "jquery_that_older_version_that_does_not_break_plugins_born_pre_prop-args_split"

The obvious issue here in all these cases is that when require'd  by any other name, not the hardcoded one, module returns Null (tested in RequireJS, Curl.js)

2. Hard-naming modules is utterly redundant, adds to complexity in use.
The only (simple, reliable) way to load a hard-named module now *and make it return non-Null ref* through a require call is to hard-code the alias (path to the module) in the config of the AMD loader:

{
    paths: {
        jquery: 'js/libs/jquery-1.7.1.custom.min'
        , jquery_from_cdn: '//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js'
        , jquery_older: 'js/libs/jquery-1.4.3.custom.min'
        , underscore: 'js/libs/underscore-1.2.3.min'
    }
    , baseUrl: './'
    // etc.
}

And can only be consumed like so:

// Curl.js promise-style API
require(['jquery'])
.then(function($){/* $ is non-Null here because we ask for it by hardcoded name */})

Because, obviously, this one will not work as expected:

// Curl.js promise-style API
require(['jquery_from_cnd'])
.then(function($){/* since '$' is Null here. */})

If the only way to consume the module properly is to force the end-developer to hard-code its name *again* in a config file, at the consumption point, (in that respect only) why waste time, effort and hard-code the name in the module in the first place (let alone cause grief to those devs who DO need to load the module under different name / from alternate sources)?

J Burke pointed out the justifications for hard-naming the two modules in question here (https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon)

These claims provide a good ground for the short discussion of this. I added numbers to each section for ease of reference:

I: They [use named Define] because they are commonly used as dependencies by other libraries, and so it is common for other code to use 'jquery' and 'underscore' as the agreed-upon name for these dependencies. This allows those other dependencies to share the same module instance.


II: jQuery and underscore are also the defacto implementations of the module interface implied by those names.


III: In addition, it is common for them to be used by other third party javascript libraries. Those third party libraries may be loaded outside of an AMD script loader that is used on the page to load the rest of the page logic.If those libraries are not loaded by an AMD script loader and there is an anonymous define call, that could lead to an error in loading, since the anonymous define call cannot be tied to a name that the loader was expecting.


IV: So, to ease into AMD loading on the web, base libraries like jQuery and underscore register as named modules, particularly since they will likely be referred to by those IDs. As AMD loading becomes even more common on the web, at some point those libraries may switch to an anonymous module call.

My (indeed, subjective) thoughts on these (in context of the real-live, browser-side usage of the mentioned named modules):

Claim I  "the names are in common use" - does not necessitate hard-naming.
Interestingly, since the recommended (and only one that works) use pattern now is to set up path alias in the AMD loader's config file anyway, making the module anonymous right now will not break any present AMD set up. Present code will continue to rely on config-time path aliasing. In that respect only, hard-coding the name for this pattern of use is just redundant.

Indeed, I too would only support standardizing the module *name* but only as *recommended coding pattern* at the point of consumption. On top of presently possible aliasing through AMD loader's config, one more "recommended pattern" will become available to end-user-developers when jQuery, underscore become anonymous:

define('jquery',['path/to/it'],function($){return $})

Both, the config-path-aliasing and the standard define above fulfill Claim (I) and provide for use of the module under standardized name, all without hard-naming the module inside the module.

Claim II - "de facto implementations" - does not imply, benefit from or necessitate hard-naming.  Because these are de facto implementations of the interface, there must be room for loading the module in a simple client-side versioning, or failover-cascade patterns, which naming a module precludes - essentially allows only one source for the module. This claim relates to Claim (I) and my thoughts for Claim (I) apply and fulfill needs of Claim (II). Claim II is actually better (more-reliably) fulfilled by anonymous module.

Claim III - "It CAN be loaded outside of  AMD loader. In that scenario we want to avoid double loading" - indeed substantially supports the need to have named module. Although the module will not be loaded twice (since it will be cached by the browser), just, possibly, evaled, at most, twice, it's worth a thought. However, same exact argument applies to any other JavaScript lib that could be loaded outside of AMD, but is AMD-compatible. Do we make them all hard-named?

Actually, we may want jQuery to be evaled second time when it's ran through AMD. With the ongoing discussion of "polluting the global scope, overriding one version with another" evaling (future) AMD-capable (non-polluting) version second time may be the solution, as opposed to a perceived problem.

Claim IV - "hard naming the modules will make adoption of AMD pattern easier"
My experience is the opposite. I could not in-line define the modules to support simple multiple-versions, failover scenarios. The hard-named modules preclude common-sense use of them through common API ( define('jquery', ['/path/to/it'], function($){return $}) ) in favor of obscure, AMD loader-specific config  tricks. Named modules are just a pile of pain - definitely do not help in adoption of AMD patterns.


I guess if i hear something like "Oh, this is the only way to fix / make work such and such framework and there is no other way." it will settle the issue.

Lacking that, as a user, i would hope to see named defines gone soon from jQuery, Underscore, I must admit that it may be too late to turn back the clock on both. Someone, somewhere, in some obscure use-case may rely on hard-named jQuery already and will scream if this carpet is pulled out. However, i don't see named-define in Underscore to gel out there yet, and can, probably, still be pulled out.

In all cases, for the love of all good, please, stop inflicting more hardcoded named defines upon other popular JS libs. Or, at least, write down some substantive blurb justifying the atrocity, so the pain can be relieved by simple Googling.

UPDATE:

Per conversation on AMD Implement list one particular realistic scenario was offered where named modules truly save the day:

In cases where existing jQuery-depending code is enhanced with AMD, and that in-turn again loads (through require) 'jquery', when 'jquery' is anonymous module, AMD loader would blow up with something like "too many anonymous defines in /path/to/jquery.file.js"

Example:


    <script src="js/libs/curl-0.5.4.min.js"></script>
    <script src="js/libs/jquery.js"></script>
    <script src="js/loader.js"></script>

Where loader.js's contents are:


    define(
        'jquery'
        , ['js/libs/jquery']
        , function($){
            return $
        }
    )

    require(['jquery'], function($) {
        // ...

Blows up on that second load of js/libs/jquery

Test3.html in this repo demonstrates the issue.

It seems until AMD loaders fix their handling of double load (first with <script, then with require() ) named modules for some of the most common libs are here to stay.