The web layer we wrote in the previous chapter was not really complete; we could add some beautiful CSS styles and some cool JavaScript behavior. CSS files, JavaScript files, as well as images do not change once your application is started, so they are usually referred to as
static assets. The most convenient way to serve them is to map a URL path to a directory of your filesystem. Play comes with an Assets
controller that does just this. Consider the following route definition:
GET /assets/*file controllers.Assets.at(path = "/public", file)
This route maps the public
directory of your application to the assets
path of your HTTP layer. This means that, for example, a public/stylesheets/shop.css
file is served under the /assets/stylesheets/shop.css
URL.
This works because Play automatically adds the public/
directory of your application to the classpath. To use an additional directory as an assets folder, you have to explicitly add it to the application classpath and to the packaging process by adding the following setting to your build.sbt
file:
unmanagedResourceDirectories in Assets += baseDirectory.value / "my-directory"
The Assets
controller is convenient to serve files whose content does not change during the application lifetime. Let's create a public/stylesheets/shop.css
file and request it:
$ curl -I http://localhost:9000/assets/stylesheets/shop.css HTTP/1.1 200 OK Last-Modified: Fri, 02 May 2014 09:35:37 GMT Content-Length: 0 Cache-Control: no-cache Content-Type: text/css; charset=utf-8 Date: Fri, 02 May 2014 09:37:58 GMT ETag: "1d2408ce266a8226416fa8901bd7865364452bd6"
There are several things to note about the Assets
controller from the preceding response:
Content-Type
HTTP header accordinglyLast-Modified
header to the last modification date obtained from the filesystem and the Etag
header to a checksum of the file contentsCache-Control
header is set to no-cache
in the development mode in order to prevent web browsers from caching the response, but in production, this value is set to 33600 (one hour) and can be overridden by configurationObviously, the Assets
controller replies with a 304 response (Not Modified) if one makes a request with an If-Modified-Since
or If-None-Match
header matching the resource, and if this resource has not changed:
$ curl -I -H "If-None-Match: "1d2408ce266a8226416fa8901bd7865364452bd6"" http://localhost:9000/assets/stylesheets/shop.css HTTP/1.1 304 Not Modified ETag: "1d2408ce266a8226416fa8901bd7865364452bd6" Last-Modified: Fri, 02 May 2014 09:35:37 GMT Cache-Control: no-cache Content-Length: 0
The Last-Modified
and Etag
response headers as well as their request counterparts, If-Modified-Since
and If-None-Match
, save bandwidth in the case of large files, but they still require an HTTP round trip, which checks that there is no newer version of the resource.
On the other hand, the Cache-Control
header tells clients that they can keep the response content in their local cache and reuse it for a given duration instead of performing an HTTP request. As previously said, in the development mode, this header is set to no-cache
in order to prevent clients from caching the responses because you might often change their content. However, when you run in the production mode, this header is set to 33600, telling clients that they can cache the response content for one hour before requesting it again.
For the sake of completeness, here is how your HTML layout template (the app/views/layout.scala.html
file) can look so that each web page loads a favicon image, CSS style sheet, and JavaScript program:
@(body: Html) <!DOCTYPE html> <html> <head> <title>Shop</title> <link rel=stylesheet src="@routes.Assets.at("stylesheets/shop.css")"> <link rel=favicon src="@routes.Assets.at("images/favicon.png")"> </head> <body> @body <script src="@routes.Assets.at("javascripts/shop.js")"></script> </body> </html>
The preceding template refers to a shop.css
file located in the public/stylesheets/
directory, a favicon.png
file in the public/images/
directory, and a shop.js
file in the public/javascripts/
directory.
Here is the possible content for the JavaScript public/javascripts/shop.js
file, which performs an Ajax call to the Items.delete
action:
(function () { var handleDeleteClick = function (btn) { btn.addEventListener('click', function (e) { var xhr = new XMLHttpRequest(); xhr.open('DELETE', '/items/' + btn.dataset.id); xhr.addEventListener('readystatechange', function () { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { var li = btn.parentNode; li.parentNode.removeChild(li); } else { alert('Unable to delete the item!'), } } }); xhr.send(); }); }; var deleteBtns = document.querySelectorAll('button.delete-item'), for (var i = 0, l = deleteBtns.length ; i < l ; i++) { handleDeleteClick(deleteBtns[i]); } })();
This code finds all the HTML buttons with the delete-item
class and sets up a click handler that performs an HTTP request on the Items.delete
route. If this request succeeds, the item is also removed from the page, otherwise, an error message is shown to the user. The code retrieves the corresponding item ID using the data-id
attribute of the button. It assumes that the following HTML markup represents an item:
<li>
<a href="@routes.Items.details(item.id)">@item.name</a>
<button class="delete-item" data-id="@item.id">Delete</button>
</li>
Let's define the public/stylesheets/shop.css
file so that the delete button is made visible only when the user hovers over an item:
li button.delete-item { visibility: hidden; } li:hover button.delete-item { visibility: visible; }
Finally, feel free to design a public/images/favicon.png
image of your choice!
3.137.217.17