Skip to content

KAJAN M.

moon indicating dark mode
sun indicating light mode

Setting up ASP.NET Core, Vue MPA with Webpack

February 15, 2021

Introduction

In my previous post, I shared my reflection on the ASP.NET Core MVC, Vue MPA project setup. While this setup has its quirks, it helped us to ship new features quickly.

In this post, I’ll walk you through setting up the project from scratch using Webpack while answering the whats and whys.

While I prefer to use Parcel/Snowpack over Webpack for new projects, I’m documenting the process hoping this might help someone.

You can find all the code related to this post in this  Github Repository

Prerequisites

This post does not discuss Vue, ASP.NET Core concepts, so having a basic understanding of its building blocks is required.

If you intend to code along, make sure you have Node.js 10.13.0+ and .NET Core SDK 3.1 are installed in your machine.

In this post, we will be using Webpack 5. To avoid surprises, you might want to install the exact version whenever you do npm install. You can refer to the package.json to know the exact versions.

What is not necessary to follow through this post

Knowing Webpack is helpful but not necessary. We will be configuring Webpack from scratch, and I’ve explained the concepts as needed.

Vote of thanks

I am thankful to Tho Vu, Tony brothers for helping me with setting up the project early in my career 🙏🤗😘.

A brief introduction to Webpack

This section discusses the whats and whys of Webpack and explains some basic concepts that we need later on.

What is Webpack

Webpack is a bundling tool. Which means it can combine two or more files into a single file.

Webpack can also transform the files before bundling. Say we decided to use TypeScriptin our project. That’s cool, but the browser does not understand TS, right? Not to worry, we can tell Webpack to transform .ts files to .js. We can also chain multiple commands, like, compile SCSS to CSS, add vendor prefixes and minify before bundling.

Why Webpack

Webpack serves as a core tool to enable/integrate the following as part of the build process.

  1. Allows us to develop using languages and frameworks that the browser does not understand natively. Using frameworks like Vue increases our development productivity, however browser only understands HTML, CSS, JavaScript. Webpack can transform Vue components into JS, CSS as part of build process, allowing us to use Vue in development time, while shipping the code in HTML, CSS and JS.

  2. Allows us to use modern JS features while supporting old browsers. We can set up Webpack to use Babel to transpile our next generation code to something that the old browser also understands.

  3. Hot module reloading. Using the Webpack development server, we can see the changes without refreshing the page.

  4. Combines many files and creates bundles. When we write code, we want to organize/group our code in separate files. For example, instead of writing everything inside a single JS file with more than 20K lines, we may want to keep the shared constants in a separate constants.js file, shared methods in a separate utils.js file, .etc. If we don’t bundle them, we have to include the script tags manually in the HTML for each file(in the correct order 🙄). While the browser can load a certain number of requests parallelly(HTTP2 does not completely solve this problem), loading many files will increase the initial load time.

  5. Code splitting When bundling multiple files, if the bundle size reaches the configured threshold size, we can ask Webpack to split it into multiple chunks.

  6. Tree shaking/dead code elimination Suppose a file/library exports 100 different things, and we import only one, Webpack will not include the rest of 99 to the bundle, that’s cool right?

  7. Optimizing the assets Want to minify and compress assets, there are Webpack plugins to do that 😉

  8. Add vendor prefixes to the CSS rules Now we can forget the vendor prefixes entirely, and leave it to the build process 🎉.

What is a loader in Webpack?

Out of the box, Webpack only understands JavaScript, JSON files. For other file types like .css, .vue, we need to install appropriate loaders.

What is a plugin in Webpack?

Similar to a loader, the plugin extends Webpack’s capability. The difference between a loader and a plugin is, usually loaders are used before bundling, and plugins are used after bundling. For example, let’s say we have some CSS files. To load them we will use css-loader. To compress the bundle we will use compression-webpack-plugin.

What is Entry in Webpack

We can specify a file path to the entry property. Webpack will build a dependency graph by looking at the import statements in that file recursively. Finally, based on the dependency graph, Webpack will combine the files and produce a bundle.

