A while ago I added this issue, which is basically a spec for a viewcomponent I urged for, in the hope that someone would face the challenge and, well, just do it. My first thought was that creating something like that would be a bit complex. Here’s the issue description:
The idea is to be able to combine and allow browsers to cache a set of files:
#blockcomponent(JSCombine)
$combiner.add("$siteroot/content/jquery-1.2.3.js")
$combiner.add("$siteroot/content/jquery-someplug.js")
$combiner.add("$siteroot/content/myjs.js")
#end
Which should output something like
519b868bbc5be1_
The hascode should be a composition of the lastmodified of each file's lastmodified attribute, so if a file is changed, the browser will request them again.
There should also be a CssCombine.
The goal is pretty simple: one request and all js is downloaded, hopefully cached, and if some file change a new hash will force the browsers to update their copy.
Today I decided to go on and implement that, was prepared to spend the whole day, but dont know how, was done in less than an hour.
Here the idea:
- Collect a list of the files
- Calculate a hash based on the last modified date of each file
- Create an in-memory aggregation of all files
- Register this content within MonoRail static resource service
- Output a script tag pointing to the newly created MR resource
And here’s the code that does exactly that:
[ViewComponentDetails("CombineJS")]
public class CombineJSViewComponent : ViewComponent
{
public override void Render()
{
var combiner = new CombinerConfig(AppDomain.CurrentDomain.BaseDirectory);
PropertyBag["combiner"] = combiner;
// Evaluate the component body, without output
RenderBody(new StringWriter());
string key = (string) ComponentParams["key"];
long hash = CalculateChangeSetHash(combiner.Files);
IStaticResourceRegistry resourceRegistry = EngineContext.Services.StaticResourceRegistry;
if (!resourceRegistry.Exists(key, null, hash.ToString()))
{
var staticContentResource =
new StaticContentResource(CombineJSContent(combiner.Files));
resourceRegistry.RegisterCustomResource(
key, null, hash.ToString(), staticContentResource,
"application/x-javascript", DateTime.Now);
}
string fullName = "/MonoRail/Files/CombinedJS?name=" + key + "&version=" + hash;
RenderText("<script type=\"text/javascript\" src=\"" + fullName + "\"></script>");
}
private string CombineJSContent(IList<string> files)
{
StringBuilder sb = new StringBuilder(1024 * files.Count);
foreach(var file in files)
{
using(StreamReader reader = File.OpenText(file))
{
sb.Append(reader.ReadToEnd());
}
sb.AppendLine();
}
return sb.ToString();
}
private static long CalculateChangeSetHash(IList<string> files)
{
long hash = 0;
foreach(var file in files)
{
if (File.Exists(file))
{
DateTime dt = File.GetLastWriteTimeUtc(file);
hash += dt.Ticks * 37;
}
}
return hash;
}
public class CombinerConfig
{
private readonly string basePath;
private readonly List<string> files = new List<string>();
public CombinerConfig(string basePath)
{
this.basePath = basePath;
}
public IList<string> Files
{
get { return files; }
}
public void Add(string jsFile)
{
Add(jsFile, null);
}
public void Add(string jsFile, IDictionary options)
{
files.Add(Path.Combine(basePath, jsFile));
}
}
}
And this is how to use it (Nvelocity view engine example)
#blockcomponent(CombineJS with "key=deflayout")
$combiner.Add("Content/js/jquery/jquery-1.2.6.min.js")
$combiner.Add("Content/js/jquery/jquery.metadata.js")
$combiner.Add("Content/js/jquery/jquery.label_over.js")
#end
Note that the key must represent an unique set of files.
Now my plan is to add js minify capabilities, either by selecting the minified file using a naming pattern, or minifying in place.
June 27th, 2008 at 8:01 pm
Brilliant, thank you!
June 27th, 2008 at 8:50 pm
// Evaluate the component body, without output
RenderBody(new StringWriter());
that’s a little tricky :)
June 27th, 2008 at 8:51 pm
That’s nice, thanks you!
Also I didn’t found IStaticResourceRegistry before, which is also nice to have.
June 28th, 2008 at 2:45 am
I have done the same but also for CSS file (combine + compress)
I don’t cache the result file as a static resource, no need but but just had an http cache header to the dynamic result file.
June 28th, 2008 at 8:36 am
Cool thing. IStaticResourceRegistry is a great hidden gem.
What is missing (or is it possible here?) is to do it CaptureFor like – subviews and components would be able to add more js files, and a combiner or something on the layout would spit a mixed together JS file for the whole request.
and allow .css files of course …
I’ve had a stub for such a thing a while ago but it got lost somewhere (was on an the old VSS server of a former employer) :(
June 28th, 2008 at 12:22 pm
Wow, just last week I wrote a helper and a controller (called it AssetHelper) that does about the same thing! Also planning to add js minifying :)
Didn’t know about IStaticResourceRegistry though, very nice.
June 30th, 2008 at 9:37 am
That’s great, hadn’t seen your original request, but was thinking of writing something similar myself, especially after watching some Yahoo! videos and taking on board their comments regarding minimizing http requests (http://developer.yahoo.com/performance/rules.html#num_http).
February 11th, 2009 at 7:31 pm
[...] Just came across a post about JavaScript compresion for MonoRail. These guys have written a great patch for Hammett’s CombineJS Component. [...]
July 15th, 2009 at 9:39 am
@Ken Egozi:
I did implement exactly that but for asp.net mvc. You can get the code from: http://pronghorn.codeplex.com and I’m sure you will be able to adapt it to use it in monorails.
January 30th, 2011 at 4:39 am
It doesn’t seem to cache? Firebug Net returns 200 OK. I posted the problem here:
http://stackoverflow.com/questions/4836873/monorails-2-0-combinejs-doesnt-cache
May 22nd, 2012 at 11:13 am
Any way I can set another domain for the combine file’s path?