As many of you know that I am currently involved in developing few UI Components for the ASP.NET MVC Framework (Hint: It is not a personal project, and we do have the plan to make it the source open dual license). In this post, I will discuss about our design decisions regarding how we plan to manage the javascript files with our UI Components.
When developing a typical web application we usually find four kinds of javascript files.
- Framework scripts like jQuery, ExtJS or maybe MS Ajax etc.
- UI Component/Plug-in scripts like jQuery UI, jQuery Tools, jQuery validation/forms plug-ins etc that depends upon the framework script.
- Application level common scripts that are shared among multiple pages and might depends upon the above two.
- Page scripts(not embedded in html, rather as external file) that might depends upon on the above three.
To make the application really responsive/fast we often have to merge/compress/cache these javascript files. Currently most of the framework in .NET world (including the latest ASP.NET AJAX 3.5) supports combining the scripts in a single response. But this is not an optimal option in most of the cases. Why? Because we are either downloading the same file content in different pages or we are downloading some unnecessary file content for a specific page (assuming that you have specified all your merged script in your master page). Certainly it is a one time issue, once the file is downloaded it will be cached and the user does not have to download it again, but does not it also indicate the incapability of your script management components, also this is not viable option in today's heavily ajax sites.. There are also few other considerations like how can I load the scripts from a CDN (free/paid), does it render the scripts at the bottom of the pages etc etc. While considering all the above facts, we think the best way to serve scripts is, if it is merged in groups. Lets consider the following scenario, each url is using the listed javascript files:
| http://mysite.com/List | http://mysite.com/View/3 | http://mysite.com/Edit/3 |
- jquery-1.3.2.js
- jquery-ui-1.7.2.custom.js
- jquery.template.js
- jquery.pagination.js
- Utility.js
- Search.js
- List.js
| - jquery-1.3.2.js
- jquery-ui-1.7.2.custom.js
- jquery.template.js
- jquery.pagination.js
- Utility.js
- View.js
| - jquery-1.3.2.js
- jquery-ui-1.7.2.custom.js
- jquery.validate.js
- jquery.form.js
- jquery.watermark.js
- Utility.js
- Edit.js
|
We can group the above scripts, in the following groups:
- jQueryBase: jquery-1.3.2.js, jquery-ui 1.7.2.custom.js.
- ListView: jquery.template.js,jquery.pagination.js.
- Form: jquery.validate.js, jquery.form.js, jquery.watermark.js.
- ListLocal: Utility.js, Search.js, List.js.
- ViewLocal: Utility.js, View.js.
- EditLocal: Utility.js, Edit.js.
Now, we can replace with the following:
| http://mysite.com/List | http://mysite.com/View/3 | http://mysite.com/Edit/3 |
- jQueryBase
- ListView
- ListLocal
| - jQueryBase
- ListView
- ViewLocal
| - jQueryBase
- Form
- EditLocal
|
The benefits of the above comparing to individual file or single file combining are:
- We are sending less request to our web server (as the files are now grouped).
- We are not downloading the same file between the page visits (comparing to single file response).
- We are downloading the files that are only required for that visiting page.
With our Script Management Component it becomes really easy to achieve the above, for example, for the List you can use the following syntax:
<% Html.jQuery().ScriptRegistrar().Scripts(script => script.AddGroup( "jqueryBase",
group => group.Add("~/Scripts/jquery-1.3.2.js")
.Add("~/Scripts/jquery-ui-1.7.2.custom.js")
)
.AddGroup( "ListView",
group => group.Add("~/Scripts/jquery.template.js")
.Add("~/Scripts/jquery.pagination.js")
)
.AddGroup("ListLocal",
group => group.Add("~/Scripts/Utility.js")
.Add("~/Scripts/Search.js")
.Add("~/Scripts/List.js")
)
)
.Render(); %>
to configure each group setting you can use:
group => group.Add("~/Scripts/jquery-1.3.2.js")
.Add("~/Scripts/jquery-ui-1.7.2.custom.js")
.Version("2.1)
.Compress(true)
.CacheDurationInDays(365)
.Combined(true)
you can also use a CDN instead of loading the each group, it becomes really handy when your application becomes popular, to set the CDN you can use:
group => group.UseContentDeliveryNetwork(true)
.ContentDeliveryNetworkPath("http//mycdn.com/myScriptGroup.js")
If you do not want to specify the same setting for each group again and again (DRY) you can set the default settings in the application start (global.asax) like the following:
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
WebAssetDefaultSettings.CacheDurationInDays = 365;
WebAssetDefaultSettings.Combined = true;
WebAssetDefaultSettings.Compress = true;
WebAssetDefaultSettings.Version = "2.1";
}
When you run the application and open the Firebug, you will find that each group is merged/compressed/cached like the following:
We have included some default behavior (AKA convention over configuration) backed into the script management components, for example when you are running the application in development mode (debug="true" in web.config) it will include the .debug.js and in release (debug="false") .min.js files no matter what filename you have mentioned in the ScriptRegistrar. If the file does not exist (.min.js/.debug.js) it will automatically fallback to the original value. When developing these components we take the YSlow rules very seriously, for example, when you use the ScriptRegistrar, it will render the script tags at bottom of the page, no matter how many ScriptRegistrar placed in the Master/Content/User Controls. Other than script files, you can also mention your startup and cleanup javascript statements in the ScriptRegistrar. For example, if you have the following:
In Master page:
<% Html.jQuery().ScriptRegistrar().Scripts(script => script.AddGroup( "jqueryBase",
group => group.Add("~/Scripts/jquery-1.3.2.js")
.Add("~/Scripts/jquery-ui-1.7.2.custom.js")
)
)
.OnPageLoad(() =>
{%>
test1.init();
<%}
)
.OnPageUnload(() =>
{%>
test1.dispose();
<%}
)
.Render(); %>
and in Content Page:
<% Html.jQuery().ScriptRegistrar().OnPageLoad(() =>
{%>
test2.init();
<%}
)
.OnPageUnload(() =>
{%>
test2.dispose();
<%}
); %>
and in User Control:
<% Html.jQuery().ScriptRegistrar().OnPageLoad(() =>
{%>
test3.init();
<%}
)
.OnPageUnload(() =>
{%>
test3.dispose();
<%}
); %>
When the page renders, it will write the following:
<script type="text/javascript" src="http://weblogs.asp.net/asset.axd?id=eyJjdCI6ImJuIjoic2hCcnVzaFhtbC5qcyJ9XX1dfQ%3d%3d"></script>
<script type="text/javascript">
//<![CDATA[
jQuery(document).ready(function(){
test1.init();
test2.init();
test3.init();
});
jQuery(window).unload(function(){
test3.dispose();
test2.dispose();
test1.dispose();
});
//]]>
</script>
</body>
</html>
There should be a little difference in the Content and User Control code. There will no Render() for those two. Only the Master will have the Render() method.
And it is View Engine independent, we have already tested it in Webforms, Spark and NHaml.
What do you think? What features it is currently missing? Comments and suggestions are really appreciated.