We can also specify multiple paths to the entry property. Webpack will produce a separate bundle for each entry. Later we will set up Webpack to dynamically add multiple entries.

Create a new ASP.NET Core MVC project

Lets’s start by creating a new ASP.NET Core MVC project.

  1. Open the terminal and typeout the following commands

    mkdir AspNetCoreMvcVueMpa
    cd AspNetCoreVueMpa
    dotnet new mvc -o AspNetCoreMvcVueMpa.Web
    dotnet new sln
    dotnet sln add AspNetCoreMvcVueMpa.Web
    git init
    dotnet new gitignore
  2. We can now start the app using dotnet run and see it in https://localhost:5001/

  3. The scaffolding template includes bootstrap related files inside the wwwroot directory. Let’s remove them. Later we’ll use npm to install JavaScript dependencies.

  4. Let’s also remove references to the deleted assets from Layout.cshtml

  5. Ignore wwwroot directory from version control

  6. Enable razor runtime compilation

    By default, changes to .cshtml files will reflect only when we do dotnet build or dotnet publish. Even if we start the app in watch mode(dotnet run --watch) we need to restart the app to see the latest changes in razor views. We can use Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation nuget package to see changes in razor views without rebuilding the project.

    Run the following command from the AspNetCoreVueMpa.Web project root directory.

    dotnet add package Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation --version 3.1.2

    Now we need to configure MVC builder to support runtime compilation of razor views.

Setup Webpack to process Vue Single File Components

Okay, so we want to build the UI entirely using Vue. Good, but the browser does not speak Vue. I catch that, you nailed it, we can use Webpack to transpile .vue files to .js. Let’s see how we can do that.

  1. Let’s first create a new directory inside our AspNetCoreVueMpa.Web project to house all of our front-end code.

    mkdir ClientApp
  2. Initialize empty npm project inside that directory

    cd ClientApp
    npm init -y

    That should create a package.json file inside the ClientApp directory, since we are not going to publish this npm project as a library, let’s remove the "main": "index.js" and add "private": true.

  3. Install webpack, webpack-cli as development dependencies

    npm i -D webpack webpack-cli
  4. Set up vue-loader

    Now we’ve Webpack installed. To process .vue files, we need vue-loader, the vue-loader documentation site suggests installing vue-template-together as well.

    Unless you are an advanced user using your own forked version of Vue's template compiler, you should install vue-loader and vue-template-compiler together

    Let’s install them.

    npm i -D vue-loader vue-template-compiler

    Now, we need to tell Webpack to use vue-loader to process .vue files. We can configure Webpack in a file called webpack.config.js. Let’s create that file with the following content.

    // ClientApp/webpack.config.js
    const VueLoaderPlugin = require('vue-loader/lib/plugin');
    module.exports = {
    module: {
    rules: [
    {
    test: /\.vue$/,
    loader: 'vue-loader',
    }
    ]
    },
    plugins: [
    new VueLoaderPlugin(),
    ]
    }

    In webpack.config.js we need to export an object. In this object, we define the loaders inside the module.rules array.

    When defining a loader, we need to tell the file type, and the loader(s) to use.

    To tell the file type, we use the test property. Webpack will match each file’s path against the provided test value to determine whether to use the loader or not. /\.vue$/ is a regular expression that matches any string ending with .vue.

    vue-loader also comes up with a plugin. In a .vue file we can write both JS and CSS. This plugin ensures that the Webpack rules specified for JS, CSS files are applied to that inside .vue files as well.

    To add a plugin, we need to import it, create a new instance and pass it to the plugins array.

Set up multiple Webpack entry points

As said earlier, Webpack starts building a dependency graph based on the provided entry points.

To address our MPA requirement, we will add an entry point for each page. Webpack will generate separate bundle for each entry point. We can load the generated bundles in the corresponding .cshtml files.

To keep ourself organized and to automatically add Webpack entries, we will follow a convention to place the entry files at ClientApp/views/[controller-name]/[action-name]/main.js.

Let’s setup a logic in webpack.config.js to dynamically resolve entries.

