Managing ASP.NET Core MVC front-end dependencies with npm and webpack (part 2)
Tags: aspnet, mvc, webpack, npm
Published: 2021-04-09 12:23:00

This post is the second part of a two-part article on managing Javascript and CSS dependencies within a multiple-page application written in ASP.NET Core MVC.
In [the first part](https://dev.to/larswillemsens/managing-asp-net-core-mvc-front-end-dependencies-with-npm-and-webpack-part-1-3jf5), we’ve specified our front-end dependencies, bumped the version numbers and set up a webpack build system.
In this part, we’ll tackle performance issues and make sure that the entire project (front-end _and_ back-end) can be built using a single command.

## Performance
While everything seems to be working okay, a few improvements must be made. When fast-clicking between pages (i.e., 'Home' and 'Privacy') you might notice that styles get applied _after_ the browser renders the page. That’s because the npm package `style-loader` plugs the CSS into the page after it is loaded, causing the browser to re-render the page!
On top of that, the Javascript bundle — which contains the CSS — is very large. It contains the entire Bootstrap CSS, some Bootstrap Javascript functions, and _all_ of JQuery!

Let’s take care of this. Our aim is the following:
* Bundling CSS into a separate CSS file that can be
  statically referenced from an HTML `link` tag
* Splitting up the JavaScript/TypeScript code into separate
  bundles. Many pages have code that is unique to them,
  and not every page needs JQuery

As mentioned in Part 1, we’re only keeping JQuery around because ASP.NET Core’s client-side form validation depends on it.

To make sure that the solution that I’m about to present here fits all use cases, let’s set up a simple prototype. These are the pages we’re going to use:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aujjp72od3ylhal99bqr.png)

All the pages include the site’s SCSS and some code that is common to all pages. The individual pages are:
* The **Index** page, which has a bit of custom
  TypeScript code running on it (`index.ts`).
* The **Privacy** page has no custom code. We'll give
  it a Bootstrap component requiring specific Bootstrap code. This 
  is similar to any other page
  since the navbar has a JS pull-down feature on
  small devices (the hamburger button). Like all pages, it needs Bootstrap’s styling to be rendered
  correctly.
* The **Contact** page has a form 
  backed by ASP.NET Core’s form validation. Validation
  can be done both server-side and client-side.
  We'll load client-side validation code from `validation.ts`.

With all of this in the pipeline you might be wondering why we went down this road in the first place. We will end up with individual CSS/JS files that are hard-referenced from the HTML pages… that’s what we started with! Well, sort of, but not quite. Here’s what’s different:
* We’re referencing our libraries with a specific version number
* The dependencies are not placed _inside_ the project tree
* We’ll end up with a performance gain (in the standard MVC template, all of JQuery is referenced from all pages)
* Our build system is extensible: Need an obscure npm dependency? No problem! Want to use the very latest ECMAScript features? You got it! Need minification or obfuscation? No problemo!

Imagine what it would be like if this was already present in the standard MVC template. Then, you’d have all this modern front-end goodness without setting it up yourself. Ha!

OK, let’s go.

## Splitting up the bundle
This is the current state of things:
```
$ ll wwwroot/dist/
total 3904
-rw-r--r-- 1 lars lars  176032 Apr  5 11:17 39795c0b4513de014cf8.woff
-rw-r--r-- 1 lars lars  130396 Apr  5 11:17 b7bcc075b395c14ce8c2.woff2
-rw-r--r-- 1 lars lars 1665226 Mar 31 12:21 site.entry.js
-rw-r--r-- 1 lars lars 2021658 Mar 31 12:21 site.entry.js.map
```

That’s over 1600K worth of Javascript and CSS code.

