Ranting and raving about anything I feel like complaining about.

Modern Web Development - Part 2

SpaghettiThis is the second of ten parts. The topics will be:

Architecting JavaScript

Working with JavaScript can be daunting. In the past, I’ve seen some projects with just a handful of huge files that become difficult to manage. So in architecting what I needed to build, I wanted to adhere to the idea that there was common code and there was view-specific code. There are two different classes of JavaScript that I care about: libraries (i.e. not my code) and site code (i.e. my code).

For now, let’s segregate these two types of code into separate directories as shown below:

web1

The scheme of these directories doesn’t matter, but I decided on keeping the Scripts directory since Nuget packages like to update to the Scripts folder. You’ll see in a later post when I talk about packaging why I separate these.  Most of the time I am ignoring “not my code” and opening “my code” quite a lot.

One strategy I’m using is to load all the scripts on every page. Our total size of my scripts is not enormous and I expect that the initial hit of caching the script(s) on the first page will mean that subsequent pages will be faster. I’ll talk about how we are going to package the scripts in a later part of this series, but for now I can just include all the scripts of our project in the ‘master page’ (i.e. _Layout view):

<!-- _Layout.cshtml -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>@ViewBag.Title</title>
  <link href="@Url.Content("~/Content/Site.css")" 
        rel="stylesheet" 
        type="text/css" />
  @RenderSection("StyleSheets", required: false)
</head>
<body>
  @RenderBody()
</body>
</html>
<!-- We'll use a packager later, don't freak out guys -->
<script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")" 
        type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/modernizr-2.0.6-development-only.js")" 
        type="text/javascript"></script>
<script src="@Url.Content("~/js/mwd.js")" 
        type="text/javascript"></script>
<script src="@Url.Content("~/js/Home.Index.js")"
        type="text/javascript"></script>


This means that each page doesn’t need to add their own scripts (in fact, when I package them, I’ll just include all the scripts in the /js/ directory). This should result in less friction at the cost of an initial page load of a larger script which I am not worried about for desktop use (for mobile, I’ll change this strategy as you’ll see in a later part of the series).

Originally I had planned on having a script per page (if necessary) then just including it on the pages required but that meant I needed to be aware of code collisions and inclusion on each page. To combat this a JavaScript namespace can help segment all of my code in it’s own object so I don’t pollute the global namespace. This is just like in .NET where you will create namespaces for your classes. Though JavaScript doesn’t have a namespace as such so a named object is good enough to do the trick:

// mwd.js
(function (m, $) {

  m.setStatusMessage = function (message) {
    $('#status-message').text(message);
  }

} (window.mwd = window.mwd || {}, jQuery));

This works because of line 7 (highlighted). It executes this anonymous function creating the “mwd” object if it doesn’t exist. It passes in this object (as the m variable) so I can add any global data/functions to the object. The first script to run will not find the ‘window.mwd’ object so it will create a brand new object. Subsequent scripts (which will also have this function wrapper around it) will find the ‘mwd’ object and just pass it in. This way every script that uses this convention will add to the namespace object with our code and data. This simplifies the problem of script loading ordering problems and prevent name collision with objects in the global namespace. You can see that in the above that the mwd object is having a new function added to it. Since it’s an object on the window object, we can access this function from any script but only exposing a single global object.

For page specific code, the function wrapper is still used:

// home.index.js
(function (m, $, undefined) {

  m.initHomeIndex = function () {
    $("#main-section button").on("click", function () {
      m.setStatusMessage("clicked");
      // This works too
      //mwd.setStatusMessage("clicked");
    });
  };

} (window.mwd = window.mwd || {}, jQuery));

This approach of using an initialization function works as this script just registers the startup function for this page but doesn’t call it. This allows me to call the script on my individual page. This is especially helpful if I need to send in some state to the initialization (i.e. some data from the controller).

You can see the highlighted line is calling a common function that is defined in a different .js file (the mwd.js) and this just works as this code isn’t executed until after all the extensions to the mwd object are already added by all the scripts.

My method for calling the initialization function is to use an optional section on the Layout view that wraps whatever the individual page uses for an initialization (though not all pages will need initialization functions):

<!-- _Layout.cshtml -->

...

<!-- I'll use a packager later, don't freak out guys -->
<script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")" 
        type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/modernizr-2.0.6-development-only.js")" 
        type="text/javascript"></script>
<script src="@Url.Content("~/js/mwd.js")" 
        type="text/javascript"></script>
<script src="@Url.Content("~/js/Home.Index.js")"
        type="text/javascript"></script>
@if (IsSectionDefined("Init"))
{
  <script type="text/javascript">
    $(document).ready(function() {
      @RenderSection("Init", required: false)
    });
  </script>
}

