webpack assets in ASP.NET

2020-03-10

As webpack continues to grow in popularity, more tooling evolves for a smooth development experience. This is true for a story with webpack and ASP.NET together as well, at least if you're running .NET Core. I want to share a framework-neutral setup with a satisfactory experience independent of ASP.NET version.

I'll describe how to:

  • Integrate webpack 4 with ASP.NET MVC 5
  • Use webpack's development server
  • Get started with Hot Module Replacement (HMR)

Getting started

The first thing to do, in the root of your web project, is to install webpack and its CLI. Run npm install webpack webpack-cli --save-dev. Once installed, I recommend adding a convenience task in package.json for executing webpack.

Build task

json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
}

If you try npm run build, you'll likely get an error stating Can't resolve './src'. To fix this, a configuration file for webpack will come in handy. In the root of your project, add a webpack.config.js file with the following content:

Initial webpack configuration

js
module.exports = {
// point this path to your entry,
// your non-minified start file
entry: "./Scripts/main.js"
};

Once saved, webpack is now configured to build your JavaScript application and output in into the folder ./dist. For now, ASP.NET has no understanding of webpack's output. Let's start integrating.

webpack 💝 ASP.NET

To be able to reference webpack's output in ASP.NET, use npm to install razor-partial-views-webpack-plugin. The plugin extends webpack's build with capability to generate Razor views for webpack assets. Which assets to create views for are configurable.

Generate Razor views with webpack

js
const RazorPartialViewsWebpackPlugin = require("razor-partial-views-webpack-plugin");

module.exports = {
entry: "./Scripts/main.js",
output: {
// runtime asset location
publicPath: "/dist/"
},
plugins: [
new RazorPartialViewsWebpackPlugin({
rules: [{
// view for default chunk
name: "main",
template: {
// Razor directive
header: "@inherits System.Web.Mvc.WebViewPage"
}
}]
})
]
};

With the plugin for generating Razor views configured, a webpack build outputs a main.cshtml. The partial view's content is a script tag referencing webpack's built JavaScript file.

Partial view for webpack asset

cshtml
@inherits System.Web.Mvc.WebViewPage

<script type="text/javascript"
src="/dist/main.js">
</script>

Now reference the generated view in ASP.NET MVC's _Layout.cshtml.

Partial view in layout

cshtml
<body>
<!-- ... -->
@Html.Partial("~/dist/main.cshtml")
</body>

That's it for using webpack's output in ASP.NET.

Improving development experience

With webpack integrated with ASP.NET, let's build on that to enhance the development experience. webpack-dev-server provides automatic page refresh and hot swapping of modules, which saves you from restarting the whole application after code updates. Install the development server with npm and extend package.json to run it for serving webpack's assets.

Run webpack development server

json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server --hot",
"build": "webpack"
}

Update webpack.config.js to include devServer and signal that assets no longer are served from ./dist folder. These configuration updates also function as scaffolding for partial views used in ASP.NET.

Serve assets with development server

js
output: {
// assets served by webpack-dev-server
publicPath: "https://localhost:8080/"
},
devServer: {
// output assets to disk for ASP.NET
writeToDisk: true,
// clearly display errors
overlay: true,
// include CORS headers
headers: {
"Access-Control-Allow-Origin": "*"
},
// use self-signed certificate
https: true
}

Run npm start and start your ASP.NET web application. Open your browser's development tools and take notice of Loading failed for the <script> with source "https://localhost:8080/main.js". The reason for the error, is the browser not trusting the development server's self-signed certificate. For now, browse to https://localhost:8080/ to accept the certificate. We'll get to a proper fix shortly.

Browse back to the web application and see your JavaScript code running. Now we reap rewards of our work. In the development console you should have webpack's development server signaling Hot Module Replacement enabled and Live Reloading enabled. Make a JavaScript update and notice the browser refresh. Happy times!

Trusted development certificate

I recommend generating a trusted development certificate for webpack-dev-server. That'll smoothen cross-browser testing, without having to hack about with getting your browser to trust a self-signed certificate. mkcert provides the feature to create an ample certificate. Install the tool, and run the setup of a local certificate authority. Thereafter, generate a development certificate in your web project root.

Generate development certificate

sh
# Install local certificate authority
> mkcert -install

# Generate development certificate
> mkcert localhost

Once the certificate is generated, update webpack's devServer configuration to start using it. Afterwards, your browser will consider the connection to the development server as secure.

Certificate for webpack-dev-server

js
const fs = require("fs");

module.exports = {
/*...*/
devServer: {
writeToDisk: true,
overlay: true,
headers: {
"Access-Control-Allow-Origin": "*"
},
// use development certificate
https: {
cert: fs.readFileSync("./localhost.pem"),
key: fs.readFileSync("./localhost-key.pem")
}
}
}

Hot Module Replacement

So far, you've gained automatic page refresh when updating JavaScript. This alone is an improvement compared to full recycle via Visual Studio's Start Debugging. With Hot Module Replacement (HMR) the feedback loop is tightened even more.

HMR enables swapping updated modules without reloading the page. When you update front-end code, take notice of the message [HMR] Aborted because [module] is not accepted.

Hot Module Replacement is activated by signaling to webpack if and when changes are accepted by using the module.hot API. Pay attention to side-effects (like appending elements to DOM), as they likely have to be accounted for before modules are reapplied.

Allow swapping modules

js
import { create } from "./state-picker";

const statePicker = create();
document.body.prepend(statePicker);

// is HMR activated?
// don't activate in production
if (module.hot) {
// accept changes
// for this module and dependencies
module.hot.accept();

module.hot.dispose(() => {
// prevent multiple
statePicker.remove();
});
}

Include module.hot.accept() in your JavaScript code. You'll notice how the page doesn't refresh on every update. Instead webpack knows what modules have been updated. Great success!

Build for production

webpack comes with decent defaults for managing development and production builds. Over time, you'll likely need to adapt your configuration more granular according to target environment. For now, adapt webpack's output configuration to match your production environment (e.g. use of CDN). Ensure paths and cache busting are properly configured.

To run a production build, execute npm run build -- --mode=production (yes, that's four hyphens, so consider another npm script).

Production output

js
// export webpack.config.js as function
module.exports = (env, argv) => ({
/*...*/
output: {
filename: argv.mode !== "production"
? "[name].js"
: "[name].[contenthash].min.js",
publicPath: argv.mode !== "production"
? "https://localhost:8080/"
: "/dist/"
}
});

Run a production build and peek into the generated main.cshtml. It now references a minifed asset. Ship it! 📦

Production script minified

cshtml
@inherits System.Web.Mvc.WebViewPage

<script type="text/javascript"
src="/dist/main.66ca108e268.min.js">
</script>

webpack is a powerful toolbox to utilize for delivering front-end assets. One usecase I thoroughly recommend using it for, is to evolve legacy applications. webpack comes with support for multiple module systems, which enables migration scenarios, like updating from AMD to ES modules.

Plugins like razor-partial-views-webpack-plugin takes the webpack ecosystem closer to ASP.NET's. Try it out! Adopt the habit of running webpack in a terminal on the side. I hope you'll appreciate it as much as I do.