These are the separate blocks that we identified in the diagram above:
* Sitewide styles (Bootstrap and custom SCSS)
* Sitewide JavaScript and TypeScript (this includes Bootstrap’s JS
  code for fancy popup buttons, the navbar's hamburger, and so on)
* Validation scripts (basically JQuery with some extras):
  for forms that use ASP.NET Core’s client-side form validation
* A sample Javascript code block that is unique to a
  specific page (let’s take ‘Home’, so `index.ts`)

This is what our view files look like after we move all of the common parts into `_Layout.cshtml`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jvjt1e0d9wt5m304jesf.png)

To split things up, we’ll dive into `ClientApp/src/ts/` and turn `site.ts` into three files:
* `site.ts`: common code needed
  on each page (includes Bootstrap). This will result in
  **separate** JS and CSS files.
* `validation.ts`: JQuery, including the validation scripts
  for our forms
* `index.ts`: some dummy code that’s only applicable to ‘Home’

Here’s what they look like:

#### site.ts
This file lost a few lines when compared to the previous version. Styling is needed on every page of the application, so we’re including all of our (S)CSS here:
```typescript
import '@popperjs/core';
import 'bootstrap';
import 'bootstrap-icons/font/bootstrap-icons.css';
import 'bootstrap/dist/css/bootstrap.css';

// Custom CSS imports
import '../scss/site.scss';

console.log('The \'site\' bundle has been loaded!');
```

#### validation.ts
These import lines were previously in `site.ts`. We’re putting them into their own file so that they can be included separately:
```typescript
import 'jquery';
import 'jquery-validation';
import 'jquery-validation-unobtrusive';

console.log('The \'validation\' bundle has been loaded!');
```

#### index.ts
… some dummy code:
```typescript
console.log('The \'index\' bundle has been loaded!');
```