// ClientApp/webpack.config.js
...
const glob = require('glob');
const entries = {};
const IGNORE_PATHS = ['unused'];
glob.sync('./views/**/main.js').forEach(path => {
const chunk = path.split('./views/')[1].split('/main.js')[0]
if (IGNORE_PATHS.every(path => !chunk.includes(path))) {
if (!chunk.includes('/')) {
entries[chunk] = path
} else {
const joinChunk = chunk.split('/').join('-')
entries[joinChunk] = path
}
}
});
...

Let’s tell Webpack to place the built bundles inside the wwwroot/js directory.

// ClientApp/webpack.config.js
module.exports = {
...
output: {
path: path.resolve(__dirname, '../wwwroot'),
filename: 'js/[name].bundle.js'
},
...
}

Suppose we have views/student/create/main.js as an entry point, the processed bundle will be placed at wwwroot/js/student-create.bundle.js

Add npm build script

Add “build” : “webpack” to the “scripts” section in the package.json

Whenever we do npm run build, it’ll invoke webpack. Note that we didn’t install webpack globally and thus can’t directly invoke webpack from the terminal. If we want to invoke webpack without using a npm script we can do npx webpack.

We will add more npm scripts later in this tutorial.

Test vue-loader

Let’s test if we are able to render a Vue SFC in the DOM.

  1. Install Vue

    npm i vue
  2. Create ClientApp/views/home/index/HelloWorld.vue with the following content.

    <template>
    <div>
    Hello {{ name }} from Vue!
    </div>
    </template>
    <script>
    export default {
    name: "HelloWorld",
    data() {
    return {
    name: "world",
    }
    },
    }
    </script>
    <style scoped>
    </style>

  3. Create views/home/index/main.js that mounts HelloWorld component to a div with id app. This main.js is going to be the entry point for our home page.

    // ClientApp/views/home/index/main.js
    import Vue from 'vue'
    import HelloWorld from "./HelloWorld.vue";
    const app = new Vue({
    el: '#app',
    render: h => h(HelloWorld)
    })

  4. Clear unnecessary markup from _Layout.cshtml and add a div with id “app”.

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>@ViewData["Title"] - AspNetCoreVueMpa.Web</title>
    </head>
    <body>
    <div id="app"></div>
    @RenderBody()
    @RenderSection("Scripts", required: false)
    </body>
    </html>

  5. Clear the markup inside Index.cshtml and add script tag to load the processed bundle.

    @{
    ViewData["Title"] = "Home Page";
    }
    @section Scripts{
    <script type="text/javascript" src="~/js/home-index.bundle.js" asp-append-version="true"></script>
    }

  6. Run the build script

    npm run build
  7. you should see a new file at wwwroot/js/home-index.bundle.js

  8. Start the dotnet app and visit the home page. You should see Hello world from Vue!

Test multiple entry points

Let’s test if multiple webpack entries are automatically resolved correctly.

  1. Add StudentController.cs with the following content

    using Microsoft.AspNetCore.Mvc;
    namespace AspNetCoreVueMpa.Web.Controllers
    {
    public class StudentController : Controller
    {
    public IActionResult Index()
    {
    return View();
    }
    }
    }

  2. Add Index.cshtml razor view

    @{
    ViewData["Title"] = "Student Index";
    }
    @section Scripts{
    <script type="text/javascript" src="~/js/student-index.bundle.js" asp-append-version="true"></script>
    }

  3. Create test Vue component

  4. Add entry file

  5. Visit https://localhost:5001/student and you should see the test Vue component in action 🎉

Set up and test SCSS, style loading

Now let’s focus on processing styles.

We can write styles in two places.

  1. Inside Vue SFC
  2. In separate CSS/SCSS files

In this section we’ll see how we can tell Webpack to extract styles from Vue SFC and inject it into the HTML head tag

