1. Overview

In this tutorial, we’ll learn how to serve static assets in the Play Framework.

First, we’ll have a tour of asset configuration options. Then, we’ll learn how to serve classic assets: images, CSS, and JavaScript. Finally, we’ll generate assets from compiled targets: CoffeeScript, TypeScript, LESS, and SASS files.

2. Setup

We don’t need any plugin or dependency to start serving classic static assets (images, CSS, JavaScript) in Play. Plugins and dependencies become mandatory when we want to serve compiled assets (supersets of CSS and JavaScript).

We’ll install some plugins and dependencies to demonstrate those use cases.

2.1. Plugins

First, let’s add the sbt-coffeescript, sbt-typescript, sbt-less, and sbt-sassify plugins:

addSbtPlugin("com.typesafe.sbt" % "sbt-coffeescript" % "1.0.2")
addSbtPlugin("com.github.platypii" % "sbt-typescript" % "4.6.4")
addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.2")
addSbtPlugin("io.github.irundaia" % "sbt-sassify" % "1.5.2")

Those plugins will be used in the upcoming sections to serve compiled assets.

Next, we’ll enable the sbt-web plugin:

enablePlugins(SbtWeb)

It enforces a file directory convention for serving assets and manages our WebJars dependencies.

It’s important to note that for JDK 1.8 on Windows, we should enable the allowAmbiguiousCommands option in our build.sbt:

javaOptions ++= Seq("-Djdk.lang.Process.allowAmbiguousCommands=true")

This is due to a regression from a security fix for JDK-8221858. The sbt-js-engine, used by sbt-coffeescript and sbt-typescript, was affected by the issue.

2.2. Dependencies

Finally, we’ll install the bootstrap-sass dependency:

libraryDependencies += "org.webjars.bower" % "bootstrap-sass" % "3.3.6"

This library will be used for styling. It’ll also be served from SASS.

3. Configuration

Managing assets means optimizing resources like images, stylesheets, and JavaScript files to enhance website performance. Specifically, we’ll organize, compress, and track those resources.

Let’s explore how to configure asset management.

3.1. Enable Asset Management

We don’t need much to start serving assets in Play. The PlayScala plugin comes with a default Assets controller. It takes care of serving assets.

Additionally, the built-in AssetsModule provides all the necessary bindings for the controller. It also enables three useful features:

  • Assets caching: setting play.assets.cache for an asset in Play configuration adds the Cache-Control header when serving that asset
  • Setting ETag headers: the value is either a generated digest or a combination of asset name and last modification date
  • Serving compressed files: sets Content-Encoding header to gzip for assets with the .gz extension. It requires configuring the sbt-gzip plugin.

If necessary, we can provide our asset module:

play.modules.disabled += controllers.AssetsModule
play.modules.enabled += modules.CustomAssetsModule

In this article, we’ll use the built-in AssetsModule.

3.2. Assets Location

Thanks to sbt-web conventions, Play provides a default directory structure for assets:

app                      → Application sources
 └ assets                → Compiled asset sources
    └ stylesheets        → LESS/SASS sources
    └ javascripts        → CoffeeScript/TypeScript sources
public                   → Public assets
 └ stylesheets           → CSS files
 └ javascripts           → Javascript files
 └ images                → Image files
target                   → Generated stuff
 └ web                   → Compiled web assets

By default, classic assets reside in the /public folder. Compiled assets are in the /app/assets folder. Particularly, WebJars will directly be extracted into the /public/lib directory by sbt-web.

In case we opt-out of Play default layout, app/ directory is replaced by src/main :

src                        → Application sources
 └ main                    → Compiled asset sources
    └ assets               → Compiled asset sources
       └ css               → LESS/SASS sources
       └ js                → CoffeeScript/TypeScript sources
    └ public               → Public assets
       └ css               → CSS files
       └ js                → Javascript files
       └ images            → Image files
target                     → Generated stuff
 └ scala-2.13
    └ resource_managed     → Managed resources (less, ...)

Besides, we can override the assets location and URL prefix:

play.assets {
  path = "/new-public"
  urlPrefix = "/dope-assets"
}

Here, HTTP GET requests from /dope-assets/file will serve the asset located at /new-public/file. This configuration saves us from specifying the asset path in routing and reverse routing:

