Managing assets from the build system

The previous sections showed how your Play application can serve static files such as JavaScript or CSS files. However, many people prefer not to write JavaScript or CSS code directly. Rather, they generate it from higher-level languages such as CoffeeScript and Less. Furthermore, you might want to minify and gzip these files as they don't need to be read by humans anymore when they are executed by web browsers and compressing them can save some bandwidth.

The build system of your Play application can manage such processing steps for you and make the produced assets available to your application as if they were static files in the public/ directory. This work is achieved by an sbt plugin family named sbt-web, which Play already depends on.

Note

You can find more information about sbt-web from http://github.com/sbt/sbt-web.

The sbt-web plugin defines a dedicated configuration scope named Assets (or web-assets from within the sbt shell) to configure the managed assets' production process. By default, the source directory for managed assets is defined as follows:

sourceDirectory in Assets :=(sourceDirectory in Compile).value / "assets"

So, in the case of a standard Play application, this directory refers to the app/assets/ directory. This means that you can place your asset source files in this directory and the build system will copy them to a public/ directory in the classpath after eventually transforming them using an sbt plugin based on sbt-web.

In practice, this means that instead of placing static files in the public/ directory of your application, you can put them in the app/assets/ directory and benefit from the managed assets' compilations and pipeline transformations such as concatenation and minification.

Producing web assets

The first category of sbt plugins based on sbt-web are those producing web assets from the assets' source files. Examples of such plugins are sbt-coffeescript, which compiles .coffee files into .js files, and sbt-less, which compiles .less files into .css files. Using them is just a matter of adding the following lines to your project/plugins.sbt file:

addSbtPlugin("com.typesafe.sbt" % "sbt-coffeescript" % "1.0.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.0.0")

You can then replace the public/javascripts/shop.js file with the following app/assets/javascripts/shop.coffee file:

(() ->
  handleDeleteClick = (btn) ->
    btn.addEventListener('click', (e) ->
      xhr = new XMLHttpRequest()
      route = routes.controllers.Items.delete(btn.dataset.id)
      xhr.open(route.method, route.url)
      xhr.addEventListener('readystatechange', () ->
        if xhr.readyState == XMLHttpRequest.DONE
          if xhr.status == 200
            li = btn.parentNode
            li.parentNode.removeChild(li)
          else
            alert('Unable to delete the item!')
      )
      xhr.send()
    )

  for deleteBtn in document.querySelectorAll('button.delete-item')do (deleteBtn) -> handleDeleteClick(deleteBtn)
)()

Replace the public/stylesheets/shop.css file with the following app/assets/stylesheets/shop.less file:

li {
  button.delete-item {
    visibility: hidden;
  }
  &:hover button.delete-item {
    visibility: visible;
  }
}

Finally, as the sbt-less plugin only looks for a file named main.less but your file is named shop.less, you need to fix the includeFilter setting in your build.sbt file:

includeFilter in (Assets, LessKeys.less) := "shop.less"

Web assets are now produced from your .coffee and .less source files. This compilation happens only once; the application then serves the resulting static files.

Pipelining web assets' transformations

You cannot use several plugins to compile Less files (this is the same for CoffeeScript files and so on); this means that plugins that produce web assets are mutually exclusive to each other in terms of their function.

On the other hand, there is another category of web assets plugins, those that transform assets whose functions can be combined. For instance, your web assets can be concatenated, minified, and then gzipped. Plugins of this category are executed after those that produce assets and are sequentially combined one after the other. You are responsible for defining their order of execution in your build.sbt file with the pipelineStages setting. Another difference with plugins that produce assets is that some of the plugins that transform assets are not executed in the development mode, but when the application is prepared for the production mode.

The production mode is the one you want to use when your application is deployed. The main difference with the development mode is that there is no hot reloading mechanism. Thus, this execution mode gives better performance. The sbt-web plugin differentiates between these execution modes because some asset transformations only have a purpose of optimization (for example, compression) and might slow down the hot reloading process in the development mode. You can execute your application in the production mode (and therefore observe the effects of the assets pipeline) by using the start sbt command instead of run. The rest of this section shows how to set up concatenation and minification of your JavaScript files, along with gzipping and fingerprinting.

Concatenating and minifying JavaScript files

Concatenation of JavaScript files is useful because JavaScript code bases are usually modularized so that the code is easier to reuse and maintain. Yet, the JavaScript language has no built-in support for modules, but several tools or libraries make modularization possible, such as Browserify or RequireJS.

At the time of writing this, there is only one plugin for RequireJS: sbt-rjs. This plugin runs the RequireJS optimizer on your code base to concatenate and minify it.

Note

RequireJS does both concatenation and minification. If you want just minification, take a look at the sbt-uglify plugin at http://github.com/sbt/sbt-uglify.

