Wednesday, July 25, 2012

On client components for web apps

This is a response to a blog post by TJ Holowaychuk about browser-based components for web applications, and Isaac's notes on TJ's post.

I am going to try to make this brief because I get tired of people in the Node community wanting to apply the same patterns from Node in the browser. I feel like I say these things on a periodic basis, but human communication is hard, and I certainly could do better. But I also want to get back to just making things. So this will be terser than I normally would like.

Web components

I suggest TJ look at volo, my attempt in this space. It does lots of what he describes already, and it can even be used as a module in another command line tool. We use volo for some things in Mozilla already.

volo uses GitHub as the component registry. It does so without the downsides that TJ mentions.

Specifically, volo uses the GitHub HTTP API to get version tags, do registry searches. I grabs .zip snapshots for a given version/github commit/branch, so the command line tool (the consumer) does not need use git. Git is not necessary on the client side.

This means the downloaded code is smaller -- no need to get a full repo and all of its commits.

volo also understands dependencies via the shorter "owner/repo/tag" IDs instead of the full github URLs.

It has a "shim" repo that means it can support installing components without needing the author of the component to conform to some new publishing system. Since it allows {version} replacement in URLs, the registry setup just needs to be done once. From then on, normal best-practice versioning via git tags is enough.

Some other notes in this post.

Base module format

Node bros, the AMD trolling is getting tiresome. Node's module system is woefully under-specified for web-based loading. While you can limp along with browserify, there are still these issues:

* For builds you need a wrapped format. For CDN deployment you need a wrapped format. browserify uses a wrapped format. AMD anyone? For that reason alone, AMD will never go away. Get used to it already.
* Web code needs a callback-style require for on-demand loading.
* Browserify's uses of file paths for IDs is awful for mixed local and CDN-based loading. Module IDs need to stay in ID format, not translated to a specific file path.
* Loader plugins reduce the need for callback-style APIs, and callback pyramid of doom, or inside-out callback hell, or the need for promise based programs. This more than makes up for the extra level of indent in AMD.

Loader plugins solve the translation issues TJ talks about, and they can participate in optimization builds, meaning templating engines can inline the JS function form of the template. Ditto for language transpilers like CoffeeScript.

By doing this:

define(function (require) {
    //node module code in here.

    //Return module value instead
    //of needing `module.exports`

you have an AMD module.

Quit dismissing AMD for surface issues. AMD avoids mandating translation layers that lead to more things for the developer to understand and fix, and more process for the user to go through to deploy code. It is a net win when the source file works when deployed anywhere, without requiring specialized builds/converters.

Even if you want to personally use Node style and always do builds before loading in the browser, AMD is a great target for the built, wrapped format. You can even use the requirejs optimizer to do this, with the cjsTranslate option.

The universal module boilerplate gets simpler when Node supports AMD's define along with Node's existing module format. If you want to help improve the ugliness, start there.

AMD comes from real world exprience in Dojo with trying to deploy an unwrapped module format that depended on XHR+eval in dev and a wrapped format for builds. Yes, you can something to to work but the second order translation and support costs are not worth it. Some environments disallow eval. CORS configuration is awkward, and potentially hazardous if your API is on the same domain and CORS is done incorrectly.

The simplicity of the complete module lifecycle is worth the function wrapping. Quit looking just at what you type once, and consider the complete code lifecycle, and how much time could be wasted there.

npm's registry as the component registry

The implied rules with npm and node's module behavior are not good fits for front end web development:
  • Forcing a directory structure is complicating project layout and loading for web-based projects. It should be possible to publish and install single JS libraries as single files. volo can do this.
  • Related: the "index.js" convention is awful for web development and debugging. Debugging 'jquery.js' instead of trying to find 'jquery/index.js' in the web tools? No thank you.
  • npm's registry namespace is already polluted. Check searches for 'jquery'. Maybe that just means having a separate npm registry-based registry for client code. But if there needs to be a separate repo, might as well use one that can adapt better to front end development. Like single JS/CSS file installs without extra Java-esque directory structures and metadata debris on the file system.
Ender is not a success story for using npm. Ask the Ender folks how well that worked out. It only works because it is small scale.

By using GitHub, it comes with user auth handled, private repos, and robust social tools that will not be matched by a something like npm because the financial incentives are not there. Plus developers already use it. For simple open source sharing, make it easy without introducing more things in the middle.

Github as the registry is not perfect, and we still need some standalone servers that can be run inside corporations/for mirrors, but I would model those standalone servers on the github API. At least the default case of a public repo can be bootstrapped very quickly.


TJ Holowaychuk said...

That's a good call on using the github API, that of course couples with Github, but who cares :p I wouldn't complain about that part. I'll take a closer look at volo

Fran├žois Marier said...

It would be nice if somehow all that was needed is what's offered by a regular git repo through its git:// or http transport.

That way, repositories could be hosted anywhere and volo would lose its proprietary dependency.

James Burke said...

Fran├žois Marier: it is possible to add git URL support to volo. It understands http and https URLs to single JS files and to zip files. Adding git would just be another protocol resolver.

Denny said...

Can volo resolve versions based on the tags in a given git repo?

For instance, I tell volo my versions are tagged "vX.Y.Z" and it asks each repo for a list of tags, installing the latest version that satisfies a semver spec in the package.json file.

One of the primary distinctions of a browser-friendly dependency system is that shared modules cannot be localized. We don't have the luxury of sticking a bunch of copies into subfolders. We have to resolve certain things in a global space.

Considering this, an extremely useful function would be to find the intersection of a set of dependencies across modules. If it turns out that two modules rely on a common module but use incompatible versions, the resolver would fail and I'd get a list of conflicts.

Any plans for this type of thing in volo?

James Burke said...

Denny: volo does understand vX.Y.Z and X.Y.Z tags as versions, and if not instructed to fetch a particular version (or any other existing branch/tag name), it will choose the latest version tag release.

Right now the version resolution is very dumb in volo: basically the first time the dependency is encountered, it is downloaded, and further requests for it will result in an "already exists" message.

In practice, for many web apps, this is fine, dependency trees for common libs are fairly small.

However, I agree that it would be nice to do the version analysis and then ask the user what version they want to install if there is a conflict. I filed this ticket to track that.