We’ll also setup Webpack to bundle external style sheets to a separate CSS bundle.

  1. Install the necessary development dependencies

    npm i -D css-loader style-loader mini-css-extract-plugin sass sass-loader
  2. Add ClientApp/assets/styles/styles.scss with the following content. This will act as our root global stylesheet. We can import other stylesheets inside it as necessary.

    // ClientApp/assets/styles/styles.scss
    $testColor: #007BFF;
    .test-global-css {
    background: $testColor;
    }

  3. Set up loaders and mini-css-extract-plugin to process external stylesheets.

    // ClientApp/webpack.config.js
    ...
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    ...
    module.exports = {
    ...
    module: {
    rules: [
    ...
    {
    test: [
    path.join(__dirname, 'assets/styles/styles.scss'),
    ],
    use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    'sass-loader',
    ]
    },
    ]
    },
    plugins: [
    ...
    new MiniCssExtractPlugin({
    filename: 'css/[name].bundle.css'
    }),
    ]
    }

    Here we chain multiple loaders. The loaders are applied from right to left.

    The sass-loader can load SASS/SCSS files and compiles them to CSS.

    The CSS loader will resolve @import, url() and add them to the dependency graph.

    MiniCssExtractPlugin.loader helps to extract the CSS to a separate file.

  4. Add global root SCSS file path to Webpack’s entry property

    // ClientApp/webpack.config.js
    ...
    const entries = {};
    entries['styles'] = path.join(__dirname, 'assets/styles/styles.scss');
    ...
    module.exports = {
    entry: entries,
    ...
    }

  5. Update HelloWorld.vue to use a class definition from the global stylesheet

    <template>
    <div class="test-global-css">
    Hello {{ name }} from Vue!
    </div>
    </template>
    ...

  6. Update _Layout.cshtml to load the bundled global stylesheet

    <head>
    ...
    <link rel="stylesheet" href="~/css/styles.bundle.css">
    </head>
    ...

  7. Run the npm build script

    npm run build
  8. Visit the home page, and you should see the styles applied

  9. Now let’s setup loaders to process styles inside Vue SFC.

    // ClientApp/webpack.config.js
    ...
    module.exports = {
    ...
    module: {
    rules: [
    ...
    {
    test: /\.(css|s[ac]ss)$/,
    use: [
    'style-loader',
    'css-loader',
    'sass-loader',
    ],
    exclude: [
    path.join(__dirname, 'assets/styles/styles.scss')
    ]
    },
    ]
    },
    ...
    }

    Here, after the css-loader, we chain style-loader which will inject the styles to the HEAD tag of the HTML document.

  10. Add some test scoped styles to the HelloWorld component

    <template>
    <div class="test-global-css test-scoped-css">
    Hello {{ name }} from Vue!
    </div>
    </template>
    <script>
    ...
    </script>
    <style scoped lang="scss">
    $fontColor: #FFF;
    .test-scoped-css {
    color: $fontColor;
    }
    </style>

  11. Run the npm build script

    npm run build
  12. Refresh the home page and the text should be white now.

Add BootstrapVue

  1. Install the necessary dependencies

    npm install bootstrap bootstrap-vue
  2. Register BootstrapVue in app entry point

    // ClientApp/views/home/index/main.js
    import Vue from 'vue'
    import HelloWorld from "./HelloWorld.vue";
    import { BootstrapVue } from "bootstrap-vue";
    Vue.use(BootstrapVue);
    const app = new Vue({
    el: '#app',
    render: h => h(HelloWorld)
    })

  3. Import bootstrap, bootstrap-vue stylesheets in the global styles.scss stylesheet

    // ClientApp/assets/styles/styles.scss
    @import "~bootstrap/dist/css/bootstrap.min.css";
    @import "~bootstrap-vue/dist/bootstrap-vue.min.css";
    ...

  4. Test if BootstrapVue is registered properly

    // ClientApp/views/home/index/HelloWorld.vue
    <template>
    <b-container class="test-global-css test-scoped-css">
    Hello {{ name }} from Vue!
    </b-container>
    </template>
    <script>
    ...
    </script>
    <style scoped lang="scss">
    ...
    </style>

