Building an ASP.NET Core Starter App on MacOS (Webpack & NPM)

In the previous post, I quickly covered how to get started in a way that smoothed some future bumps in the road for me.  In this post, we’ll cover replacing the default front-end frameworks and add tooling to support front-end dependency management and static asset assembly.  An example of the configuration mentioned in this post (along with the source code for all future posts) is available at: https://github.com/jrodenbostel/example-web-application.

If you followed the tutorial mentioned in the previous post, chances are you generated an application similar to the result of running `dotnet new mvc …` from the CLI.  If you did, you’re mostly looking at a reasonable shell of an application.  In my opinion, there are some issues with how the front-end tools are made available that are worth remediating.  The two big items are there’s no tool being used to manage front-end dependencies like jQuery and Bootstrap, and most if not all of the files in the wwwroot folder could be considered build artifacts or transient dependencies.  In most cases, build artifacts and dependencies shouldn’t be checked into version control, but rather assembled or retrieved at build time. Here’s a some background information for those unfamiliar with this concept: https://stackoverflow.com/questions/31930370/what-is-npm-and-why-do-i-need-it. I’m not a front-end expert by any means, but I appreciate being able to join a team and quickly get up and running, and I appreciate the consistency and speed of automated build and deployment processes.

Just to change things up for this project, I’ll be using Vue.js (https://vuejs.org/) instead of jQuery and Bulma (https://bulma.io/) instead of Bootstrap.  We’ll change our project to use Sass (https://sass-lang.com/) instead of plain CSS.  We’ll remove the defaults and manage all those things with NPM (https://www.npmjs.com/) and Webpack (https://webpack.js.org/).  We’ll also plug the new tools into the default Layout.

If you’re looking at a freshly generated MVC application, you’ll see the `wwwroot` folder along with subfolders `css`, `js`, and `lib`, and also you’ll also see there are files in each of those folders as well.  These files are referenced in the generated layout (`/Views/Shared/_Layout.cshtml`).

1

I started by deleting everything in the wwwroot folder, including the subfolders.  I created a .gitignore file in the root of my project and added the following entries:

2

If you’ve already made some commits to git, you might have to remove these files from git in addition to deleting them.  I should also note that since this is currently a single project solution, and since I am a Rider user, the following entries are also useful:

3

NPM

In the previous post, we covered installing node.js as a prerequisite to getting the example app up and running.  In this section, we’re going to use the Node Package Manager (NPM) to install all the dependencies we’ll need for this tutorial.  While I hope this helps readers get started quickly, it’s always a good idea to take some time to understand what each of these dependencies does to ensure that you’re not installing anything unnecessary.

First, we’ll create a `package.json` file where all of our dependencies will be recorded.  Soon, we’ll also use this file to set up some automated scripts that leverage our dependencies.  Create `package.json` in the root of your project with the following contents:


{

"name": "example-web-application",

"version": "1.0.0",

"description": "",

}

Next, we’ll install all the tools we need to get a reasonable static asset assembly process setup – specifically, this will install tools needed to incorporate Sass and Webpack into the project.  Run this command from the root of your project:

`npm install css-loader extract-text-webpack-plugin file-loader html-webpack-plugin mini-css-extract-plugin minimist node-sass optimize-css-assets-webpack-plugin sass-loader url-loader webpack webpack-cli webpack-merge –save-dev`

When that is complete, the following path should be valid in your filesystem `<project_root>/node_modules`, and the node_modules folder should have about 83MB  of files in it.  You should also see your package.json file updated to include a listing of the files we just installed.  We’ll add more dependencies later, but this will help us get Webpack configured appropriately.

Webpack

Before we go further, I know for sure I had some success following this tutorial: https://webpack.js.org/guides/getting-started/.  I know for sure that I followed others.  I was not able to find which webpack tutorial I followed in my notes.  With that out of the way, we need a few other things to get started with Webpack.  First we’ll start with the Webpack config.  We’re going to create three files:

`webpack.common.js`: common configuration

`webpack.dev.js`: configuration for ‘development’ environments

`webpack.prod.js`: configuration for ‘production’ environments

webpack.common.js

This file contains file loader plugin configuration (for fonts), and has references to file locations.


var path = require('path');

const webpack = require('webpack');

const HtmlWebpackPlugin = require('html-webpack-plugin');

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

 

module.exports = {

entry: {

app: './Scripts/app.js'

},

output: {

path: path.resolve(__dirname, 'wwwroot'),

filename: 'js/[name].js',

publicPath: '/'

},

plugins: [

new MiniCssExtractPlugin({

filename: 'css/site.css',

chunkFilename: '[name].css'

})

],

module: {

rules: [

{

test: /\.(png|jpg|jpeg|gif|woff|woff2|ttf|eot|svg)(\?.*)?$/,

loader: 'file-loader?name=fonts/[name].[ext]',

},

{

test: /\.s[ac]ss$/i,

use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],

},

]

}

};

 

