views:

134

answers:

4

I'm creating a controller that will serve the combined/minified versions of my JavaScript and CSS. I need to somewhere along the line define which scripts/styles to be loaded.

When a request is made, for example for style.css?VersionNumberHere, it will check if the combined/minified data is already in the HttpContext.Cache, if so spit it out. Otherwise, I need to look up the definition that makes up style.css.

I created a Script/StyleBuilder (that inherits from ContentBuilder) that will store all the paths that need to be combined and then squished (so this would be the definition of style.css).

Where should I be storing these references to the "builders"? Should they be in a static class or a singleton that implements an interface so that it can be tested?

Here's the interface that the abstract class ContentBuilder implements (you can easily imagine the implementation):

public interface IContentBuilder : IEnumerable<string>
{
    string Name { get; }
    int Count { get; }
    string[] ValidExtensions { get; }
    void Add(string path);
    bool ValidatePath(string path);
    string GetHtmlReference(); // Spits out <script>, or <link> depending on implementation.
    string Build(); // Minifies, combines etc.
}

And here is ideally what I'd like to be able to do with these:

ContentBuilderContainer.Current.Add("main.js", c => new ScriptBuilder()
{
    "/path/to/test.js",
    "/path/to/test2.js",
    "/path/to/test3.js"  
});

ContentBuilderContainer.Current.Add("style.css", c => new StyleBuilder()
{
    "/path/to/style.css",
    "/path/to/test.less"
});

Then to output all the HTML for all registered IContentBuilder:

ContentBuilder.Container.Current.BuildHtml();
A: 

Attach caching attributes to your controller actions and cache by parameter like this:

[OutputCache(Duration = 7200, Location = OutputCacheLocation.Client, VaryByParam = "jsPath;ServerHost")]
[CompressFilter]
// Minifies, compresses JavaScript files and replaces tildas "~" with input serverHost address 
// (for correct rewrite of paths inside JS files) and stores the response in client (browser) cache for a day
[ActionName("tildajs")]
public virtual JavaScriptResult ResolveTildasJavaScript(string jsPath, string serverHost)
...
mare
I know how to do caching - that isn't the problem. The problem is combining *multiple files* into one. Then a name is generated (like `script.js?a4fj6k4j93k`) and then it is the *only* script that is referenced.
TheCloudlessSky
A: 

I made the following interface:

public interface IContentBuilderContainer
{
    int Count { get; }
    bool Add(string name, Func<IContentBuilder> contentBuilder);
    string RenderHtml();
}

And then in the implmentation of ContentBuilderContainer:

public class ContentBuilderContainer : IContentBuilderContainer
{

    // Other members removed for simplicity.

    #region Static Properties

    /// <summary>
    /// Gets or sets the current content builder container.
    /// </summary>
    public static IContentBuilderContainer Current
    {
        get;
        set;
    }

    #endregion

    #region Static Constructors

    static ContentBuilderContainer()
    {
        ContentBuilderContainer.Current = new ContentBuilderContainer();
    }

    #endregion

}

This way there's a single ContentBuilderContainer living at one time.

TheCloudlessSky
A: 

I helped write some code that did this recently. Here's a high level overview of the solution that was implemented. Hopefully it will give you some good ideas.

Configuration: We created custom configuration elements that define a key and their a corresponding list of directories. So the key JS is linked to our /Content/Scripts folder, and CSS is linked to our /Content/Styles folder. I have seen other solutions where the configuration allowed for individual files to be listed.

Controller: The controller was set up to receive requests something along the lines of /Content/Get/JS and /Content/Get/CSS. The controller uses the configuration key and client request headers to come up with a cache key that identifies the content we want to serve: JS-MSIE-ZIP, CSS-FFX, etc. The controller then checks our cache service. If the content is not there, it gets concatenated, minified, compressed, cached and then served. Handy fallout is that the content is compressed before going into the cache instead of every time it's served.

View: In the View, the links are set up like this:

<link href="<%: Url.Action("Get", "Content", new { key = "CSS" }) %>" rel="stylesheet" type="text/css" />

Cache Service: We're using an existing cache service we have that just wraps the application cache. At some point we'll probably move that to Velocity or something similar. If the amount of CSS and JS we cache keeps growing, we'll probably change the format of the key to a proper filename and move the content to the file system. But, memory's pretty cheap, we'll see what happens.

Reasoning: (if it matters)

We did this in order to keep the JavaScript for different features in separate files in source control without having to link to all of the files individually in the HTML. Because we configure our content by directory and not individual files, we can also run a full minification during production builds to speed up the whole run time process somewhat. Yet we still get the benefit of determining which content to serve based on the client browser, and cached compressed versions.

In development, the system can be set up with a quick configuration change so that every request rebuilds the JS. The files are concatenated with file names injected in comments for easy searching, but the content is not minified and nothing is cached anywhere. This allows us to change, test and debug the JS without recompiling the application.

Couldn't quite find all these features in a package out there so we spent a couple of days and built it. Admittedly some features were just for fun, but that's why we like doing what we do. =)

Chuck
@Chuck - Yeah this is sort of the thing that I'm going towards. I can't really find anything that *fully* suits my needs so I'm rolling my own for my own projects. Perhaps in the future, I'll make it open source. This project is also just for my learning benefit as well :).
TheCloudlessSky
+1  A: 

Maybe you should check out SquishIt. Some more info on it in this blog post. We use it in production.

qstarin
That has some nice features. I like not needing the controller or the configuration.
Chuck
Yeah I looked at SquishIt before going this route. I'm mostly doing it for my own learning benefit. I'd also like to make the minifiers etc swappable for another framework (perhaps something other than YUI etc).
TheCloudlessSky
Scratch what I just said about swappable minifiers etc in SquishIt; it does have support for them :).
TheCloudlessSky