or image files from your templates/code. The reference may look like:
During the render phase Tapestry5 converts the
${context:/css/all.css} part to asset URL, which may look like the following (see Asset URLs section
here):
<link rel="stylesheet" type="text/css" href="/assets/stage-20120310/ctx/css/all.css" />
Here "
stage-20120310" -- is an application version string, which Tapestry5 adds to asset URLs to manage assets versioning. When running in production Tapestry5 adds a far future expires header for the asset, which will encourage the client browser to cache it.
When you change one of your assets you have to change application version number in your
AppModule.java, so that Tapestry5 generate new asset URLs and browser fetched new assets instead of using the ones from cache.
One disadvantage of such approach is that client browser will have to get
all the assets once again, not just the one that was changed.
For the majority of assets the asset URL is generated by Tapestry5. Exceptions are assets, that are referenced from
*.css files by the relative URL, like this (file
all.css):
a.external {
background: transparent url(../images/external.png) no-repeat scroll right center;
display: inline-block;
margin-left: 2px;
height: 11px;
width: 11px;
zoom: 1;
}
In this case browser will form the URL itself relatively to
"/assets/stage-20120310/ctx/css/all.css", and the resulting URL will be
"/assets/stage-20120310/ctx/images/external.png".
So you
have to change application version in
AppModule.java if you provide new version of "
external.png".
But, for the majority of assets it would be enough to append MD5/SHA1/... checksum as a GET-parameter to asset URL and make them look like:
<link rel="stylesheet" type="text/css" href="/assets/stage-20120310/ctx/css/all.css?5ef25ac1ec38f119e283f338e6c120a4e53127b1" />
In Tapestry5 you have the ability to provide your own implementation of
AssetPathConverter service and append this checksum manually. But, in this interface you only have original asset URL, and don't have the resource itself to calculate the checksum.
There are several ways this may be implemented. Ideally, I'd like this to be implemented in Tapestry5 core.
There's one thing I don't like about Tapestry5 assets handling, though, even if the above solution will be implemented -- is that assets are not static.
This means every asset URL is handled by the Java code, and in most cases assets handling is just streaming of existing files from filesystem to browser (with optional
minimization and gzip-compression).
Once the asset was handled, Tapestry5 caches the response and uses it in further responses, but still this is all done in Java.
In Ping Service we've implemented "assets precompilation", and placed all the rendered assets as static files in the web app root folder.
This is done using
custom implementation of
org.apache.tapestry5.internal.services.ResourceStreamer, which is responsible for streaming every asset to client. During resource streaming we calculate asset checksum and store in a
static.properties file, where we put asset URL as a key, and checksum as a value:
#Static Assets For Tapestry5 Application
#Sat Mar 10 19:42:38 UTC 2012
/assets/stage-20120310/ctx/css/all.css=5ef25ac1ec38f119e283f338e6c120a4e53127b1
/assets/stage-20120310/ctx/css/analytics.css=ee470432c344820e43995fb4632ab4bee3b92e38
/assets/stage-20120310/tapestry/t5-prototype.js=95e30b840a5654b82e6a0334a14a2766c57c4d99
...
Our implementation of
AssetPathConverter uses this property file to modify asset URLs.
We run our implementation of
ResourceStreamer only in production mode, since Google App Engine doesn't allow writing to the filesystem.
Also we've implemented it to work only if special HTTP-header passed with the request. To pass this header and to trigger every asset we have in our application, we use
Selenium-powered integration test that queries every single page. We run this test before deploying new version to production.
Now Tapestry5 asset URLs and URLs of static files are the same in our application. So Google App Engine runtime won't even pass the request to Java. Also it uses
its own facilities to serve
static files, i.e. gzip-compression, etc.