webpack.dev.js

The contents of this file will be combined with `webpack.common.js` when executed. This file contains a reference to the ‘inline-source-map’ devtool configuration, which is a Webpack option for copying files as-is from location to location ie without minifying or obfuscating them.  This configuration combined with the common configuration should result in files being copied from `/Scripts` to their homes in ‘wwwroot’ with the only transformation being css extracted from the scss/sass files in `/Scripts`.map’ devtool configuration, which is a Webpack option for copying files as-is from location to location ie not   We’ll cover how to run these commands shortly.


const webpack = require('webpack');

const merge = require('webpack-merge');

const common = require('./webpack.common.js');

 

module.exports = merge(common, {

mode: 'development',

devtool: 'inline-source-map'

});

 

webpack.prod.js

The contents of this file will also be combined with `webpack.common.js` when executed.  This file contains references to Webpack plugins used to minify and obfuscate files for optimized loading.  This configuration combined with the common configuration should result in file being copied from `/Scripts` to their homes in `wwwroot`, but this time, they’ll be optimized by the aforementioned plugins.


const webpack = require('webpack');

const merge = require('webpack-merge');

const common = require('./webpack.common.js');

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');

const TerserJSPlugin = require('terser-webpack-plugin');

 

module.exports = merge(common, {

mode: 'production',

optimization: {

minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],

}

});

 

Execution Convenience

Now that we have Webpack configured, we need to be able to test it.  Before we test it, we’ll set up an easy way to execute but adding the following entry to the root of our `package.json` file:

“scripts”: {

“dev”: “webpack –config webpack.dev.js –watch”,

“build”: “webpack –config webpack.prod.js”

}

Now, from the root of our project folder (the same folder where `package.json` is located), we can run:

`npm run dev` which runs our dev configuration, and watches the filesystem for changes.  When changes are detected, Webpack is run again with the same configuration. This is useful while in active development as keeping this running in the background will keep you assets building and bundled as you go.

`npm run build` which runs our prod configuration, and is useful when deploying to a shared environment. It executes once and will not run again upon completion.

Next let’s add Vue and Bulma as dependencies to our project and test our Webpack configuration.

From the root of your project, run the following command to install Bulma and Vue as dependencies of our project:


npm install bulma vue --save-dev

Now, let’s setup a JavaScript file and a Sass file for our project and test out our Webpack config.  Note that the file names and paths below are referenced in our Webpack config.

/Scripts/app.js


require('./site.scss');

import Vue from 'vue/dist/vue';

 

/Scripts/site.scss


@charset "utf-8";

 

@import "~bulma/bulma";

 

When we run ‘npm run dev’ or ‘npm run build’ from the root of our project, we should see files appear in wwwroot.

You’ll want to update your `/Views/Shared/_Layout.cshtml` to include references to the new files, and remove references to what was previously there.  You’ll see examples of that in the file below:

/Views/Shared/_Layout.cshtml


<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="utf-8" />

