Bundling in .NET Core MVC Applications with webpack and typescript

Jamie
16 min readJan 5, 2017

--

Before we begin in earnest, I just wanted to point out that this is a repost from my blog on .NET Core (see the bottom of this article for a direct link to it). The information contained in this article was correct at the time of writing, but for more up to date information, please head to my blog.

What Is Bundling And Why Would You Need It?

Let’s say that you have a massive MVC application. It’s a multi-page application, with JavaScript on every page. Maybe you have jQuery, Bootstrap’s JavaScript, and Knockoutjs, all of which needs to be loaded on each page, and a different set of view models for each one of those multiple pages.

You can include jQuery, Bootstrap and Knockout.js in your shared layout file, but including each of the view models on their respective pages can get tedious (especially if you end up using a set of several view models on those pages).

Which can definitely happen. Better to re-use than re-implement, and all that.

A little about how web browsers work

This isn’t going to be an in depth description on how a web browser works. I am, however, going to give a brief overview of how a browser gets most of the resources for a page (css, js, images, anything that isn’t HTML, etc.)

Let’s say that you have the following _layout.cshtml file:

<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Awesome Site</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
crossorigin="anonymous">
<link rel="stylesheet" href="/css/site.css" />
</head>
<body>

<div class="container body-content">
<div class="row">
<div class="col-xs-12">
<h3>The best content ever!</h3>
</div>
</div>

< /hr>
<footer>
<p>Why're you looking down here? The content is up there!</p>
</footer>

</div>

<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"
integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.1/knockout-min.js"></script>
<script src="/js/site.js"></script>
</body>

</html>

Once your browser has received this file, it will queue up requests for the external resources.

My use of external here refers to any resource that’s external to the HTML file

The resources it starts to request are:

What’s wrong with this? Well, there’s nothing wrong with this at all. The problem is that it would be slow to load all resources and fully render the page.

Each time that a web browser finds a link or script tag, it needs to put in a request for the external file. It has to establish a connection with the server, request the file and wait for it to download. Modern web browsers can do this in parallel, but there’s an upper limit to how many simultaneous requests that your browser can make.

The upper limit differs depending on which browser you are using.

Google Chrome’s network tab showing a selection of the get requests needed to render Amazon UK’s front page

In the above example, we can see some of the requests for files that are needed to render the front page of Amazon’s UK website. You can see that each file takes some time to download, each of these files needs to be downloaded before the page is completely loaded.

You can see that it took just under 2 seconds to download the DOM content, just over 3 seconds to download external resources, but almost 40 seconds to parse and paint everything.

Although most of the content that took 40 seconds to load and render was below the fold, so you wouldn’t see it until you scroll down far enough.

Bundling helps to reduce the number of simultaneous requests that the browser has to perform (and wait on) before it can render a web page.

Seriously, in 2008 Amazon calculated that a 1 second slow down on page load could cost them $1 billion

Minification is another trick that we can use to decrease page load time, but we’ll cover that another time.

Bundling in .NET Core MVC Applications

Over in .NET Framework land, enabling bundling is pretty easy. There’s a bit of set up required, but once you’ve done that it’s easy to add to.

However, there are no native bundling options in .NET Core. This is because bundling is seen as a design time action for .NET Core.

Bundling and minifying before deployment provides the advantage of reduced server load. However, it’s important to recognize that design-time bundling and minification increases build complexity and only works with static files.

So how do we bundle our JavaScript and css in .NET Core? The answer is external tools.

Options

There are a plethora of options available to help with bundling. There’s:

  • Gulp
  • Grunt,
  • Bower,
  • webpack
  • BundlerMinifier.Core

to name just a few.

All of these have their benefits and drawbacks (mostly in the amount of set up required to get them up and running), and most of them are JavaScript Task Runners (or just JavaScript applications) and will require npm to be installed on your development machine (and your build server, if you’re using one) before you can use any of them.

Zac talked about how to install npm in his guest post.

Today I’ll talk webpack.

I’ll write about the others in separate posts.

webpack

The creators of webpack describe it like this:

webpack is a module bundler for modern JavaScript applications

Once it’s configured, it will take a collection of entry points, build a graph of all of that entry point’s dependencies, and bundle them all together into one file.

Let’s say that you have a series of pages, and each one is related to one of the following:

  • searching for products
  • account management (signing up, viewing orders, changing payment details, etc.)
  • providing feedback