Load images

  1. Next, let’s setup Webpack to process images.

    // ClientApp/webpack.config.js
    ...
    module.exports = {
    ...
    module: {
    rules: [
    ...
    {
    test: /\.(png|jpe?g|gif)$/i,
    type: 'asset/resource',
    }
    ]
    },
    ...
    }

    In Webpack 4 and older versions we need to use a file-loader instead

  2. We can test by adding an image in our HelloWorld.vue component

Set up Babel

Babel allows us to use latest JS syntax, during the build process Babel will compile our code to that the browser supports. We can specify what versions of browsers we want to support in .browserlistrc file.

  1. Install necessary dependencies

    npm install -D babel-loader @babel/core @babel/preset-env
  2. Setup webpack to compile JS using babel

    // ClientApp/webpack.config.js
    ...
    module.exports = {
    ...
    module: {
    rules: [
    ...
    {
    test: /\.js$/,
    exclude: /node_modules/,
    use: {
    loader: 'babel-loader',
    options: {
    presets: [
    ['@babel/preset-env', { targets: "defaults" }],
    ]
    }
    },
    },
    ]
    },
    ...
    }

  3. Configure Babel using babel.config.json

    // ClientApp/babel.config.json
    {
    "presets": [
    "@babel/preset-env"
    ]
    }

  4. Tell Babel the list of browsers that we want to support using .browserlistrc

    // ClientApp/.browserlistrc
    default

Automatically clear the wwwroot directory as part of the webpack build process

  1. Install necessary dependencies

    npm install -D clean-webpack-plugin
  2. Add clean-webpack-plugin to the Webpack’s plugins array

    // ClientApp/webpack.config.js
    ...
    const { CleanWebpackPlugin } = require('clean-webpack-plugin');
    module.exports = {
    ...
    plugins: [
    ...
    new CleanWebpackPlugin(),
    ]
    }

Set up Webpack mode(production/development)

We can set the mode webpack property to either development, production or none. Webpack has different built-in optimizations for each mode.

To pass the mode to Webpack configuration, we can set environment variable to an appropriate value via npm scripts.

  1. Install necessary dependencies Setting environment variables in Windows is different from Linux. We can use cross-env package to handle that problem.

    npm install -D cross-env
  2. Update npm scripts

    // ClientApp/package.json
    {
    "scripts": {
    "set_node_env:dev": "cross-env NODE_ENV=development NODE_OPTIONS=--max_old_space_size=8192",
    "set_node_env:prod": "cross-env NODE_ENV=production NODE_OPTIONS=--max_old_space_size=1024",
    "build:dev": "npm run set_node_env:dev webpack",
    "build:prod": "npm run set_node_env:prod webpack"
    }
    }

    Node process has a memory limit. Once the limit is reached, the process will crash. We can override the limit using max_old_space_size option.

  3. Update Webpack configuration to get development mode from the NODE_ENV environment variable.

    // ClientApp/webpack.config.js
    ...
    const isProduction = (process.env.NODE_ENV === 'production');
    if (isProduction) {
    console.log("Bundling in PRODUCTION mode")
    } else {
    console.log("Bundling in DEVELOPMENT mode")
    }
    ...
    module.exports = {
    ...
    mode: isProduction ? 'production' : 'development',
    ...
    }

Set up sourcemap

The code that we write is not sent to the browser as it is. Only the compiled code is sent. This can cause confusion during debugging. Sourcemaps contain mapping between the compiled code and source code so that we can see our original code during debugging.

Sourcemap generation is already controlled by the mode webpack property.

We can also customize it using devTool property, or using SourceMapDevToolPlugin.