<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<title>@ViewData["Title"] - ExampleWebApplication</title>

<link rel="stylesheet" href="~/css/site.css" />

</head>

<body>

<header>

<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">

<div class="container">

<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">ExampleWebApplication</a>

<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"

aria-expanded="false" aria-label="Toggle navigation">

<span class="navbar-toggler-icon"></span>

</button>

<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">

<ul class="navbar-nav flex-grow-1">

<li class="nav-item">

<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>

</li>

<li class="nav-item">

<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>

</li>

</ul>

</div>

</div>

</nav>

</header>

<div class="container">

<main role="main" class="pb-3">

@RenderBody()

</main>

</div>

 

<footer class="border-top footer text-muted">

<div class="container">

&copy; 2020 - ExampleWebApplication - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>

</div>

</footer>

<script src="~/js/app.js" asp-append-version="true"></script>

</body>

</html>

Last, you’ll probably want to re-style your layout to use Bulma classes, and I’ve added a quick Vue component to handle toggling the navbar styles based on the viewport size.  Here’s an update:


<!DOCTYPE html>

<html lang="en" class="has-navbar-fixed-top">

<head>

<meta charset="utf-8"/>

<meta name="viewport" content="width=device-width, initial-scale=1.0"/>

<title>@ViewData["Title"] - Example Web Application</title>

<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>

</head>

<body>

<header id="navbar-root">

<vue-navbar inline-template>

<nav class="navbar is-fixed-top" role="navigation" aria-label="main navigation">

<div class="navbar-brand">

<a asp-controller="Home" asp-action="Index" class="navbar-item">

<span>Example Web Application</span>

</a>

<a role="button" v-on:click="toggle" v-bind:class="{ 'is-active': burgerActive }" class="navbar-burger burger" data-target="menuBar">

<span></span>

<span></span>

<span></span>

</a>

</div>

<div id="menuBar" v-bind:class="{ 'is-active': burgerActive }" class="navbar-menu">

<div class="navbar-start">

<a class="navbar-item">

Menu 1

</a>

<a class="navbar-item">

Menu 2

</a>

</div>

</div>

</nav>

</vue-navbar>

</header>

<section>

<div class="container">

<div class="columns">

<div class="column is-10 is-offset-1">

<h1 class="title is-1">@ViewData["Title"]</h1>

<h3 class="title is-3 has-text-info">@ViewData["Information"]</h3>

<h3 class="title is-3 has-text-danger">@ViewData["Error"]</h3>

<h3 class="title is-3 has-text-info">@TempData["Information"]</h3>

<h3 class="title is-3 has-text-danger">@TempData["Error"]</h3>

@RenderBody()

</div>

</div>

 

</div>

</section>

 

<footer class="footer">

<div class="content has-text-centered">

&copy; 2020 - yoursitename.com - <a asp-controller="Home" asp-action="Privacy">Privacy</a>

</div>

</footer>

<script src="~/js/layout.js" asp-append-version="true"></script>

</body>

</html>

We’ll also need new Javascript to handle some of the Vue directives referenced in the previous file:

/Scripts/app.js


require('./site.scss');

 

import Vue from 'vue/dist/vue';

 

Vue.component('vue-navbar', {

data() {

return {

width: 0,

height: 0,

burgerActive: false

}

},

methods: {

toggle(e) {

this.burgerActive = !this.burgerActive;

},

handleResize() {

this.width = window.innerWidth;

this.height = window.innerHeight;

this.burgerActive = false;

}

},

created() {

window.addEventListener('resize', this.handleResize);

this.handleResize();

},

destroyed() {

window.removeEventListener('resize', this.handleResize);

}

});

 

new Vue({

el: '#navbar-root'

});

 

At this point, we should have a working Webpack config, Vue and Bulma installed and incorporated into our layout, and a simple Vue component that helps keep the navbar responsive when the viewport size is reduced.

4

In the next post, I’ll focus much more on code, covering adding custom authentication to the project.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s