My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Sunday, January 15, 2012

Y U NO use libraries and add stuff

This is an early introduction to a project I have been thinking about for a while.
The project is already usable in github but the documentation is lacking all over the place so please be patient and I'll add everything necessary to understand and use yuno.

Zero Stress Namespace And Dependencies Resolver

Let's face the reality: today there is still no standard way to include dependencies in a script.
If we are using a generic JS loader, the aim is to simply download files and eventually wait for one or more dependency in order to be able to use everything we need.
The require logic introduced via node.js does not scale in the browser due synchronous nature of the method itself plus the sandbox not that easy to emulate in a browser environment.
The AMD concept is kinda OKish but once we load after dependencies, there is no way to implement a new one within the callback unless we are not exporting.
I find AMD approach surely the most convenient but still not the best one:
  1. we cannot implement a provide like procedure, whenever this could be handy or not
  2. it's not clear within the module code itself, what we are exporting exactly

Specially the last point means that AMD does not scale properly with already combined code because AMD relies in the module/folder structure itself ... so, cannot we do anything better than what we have so far?

The yuno Concept


Directly out of a well known meme, yuno logic is quite straightforward:
  • automagically resolved path, you point once to yuno.js file in your page and you are ready to go
  • compatible with already combined files (smart builder coming soon)
  • yuno.use() semantic method to define dependencies, if necessary
  • yuno.use().and() resolved callback to receive modules AMD style once everything has been loaded
  • yuno.add() standard ES5 way to define new namespaces, objects, properties, or constructors ( so no extra note in the documentation is needed )
  • cross referenced dependencies automagically resolved: if two different scripts needs same library, this will be loaded once for both
  • external url compatible, because you may want to include a file from some known CDN rather than put all scripts in your own host ( speed up common libraries download across different libraries that depend on same core, e.g/ jQuery )
  • modules, namespaces, or global objects, cannot be reassigned twice, which means if we are adding twice same thing we are doing it wrong, but if we are not aware of other script that added same thing before we have a notification
  • something else I may decide to add after this post
Here some example:

// define a jQuery plugin
yuno.use(
"jQuery",
"extraStuff"
).and(function (jQuery, extraStuff) {
yuno.add(jQuery.fn, "myPlugin", {value:function () {
// your amazing code here
}});
// we may opt for just this line
jQuery.fn.myPlugin = function () {};
// in order to export our plugin
// however, the purpose of yuno is to have
// a common recognizable way to understand
// what the module is about
// plus the "add" method is safer
});

Let's imagine that extraStuff contains similar code:

// define extraStuff
yuno.use(
"jQuery"
).and(function (jQuery) {
yuno.add(jQuery.fn, "extraStuff", {value:function () {
// your amazing code here
}});
});

Both plugin and extraStuff needs jQuery to be executed ... will jQuery be loaded twice? Nope, it's simply part of a queue of modules that needs to be resolved.
As soon as it's loaded/added once, every module that depends on jQuery will be notified so that if the list of dependencies is fully loaded, the callback passed to and will be executed.

Y U NO Add

Modules are only one part of the proposal since we may define a script where no external dependency is needed.

// note: no external dependency, just add
yuno.add(this, "MyFreakingCoolConstructor", {value:
function MyFreakingCoolConstructor() {
// freaking cool stuff here
}
});
// this points to the global object so that ...
MyFreakingCoolConstructor.prototype.doStuff = function () {
// freaking cool method
};

The yuno.add method reflects Object.defineProperty which means for ES5 compatible browsers getters, setters, and values, are all accepted and flagged as not enumerable, not writable, and not configurable by default.
Of course we can re-define this behavior but most likely this is what we need/want as default in any case ... isn't it?
For those browsers not there yet, the Object.defineProperty method is partially shimmed where __defineGetter/Setter__ or simply the value property will be used instead.
Bear in mind this shim may change accordingly with real needs but so far all mobile browsers should work as expected plus all Desktop browsers except IE less than 9 ... not so common targets for modern web sites.
Last, but not least, yuno logic does not necessarily need the add call so feel free to simply define your global object or your namespace the way you want.
However, as I have said before, the add method is a simple call able to make things more robust, to speedup and ensure notifications, and to use a standard, recognizable pattern, to define our own objects/functions being sure nobody did before thanks to defaults descriptor behavior which is not writable and not configurable indeed.

To DOs

This is just an initial idea of what the yuno object is able to do but few things are in my mind. On top of the list we have the possibility to shortener CDN calls via prefixes such "cdn:jQuery", as example, in order to use most common CDNs to load widely shared libraries.
Last, but not least, the reason I am writing this is because I am personally not that happy with any solution we have out there so if you are willing to contribute, please just leave a comment, thanks.