Each of these groups of pages will have different JavaScript requirements. The search page, for example, might have validation on search strings being a certain length and code to perform ajax GET requests for the search results. But your account management JavaScript will be different to that (you might need client side verification of password length or complexity, for example).

Never forgetting to to server side validation too, of course.

The JavaScript required for each of these pages would be perfect as entry points in our webpack configuration. Each of the modules would be added to an entry point, then bundled together and outputted as a single js file.

webpack Worked Examplex

What we’re going to do here is build a trivial example using the yeoman generator and some npm magic. Along the way, we’ll introduce TypeScript (but we won’t touch on it much), then bring in webpack and bundle our transpiled JavaScript together.

In fact, what we’ll actually do is recreate the code in this GitHub repo. So feel free to head there for a sneak peak.

Empty Web Application

So the first thing we’re going to do is have yeoman generate an Empty Web Application.

We’re going to make this into an MVC application by hand, because we’ve taken the easy road so far.

You should be pretty familiar with the yoeman generator by now. But I’ll list each command that we need to run for completeness.

yo aspnet

Choose “Empty Web Application” and give it the name “webpack-ts”, then restore it’s packages.

cd webpack-ts
dotnet restore

Adding MVC

Next we want to add MVC and Static Files to the project. Add the following two lines to the project.json:

"Microsoft.AspNetCore.Mvc" : "1.1.0",
"Microsoft.AspNetCore.StaticFiles": "1.0.0",

Then restore packages again

dotnet restore

Then we need to alter our startup.cs. Just take this version and paste it over the current class:

public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

builder.AddEnvironmentVariables();
Configuration = builder.Build();
}

public IConfigurationRoot Configuration { get; }