This way if the Init section is defined on a view, I wrap the init code in a jQuery ready call so that it gets called once the page has loaded.  The individual view looks like so:

@{
  ViewBag.Title = "Index";
}
@section Stylesheets
{
  <link rel="stylesheet" href="@Url.Content("~/Content/Home.Index.css")" />
}
@section Init
{
  mwd.initHomeIndex();
}
<h2>
  Index</h2>
<section id="main-section">
  <div>
    This is a client-side example!</div>
  <button>
    Show Message</button>
</section>
<footer>
  <div id="status-message"></div>
</footer>

If this were a strongly typed view I could have send date into the initHomeIndex function by JSON serializing onto the page.  This allows me to efficiently have my view code added to every view but only call it on the particular views I need it. This is not just for calls that are necessary per view, but could be any shared code as well.

There are other strategies for doing this (as Dave Ward (@encosia) explained using a class on a body tag with the controller and view name) but for our needs this was the most straightforward since we needed initialization data on a number of different views.

This is a pretty universal way of handling this that worked well for me. One thing I ran into was that our Layout view wanted to have some code/markup that was only to be used during release builds (i.e. Google Analytics), I was surprised that Razor didn’t allow me to test for #DEBUG builds. To fix this, I added an extension method to the HtmlHelper class:

using System.Web.Mvc;

namespace ModernWebDev.Helpers
{
  public static class HelpHelpersExtensions
  {
    public static bool IsDebug(this HtmlHelper htmlHelper)
    {
#if DEBUG
      return true;
#else
      return false;
#endif
    }
  }
}

As this HtmlHelpersExtensions class would probably be a placeholder for a number of extensions for our use, I changed the web.config in the Views directory (not the site-wide one) to include my Helpers namespace on every view:

<?xml version="1.0"?>
<configuration>

  ...

  <system.web.webPages.razor>
    <host factoryType="..." />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
        <add namespace="ModernWebDev.Helpers" />
      </namespaces>
    </pages>
  </system.web.webPages.razor>

...

</configuration>

This in a convenience and not necessary, but knowing you can add namespaces to every razor view is a great help so you don’t have to annotate a number of @using statements at the top of every view.

I could then use the new extension method to have conditional code in our Layout view:

<!-- _Layout.cshtml -->

...

@if (!Html.IsDebug())
{
  <div>Placeholder for Google Analytics</div>
}

What I’ve shown you so far may have some of you scratching your head because the separation of these JavaScript files means that you’re probably not getting any intellisense. I made a decision to just forgo it. If Intellisense is crucial to you, there are some options (none of these were worth my trouble):

  • Adding a comment on top of every .js file to the Visual Studio intellisense file to get jQuery intellisense but it didn’t help with overall intellisense.
  • Using a 3rd party plugin (i.e. R#) to add the intellisense.
  • Use a non-Visual Studio editor (i.e. WebStorm or other editors)

All of these are adequate solutions, but they all seemed to slow down my development and I am an old guy who can get by without it.  YMMV.

Much of the process of writing the JavaScript can’t be adequately summarized until we get to the debugging process I used in this project which I will cover in the next part of this series. Good hunting!

Here’s a link to the version of the source code so far:

 
 

Comments

Gravatar Thanks for the set of blog posts, I really look forward to reading the rest of the series!
Gravatar Thanks a lot for publishing this series. I can't wait for the whole list.
Gravatar Really enjoying this series and looking forwards to the rest, great stuff, thanks.
Gravatar I have converted to Sublime Text 2 for all of my JS and CSS development.
Gravatar Great series. Can't wait for the next one. Learnt a couple of new ways of doing things already. Keep them coming.
Gravatar Thanks for trying to educate us Shawn. Coming from a more Java/.NET type of world I feel the learning curve is pretty steep. In honesty, I was lost halfway through the first article, not understanding the link between the different files and the order of execution - I tried running the code in a debugger and set breakpoints, but that did not really help me understanding what is going on behind the scenes; insights into the plumbing that drives this all would be greatly appreciated.

I am not sure I also fully understood what jQuery core achieves, is this just making up for Javascript/HTML shortcomings or lack of decent syntax?
Gravatar Alfred,

Let me see if I can help. Think of what I'm doing with the functional syntax as preparing a class for compilation. So that at runtime I can call the properties of the 'compiled' class (called mwd).

jQuery is just a more browser agnostic way of getting at the DOM and will smooth out inconsistencies between browsers.
Gravatar Love the series Shawn!

Sorry to be really pedantic but e.g. means "exempli gratia" and is used for examples whereas i.e. means "id est" which means that is.

So I think i.e. is better than e.g. in "libraries (e.g. not my code) and site code (e.g. my code)."

Jason
Gravatar Jason,

I don't think you're sorry about being pedantic. I know the difference, but missed it this time. Thanks.
Gravatar Shawn,

Your idea for the Init section to define script which should be rendered in a ready event is nice and clean.
But is does not work well when you have multiple sources which produce code which should be in the ready event. I'm using the Html.EditorFor() method and using EditorTemplates. These templates can also have script to init the editor. I could script that right away in the template. But I think it is more appropriate to have all the script in one ready event at the end of the document. So I created a helper method which saves all the init scripts an a list (saved in the HttpContext.Items during the request) and is rendered at once in the layout file.

Example:

@{
Html.ScriptRegistrar().OnDocumentReady(
@<text>
function adjustFooter() {
footer = $('#mainfooter');
if (footer.length) {
footer.css('top', $(window).height() - footer.outerHeight(true) + 'px');
}
}
adjustFooter();
$(window).resize(function () { adjustFooter(); });
</text>);
}

And then at the bottom of the layout view:

@Html.ScriptRegistrar()

which renders the ready event.

Regards, Jaap
Gravatar Jaap,

I don't use any of the server-side script handling functionality to combat this. The one init on the page could then call multiple if the page needed it, but multiple inits per page don't make sense to me. If you want to send me an example via the contact page, go ahead and maybe then I can see what you mean.

This example would have a lot of the code in the page which is exactly what I am avoiding. All initialization is in the .js file, not in the page.
Gravatar Hi Shawn, and first thanks for your series, it helps me getting started in setting up a convenient dev env with MVC3!

I have a question concerning the declaration of the home.index.js wrapper:

function(m, $, undefined)...

What is the undefined parameter for? You didn't declare it in the mwd equivalent? That's the only point I don't understand clearly.

Thanks.

By the way, e.g. aka i.e. ;)
Gravatar The 'undefined' is a JavaScript-ism to make sure that you can compare against undefined easier. I've never used it actually in code, but it's common way to define these executing blocks so I am following the convention.
Gravatar What are your thoughts on RequireJS and other JS loaders?