### Configuring the webpack build
Separate files means separate [entries in webpack](https://webpack.js.org/guides/code-splitting/). Each entry is handled as a separate module, resulting in a separate Javascript file. The resulting file for each entry will be named after the entry followed by the `.entry.js` suffix.  
While we’re at it, we’ll extract the CSS from the Javascript bundle. Instead of using the `style-loader` npm package, we’ll use `mini-css-extract-plugin`, which takes care of the extraction.

Brace yourself, `webpack.config.js` is coming…

```diff
 const path = require('path');
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");

 module.exports = {
     entry: {
-        site: './src/js/site.js'
+        index: './src/ts/index.ts',
+        site: './src/ts/site.ts',
+        validation: './src/ts/validation.ts'
     },
     output: {
         filename: '[name].entry.js',
         path: path.resolve(__dirname, '..', 'wwwroot', 'dist'),
         clean: true
     },
     devtool: 'source-map',
     mode: 'development',
     resolve: {
         extensions: [".ts", ".js"],
         extensionAlias: {'.js': ['.js', '.ts']}
     },
     module: {
         rules: [
             {
                 test: /\.ts$/i,
                 use: ['ts-loader'],
                 exclude: /node_modules/
             },
             {
                 test: /\.s?css$/,
-                use: ['style-loader', 'css-loader', 'sass-loader']
+                use: [{ loader: MiniCssExtractPlugin.loader }, 'css-loader', 'sass-loader']
             },
             {
                 test: /\.(png|svg|jpg|jpeg|gif|webp)$/i, 
                 type: 'asset'
             },
             {
                 test: /\.(eot|woff(2)?|ttf|otf|svg)$/i,
                 type: 'asset'
             }
         ]
-    }
+    },
+    plugins: [
+        new MiniCssExtractPlugin({
+            filename: "[name].css"
+        })
+    ]
 };
```

At the very top and the very bottom you can see we’re importing an npm package and adding it as a plugin respectively. Most plugins have a wide range of configuration options, but we only need to specify which filename to use for CSS files (`[name].css`).

In the `entry` section the different entries are defined and near the center of the file we’ve replaced `style-loader` with the plugin.

So one npm package is being replaced by another. Update `package.json` accordingly:
```diff
     "devDependencies": {
         "@tsconfig/recommended": "^1.0.5",
         "@types/bootstrap": "^5.2.10",
         "sass": "^1.72.0",
         "sass-loader": "^14.1.1",
         "ts-loader": "^9.5.1",
         "typescript": "^5.4.3",
         "webpack": "^5.91.0",
         "webpack-cli": "^5.1.4",
         "css-loader": "^6.10.0",
-        "style-loader": "^3.3.4"
+        "mini-css-extract-plugin": "^2.8.1"
     },
```

Let’s generate those new bundles. From the `ClientApp` directory, enter:

```
$ npm install
$ npm run build
```

Which build artifacts were generated this time?

```
$ ll ../wwwroot/dist/
total 2540
-rw-r--r-- 1 lars lars 176032 Apr  5 11:52 39795c0b4513de014cf8.woff
-rw-r--r-- 1 lars lars 130396 Apr  5 11:52 b7bcc075b395c14ce8c2.woff2
-rw-r--r-- 1 lars lars    295 Apr  5 11:52 index.entry.js
-rw-r--r-- 1 lars lars    246 Apr  5 11:52 index.entry.js.map
-rw-r--r-- 1 lars lars 387938 Apr  5 11:52 site.css
-rw-r--r-- 1 lars lars 506712 Apr  5 11:52 site.css.map
-rw-r--r-- 1 lars lars 290672 Apr  5 11:52 site.entry.js
-rw-r--r-- 1 lars lars 259544 Apr  5 11:52 site.entry.js.map
-rw-r--r-- 1 lars lars 363314 Apr  5 11:52 validation.entry.js
-rw-r--r-- 1 lars lars 467211 Apr  5 11:52 validation.entry.js.map
```

### The Views
After splitting the bundle into multiple smaller bundles, we now have to review our `link` and `script` tags.

With `mini-css-extract-plugin` in the picture, the CSS must be imported statically. CSS is used everywhere, so we jump into `_Layout.cshtml`:

```diff
     ...

     <title>@ViewData["Title"] - Net8NpmWebpack</title>
     <script src="~/dist/site.entry.js" defer></script>
+    <link rel="stylesheet" href="~/dist/site.css">
 </head>
 <body>
     <header>

     ...
```

The `Home/Index.cshtml` page has custom code:

```diff
 @{
     ViewData["Title"] = "Home Page";
 }

+@section Scripts
+{
+    <script src="~/dist/index.entry.js" defer></script>
+}
+
 <div class="text-center">

 ...
```

The `Privacy.cshtml` page gets a fancy Bootstrap component. Let’s pick
a [dropdown menu button](https://getbootstrap.com/docs/5.3/components/dropdowns/#single-button)!

```diff
 @{
     ViewData["Title"] = "Privacy Policy";
 }
 <h1>@ViewData["Title"]</h1>

 <p>Use this page to detail your site's privacy policy.</p>
+
+<div class="dropdown">
+    <button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+        Dropdown button
+    </button>
+    <ul class="dropdown-menu">
+        <li><a class="dropdown-item" href="#">Action</a></li>
+        <li><a class="dropdown-item" href="#">Another action</a></li>
+        <li><a class="dropdown-item" href="#">Something else here</a></li>
+    </ul>
+</div>
```

… and then there’s the Contact page, a new page we’ll build from scratch to test form validation. We’ll need a **view**, a **view-model**, some new **actions** in the controller, and a **link** on the site’s navigation bar.

Let’s start with the form itself, a new view created as `Home/Contact.cshtml`:

```html
@model ContactViewModel

@{
    ViewBag.Title = "Contact";
    Layout = "_Layout";
}

@section Scripts
{
    <script src="~/dist/validation.entry.js" defer></script>
}

<h1>@ViewBag.Title</h1>

<form asp-controller="Home" asp-action="Contact">
    <div class="mb-3">
        <label asp-for="Subject" class="form-label"></label>
        <input asp-for="Subject" class="form-control"/>
        <span asp-validation-for="Subject" class="small text-danger"></span>
    </div>

    <div class="mb-3">
        <label asp-for="Message" class="form-label"></label>
        <textarea asp-for="Message" class="form-control"></textarea>
        <span asp-validation-for="Message" class="small text-danger"></span>
    </div>

    <button class="btn btn-primary" type="submit">Submit</button>
</form>
```

`Subject`, `Message`, `ContactViewModel`, … what are you on about!?
Let’s move out of the `Views` directory and into `Models`…
`ContactViewModel.cs`:

```csharp
using System.ComponentModel.DataAnnotations;

namespace Net8NpmWebpack.Models;

public class ContactViewModel
{
    [Required]
    [StringLength(30, MinimumLength = 3)]
    public string Subject { get; set; }

    [Required(ErrorMessage = "Please enter a message.")]
    public string Message { get; set; }
}
```

… but the form is GET-ing and POST-ing all over the place, how is that handled?
Time to edit `HomeController`:

```diff
         ...

+        [HttpGet]
+        public IActionResult Contact()
+        {
+            return View();
+        }
+
+        [HttpPost]
+        public IActionResult Contact(ContactViewModel contactVM)
+        {
+            if (ModelState.IsValid)
+            {
+                // Send an email or save the message in a table...
+                // Redirect to a page that says "Thanks for contacting us!"...
+
+                return RedirectToAction("Index");
+             }
+
+            return View();
+        }
        
        ...
```

… ok and the link?
Back to `_Layout.cshtml`:

```diff
 ...

                         <li class="nav-item">
                             <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                         </li>
+                        <li class="nav-item">
+                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Contact">Contact</a>
+                        </li>
                     </ul>
                 </div>

 ...
```

Done! (almost)
We now have a full-blown webpack and NPM-powered front-end with excellent performance and modern Javascript goodness.

We don’t need `_ValidationscriptsPartial.cshtml` anymore, so be sure to remove that one from your repository:

```
$ rm Views/Shared/_ValidationScriptsPartial.cshtml
```

If you’re consistently adding `defer` to your script tags (and you should be! 🙂), you can go one step further and **move** the `Scripts` section inside `_Layout` to that page’s `head` section.

```diff
 <!DOCTYPE HTML>
 <html lang="en">
 <head>
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>@ViewData["Title"] - Net8NpmWebpack</title>
     <script src="~/dist/site.entry.js" defer></script>
+    @await RenderSectionAsync("Scripts", required: false)
     <link rel="stylesheet" href="~/dist/site.css">
 </head>
 <body>

     ...

     <footer class="border-top footer text-muted">
         <div class="container">
             <i class="bi bi-c-circle"></i> 2024 - Net8NpmWebpack - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
         </div>
     </footer>
-    @await RenderSectionAsync("Scripts", required: false)
 </body>
 </html>
```

The project so far can be found on GitLab as [version 4](https://gitlab.com/kdg-ti/integratieproject-1/guides/net8npmwebpack/-/tags/version-4) of [NetCoreNpmWebpack](https://gitlab.com/kdg-ti/integratieproject-1/guides/net8npmwebpack).  
Give it a spin. You’ll notice that the performance is good.

## Building the project
Running the project is perhaps easier said than done. Let’s recap:

``` bash
$ npm install          # only after a modification to package.json
$ npm run build
$ dotnet build
$ dotnet run
```

That’s too much typing for anyone. Let’s automate that a bit.

The `.csproj` file can be extended with some extra build commands. Honestly, csproj-Hocus Pocus is a bit of uncharted territory for me (although it reminds me of the [Ant build system](https://en.wikipedia.org/wiki/Apache_Ant)), but this seems to work fine:

```diff
 <Project Sdk="Microsoft.NET.Sdk.Web">

     <PropertyGroup>
         <TargetFramework>net8.0</TargetFramework>
         <Nullable>disable</Nullable>
         <ImplicitUsings>enable</ImplicitUsings>
+        <IsPackable>false</IsPackable>
+        <MpaRoot>ClientApp\</MpaRoot>
+        <WWWRoot>wwwroot\</WWWRoot>
+        <DefaultItemExcludes>$(DefaultItemExcludes);$(MpaRoot)node_modules\**</DefaultItemExcludes>
     </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.3"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <!-- Don't publish the MPA source files, but do show them in the project files list -->
+        <Content Remove="$(MpaRoot)**"/>
+        <None Remove="$(MpaRoot)**"/>
+        <None Include="$(MpaRoot)**" Exclude="$(MpaRoot)node_modules\**"/>
+    </ItemGroup>
+
+    <Target Name="NpmInstall" BeforeTargets="Build" Condition=" !Exists('$(MpaRoot)node_modules') ">
+        <!-- Ensure Node.js is installed -->
+        <Exec Command="node --version" ContinueOnError="true">
+            <Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
+        </Exec>
+        <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE."/>
+        <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..."/>
+        <Exec WorkingDirectory="$(MpaRoot)" Command="npm install"/>
+    </Target>
+
+    <Target Name="NpmRunBuild" BeforeTargets="Build" DependsOnTargets="NpmInstall">
+        <Exec WorkingDirectory="$(MpaRoot)" Command="npm run build"/>
+    </Target>
+
+    <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
+        <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
+        <Exec WorkingDirectory="$(MpaRoot)" Command="npm install"/>
+        <Exec WorkingDirectory="$(MpaRoot)" Command="npm run build"/>
+
+        <!-- Include the newly-built files in the publish output -->
+        <ItemGroup>
+            <DistFiles Include="$(WWWRoot)dist\**"/>
+            <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
+                <RelativePath>%(DistFiles.Identity)</RelativePath>
+                <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+                <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+            </ResolvedFileToPublish>
+        </ItemGroup>
+    </Target>
+
+    <Target Name="NpmClean" BeforeTargets="Clean">
+        <RemoveDir Directories="$(WWWRoot)dist"/>
+        <RemoveDir Directories="$(MpaRoot)node_modules"/>
+    </Target>

 </Project>
```

As you may have figured out from the diff above, the `npm install` command is _only_ executed in case the `node_modules` directory is absent. That’s something to keep in mind in case you make modifications to `package.json`!

Now, we can build the project in its entirety using the `dotnet build` command. Excellent! (pressing the run or compile button from your IDE works just as well)

### Auto-building the bundle
To make life even easier, we want to automagically rebuild the bundle whenever the front-end code changes. At the same time, we _don’t_ want to restart ASP.NET Core’s HTTP server (Kestrel) when that happens.

To make this happen, we’ll add a webpack watcher for the front-end files to trigger a rebuild. In `package.json`:

```diff
 ...
     },
     "scripts": {
-        "build": "webpack"
+        "build": "webpack",
+        "watch": "webpack --watch"
     }
 }
```

While editing front-end code, our workflow will look like this:
1. `npm run watch` (executed from within the `ClientApp` directory)
2. `dotnet run`

(Note: I would advise **against** using `dotnet watch` since it seems to continuously detect changes to the bundle, causing an endless rebuild loop)

[Version 5 of the sample project can be found here.](https://gitlab.com/kdg-ti/integratieproject-1/guides/net8npmwebpack/-/tags/version-5)

## Wrapping up
We now have a flexible and extensible project that uses modern front-end technologies and has excellent performance.

We’ve had to cover quite a bit of ground since many of these techniques are absent in most tutorials. Bower, Grunt, and Gulp were dominant just a few years ago but are now on their decline. Many sources on the internet still refer to these kings of yesteryear. However, on Bower’s website, you can see that they are actively recommending alternatives.

This guide may have filled a gap by bringing npm and webpack into MVC and MPA applications, specifically .NET Core and .NET 8 apps.

### What’s left?
There is no distinction yet between “Development” and “Production”. Minification of JavaScript code and CSS pruning are still to be added. I’m confident, though, that the flexibility of the build system won’t make that too challenging.

If you have any other suggestions, then please let me know in the comments below.

Good luck building your MVC application!