public void ConfigureServices(IServiceCollection services)
{
// here is where we add the MVC service
services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();

if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// here is where we'll add the Static Files service
app.UseStaticFiles();
// and here is where we add our routing
app.UseMvc(routes => {
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}

What we’ve done here is added told the startup class that we want to add MVC to the list of services available to our application (at line 22). Then we’ve told the ApplicationBuilder that we want to use StaticFiles, and default MVC routing (between lines 37 and 43).

Adding Controllers and Views

Now we’ll need to add a controller and a view. You can do this from the terminal, but I’ll show you how to do it from within VS Code.

Because we’re going to spend enough time in the terminal today

The webpack-ts project, when initially opened in VS Code

Right click on the webpack-ts node, choose New Folder, and give this folder the name “Controllers”. Then do it again to add a folder called Views.

Right click on the Controllers folder, choose New File, and give this file the name “HomeContoller.cs”. Then paste the following code into it

using Microsoft.AspNetCore.Mvc;

namespace webpack_ts
{
public class HomeController : Controller
{
// GET: /<controller>/
public IActionResult Index()
{
return View();
}

public IActionResult SayName()
{
return View();
}
}
}

What we’ve done here is added two Action Methods (Index and SayName) which return views. We need to go add those views now.

Right click on the Views node, choose New Folder, and give this folder the name “Shared”. Then do it again (on the Views node) and name this newer folder “Home”.

Right click on your Shared node, choose New File, and give it the name “_layout.cshtml” (the underscore is important). Then paste the following markup into it:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webpack.ts</title>
<link rel="stylesheet" href="~/css/bootstrap3-custom/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>&copy; @System.DateTime.Now.Year - GaProgMan</p>
</footer>
</div>

<script src="~/js/site.js" asp-append-version="true"></script>
</body>
</html>

There’s nothing too special here, we’re just declaring what our master page is going to look like.

Right click on the Home node (under the Views node), choose New File, and give this file the name “Index.cshtml”. Then do it again (on the Views node) and name this newer file “SayName.cshtml”.

Paste the following markup into the Index.cshtml file:

<h1 id="greeting"></h1>

<script src="~/app/bundle.js"></script>

And the following markup into the SayName.cshtml file:

<h1 id="greeting"></h1>

<script src="~/app/someOtherBundle.js"></script>

Finally, right click on the Views node, choose New File, and give this file the name “_ViewImports.cshtml”. Then do it again (on the Views node) and name this newer file “_ViewStart.cshtml”.

Here’s the content for those two files — first the “_ViewImports.cshtml:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

And now the “_ViewStart.cshtml”:

@{
Layout = "_Layout";
}

Adding TypeScript Support

This part isn’t 100% essential, but TypeScript is really useful (if a little terse at first). Wikipedia describes TypeScript as:

[TypeScript] is a strict superset of JavaScript, and adds optional static typing and class-based object-oriented programming to the language

TypeScript is along the same lines as CoffeeScript in that it attempts to make JavaScript easier to use by hiding away some of the idiosyncrasies and edge cases of JavaScript syntax, and by enabling full object oriented support (including inheritance).

I’ll explain the contents of the TypeScript files as I add them, but first we need to go do some npm magic.

Go to your terminal and run this command (you might need to do it with sudo or as root, if you’re running a Linux distribution or a Unix-like):

npm i -g typescript@next

This tells npm to install the latest version of the official TypeScript package. Then issue this command:

npm i -g typings

This tells npm to install the latest version of the TypeScript Typings package.

Each external library that you use with TypeScript requires a “.d.ts” (or typings) file to be added to the project. This file tells TypeScript all about the types that the library contains (for example, what the jQuery selector returns, etc.). Without them, you can’t transpile a TypeScript file which uses an external library.

Well, not without using the any keyword, which isn’t great practice because it undermines the whole point of TypeScript.

Adding TypeScript Files

Right click on the webpack-ts node, choose New Folder, and give this folder the name “Content”. Then right click on the Content node, choose New Folder, and give this folder the name “js”.

Right click on the “js” node, choose New File, and give this file the name “greeter.ts”. Then past the following TypeScript into it:

// create the class Greeter and export it
export class Greeter {

constructor(private message: string){
}

sayHello(){
console.log(this.greetingMessage);
}

get greetingMessage() : string{
return `Hello ${this.message} from TypeScript within a webpack bundle`;
}
}

We’re creating the Greeter class as a CommonJs Module and exporting it, so that it can be consumed by any other module or file.

Let’s have a quick look at some of the contents of the file.

constructor(private message: string){
}

Here we’re creating the Greeter class’s constructor. We’re also passing in an argument (called message) of type string.

The most interesting thing here is the use of the private keyword. This tells TypeScript that we want the Greeter class to have a private variable called message, of type string, and that it should initialise it’s value to whatever we pass into the constructor as the first parameter.

get greetingMessage() : string{
return `Hello ${this.message} from TypeScript within a webpack bundle`;
}

Here we’re telling TypeScript to create a get method (called GreetingMessage) which will return a string value. It uses string interpolation to create the returned value.

String Interpolation is a feature of ECMA Script 6

Right click on the “js” node, choose New File, and give this file the name “main.ts”. Then past the following TypeScript into it:

import { Greeter } from './greeter'

export class Main {
private greeter: Greeter;
constructor(private defaultElementId: string) {
this.greeter = new Greeter("there");
}

sayHello () {
this.greeter.sayHello();
document.getElementById(this.defaultElementId).innerHTML =
this.greeter.greetingMessage;
}

get greetingMessage() : string {
return this.greeter.greetingMessage;
}
}

// testing Main class
var instanceOfMain = new Main('greeter');
instanceOfMain.sayHello();

The first thing we’re doing here is importing the Greeter class, we do this by looking the the same directory as the main.ts file (“./”) for a file called greeter (note that we don’t need to supply a file extension).

As with the Greeter class, we’re making a class called Main and exporting it as a CommonJs Module. This class uses an instance of the greeter class, and uses it’s methods to interact with the page it sits on.

After the class declaration, we use an instance of it to set the value of the element with the Id of ‘greeting’ to ‘Hello there’ and prints to the console.

Only one file left to add to the “js node: someOther.ts. Here are it’s contents:

export class someOther {
private myName: string;
private elementId: string;

constructor(defaultName: string, idOfElement: string){
this.myName = defaultName;
this.elementId = idOfElement;
}

sayMyName() {
console.log(`${this.myName}`);
document.getElementById(this.elementId).innerHTML = `Hello ${this.myName}`;
}
}

var instanceOfSomeOther = new someOther('Geoff', 'greeting');
instanceOfSomeOther.sayMyName();

This file is a combination of both Main and Greeter (without any internal instances or either).

Adding tsconfig

Now that we’ve added our TypeScript files, we want to tell TypeScript how to transpile them. To do this we need to add a file called “tsconfig.json” to the root of our project. Here are the contents of that file:

{
"compilerOptions":{
"target": "es5",
"module": "commonjs",
"sourceMap": true,
"sourceRoot": "content/js",
"outDir": "wwwroot/app"
}, "exclude":[
"node_modules"
]
}

This file tells TypeScript that we want to :

  • Transpile to ECMA Script 5
  • Use CommonJs’s Module pattern
  • Include a Source Map (these allow us to place breakpoints in the JS in Chrome’s dev tools, for instance)
  • Where the root of the source files are (/content/js)
  • Where to place the transpiled files

Adding webpack

Finally onto the fun stuff. Up until this point, none of our TypeScript has been transpiled, let along bundled. Let’s do both now.

Back to the terminal:

npm i -g webpack@next

This tells npm to install the latest version of webpack.

We still can’t bundle our TypeScript files, because they need to be transpiled to JavaScript first. We could to this by hand by running tsc in the terminal, like this:

tsc

This will read our tsconfig.json file and produce a js and map file for each of our TypeScript files.

Our transpiled javascript source and map files

But we’d have to bundle the files after transpiling them. Can webpack do both steps for us?

Of course.

Adding awesome-typescript-loader

awesome-typescript-loader is a plugin for webpack that takes care of the transpiling step for you. This means that when you tell webpack to bundle all of your javascript

And we’ll get to that in a minute

it’ll do the transpiling from TypeScript for you in memory before bundling them. This means that the transpiled JavaScript files (the main.js and main.js.map files that are created from the main.ts file, for instance) wont appear on disk and you wont need to clean them up afterwards — the bundles will appear on disk, obviously.

Back at the terminal:

npm i -g awesome-typescript-loader

Now that we have awesome-typescript-loader installed, we should create a webpack config file.

Adding a webpack.config.json

If you run the webpack command without any arguments (or a config file present), you’re going to get a large cli help message.

webpack 1.14.0
Usage: https://webpack.github.io/docs/cli.html

Options:
--help, -h, -?
--config
--context
--entry
--module-bind
--module-bind-post
--module-bind-pre
--output-path
--output-file
--output-chunk-file
--output-named-chunk-file
--output-source-map-file
--output-public-path
--output-jsonp-function
--output-pathinfo
--output-library
--output-library-target
--records-input-path
--records-output-path
--records-path
--define
--target
--cache [default: true]
--watch, -w
--watch which closes when stdin ends
--watch-aggregate-timeout
--watch-poll
--hot
--debug
--devtool
--progress
--resolve-alias
--resolve-loader-alias
--optimize-max-chunks
--optimize-min-chunk-size
--optimize-minimize
--optimize-occurence-order
--optimize-dedupe
--prefetch
--provide
--labeled-modules
--plugin
--bail
--profile
-d shortcut for --debug --devtool sourcemap --output-pathinfo
-p shortcut for --optimize-minimize
--json, -j
--colors, -c
--sort-modules-by
--sort-chunks-by
--sort-assets-by
--hide-modules
--display-exclude
--display-modules
--display-chunks
--display-error-details
--display-origins
--display-cached
--display-cached-assets
--display-reasons, --verbose, -v

Output filename not configured.

Told you so.

Also, this is for the version I have installed, and yours might vary.

Using those cli switches we could perform the webpack steps that we need, but it wouldn’t be easy to manage. So we’ll add a confg file.

Add a file called “webpack.config.js” to the root of the project and paste these contents in:

module.exports = {
"entry":{
"bundle": "./content/js/main.ts",
"someOtherBundle": './content/js/someOther.ts'
},
"output": {
"path": __dirname + "/wwwroot/app",
"filename": "[name].js"
},
"resolve": {
"extensions": ['', '.ts', '.webpack.js', '.web.js', '.js']
},
"devtool": 'source-map',
"module": {
"loaders": [
{
"test": /\.ts$/,
"loader": 'awesome-typescript-loader'
}
]
}
};

There’s a lot going on here, so lets dissect it in chunks:

"entry":{
"bundle": "./content/js/main.ts",
"someOtherBundle": './content/js/someOther.ts'
},

These are our entry points to the webpack dependency graph. If we take the path of the “bundle” entry we:

  • Find the file ./content/js/main.ts
  • Read it’s contents and find any modules that it lists as dependencies (via the import keyword)

It only has one: greeter.ts

  • Recursively read the contents of the modules that they rely on

It doesn’t have any

  • Place these files in memory, ready for outputting
"output": {        
"path": __dirname + "/wwwroot/app",
"filename": "[name].js"
},

Here we’re defining the rules for the outputted bundles.

The variable __dirName is a Node variable which means “the path we’re running this command in”, so __dirname + “/wwwroot/app” evaluates to the app directory found in the wwwroot directory in the root of our project (which is where webpack’s config file is located, thus where it runs from).

[name] is a reserved token in webpack and means “the name of the entry point”. This means that our two entry points will produce two bundles, one named “bundle.js” and one named “someOtherBundle.js”

"resolve": {
"extensions": ['', '.ts', '.webpack.js', '.web.js', '.js']
},

Here we’re telling webpack’s resolver that we’re only interested in files with one of the following extensions:

  • <blank extension>
  • .ts
  • .webpack.js
  • .web.js
  • .js

Any other files that webpack finds as a dependency for the entry points are ignored.

"devtool": 'source-map',

This tells webpack to create a source map for each bundle that it creates.

"module": {
"loaders": [
{
"test": /\.ts$/,
"loader": 'awesome-typescript-loader'
}
]
}

Here is where we call awesome-typescript-loader as a plugin.

We do this by creating an array of loaders within a temporary module. The loaders array contains one entry (but we can easily add more to enhance the processing of the bundle files), which is for our plugin.

The value of test is a regular expression which is tested against the names of the files within the bundle. Any files that match are processed by the loader, any that don’t match are ignored.

The list of matching files are then passed to awesome-typescript-loader for processing. And awesome-typescript-loader is what will do the transpiling for us.

Running Everything

Now that we’ve set everything up, we need to run two commands in the terminal. First we need to tell webpack to do it’s thing:

webpack

Now that we have a config file webpack will use that to do it’s processing, and will produce something similar to this as output:

[at-loader] Using typescript@2.2.0-dev.20161226 from typescript and "tsconfig.json" from /path/to/source/webpack-ts/tsconfig.json.
[at-loader] Checking started in a separate process...
[at-loader] Ok, 0.001 sec.
Hash: 438ca430aa379e6d358d
Version: webpack 1.14.0
Time: 1320ms
Asset Size Chunks Chunk Names
bundle.js 2.92 kB 0 [emitted] bundle
someOtherBundle.js 2.06 kB 1 [emitted] someOtherBundle
bundle.js.map 3.18 kB 0 [emitted] bundle
someOtherBundle.js.map 2.44 kB 1 [emitted] someOtherBundle
+ 3 hidden modules

The first three lines tell us that awesome-typescript-loader has found our tsconfig.json file and that it transpiled all of our TypeScript files to JavaScript:

[at-loader] Using typescript@2.2.0-dev.20161226 from typescript and "tsconfig.json" from /path/to/source/webpack-ts/tsconfig.json.
[at-loader] Checking started in a separate process...
[at-loader] Ok, 0.001 sec.

Then webpack takes over and builds our bundled JavaScript files and their source maps:

Hash: 438ca430aa379e6d358d
Version: webpack 1.14.0
Time: 1320ms
Asset Size Chunks Chunk Names
bundle.js 2.92 kB 0 [emitted] bundle
someOtherBundle.js 2.06 kB 1 [emitted] someOtherBundle
bundle.js.map 3.18 kB 0 [emitted] bundle
someOtherBundle.js.map 2.44 kB 1 [emitted] someOtherBundle
+ 3 hidden modules

Heading back to VS Code, we can see the generated files and their source maps:

Since we’ve now bundled our JavaScript, all the remains to be done is to build and run the application:

dotnet run

One caveat to remember when browsing to your server is that you’ll have to explicitly mention the HomeController in your request (i.e. “localhost:5000/Home” for the index page).

Accessing Your Pages

If we send a request for /Home/Index (or just /Home) we’ll get the following response:

I’ve included the Chrome dev tool view of the included JavaScript in both screen shots so that you can see that the relevant bundles are loaded in.

If we send a request for /Home/SayName, we’ll get the following response:

Geoff was just the first name that came to mind.

… I may have been watching a lot of Eddie Izzard recently.

And That’s About It

As I said at the top of this article, there are many different options available and webpack is just one of them. It seems to be the bundler of choice these days, Gulp and Grunt having fallen out of favour simply being not being as new as webpack.

Developers can be very fickle things. Especially front end developers.

Originally published at dotnetcore.gaprogman.com on January 5, 2017.

--

--

Jamie

I’m a .NET developer specialising in .NET MVC websites and services, and I blog about .NET Core things