5 comments:

James Burke said...

"we cannot implement a provide like procedure, whenever this could be handy or not"

When is this actually handy though? And is it actually worth the extra boilerplate? What capabilities are lost by not having this?

The define() call and its return value/use of "exports" is the act of providing functionality in AMD. Most importantly, this can be done without the module defining the functionality giving a named to it. This helps avoid conflicts.

"it's not clear within the module code itself, what we are exporting exactly"

The return statement, or the use of exports in an AMD module indicate what is being exported. Why is this not sufficient?

If the concern is that a module may modify another object and you do not want that to happen more than once, I do not believe the add() concept pays off:

1) The only case .add() is useful is if two different modules provide the same functionality, but they actually resolve to two different IDs. It is far more likely they will resolve to the same ID.

In an AMD loader, the module name would be the same, and usually just the first define() call is executed, or the loader could choose to error out. In any case, the conflict is resolved (first one wins) or the developer is notified of the conflict (as I expect .add() would).

2) The .add concept would only help if modules that resolved to two different IDs modified the same resource. This is much less likely, but it could happen. But then what is the mitigation?

.add() will probably throw an error. An error may happen in the AMD case, or it may not. If it is a non-destructive double add, then there is no problem. But if it is destructive second add, an error will still occur. It may be harder to track down, but given the likelihood of this possibility, I think this is a fine tradeoff since .add() cannot catch all double-adds -- developer is *required* to call add() before modifying an object.

.add() creates a tax on module authors that do things right, but does not fully prevent module authors who do it wrong from doing it wrong.

For objects that are shared, like jQuery, if that object wants to upgrade to defineProperty/freeze things for its fn modification, that can be handled independently of the module system. IOW, that is a detail of that object API, not the module system. It would want to do that anyway in case a module did not call .add() but still added something to jQuery.fn.

Furthermore, the .use().and() concept encourages the use of globals for module exports, or having a module name itself. This is not scalable, and leads to conflicts where there does not need to be ones if the module exports its functionality without a name.

AMD's anonymous modules allows things like being able to use zepto.js instead of jquery.js but without having consuming modules look for both jQuery and Zepto. It also allows for two separate module "containers" on a page load two different versions of a module.

Andrea Giammarchi said...

as I have said, AMD does not scale with pre built packages ... does it?

Let's say I want to create a script that already includes all packages, with yuno everything will work simply fine, with AMD or require it will not.

add accept a descriptor object, if you want to make the object modifiable then pass writable and configurable as true, no problem there.

add does not require usage of globals but I understand your point so that "this" inside the callback will point to the global shared object.

AMD as well as require do not load same module twice, does it? It should not, since the structure of the project would be based on folders and per one module there should be a single file so, AFAIK, .add() is much more useful here to redefine namespaces or modules.

I should write more examples and this is coming soon, all I can say is that I don't see side effects with an explicit add or a magic assignment, without add() call, while I find AMD style to bring dependencies through arguments handy having through and() or direct assignment the possibility to include all projects in a single file, minify, and distribute.

Can we do the same with AMD?

Miller Medeiros said...

TL;DR Start using AMD and be happy...

Nowadays most of my projects have 100-200 JS files during development and 1-10 during deploy, the manual namespace approach or concatenating files manually won't scale that far without lots of headaches... In my opinion you are trying to solve a problem which was already solved better by AMD.

I feel that most people who criticize AMD never really used it or didn't even spent enough time thinking how much it could improve the development workflow, after the first project there is no going back... Cheers.

Andrea Giammarchi said...

maybe you read then you re-comment? Never said AMD is bad, I have said it does not scale when it comes to packaging as it is, am I wrong?

James Burke said...

Andrea: You mentioned:

"as I have said, AMD does not scale with pre built packages ... does it?"

I think this is where the disconnect is. I am not sure what that means, maybe you could expand on it.

However, AMD is designed to deliver built files that have multiple modules defined in it.

AMD allows you to create anonymous modules for development, but then an AMD-aware optimizer, when it traces the dependencies and then concatenates the files together, inserts a name for the module as the first argument passed to define().

Maybe that explains the missing piece? If not please feel free to expand on the case you think would fail in AMD.

Also, as you mention, if a module ID has already been loaded, it will not be loaded again by an AMD loader.

However, a file that has some built resources/multiple modules in it could get loaded, and one of those modules could have a define() call that matches an ID of a module that was already loaded separately by itself.

In that case, behavior is loader specific, but requirejs just ignores the second one. However a loader could choose to throw an error in that case.