views:

2363

answers:

7

So I have an ASP.NET MVC app that references a number of javascript files in various places (in the site master and additional references in several views as well).

I'd like to know if there is an automated way, and if so what is the recommended approach, for compressing and minimizing such references into a single .js file where possible. Such that this ...

<script src="<%= ResolveUrl("~") %>Content/ExtJS/Ext.ux.grid.GridSummary/Ext.ux.grid.GridSummary.js" type="text/javascript"></script>
<script src="<%= ResolveUrl("~") %>Content/ExtJS/ext.ux.rating/ext.ux.ratingplugin.js" type="text/javascript"></script>
<script src="<%= ResolveUrl("~") %>Content/ExtJS/ext-starslider/ext-starslider.js" type="text/javascript"></script>
<script src="<%= ResolveUrl("~") %>Content/ExtJS/ext.ux.dollarfield.js" type="text/javascript"></script>
<script src="<%= ResolveUrl("~") %>Content/ExtJS/ext.ux.combobox.js" type="text/javascript"></script>
<script src="<%= ResolveUrl("~") %>Content/ExtJS/ext.ux.datepickerplus/ext.ux.datepickerplus-min.js" type="text/javascript"></script>
<script src="<%= ResolveUrl("~") %>Content/ExtJS/SessionProvider.js" type="text/javascript"></script>
<script src="<%= ResolveUrl("~") %>Content/ExtJS/TabCloseMenu.js" type="text/javascript"></script>
<script src="<%= ResolveUrl("~") %>Content/ActivityViewer/ActivityForm.js" type="text/javascript"></script>
<script src="<%= ResolveUrl("~") %>Content/ActivityViewer/UserForm.js" type="text/javascript"></script>
<script src="<%= ResolveUrl("~") %>Content/ActivityViewer/SwappedGrid.js" type="text/javascript"></script>
<script src="<%= ResolveUrl("~") %>Content/ActivityViewer/Tree.js" type="text/javascript"></script>

... could be reduced to something like this ...

<script src="<%= ResolveUrl("~") %>Content/MyViewPage-min.js" type="text/javascript"></script>

Thanks

+4  A: 

Scott Hanselman recently blogged about combining and moving scripts to static files, basically using the ScriptManager with CompositeScript references and a Path attribute:

<asp:ScriptManager runat="server">
    <CompositeScript path="http://www.example.com/1.js"&gt;
     <Scripts>
      <asp:ScriptReference />
      <asp:ScriptReference />
      <!-- etc. -->
     </Scripts>
    </CompositeScript>
</asp:ScriptManager>

In terms of minifying the static files, you probably have to (and should) use minifying tools at build/deployment time.

Josef
thanks for the heads up on this one Josef. i marked the one above as the correct answer simply because it included both compression and minimization.
wgpubs
+11  A: 

I personally think that keeping the files separate during development is invaluable and that during production is when something like this counts. So I modified my deployment script in order to do that above.

I have a section that reads:

<Target Name="BeforeDeploy">

     <ReadLinesFromFile File="%(JsFile.Identity)">
      <Output TaskParameter="Lines" ItemName="JsLines"/>
     </ReadLinesFromFile>

     <WriteLinesToFile File="Scripts\all.js" Lines="@(JsLines)" Overwrite="true"/>

     <Exec Command="java -jar tools\yuicompressor-2.4.2.jar Scripts\all.js -o Scripts\all-min.js"></Exec>

    </Target>

And in my master page file I use:

if (HttpContext.Current.IsDebuggingEnabled)
   {%>
    <script type="text/javascript" src="<%=Url.UrlLoadScript("~/Scripts/jquery-1.3.2.js")%>"></script>
    <script type="text/javascript" src="<%=Url.UrlLoadScript("~/Scripts/jquery-ui-1.7.2.min.js")%>"></script>
    <script type="text/javascript" src="<%=Url.UrlLoadScript("~/Scripts/jquery.form.js")%>"></script>
    <script type="text/javascript" src="<%=Url.UrlLoadScript("~/Scripts/jquery.metadata.js")%>"></script>
    <script type="text/javascript" src="<%=Url.UrlLoadScript("~/Scripts/jquery.validate.js")%>"></script>
    <script type="text/javascript" src="<%=Url.UrlLoadScript("~/Scripts/additional-methods.js")%>"></script>
    <script type="text/javascript" src="<%=Url.UrlLoadScript("~/Scripts/form-interaction.js")%>"></script>
    <script type="text/javascript" src="<%=Url.UrlLoadScript("~/Scripts/morevalidation.js")%>"></script>
    <script type="text/javascript" src="<%=Url.UrlLoadScript("~/Scripts/showdown.js") %>"></script>
<%
   }  else  {%> 
  <script type="text/javascript" src="<%=Url.UrlLoadScript("~/Scripts/all-min.js")%>"></script>
<% } %>

The build script takes all the files in the section and combines them all together. Then I use YUI's minifier to get a minified version of the javascript. Because this is served by IIS, I would rather turn on compression in IIS to get gzip compression. ** Added ** My deployment script is an MSBuild script. I also use the excellent MSBuild community tasks (http://msbuildtasks.tigris.org/) to help deploy an application.