Thanks for the article.

Gravatar Shawn B,

I haven't used them or needed them so i don't have an opinion. What are they trying to do for you? You'll see in a subsequent part of the article why I am not using loaders as I am packaging all my .js together.
Gravatar Nice series Shawn. I like how you take everyone through the process and do things that you know wont be there in the end. Revealing the journey is key to understanding why you do it the "right" way.

I also use a scripts folder and a js folder to separate my scripts from ones I grab off github or NuGet. The js folder is for my javascript behind my views/html.

Regarding intellisense, I've noticed no slow downs from Resharper 6.1 in VS and its an amazing productivity enhancer for JavaScript for me. Maybe its my SSD drive tho.

Great job man!
Gravatar John,

Thanks! That's the rub, I am using Sublime Text 2 almost exclusively for my text editing in HTML/JS/LESS these days. But I am an old guy who wasn't brought up with Intellisense so I don't miss it as much as some.
Gravatar Your sample doesn't work in IE9. clicking the button doesn't update the message. But it works in Chrome. I think it's because section tag is not supported in IE 9.
Gravatar Mher,

You're right, I forgot to include the HTML5 CSS resetter in the style. I'll update for the next example.
Gravatar great job, thank you for sharing your precious experiences
Gravatar Did you notice the improvements in VS11beta regarding the Javascript intellisense? See http://www.hanselman.com/blog/FeaturesNOONENOTICEDInVisualStudio11ExpressBetaForWeb.aspx (paragraph "Smart Javascript References").
Works fine. And it is now even working inside function closures like:
(function ($) {
$(.....
})(jQuery);
Gravatar Jaap,

Yes I have but in VS11 it still requires references hacks. I don't know why they wouldn't do what they're doing for Metro's JS projects (intellisense across .js in the project instead of forcing reference paths to be checked in).
Gravatar I would suggest a slightly different approach... generally, I have a "Styles" section in the head, and a "Scripts" section by the closing body tag (after site wide scripts, before tracking and advertising scripts. I also will use a few Helpers or @ViewBag properties for setting "BodyId" and "BodyClasses" this way I can have my sitewide js look at $('body').attr('id') and if there is a site.page[id] method, it gets called in the main startup event for wiring. I also use the latest chirpy for min/merge support. ymmv.
Gravatar Michael,

I've seen this approach and I like it too but decided against it for the series as I find it not as clear for teaching the topic (versus what works when you develop a solution).

Add a Comment

*
*
*