Configure Webpack to compress assets in production mode

  1. Install necessary dependencies

    npm install compression-webpack-plugin
  2. Add compression-webpack-plugin to the Webpack plugins array

    // ClientApp/webpack.config.js
    ...
    const CompressionWebpackPlugin = require('compression-webpack-plugin');
    ...
    module.exports = {
    ...
    }
    if (isProduction) {
    module.exports.plugins = (module.exports.plugins || []).concat([
    new CompressionWebpackPlugin(),
    ])
    }

  3. This will create gzipped versions of the js, css bundles. Let’s update _Layout.cshtml to load them in the production environment.

    ...
    <head>
    ...
    <environment include="Development">
    <link rel="stylesheet" href="~/css/styles.bundle.css">
    </environment>
    <environment exclude="Development">
    <link rel="stylesheet" href="~/css/styles.bundle.css.gz">
    </environment>
    </head>
    ...
    </html>

  4. Setup ASP.NET Core static file middleware to set Content-Type, Content-Encoding, response headers accordingly.

    // AspNetCoreVueMpa.Web/Startup.cs
    ...
    namespace AspNetCoreVueMpa.Web
    {
    public class Startup
    {
    ...
    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
    ...
    app.UseStaticFiles(new StaticFileOptions
    {
    OnPrepareResponse = context =>
    {
    var headers = context.Context.Response.Headers;
    var contentType = headers["Content-Type"];
    if (contentType == "application/x-gzip")
    {
    if (context.File.Name.EndsWith("js.gz"))
    {
    contentType = "application/javascript";
    }
    else if (context.File.Name.EndsWith("css.gz"))
    {
    contentType = "text/css";
    }
    headers.Add("Content-Encoding", "gzip");
    headers["Content-Type"] = contentType;
    }
    }
    });
    ...
    }
    }

Configure webpack to optimize CSS assets in production mode

  1. Install necessary dependencies

    npm install -D optimize-css-assets-webpack-plugin
  2. Update Webpack plugins array

    // ClientApp/webpack.config.js
    ...
    const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
    ...
    module.exports = {
    ...
    }
    if (isProduction) {
    module.exports.plugins = (module.exports.plugins || []).concat([
    ...
    new OptimizeCssAssetsPlugin(),
    ])
    }

    This plugin will compact the CSS files using cssnano.

Set up code splitting

At this point, both vendor code and our code are bundled in the same file. This results in a large bundle size. We can tell Webpack to bundle third party libraries to a separate bundle. Another advantage is, if we don’t update any library, the vendor bundle remains unchanged and browser can use the cached version even if we change our code.

  1. Update webpack.config.js to produce multiple bundles.

    // ClientApp/webpack.config.js
    ...
    module.exports = {
    ...
    optimization: {
    runtimeChunk: 'single',
    splitChunks: {
    minSize: 0,
    cacheGroups: {
    core: {
    name: 'core',
    chunks: 'all',
    test: /[\\/]node_modules[\\/](bootstrap-vue|vue|vuelidate|font-awesome|popper.js|portal-vue|process|regenerator-runtime|setimmediate|vue-functional-data-merge)[\\/]/,
    priority: 20,
    enforce: true
    },
    vendor: {
    name: 'vendor',
    chunks: 'all',
    test: /[\\/]node_modules[\\/]/,
    priority: 10,
    enforce: true
    }
    }
    }
    }
    }
    ...

    Here we split third party libraries into three bundles. In addition to the core, vendor bundles, a shared runtime chunk is created.

  2. Update _Layout.cshtml to load all bundles

    ...
    <head>
    ...
    <environment include="Development">
    <link rel="stylesheet" href="~/css/core.bundle.css" asp-append-version="true" type="text/css">
    <link rel="stylesheet" href="~/css/vendor.bundle.css" asp-append-version="true" type="text/css">
    <link rel="stylesheet" href="~/css/styles.bundle.css">
    </environment>
    <environment exclude="Development">
    <link rel="stylesheet" href="~/css/core.bundle.css.gz" asp-append-version="true" type="text/css">
    <link rel="stylesheet" href="~/css/vendor.bundle.css.gz" asp-append-version="true" type="text/css">
    <link rel="stylesheet" href="~/css/styles.bundle.css.gz">
    </environment>
    </head>
    <body>
    ...
    <environment include="Development">
    <script type="text/javascript" src="~/js/runtime.bundle.js" asp-append-version="true"></script>
    <script type="text/javascript" src="~/js/core.bundle.js" asp-append-version="true"></script>
    <script type="text/javascript" src="~/js/vendor.bundle.js" asp-append-version="true"></script>
    </environment>
    <environment exclude="Development">
    <script type="text/javascript" src="~/js/runtime.bundle.js.gz" asp-append-version="true"></script>
    <script type="text/javascript" src="~/js/core.bundle.js.gz" asp-append-version="true"></script>
    <script type="text/javascript" src="~/js/vendor.bundle.js.gz" asp-append-version="true"></script>
    </environment>
    </body>
    </html>