I'm not going to post my entire script file here, but here are some relevant lines to should demonstrate most of what it does.

The following section will run the build in asp.net compiler to copy the application over to the destination drive. (In a previous step I just run exec net use commands and map a network share drive).

<Target Name="Precompile" DependsOnTargets="build;remoteconnect;GetTime">

      <MakeDir Directories="%(WebApplication.SharePath)\$(buildDate)" />

      <Message Text="Precompiling Website to %(WebApplication.SharePath)\$(buildDate)" />

      <AspNetCompiler
       VirtualPath="/%(WebApplication.VirtualDirectoryPath)"
       PhysicalPath="%(WebApplication.PhysicalPath)"
       TargetPath="%(WebApplication.SharePath)\$(buildDate)"
       Force="true"
      Updateable="true"
      Debug="$(Debug)"
       />
      <Message Text="copying the correct configuration files over" />

      <Exec Command="xcopy $(ConfigurationPath) %(WebApplication.SharePath)\$(buildDate) /S /E /Y" />

     </Target>

After all of the solution projects are copied over I run this:

    <Target Name="_deploy">
     <Message Text="Removing Old Virtual Directory" />
     <WebDirectoryDelete
      VirtualDirectoryName="%(WebApplication.VirtualDirectoryPath)"
      ServerName="$(IISServer)"
      ContinueOnError="true"
      Username="$(username)" 
      HostHeaderName="$(HostHeader)"
      />

     <Message Text="Creating New Virtual Directory" />

     <WebDirectoryCreate 
      VirtualDirectoryName="%(WebApplication.VirtualDirectoryPath)" 
      VirtualDirectoryPhysicalPath="%(WebApplication.IISPath)\$(buildDate)"
      ServerName="$(IISServer)"
      EnableDefaultDoc="true"
      DefaultDoc="%(WebApplication.DefaultDocument)"
      Username="$(username)"
      HostHeaderName="$(HostHeader)"
      />
</Target>

That should be enough to get you started on automating deployment. I put all this stuff in a separate file called Aspnetdeploy.msbuild. I just msbuild /t:Target whenever I need to deploy to an environment.

Min
I like your solution but I'm not so sure I understand it fully.1. What is Url.UrlLoadScript()? I don't see this method available by default.2. What is your automated deployment process? I'm still doing this old school by simply doing a release build and then right-clicking the project and selecting "Publish." So I'm not sure how to even set this up nor how your deployment script fits into it. Would you mind going a bit more in depth here? Thanks
wgpubs
Up-Voted from me :) This is exactly what we do in our process as a custom build task with our CI solution. For the answer to your #2, take a look at MSBuild options and creating build scripts. When you "publish" you're actually executing a build script (all .proj and .sln are actually msbuild scripts). So he's created a build task that gets executed, either via CI solution like we do, or from visual stuido by intercepting some build events.
datacop
Woops... Haha, it's an extension method. It's basically the same as the Url.Content except I put an http revision number at the end. The revision number should be updated to the SVN revision number, so it remains unique.
Min
Even though I'm not sure where to start with MSBuild I'm going to mark this as the correct answer.Do these msbuild scripts go into a separate project or are they just sitting around in some indepedent spot?Any recommended tutorials / how-to's for getting started with MSBuild?Thanks again!
wgpubs
I just put the MSBuild script in a separate file. I don't really remember reading any single tutorial on MS Build actually. I sort of got my understanding by reading lots of MSDN docs on it. I would start with the overview first. http://msdn.microsoft.com/en-us/library/ms171452.aspx
Min
One last question. I reference javascript files via the <script> tag in both my site.master and in a number of views. This means that *some* views will include more javascript files than others. Given this what would be best? Should I simply include ALL javascript files in site.master and follow your procedure above -OR- should I simply minimize all the individual .js files and not worry about joining them into a single .js file?Thanks.
wgpubs
Take the easier path.
Min
I would suggest using `ViewContext.HttpContext` instead of `HttpContext.Current`
Anton
ViewContext is part of system.web.mvc. For better or worse, not all projects are in MVC.
Min
+2  A: 

As others have suggested, you'd be best off creating a static minified build. Alternatively, there's a version of YUICompressor available for .NET here: http://www.codeplex.com/YUICompressor

Evan Trimboli
+5  A: 

Actually there is a much easier way using Web Deployment Projects (WDP). The WDP will manage the complexities of the aspnet__compiler and aspnet__merge tool. You can customize the process by a UI inside of Visual Studio.

As for the compressing the js files you can leave all of your js files in place and just compress these files during the build process. So in the WDP you would declare something like this:

<Project>
   REMOVE CONTENT HERE FOR WEB

<Import 
  Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets" />
<!-- Extend the build process -->
<PropertyGroup>
  <BuildDependsOn>
    $(BuildDependsOn);
    CompressJavascript
  </BuildDependsOn>
</PropertyGroup>