To use it, you first need to add it to the build in the project/plugins.sbt file:

addSbtPlugin("com.typesafe.sbt" % "sbt-rjs" % "1.0.1")

Then, add it to the assets pipeline process in your build.sbt file:

pipelineStages := Seq(rjs)

You also need the require.js client-side runtime in order to load modules from the client side. Get it from http://requirejs.org and place it somewhere in your assets source directory, for example, in app/assets/lib/requirejs/require.js.

Now, let's modularize your code base. Define two modules, logic.coffee and ui.coffee, decoupling the application's behavior and user interface:

// ui.coffee
define(() ->
  (node) ->
    delete: () ->
      li = node.parentNode
      li.parentNode.removeChild(li)
    forEachClick: (callback) ->
      node.addEventListener('click', callback)
)

// logic.coffee
define(['ui'], (Ui) ->
(node, id) ->
    ui = Ui(node)
    ui.forEachClick(() ->
      xhr = new XMLHttpRequest()
      route = routes.controllers.Items.delete(id)
      xhr.open(route.method, route.url)
      xhr.addEventListener('readystatechange', () ->
        if xhr.readyState == XMLHttpRequest.DONE
          if xhr.status == 200
            ui.delete()
          else
            alert('Unable to delete the item!')
      )
      xhr.send()
    )
)

The ui module defines a function that takes as a parameter a root DOM node corresponding to an item's delete button and returns an object with two methods. The first one, delete, removes the item from the DOM, and the second one, forEachClick, registers a callback on click events on the item delete button. The logic module depends on the ui module and defines a function that takes an item delete button node and ID as parameters and sets up its behavior.

Finally, update the shop module to use the logic module:

require(['logic'], (Logic) ->
  for deleteBtn in document.querySelectorAll('button.delete-item')
    do (deleteBtn) -> Logic(deleteBtn, deleteBtn.dataset.id)
)

The preceding code finds all item delete buttons and sets up their logic.

Tip

By default, the CoffeeScript compiler wraps the generated JavaScript in an anonymous function. Unfortunately, the RequireJS optimizer is unable to detect and process AMD module definitions when they are wrapped in an anonymous function. To solve this issue, set the bare option of the CoffeeScript compiler to true:

CoffeeScriptKeys.bare := true

The RequireJS optimizer can usually be configured by command-line arguments or by using a JavaScript configuration object. With sbt-rjs, you can set up such a JavaScript configuration object from your build.sbt file. For instance, here is how we can set the main module's name to be shop, instead of the default main:

RjsKeys.mainModule := "shop"

Refer to the documentation of the sbt-rjs plugin for more information.

Note

The logic module also has a dependency to the JavaScript router, but this one is not an AMD module, so you can't load it with RequireJS. As a work-around, you can tweak the reverse router generation to produce an AMD module:

def javascriptRouter = Action { implicit request =>
    val router = Routes.javascriptRouter("routes")(
      routes.javascript.Items.delete
    )
Ok(JavaScript(
  s"""
    define(function )() { $router; return routes })
  """
))"
  }

Then, you can load it using RequireJS in your logic module:

define(['ui', 'routes'], (Ui, routes) ->
  …
)

Be sure to tell the optimizer to ignore the routes module in its paths configuration:

RjsKeys.paths += "routes" -> ("routes", "empty:")

Now, if you run your application in the production mode (using the start sbt command), your JavaScript and CSS files will be minified and all the dependencies of the main module will be concatenated in a single resulting file.

Tip

