One of my responsibilities on the Play team is to enhance the JavaScript development experience. We will shortly be releasing a strategy document on what is coming for Play 2.3 in this regard. As a preview though, one of the things the strategy will be advocating for is the use of WebJars. WebJars are JavaScript web libraries deployed to well known repositories including Maven Central. There are many popular JavaScript libraries already available as WebJars and the number is increasing.
Preamble
Why is there a need for WebJars? Managing the versions and dependencies of JavaScript libraries is just as important as for any other language. WebJars utilises familiar and established repositories instead of relying on newly introduced ones. I don't think that this can be understated; many organisations are already using and hosting Maven and Ivy based repositories so it makes sense to leverage them. To further support this, over 8 billion downloads occurred on Maven Central in 2012.
Why is dependency management important for JavaScript libraries? Some libraries are standalone of course but others are not. Many popular libraries have dependencies and it is the responsibility of the developer to source them and ensure they are available before the target library is sourced e.g. the popular bootstrap library depends on jQuery. Having a system that automatically manages the complexities of dependency management makes the JavaScript programmer more productive. WebJars enable such systems to be used.
The Play Framework makes it easy to build web applications with Java & Scala. Play is based on a lightweight, stateless, web-friendly architecture. What we on the Play team have done as a first step is extend the work of my colleague, James Ward, so that WebJars offers first class support of requirejs when used with Play. Requirejs is a popular implementation of the AMD specification - a means by which JavaScript applications can be modularised. The easiest way of thinking about AMD is that it is JavaScript's equivalent of package and import statements (or namespace and include statements depending on your preferences!).
JavaScript and Play
The first thing to state is that we want to make developing web applications in conjunction with Play as familiar as possible when it comes to authoring JavaScript. To use a WebJar the programmer declares its dependency in Play's Build.scala file. This file describes a Play project in a similar way that a Maven pom or NPM's package.json would. Here's what a typical build file looks like using a variation of the angular-seed project extended for Play and WebJars:
object ApplicationBuild extends Build {
val appName = "angular-seed-play"
val appVersion = "1.0-SNAPSHOT"
val appDependencies = Seq(
"org.webjars" % "angularjs" % "1.0.5",
"org.webjars" % "requirejs" % "2.1.1",
"org.webjars" % "webjars-play" % "2.1.0-1")
val main = play.Project(
appName, appVersion, appDependencies)
}
What is important to note is that the variable declared appDependencies specifies a list of WebJars that are required directly by the project. angularjs, requirejs and the webjars-play plugin are declared along with their versions. webjars-play actually depends on requirejs and so the above dependency declaration is not strictly required. However it is there to show that whatever requirejs version is declared by webjars-play, a different version can override it e.g. webjars-play depends on requirejs version 2.1.1 and so when requirejs version 2.2 is released then 2.2 can be specified above.
Knowledge of Scala is not required in order to declare dependencies. The above constitutes the total amount of Scala that the JavaScript programmer will be exposed to (unless they wish to delve into Scala which we would actively encourage of course!).
The WebJars website allows the easy selection of a WebJar and its version and then outputs the declaration required to download it for Play, SBT, Maven, Gradle and others.
The next requirement is to tell Play how WebJar assets are going to be resolved. Play's conf/routes file is used to do this. Here is the part of the routes file for the angular-seed-play project concerned specifically with WebJars:
# Obtain require.js with built-in knowledge of
# how webjars resources can be resolved
GET /lib/require.js controllers.WebJarAssets.requirejs
# Enable webjar based resources to be returned
GET /webjars/*file controllers.WebJarAssets.at(file)
The above tells Play to return a wrapper of requirejs whenever /lib/require.js is requested. This wrapper configures requirejs so that it knows how to resolve files hosted within WebJars. The /webjars declaration takes a file path and locates the corresponding resource from a WebJar.
The JavaScript programmer need do very little else to have Play deliver a JavaScript application.
Requirejs usage
Declaring the use of requirejs should look quite familiar. Here is a sample HTML snippet:
<script data-main="js/app" src="lib/require.js"></script>
Given the routes declaration from the previous section the above will bring in js/app.js after requirejs has been loaded. Note that Play must also understand the routing in terms of how to load resources from the js path:
GET /js/*file controllers.Assets.at(path="/public/js", file)
Here is what the angular-seed-play js/app.js file looks like with the less relevant bits removed:
require([
'./controllers',
'./directives',
'./filters',
'./services',
'webjars!angular.js'], function(controllers) {
// Declare app level module which depends on filters,
// and services
...
});
The first few lines of the require statement declare dependencies on JavaScript files relative to the current one via the ./ convention. These files are required for the angular-seed project itself. The line of interest for this topic though is "webjars!angular.js". What this does is call upon the WebJars requirejs plugin to load a file that is contained in a WebJar declared as a dependency.
…and that's about all there is to it.
But wait, there's more...
Let's say that bootstrap is required. Bootstrap has a dependency on jQuery. Ordinarily the JavaScript programmer is required to ensure that jQuery is loaded before bootstrap given its dependency. When using bootstrap's WebJar, jQuery is declared as a dependency such that:
require(["webjars!bootstrap.js"], function () {
...
});
...will automatically load jQuery. This is achieved given that bootstrap's WebJar declares how jQuery is to be located within a repository. Here's a snippet from the bootstrap WebJar pom.xml file:
<dependencies>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>1.9.0</version>
</dependency>
</dependencies>
The other thing that bootstrap's WebJar provides is a file named "webjars-requirejs.js" in a well-known location within the jar. Here are the contents of that file:
requirejs.config({
shim: {
bootstrap: [ 'webjars!jquery.js' ]
}
});
The above declares to requirejs that whenever "webjars!bootstrap.js" is depended on, "webjars!jquery.js" will be loaded first. requirejs.config along with the shim property is standard requirejs configuration. In addition to requirejs behaviour, whenever a "webjars!" module is specified we strip off the "webjars!" prefix and the ".js" suffix and end up with a module name i.e. "bootstrap" in the case of "webjars!bootstrap.js". This module name is then looked up within the shim property of requirejs.config and, if found, the dependencies that are declared are loaded prior to bootstrap.
One more thing...
If there is a requirement to avoid using "webjars!" in JavaScript AMD dependencies then something like the following can be done:
define("jquery", [ "webjars!jquery.js" ], function() {
return $;
});
Thus any time that "jquery" is specified as a dependency its webjar will also be loaded e.g.:
define([ "jquery" ], function($) {
…
});
The above is just as if jQuery was declared to use AMD itself (which it can do).
We intend to enhance the JavaScript development experience further with the goal of making Play the #1 choice for web development.
Happy Play-ing with WebJars!
8 comments:
Good article! Thanks!
There is a small error in:
define("jquery", [ "webjars!jquery.js" ], function() {
return $;
}); :
$ is not declared as argument.
Thanks Martin. $ is in the global scope and isn't returned by jQuery either.
Very interesting, just one question: how does this tie to CDN usage? Can they be defined as part of the dependency structure?
Sounds great!
When can we expect to see these feature "in production"?
Regarding CDNs, we have some ideas for that outside of RequireJS/WebJars i.e. via SBT plugins and Play. There's no explicit CDN support at the moment though.
Regarding production, this is already available for production. :-)
Hi Chris,
Great article! Can you however, elaborate more on the CDN part? I've James Ward's article on using Play with CloudFront but he is not using Require.js for that.
Any good pointers are very appreciated.
Thanks in advance!
Hi Ray,
I'm actually looking at that side of things within Play this week. I shall put another blog post up when I have some guidance there for you.
Amazing article Thanks for the post
Post a Comment