Doing this as a gist, because I don't have time for a polished post, I apologize:
If you had to define an optimal module system that had to work with async, networked file IO (the browser) what would that look like? It would not be node's system, for the following reasons:
- the
require('')
in node is very imperative. It can be called at any time and mean a module should be synchronously loaded and evaluated when it is encountered. How would this work if this call is encountered in the browser case:require(someVariable + '/other')
? How would you know what module to include in any bundle? For those cases, you should allow for an async require to fetch, and leave dependencies that can be bundled to therequire('StringLiteral')
format. Node has no allowance for this, and browserify will not do a good job with those types of dependencies. It will need hints, or explicit out-of-module listing of what to include.
Once you have an MVC system in the browser, it will be common to delay loading of a view and its controller until it is needed, and rely on routing via URLs or button actions to know what module to imperatively ask for next. This works well in AMD since it has a callback-style require for this. This really helps performance: the fastest JS is the JS you do not load. This is a very common practice in use with AMD systems. browserify cannot support it natively. The suggestion when using browserify is to choose your own browser script loader and figure out the loading yourself.
- Similarly, since there is no synchronous module fetching and execution in the browser (at least it would be madness to want to do that), you cannot reliably preload a modifier to the module system so that it takes effect for the rest of the module loading. Example in node is requiring coffeescript in your top level app so that any .cs files are considered in module loading.
Instead, AMD has loader plugins that allow you to specify the type of resource that is being requested. This is much more robust, and leads to better, explicit statements of dependencies. Plus, loader plugins can participate in builds. This is better than the browserify plugins for this type of thing because of the imperative nature of JS code: fs.readFile(someVariable + 'something.txt')
.
Static declaration of dependencies is better for builds. So if you like builds, favor a system that enforces static declaration, and has an async form for non-declarative module uses. ES modules will go this route, and explicitly not use node's module system because of this imperative dependency issue that node has.
The parts that are ugly about AMD:
- It explicitly asks for a function wrapper. Node actually adds one underneath the covers when it loads modules. browserify does too when it combines modules. While it would be nice to not have the function wrapper in source form, it has these advantages:
a) Using code across domains is much easier: no need to set up CORS or restrict usage to CORS enabled browsers. You would be amazed by how many people would have trouble with this as it is a hidden, secondary requirment. So, they are developing just fine on their local box, do a production deployment, then things don't work. This is confusing and not obvious as to why it fails. You may be fine with knowing what to do for this, but the general population still has trouble with this.
b) Avoids the need to use eval(). Eval support in the browser is uneven traditionally (scope differences), and now with CSP makes it even more of a hazard to use. I know there has been at least once case of a "smart" proxies that would try to "protect" a user by stripping out eval statements in JS.
In short, the function wrapper is not the ideal, but it avoids hard to trace secondary errors, and it really is not that much more typing. Use the sugar form if you want something that looks like commonjs/node.
- Possibility for configuration blocks: front end developers have much more varied expectations on project layout. Node users do not. This is not the fault of requirejs or other AMD loaders though. At the very least, supporting a configuration block allows more people to participate in the benefits of modular code.
However, there is a convention in AMD loaders of baseUrl + module ID + '.js'
. If a package manager lays out code like this, then there is no config block needed in an AMD loader. volo does this.
npm could even do this to bridge the gap: when installing a package called dep
, put it at node_modules/dep
and create a node_modules/dep.js
that just requires dep
's main module. That would also then work for AMD loaders (if the modules installed were AMD compatible or converted) if the baseUrl was set to node_modules
.
So, it is entirely possibly to stick with the AMD convention and avoid config blocks. Package manager tools have not caught up yet though. Note that this is not the fault of the core AMD module system. This is a package manager issue. And frankly, package managers have been more focused on layouts that make it easy for them vs. what is best for the runtime use and that avoid user configuration. This is the wrong decision to make. Making it easier for users and runtimes does not actually make it that much more complicated for the package manager.
On package managers:
It is important to separate what a package manager provides and what the runtime module system provides. For example, it is possible to distribute AMD-based code in npm. amdefine can help use that code in a node environment. Particularly if the dep.js
style of file is written out in the node_modules directory.
I would suggest that npm only be used for node-based code though, to reduce confusion on what can be used where. Particularly given the weaknesses of node's module system for browser use. However, some people like to distribute code targeted for the browser in node because they like npm. So be it.
But also note that a strength for node-based npm use, nested node_modules for package-specific conflicting dependencies, is actually a weakness for browser use: while disk space is cheap for node uses, delivering duplicate versions of code in the browser is really wasteful. Also, there is not a need for compiled C code dependencies in the browser case. So some of npm's capabilities around that are unneccessary.
It would be better to use a different type of package manager for front end code that tried to reuse existing module versions installed, possibly warn the user of diffs, but if really needed, then write out an AMD map config in the case that it is really needed.
In closing:
My biggest complaint is that node explicitly ignored browser concerns when designing itself and choosing its module system, but then some people want to use those design decisions and force them on browser use of modules, where they are not optimal. Just as node did not want to compromise to meet existing browser uses of JS, I do not want to use a less-capable module system in the browser.
I am hopeful that ES modules will have enough plumbing to avoid the function wrapper of AMD. But be aware that ES semantics will much more like AMD's than node's. And the ES module loader API will be robust enough to support config options and AMD loader plugins.
But note: this is not to say that someone cannot use node modules with npm and browserify to make something useful that runs in the browser. Far from it. But it will be restricted to the constraints above. There are still wonderful things that fit in that box, so more power to them for constraining themselves to that box and still shipping something useful. There is just more to browser-based module usage though. And I do not want their unwillingness to address that wider world as a reason to accept less for myself. The good news is that the internet is big enough for both sets of users. So let's all hug and move on to just making things.
(note this is a gist, so I am not notified of comments. I may delete this at some point if I get around to doing something more polished for a blog post, or just do not want to see its rough form any more)
For the many arguments about how 3 versions of jQuery is a desirable feature, I don't believe that any of you would ship an app that worked like this. Either way, it's the wrong argument to make that it's a good feature because it's entirely possible in AMD, just as it is in node+browserify. It takes a line of configuration, because no one thinks it's good default behavior, but in throw away code, I've certainly used it. http://requirejs.org/docs/api.html#config-map -- so this isn't some superior feature in browserify or node modules, most front-end devs just recognize that it's not a good practice, and therefore it's not default behavior.
To the point that has also been brought up that it's the fault of jQuery being monolithic that it doesn't work in the system, sure. I totally agree. I pushed for this in jQuery core during the jQuery team meetings and jQuery is more modular now (with AMD). I wrote the original proof of concept for a fully tiny modular jQuery 2 years ago, long before npm/browserify: https://github.com/SlexAxton/jquery/tree/modular/src/jquery/core -- https://github.com/SlexAxton/jquery/blob/modular/src/jquery/core/noop.js -- it's a good practice indeed.
However, I think that the only problem that this solves for browserify is that instead of having 3 versions of one big piece of code, you end up with 3ish versions of a lot of smaller code. I don't know what your node_modules folders look like, but even after a
dedupe
it's anything but minimal.AMD isn't some sort of crazy one-off set of old java dev programmers that have big monolithic apps as @mikeal loves to paint it, that's such a weird stereotype of AMD. I never wrote big monolithic apps. AMD is much more popular than people are giving it credit here. I'm not sure why it's an argument that AMD is for big monolithic apps and node modules are for nimble cool quick coolguy apps, but you're just projecting. They're modules, guys. The npm ecosystem is pretty cool, but it totally works with AMD and a config file, so I don't have any issues with small discoverable modules in my coolguy non-monolithic apps.
Absolutely not. But they don't need to be running a node server in order to develop their application. This is one of my beefs with npm/browserify. It assumes my stack is node. It's great for a node stack probably. Not that AMD also wouldn't be great, but AMD can be great in other stacks as well, while I've found browserify significantly more cumbersome (but possible, with the exception of a static file server). I love developing with
serve .
.We don't build during dev but our code isn't any different between dev and production, it's just concatenated. That's why it's beautiful. There are plugins that allow you to do what browserify calls 'transforms' now, but for the most part, the code you load runs the same way it runs always. The wrapper executes, saves the function into a hash, and grabs its dependencies out of the hash. If ever the dependency isn't in the hash, it makes an async request and waits til it's in the hash.
Async loading is used in every AMD app that I've ever built. When you run the build, you can declare things to end up there or not. With this power, you can build up logical blocks of code that only need to be loaded in the parts of the app where you need it. For your concrete example, Bazaarvoice Ratings and Reviews loads on every product page on its clients' web pages. However, only some insanely small percentage of people ever click on the 'write a review' button. It would be silly for us to package that in the initial download, so we don't.
The simple code for this would look like this:
It's async by default, it works just like a dependency. It's the same code that runs in devmode with the same loader doing the same stuff. The require.js runtime is the same as the require.js dev time. There aren't any surprises here.
Here's a gif in case that wasn't concrete enough:
There are two ways to parse your first sentence that have different points of view. I think you are saying that a module loader shouldn't have to solve this problem. I think require.js agrees and does not.
I appreciate that you were around when this stuff was going down, but this just isn't true. I can't count the number of times people talk about how they didn't have to worry about the constraints of the browser. The fact that browserify didn't come until years later from someone outside of this original group only makes that picture more clear. Node modules get some things right in the browser because it's all javascript and it's a good module system, but it can't be backronym'd into being on purpose.
You keep showing that graph. I don't think it means what you think it means. That node is popular and has a module system does not automatically make that module system best, and it especially doesn't make it best for the browser. Not to mention that there are countless AMD modules in npm along with every other conceivable module type.
I don't think anyone will read much farther so I'll stop.
If I may paint my own projection: people who run node.js full-stack and completely buy into the node and npm ecosystem and also probably usually don't have that much to do on the front-end can and should use npm/browserify for their front-end module/build system, for anyone who needs to write a significantly complex application that does not need to rely on a compatible backend, there's AMD. AMD is for the web.