GET /dope-assets/*file controllers.Assets.versioned(file)
<link rel="stylesheet" href="@assetsFinder.path("assets/stylesheets/main.css")">

In this context, we shouldn’t specify that the file is of type Asset. The reverse router uses AssetFinder to compute the original resource path: /new-public/styelessheets/main.css.

In the next sections, we’ll use the default assets location.

3.3. Assets Routing and Reverse Routing

First, let’s explore asset routing. Asset routing is mapping a URL to an asset. By default, the /public directory is mapped to the /assets URL path:

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

Here, we serve the asset located at the /public/file path. Calling /assets/images/favicons.png returns the image located at /public/images/favicons.png.

Assets.versioned() instructs the controller to fingerprint and minify our static resources in both production and development modes. Fingerprinting changes the names of our assets when their contents change. Consequently, Play recompiles all the templates that use the asset.  Also, the browser will know that it needs to download the new asset and discard the cached one.

Next, let’s see what asset reverse routing is. Reverse routing consists of deducing the final location of an asset from its relative path. We could skip reverse routing by hardcoding the assets paths:

<link rel="stylesheet" href="/assets/stylesheets/style.css"> <!-- bad -->

This is a bad pattern because if we change the URL from /assets/*file  to /dope-assets/*file, we’ll need to change our template files. Instead, using the reverse router will automatically handle that for us:

<link rel="stylesheet" href="@routes.Assets.versioned("stylesheets/style.css")"> <!-- good -->

In the next sections, we’ll prefer reverse routing to hardcoding asset paths.

4. Serving Images, CSS and JavaScript

Classic assets reside in the /public folder and require no compilation step. First, let’s define a route for all our assets:

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

Next, let’s add a favicon.png inside /public/images. That folder should contain all our subsequent image assets. Opening localhost:9000/assets/favicon.png displays our image.

Moving on, we’ll create a main.js file under /public/javascripts:

const main = document.createElement('main');
main.textContent = "hello from javascript";
document.body.appendChild(main);

We’ll link this resource to our index.scala.html:

<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>

It’s served when accessing localhost:9000.

Finally, let’s add some styling to the text above. We’ll define a main.css file inside /public/stylesheets:

main {
    color: yellow;
    background-color: darkcyan;
}

After reloading the browser, we can see all styles applied to the main element.

5. Serving Compiled Assets

Let’s explore how to serve compiled assets in Play. They will be under the app/assets directory and will be lazily compiled.

5.1. CoffeeScript

All CoffeeScript files need to be inside the /app/assets/javascripts folder. Let’s define our hello-world.coffee there:

greetings = document.createElement('h2');
greetings.textContent = "hello world from coffeescript!";
greetings.className = "coffeescript";
document.body.appendChild(greetings);

The compiled asset is referenced in the index.scala.html view:

<script src="@routes.Assets.versioned("javascripts/hello-world.js")"></script>

When accessing the view on accessing localhost:9000, the sbt-coffescript plugin will compile all *.coffee files and place the resulting JavaScript files under target/web/public/main/javascripts. It’ll also generate the related source maps. We can opt out of it by setting CoffeeScriptKeys.sourceMap := false in our build.sbt.

Finally, the browser displays the text “hello world from coffeescript!”.

5.2. TypeScript

Similarly to CoffeeScript assets, we’ll place our salute.ts file under app/assets/javascripts:

const greetings = document.createElement('h1');
greetings.textContent = "hello from typescript! Styling is courtesy of SASS!";
greetings.className = "typescript";
document.body.appendChild(greetings);

Additionally, we’ll target ES5 for the JavaScript-generated version in a root tsconfig.json.

sbt-typescript will compile our file when accessing index.scala.html:

<script src="@routes.Assets.versioned("javascripts/salute.js")"></script>

The browser will display “hello from typescript! Styling is courtesy of SASS!” when we open localhost:9000.

5.3. LESS

All our LESS files will be under app/assets/stylesheets. Let’s define a style.less file:

.typescript {
  color: blue;
}

It makes the text inserted by salute.ts red.

By default, sbt-less only compiles main.less file. To compile all LESS files, we’ll update our build.sbt:

Assets / LessKeys.less / includeFilter := "*.less"

Next, we’ll link our style to index.scala.html view:

<link rel="stylesheet" href="@routes.Assets.versioned("stylesheets/style.css")">

After refreshing the browser, we’ll see that the text inserted by salute.ts is now blue. Reloading downloads a new version of assets because we use reverse routing.

5.4. SASS

First, let’s define our salute.scss file inside app/assets/stylesheets:

@import "utils/lang";

h3 {
  background-color: yellow;
}

All SASS files must have the same extension: *.scss or *.sass. We can instruct the sbt-sassify plugin to detect the right one:

SassKeys.syntaxDetection := ForceScss

The plugin compiles all detected *.scss files under app/assets/stylesheets.

Next, we’ll link the generated CSS file inside index.scala.html:

<link rel="stylesheet" href="@routes.Assets.versioned("stylesheets/salute.css")">

Finally, we’ll see different background colors for the texts generated by salute.ts and hello-world.coffee. The styles are defined inside the _lang.scss:

@import "lib/bootstrap-sass/assets/stylesheets/bootstrap";

.coffeescript {
  color: aliceblue;
  background-color: darken(brown, 12%);
}

.typescript {
  color: bisque;
  background-color: blueviolet;
}

Likewise, we can use all the other great SASS features.

6. Conclusion

In this article, we learned how to serve public assets in a Play application. First, we looked into the different configurations available. Next, we served classic static assets. Finally, we served compiled assets.


« 上一篇: ZIO中的纤程介绍