<Target Name="CompressJavascript">
  <ItemGroup>
    <_JSFilesToCompress Include="$(OutputPath)Scripts\**\*.js" />
  </ItemGroup>
  <Message Text="Compresing Javascript files" Importance="high" />
  <JSCompress Files="@(_JSFilesToCompress)" />
</Target>
</Project>

This uses the JSCompress MSBuild task from the MSBuild Community Tasks which I think is based off of JSMin.

The idea is, leave all of your js files as they are (i.e. debuggable/human-readable). When you build your WDP it will first copy the js files to the OutputPath and then the CompressJavascript target is called to minimize the js files. This doesn't modify your original source files, just the ones in the output folder of the WDP project. Then you deploy the files in the WDPs output path, which includes the pre-compilied site. I covered this exact scenario in my book (link below my name).

You can also let the WDP handle creating the Virtual Directory as well, just check a checkbox and fill in the name of the virtual directory.

For some links on MSBuild:

Sayed Ibrahim Hashimi
Thanks Sayed. Just wondering how a CI tool like TeamCity might be able to interact with WDPs? Or, given a CI tool, is it better to go with build the msbuild .xml file?thanks
wgpubs
WDP files are MSBuild files. So if you can install the WDPs on your build server or you can place the files it contains in source control, and update paths manually if you want.
Sayed Ibrahim Hashimi
Cool. I asked this above in another comment but I'll ask you too. "One last question. I reference javascript files via the <script> tag in both my site.master and in a number of views. This means that some views will include more javascript files than others. Given this what would be best? Should I simply include ALL javascript files in site.master and follow your procedure above -OR- should I simply minimize all the individual .js files and not worry about joining them into a single .js file? Thanks."Your thoughts?Thanks much. Good stuff.
wgpubs
With this approach, you don't have to change how you include your js files. Just do whatever you would have if they were not minified. If all the js files are small then putting them all in the site.master is fine, but if you have some large ones I wouldn't include those. In reality either approach is fine because these js files should be cached on the client machine once they get there.
Sayed Ibrahim Hashimi
A: 

I have written something to handle this for me automatically. It uses google's closure compiler. You can read the code here:

http://www.picnet.com.au/blogs/Guido/post/2009/12/10/Javascript-runtime-compilation-using-AspNet-and-Googles-Closure-Compiler.aspx

Thanks

Guido

gatapia
+1  A: 

You could use MvcContrib.IncludeHandling. It:

  • Supports CSS and JS
  • Combines to a single request
  • Minifies
  • Gzip/Deflate compresses
  • Sets up cache headers
  • Uses HTMLHelper extension methods to register includes to then combine them at run-time
  • Is pluggable via IoC

Under the covers, it uses YUICompressor.

Peter Mounce
+1  A: 

MvcContrib.IncludeHandling works well for this situation. In the example, I have a Model with a collection of styles (string). Also if I need to add a custom Style/JS to the page then can do that as well. Then calling Html.RenderCss combines all the styles/js together in one file and minifies it.

<head>
<% foreach (var styleSheet in Model.Styles) {%>
<% Html.IncludeCss(styleSheet)); 

<% } %>
<% Html.IncludeCss("~/Scripts/jquery.1.4.2.js")); 

<%= Html.RenderCss() %>
</head>

Same way for javascript.

<%
Html.IncludeJs("~/scripts/ConsoleLogger.js");
Html.IncludeJs("~/scripts/jquery.log.js");
Html.IncludeJs("~/Scripts/2010.1.416/jquery.validate.min.js");
Html.IncludeJs("~/Scripts/2010.1.416/telerik.calendar.min.js");
Html.IncludeJs("~/Scripts/2010.1.416/telerik.datepicker.js");
Html.IncludeJs("~/scripts/jquery.ui.datepicker-en-GB.js");
%>

<%=Html.RenderJs()%>

When this gets rendered to the client the output looks like this (minified combined 1 file)

<link rel='stylesheet' type='text/css' href='/include/css/-QdUg9EnX5mpI0e4aKAaOySIbno%40'/>

The API also offers a debug flag which when on doesn't minify or combine the scripts when set which is greatly useful.

In a matter of minutes I went from Yslow score of F to B. (24 scripts down to 2)... Awesome! And a drop of 40kbs.

Obvious downside is the server is doing the compression on the fly. I think there are options to cache the combined script for a defined period which would quickly alleviate this though.

Jafin
re: caching - yes, there are. It's a configuration option, "cacheFor={timespan}" on each include-type - see http://github.com/mvccontrib/MvcContrib/blob/master/src/MvcContrib.IncludeHandling/Configuration/IIncludeTypeSettings.cs and http://github.com/mvccontrib/MvcContrib/blob/master/src/MVCContrib.UnitTests/IncludeHandling/configs/CanChangeAllTheDefaultsEvenThoughIShouldntWriteATestWithABigSurfaceAreaLikeThisNaughtyPete.xml. It's set to default to a year.Happy to hear you like it :-)
Peter Mounce
Also note Html.IncludeCss has a params overload, as well as a IEnumerable<string> includes overload, so you ought to be able to do Html.IncludeCss(Model.Styles) in your example.
Peter Mounce