webpack assets in ASP.NET

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(opens in a new window). 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(opens in a new window)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 use case 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(opens in a new window)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.