Setting up ASP.NET Core, Vue MPA with Webpack
February 15, 2021
vueasp-net-corewebpackmulti-page-applicationhow-toIntroduction
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
Table of Contents
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 TypeScript
in 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.
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 transformVue
components intoJS
,CSS
as part of build process, allowing us to use Vue in development time, while shipping the code inHTML
,CSS
andJS
.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.Hot module reloading. Using the Webpack development server, we can see the changes without refreshing the page.
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 separateutils.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.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.
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?
Optimizing the assets Want to minify and compress assets, there are Webpack plugins to do that π
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.
Open the terminal and typeout the following commands
mkdir AspNetCoreMvcVueMpacd AspNetCoreVueMpadotnet new mvc -o AspNetCoreMvcVueMpa.Webdotnet new slndotnet sln add AspNetCoreMvcVueMpa.Webgit initdotnet new gitignoreWe can now start the app using
dotnet run
and see it inhttps://localhost:5001
/The scaffolding template includes bootstrap related files inside the
wwwroot
directory. Letβs remove them. Later weβll usenpm
to installJavaScript
dependencies.Letβs also remove references to the deleted assets from
Layout.cshtm
lIgnore
wwwroot
directory from version controlEnable razor runtime compilation
By default, changes to
.cshtml
files will reflect only when we dodotnet build
ordotnet 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 useMicrosoft.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.2Now 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.
Letβs first create a new directory inside our
AspNetCoreVueMpa.Web
project to house all of our front-end code.mkdir ClientAppInitialize empty
npm
project inside that directorycd ClientAppnpm init -yThat should create a
package.json
file inside theClientApp
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
.Install
webpack
,webpack-cli
as development dependenciesnpm i -D webpack webpack-cliSet up
vue-loader
Now weβve Webpack installed. To process
.vue
files, we needvue-loader
, thevue-loader
documentation site suggests installingvue-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 togetherLetβs install them.
npm i -D vue-loader vue-template-compilerNow, we need to tell
Webpack
to usevue-loader
to process.vue
files. We can configureWebpack
in a file calledwebpack.config.js
. Letβs create that file with the following content.// ClientApp/webpack.config.jsconst 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 themodule.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 providedtest
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.jsmodule.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.
Install Vue
npm i vueCreate
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>Create
views/home/index/main.js
that mountsHelloWorld
component to adiv
with idapp
. Thismain.js
is going to be the entry point for our home page.// ClientApp/views/home/index/main.jsimport Vue from 'vue'import HelloWorld from "./HelloWorld.vue";const app = new Vue({el: '#app',render: h => h(HelloWorld)})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>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>}Run the build script
npm run buildyou should see a new file at
wwwroot/js/home-index.bundle.js
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.
Add
StudentController.cs
with the following contentusing Microsoft.AspNetCore.Mvc;namespace AspNetCoreVueMpa.Web.Controllers{public class StudentController : Controller{public IActionResult Index(){return View();}}}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>}Create test Vue component
Add entry file
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.
- Inside Vue SFC
- 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.
Install the necessary development dependencies
npm i -D css-loader style-loader mini-css-extract-plugin sass sass-loaderAdd
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;}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.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,...}Update
HelloWorld.vue
to use a class definition from the global stylesheet<template><div class="test-global-css">Hello {{ name }} from Vue!</div></template>...Update
_Layout.cshtml
to load the bundled global stylesheet<head>...<link rel="stylesheet" href="~/css/styles.bundle.css"></head>...Run the
npm
build
scriptnpm run buildVisit the home page, and you should see the styles applied
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 chainstyle-loader
which will inject the styles to the HEAD tag of the HTML document.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>Run the
npm
build
scriptnpm run buildRefresh the home page and the text should be white now.
Add BootstrapVue
Install the necessary dependencies
npm install bootstrap bootstrap-vueRegister
BootstrapVue
in app entry point// ClientApp/views/home/index/main.jsimport 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)})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";...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
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
insteadWe 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.
Install necessary dependencies
npm install -D babel-loader @babel/core @babel/preset-envSetup 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" }],]}},},]},...}Configure Babel using
babel.config.json
// ClientApp/babel.config.json{"presets": ["@babel/preset-env"]}Tell Babel the list of browsers that we want to support using
.browserlistrc
// ClientApp/.browserlistrcdefault
Automatically clear the wwwroot directory as part of the webpack build process
Install necessary dependencies
npm install -D clean-webpack-pluginAdd
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.
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-envUpdate
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.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
Install necessary dependencies
npm install compression-webpack-pluginAdd
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(),])}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>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
Install necessary dependencies
npm install -D optimize-css-assets-webpack-pluginUpdate 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.
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.
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
Install necessary dependencies
npm install -D webpack-dev-serverUpdate 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')],},...}...Add
npm
script to startwebpack-dev-server
// ClientApp/package.json{..."scripts": {..."serve": "npm run set_node_env:dev webpack serve",},...}After starting the
webpack-dev-server
bynpm run serve
you can visithttps://localhost:9000/webpack-dev-server
to see what contents are served by thewebpack-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.
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>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'));...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">...</styleNote that, since we registered
DefaultLayout
as a global component, we donβt need to add it to the components array in theHelloWorld.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.
Create
ClientApp/utils/app-init.js
with the following content// ClientApp/utils/app-init.jsimport Vue from 'vue';import BootstrapVue from 'bootstrap-vue';Vue.use(BootstrapVue);Vue.component('default-layout', () => import(/* webpackChunkName: "layout-container" */ '../components/layout/DefaultLayout.vue'));Update all
main.js
files by importing theapp-init.js
// ClientApp/views/home/index/main.jsimport 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.jsimport 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.
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
Pass data from controller to view Initialize data to a js variable from server side Use the data in Vue component// AspNetCoreVueMpa.Web/Utils/ObjectExtensions.csusing 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
// 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);}...}}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>}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 && npm install" IgnoreExitCode="true" /><Exec Command="cd ClientApp && 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 π¬.