Note that you can enable the assets pipeline in the development mode (so that you don't need to execute your application in the production mode to observe the assets transformations) by scoping the pipelineStages setting to the Assets configuration:

pipelineStages in Assets := Seq(rjs)

Gzipping assets

Gzipping web assets can save some bandwidth. The sbt-gzip plugin compresses all the .html, .css, and .js assets of your application. For each asset, it produces a compressed file with the same name suffixed with .gz. The Play Assets controller handles these files for you; when it is asked to serve a resource, if a resource with the same name but suffixed with .gz is found and if the client can handle the gzip compression, the compressed resource is served.

To use it, add the plugin to your project/plugins.sbt file:

addSbtPlugin("com.typesafe.sbt" % "sbt-gzip" % "1.0.0")

Add the gzip task to the asset's pipeline in your build.sbt file:

pipelineStages := Seq(rjs, gzip)

Now, run your application and request, for instance, the /assets/stylesheets/shop.css resource, and while setting this, you can handle the gzip compression (by using the Accept-Encoding header):

$ curl -H "Accept-Encoding: gzip" http://localhost:9000/assets/stylesheets/shop.css 
□□TH*-)□□□KI□I-I□□,I□U□□RP(□,□L□□□,□□R□□LIIͳ□□□□ɴ□□/K-"F□□□
□□owTg%

You get a compressed version of the resource, as expected. If you don't set the Accept-Encoding header, you get the uncompressed version, as expected too:

$ curl http://localhost:9000/assets/stylesheets/shop.css 
li button.delete-item {
  visibility: hidden;
}
li:hover button.delete-item {
  visibility: visible;
}

Fingerprinting assets

As explained previously, in the production mode, the Assets controller sets the Cache-Control header to max-age=3600, telling web browsers that they can cache the result for one hour before requesting it again.

However, typically your assets won't change until the next deployment, so web browsers can probably cache them for a duration longer than one hour. However, if a client makes a request in the hour preceding a redeployment, it will keep outdated assets in its cache.

You can solve this problem by following this principle: if you want a client to cache a resource, then this resource should never change. If you have a newer version of the resource, then you should use a different URL for it.

Assets fingerprinting helps you achieve this. The idea is that when your application is packaged for production, an MD5 hash of each web asset is computed from its contents and written in a file with the same name, but suffixed with .md5. When the application is running, if the Assets reverse router finds a resource along with its hash, it generates the resource URL by concatenating the file name to its hash. The next time the application is deployed, if an asset has changed, it also changes its hash and then its URL. So, web browsers can cache versioned assets for an infinite duration.

To enable assets versioning, you must use the versioned action of the Assets controller:

GET  /assets/*file   controllers.Assets.versioned(path="/public", file)

Also, update all the places (for example, in the HTML templates) where you reverse routed the Assets.at action to use the Assets.versioned action.

When reverse routed, this action looks for a resource with the same name as file, but suffixed with .md5, containing the file hash, to build a URL composed of the hash contents and the file name. So, you need to generate a hash for each of your web assets, and this is exactly what the sbt-digest plugin does.

Enable this plugin by adding it to your project/plugins.sbt file:

addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.0.0")

Add the digest task to the assets pipeline in your build.sbt file:

pipelineStages := Seq(rjs, gzip, digest)

Now, when your application is prepared for production, a hash file is generated for each of your web assets so that the Assets controller can generate a unique URL for a given asset. Also, as your asset URLs refer to resources that never change, the caching policy is more aggressive; now, the Cache-Control header is set to max-age=31536000, thus telling web browsers that they can cache the result for one year.

Tip

If there is no hash file for a given resource, the Assets.versioned action falls back to the unversioned behavior, so everything still works the same if you use Assets.versioned instead of Assets.at. Actually, I recommend that you always use Assets.versioned in your projects.

If you are curious, take a look at the URLs generated by the reverse routing process. Consider, for instance, the following line in the layout.scala.html template:

@routes.Assets.versioned("stylesheets/shop.css")

It produces the following URL:

/assets/stylesheets/91d741f219aa65ac4f0fc48582553fdd-shop.css

Managing JavaScript dependencies

The sbt-web plugin supports WebJars, a repository of client-side artifacts. This means that, provided your JavaScript library is hosted on WebJars, sbt can download it and place it in a public/lib/<artifact-id>/ directory.

For instance, instead of manually downloading the RequireJS runtime, we can add a dependency on it in our build.sbt file:

libraryDependencies += "org.webjars" % "requirejs" % "2.1.11-1"

The requirejs artifact content is downloaded and copied to the public/lib/requirejs directory so that you can refer to the require.js file within a script tag in an HTML page as follows:

<scriptsrc="@routes.Assets.versioned("lib/requirejs/require.js")"></script>

The WebJars repository does not host as many artifacts as npm or bower registries. Consequently, if you want to automatically manage such dependencies, you should use a node-based build system such as Grunt (besides using sbt to manage your Play application). Nevertheless, it is worth noting that sbt-web is able to run npm so that we can expect an sbt-grunt plugin unifying the two build systems. However, at the time of writing this, such a plugin does not exist.

Running JavaScript tests

As your JavaScript code grows, you will surely want to test it. The sbt-mocha plugin runs Mocha tests from the sbt test runner. Enable it in your project/plugins.sbt file:

addSbtPlugin("com.typesafe.sbt" % "sbt-mocha" % "1.0.0")

Then, you can write JavaScript tests using Mocha and they will be run when you execute the test sbt command. By default, all files under the test/ directory and that end with Test.js or Spec.js are considered to be tests to run.

For instance, you can add the following code to the test/assets/someTest.coffee file:

var assert = require("assert")
describe("some specification", () ->
  it("should do something", (done) ->
      assert(false)
      done()
  )
)

Then, running the test sbt command produces the following output:

[info] some specification
[info]   x should do something
[error] AssertionError: false == true

Tests are executed in a node environment, so you can use node's require command to load CommonJS modules, but if you want to load AMD modules, you first need to get an AMD module loader such as the RequireJS runtime. Read the sbt-mocha documentation for more details on how to achieve this.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.31.125