Set up hot-module-reloading

At this point, every time we may a change in our ClientApp we need to manually build the changes. Not cool, right?

With hot module reloading, we can see the changes without refreshing the page. We can use webpack-dev-server for that. webpack-dev-server serves the static assets(webpack output) from the memory(RAM) thus it’s fast.

However, now we have two servers in the development environment, webpack-dev-server that serves static assets with HMR and ASP.NET Core server which serves HTML views and API requests.`

We need to setup webpack-dev-server to talk to the ASP.NET Core server if it does not find the requested content to serve. We can do so by setting devServer.proxy property in the webpack.config.js

  1. Install necessary dependencies

    npm install -D webpack-dev-server
  2. Update webpack.config.js devServer property

    // ClientApp/webpack.config.js
    ...
    module.exports = {
    ...
    devServer: {
    historyApiFallback: false,
    hot: true,
    noInfo: true,
    overlay: true,
    https: true,
    port: 9000,
    proxy: {
    '*': {
    target: 'https://localhost:5001',
    changeOrigin: false,
    secure: false
    }
    },
    contentBase: [path.join(__dirname, '../wwwroot')],
    },
    ...
    }
    ...

  3. Add npm script to start webpack-dev-server

    // ClientApp/package.json
    {
    ...
    "scripts": {
    ...
    "serve": "npm run set_node_env:dev webpack serve",
    },
    ...
    }

    After starting the webpack-dev-server by npm run serve you can visit https://localhost:9000/webpack-dev-server to see what contents are served by the webpack-dev-server

Add layout component with code splitting

A layout component is where we put the nav, aside, footer, .etc.

We can create the layout either in _Layout.cshtml or in a separate Vue component.

By putting the layout content in _Layout.cshtml, we can immediately see something on the screen.

By putting the layout content in a Vue component, we can leave all UI related stuff to Vue.

In this section we’ll see how we can set up a layout component using Vue and how to split it to a separate chunk.

  1. Create ClientApp/components/layout/DefaultLayout.vue

    // ClientApp/components/layout/DefaultLayout.vue
    <template>
    <div>
    <nav>
    Nav goes here
    </nav>
    <main>
    <slot></slot>
    </main>
    <footer>
    Footer goes here
    </footer>
    </div>
    </template>
    <script>
    export default {
    name: "LayoutContainer"
    }
    </script>
    <style scoped>
    </style>

  2. Register DefaultLayout as global Vue component with code splitting

    // ClientApp/views/home/index/main.js
    ...
    Vue.component('default-layout', () => import(/* webpackChunkName: "layout-container" */ '../../../components/layout/DefaultLayout.vue'));
    ...

  3. Use the layout component in HelloWorld.vue component

    // ClientApp/views/home/index/HelloWorld.vue
    <template>
    <default-layout>
    <b-container class="test-global-css test-scoped-css">
    Hello {{ name }} from Vue!
    <img src="../../../assets/images/kajan.png">
    </b-container>
    </default-layout>
    </template>
    <script>
    ...
    </script>
    <style scoped lang="scss">
    ...
    </style

    Note that, since we registered DefaultLayout as a global component, we don’t need to add it to the components array in the HelloWorld.vue

Move duplicate code inside entry files to a separate shared file

From registering bootstrap-vue to registering the layout-component, we need to repeat the logic inside all webpack entry main.js files. We can reduce the duplicate by moving the shared logic to a separate js file and importing it in each entry file.

  1. Create ClientApp/utils/app-init.js with the following content

    // ClientApp/utils/app-init.js
    import Vue from 'vue';
    import BootstrapVue from 'bootstrap-vue';
    Vue.use(BootstrapVue);
    Vue.component('default-layout', () => import(/* webpackChunkName: "layout-container" */ '../components/layout/DefaultLayout.vue'));

  2. Update all main.js files by importing the app-init.js

    // ClientApp/views/home/index/main.js
    import Vue from 'vue'
    import HelloWorld from "./HelloWorld.vue";
    import '../../../utils/app-init.js';
    const app = new Vue({
    el: '#app',
    render: h => h(HelloWorld)
    })

    // ClientApp/views/student/index/main.js
    import Vue from 'vue'
    import StudentIndex from "./StudentIndex.vue";
    import '../../../utils/app-init.js';
    const app = new Vue({
    el: '#app',
    render: h => h(StudentIndex)
    })

Passing data from server-side to Vue components

One advantage in this approach is we have a flexibility to pass initial data in the server-side rendered HTML instead of resolving the data by making an AJAX request.

The idea is to set the JSON string representation of the data to a JS variable from server-side.

  1. Let’s add an extension method to create a JSON string representation of any object. This logic relies on Newtonsoft.Json, make sure to add it to the project.

    dotnet add package Newtonsoft.Json

    // AspNetCoreVueMpa.Web/Utils/ObjectExtensions.cs
    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;
    namespace AspNetCoreVueMpa.Web.Utils
    {
    public static class ObjectExtensions
    {
    public static string ToJson(this object obj)
    {
    var settings = new JsonSerializerSettings
    {
    ContractResolver = new CamelCasePropertyNamesContractResolver(),
    NullValueHandling = NullValueHandling.Ignore,
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
    };
    return JsonConvert.SerializeObject(obj, settings);
    }
    }
    }

    Pass data from controller to view Initialize data to a js variable from server side Use the data in Vue component
  2. Pass data from controller to view

    // AspNetCoreVueMpa.Web/Controllers/HomeController.cs
    ...
    namespace AspNetCoreVueMpa.Web.Controllers
    {
    public class HomeController : Controller
    {
    ...
    public IActionResult Index()
    {
    var viewModel = new List<int>{1, 2, 3};
    return View(viewModel);
    }
    ...
    }
    }

  3. Initialize data to a JS variable from razor view

    // AspNetCoreVueMpa.Web/Views/Home/Index.cshtml
    @using AspNetCoreVueMpa.Web.Utils
    @model List<int>
    @{
    ViewData["Title"] = "Home Page";
    }
    @section Scripts{
    <script type="text/javascript">
    var viewModel = @Model.ToJson();
    </script>
    <script type="text/javascript" src="~/js/home-index.bundle.js" asp-append-version="true"></script>
    }

  4. Use data in the Vue component

    <template>
    <default-layout>
    <b-container class="test-global-css test-scoped-css">
    Hello {{ name }} from Vue!
    <img src="../../../assets/images/kajan.png">
    <pre class="text-white">
    {{ JSON.stringify(viewModel, null, 2) }}
    </pre>
    </b-container>
    </default-layout>
    </template>
    <script>
    export default {
    name: "HelloWorld",
    data() {
    return {
    name: "world",
    viewModel: viewModel,
    }
    },
    }
    </script>
    <style scoped lang="scss">
    ...
    </style>

Ensure front end code is freshly built as part of dotnet publish

We can use .csproj file to hook our own logic into the server build/publish process.

// AspNetCoreVueMpa.Web/AspNetCoreVueMap.Web.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
...
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec Command="cd ClientApp &amp;&amp; npm install" IgnoreExitCode="true" />
<Exec Command="cd ClientApp &amp;&amp; npm run build:prod" IgnoreExitCode="true" />
<ItemGroup>
<DistFiles Include="wwwroot\**" />
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
<RelativePath>%(DistFiles.Identity)</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>

Final thoughts

I think I have explained the main concepts in setting up the project. However, since I am not an expert there is a good chance I missed something. Feel free to point them out in the comment. If you find this article useful please share it with others or star the GitHub repository 😬.


👋 Hi! Welcome to my blog. I'm Kajan, a full-stack engineer focusing on JavaScript(Vue, React) and ASP.NET Core stack. I am thankful to the internet community for helping me out on various occasions 🙏😘. I hope to give back to the community by sharing my experience, and knowledge.