Substitute View of MVC or build front-end of Web API 2 with React JS. This template bundles, splits, minifies & injects scripts & styles on _Layout.cshtml file dynamically at build time.
The template has been updated with Webpack 5 & React 17. Read here
Visual Studio 2019 comes with React JS template but only for .Net Core. For the .Net Framework-based projects, two of the many possible things can be done (consuming third-party API is not considered in this blog as it will be pure React app).
An ideal application would be the first choice; however, it has its constraints. Some of the constraints could be:
Above mentioned limitations give a reason to make a custom .Net Framework template for the web application.
The good news is, it's not difficult to create and use a custom React template which is not only well integrated with ASP.NET MVC/ Web API but also substitutes Razor with React JS. Moreover, bundling with Webpack leverage ES6+ to the project, which Visual Studio bundler (default bundler for .net framework web application) is unable to do.
Advantages of the template:
[Auth(Roles = "role_for_this_service, another_role")]
public class HomeController : Controller
Confused with the jungle of .Net Framework, .Net Core, .Net 5 and .Net Standard? I'll be writing a blog for that. In this article, I will interchangeably be using .Net Framework and ASP.NET MVC/Web API for the web application that uses .Net Framework 4.x.x.
TL;DR The template can be downloaded from the Github
This template is created using (and assumed they are already installed):
D:\Workspace\AspNetFrameworkReactTemplate\wwwroot>
. This blog assumes React app root folder as wwwroot.npm init
creates package.json file.
npm init
creates an interactive session, which gives a chance to input various entries.
npm install --save-dev webpack
and webpack-cli for > v4.x.x npm install --save-dev webpack-cli
installs latest version of Webpack as development dependency, i.e., files will not be bundled for production.
We need webpack config files for bundling related operations. We will create 3 files, one common (containing basic settings) and two for development and production respectively. It is also possible to create 4th for staging with little effort.
"While we will separate the production and development specific bits out, note that we'll still maintain a "common" configuration to keep things DRY. In order to merge these configurations together, we'll use a utility called webpack-merge. With the "common" configuration in place, we won't have to duplicate code within the environment-specific configuration" - webpack
npm install --save-dev webpack-merge
"scripts": {
"start": "webpack --config webpack.dev.js --watch",
"build": "webpack --config webpack.prod.js",
},
"babel": {
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
},
Let's install some useful packages for webpack. Install all of them using npm install --save-dev PACKAGE-NAME-WRITTEN-BELOW
copy and paste the content of following three files in their respective file.
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
//This will inject correct path of scripts in _Layout.cshtml via _Layout_Template.cshtml otherwise it will omit /research
const ASSET_PATH = process.env.ASSET_PATH || '/wwwroot/dist/';
module.exports = {
entry: {
app: './src/index.js',
},
plugins: [
// new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Production',
filename: '../../Views/Shared/_Layout.cshtml',
template: '../Views/Shared/_Template.cshtml',
inject: false
}),
],
module: {
rules: [
{
use: {
loader: "babel-loader"
},
test: /\.js$|jsx/,
resolve: {
extensions: [".js", ".jsx"]
},
exclude: /node_modules/
},
{
use: [MiniCssExtractPlugin.loader, 'css-loader'],
test: /\.css$/
},
{
test: /\.svg$/,
exclude: path.resolve(__dirname, 'node_modules', 'font-awesome'),
use: ['babel-loader', 'react-svg-loader'],
},
]
},
};
const path = require('path');
const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
devServer: {
contentBase: './dist',
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[id].css',
}),
],
watch: true,
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
});
const path = require('path');
const { merge } = require('webpack-merge');
const TerserJSPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].css',
}),
],
output: {
filename: '[name].[contenthash].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
});
Why output on both prod and dev file, and why contenthash on prod file name?
Web-browsers tend to cache scripts and styles. Having contenthash allows hashed filename on build (if changes has been made). This forces browser to discard cached content and load from the server.
Now, our settings for the webpack is almost done (bundle splitting comes later). We need to prepare our _Layout.cshtml file ready to be injected with the js and css file links by webpack on build time.
If you look at the webpack.common.js, we have already told webpack file; where to looks for template and the destination file (_Layout.cshtml) to be injected.
new HtmlWebpackPlugin({
title: 'Production',
filename: '../../Views/Shared/_Layout.cshtml',
template: '../Views/Shared/_Template.cshtml',
inject: false
}),
This file will be similar to the \Layout.cshtml_. The only difference will be code to inject script and styles.
<head></head>
section: <% for (var style in htmlWebpackPlugin.files.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.css[style] %>" />
<% } %>
</body>
section (end of the body): <% for (var chunk in htmlWebpackPlugin.files.js) { %>
<script src="<%= htmlWebpackPlugin.files.js[chunk] %>"></script>
<% } %>
CAUTION: Remember to make any changes on _Template.cshtml as _Layout.cshtml file, now, onwards will be generated dynamically!
Go the the Github repo of this project. Copy src folder and paste inside wwwroot folder of the project.
npm i react react-dom
Now, we have install every dependencies to run our React project.
Go the terminal/cmd and type (must be at the root of the project, in our case wwwroot )
npm run start
It creates the development bundle of scripts and styles and inject them in to the \Layout.cshtml_ via \Template.cshtml. You can confirm it by looking into the _/dist folder. The project structure looks like below:
If Script and Style links are injected in the \Layout.cshtml_, then the project is ready to run. Remember to add folders/files with React demo app (the demo app is copied from Create React App).
Remember, we have not deleted files that comes with default ASP.NET MVC template. We gonna do that later on. It's not necessary to delete them if you need, however, they would increase the bundle size and we gonna remove bundle.config too as we use Webpack.
Press F5 or click Start to run the project. This is necessary, as we are using IIS Express (shipped with Visual Studio) to act as our dev server.
If if runs without an error, we should be able to see a page similar to the below:
React app is running along with the ASP.NET MVC template's Bootstrap navbar! Isn't it a beautiful view!
Now, we can go ahead and remove all nuget packages that is not needed.
The template is ready. However, there is one more important thing (probably there are many ;-) ). When the project grows so the build size. Big bundle size could give problems such as,
After running npm run build or start
, we should be able to similar bundle files as shown below.
Asset Size Chunks Chunk Names
../../Views/Shared/_Layout.cshtml 1.49 KiB [emitted]
app.067438a29d63ee7bf581.css 768 bytes 0 [emitted] [immutable] app
app.542ea302f9487596bd9f.bundle.js 130 KiB 0 [emitted] [immutable] app
runtime.e9d0f715e4067b817a45.bundle.js 1.46 KiB 1 [emitted] [immutable] runtime
app.542ea302f9487596bd9f.bundle.js.LICENSE.txt 790 bytes [emitted]
NOTE: I installed moment and react-router to increase the build size so that more chunks will be created, just for demonstration purpose, without success. However, I have tried this in my project and this works! I can assure you about that.
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
//Use this if your domain name has path. Example, www.example.com/myapp
//This will inject correct path of scripts in _Layout.cshtml via _Layout_Template.cshtml otherwise it will omit / research
//const ASSET_PATH = process.env.ASSET_PATH || '/myapp/wwwroot/dist/';
module.exports = {
entry: {
app: './src/index.js',
},
output: {
// publicPath: ASSET_PATH,
filename: '[name].[contenthash].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
// new CleanWebpackPlugin(['dist/*']) for < v2 versions of CleanWebpackPlugin
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Production',
filename: '../../Views/Shared/_Layout.cshtml',
template: '../Views/Shared/_Template.cshtml',
inject: false
}),
],
module: {
rules: [
{
use: {
loader: "babel-loader"
},
test: /\.js$|jsx/,
resolve: {
extensions: [".js", ".jsx"]
},
exclude: /node_modules/
},
{
use: [MiniCssExtractPlugin.loader, 'css-loader'],
test: /\.css$/
},
{
test: /\.svg$/,
exclude: path.resolve(__dirname, 'node_modules', 'font-awesome'),
use: ['babel-loader', 'react-svg-loader'],
},
]
},
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
moduleIds: "hashed",
runtimeChunk: "single",
splitChunks: {
chunks: "all",
maxInitialRequests: Infinity,
minSize: 300000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// // get the name. E.g. node_modules/packageName/not/this/part.js
// //or node_modules/packageName
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
// //npm package names are URL-safe, but some servers don't like @ symbols
return `react-dot-net-app.${packageName.replace("@", "")}`;
}
}
}
}
}
};
<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
<script src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
<% } %>
We need to specify publicpath in the webpack.common.js
....... code removed for brevity
const ASSET_PATH = process.env.ASSET_PATH || '/myapp/wwwroot/dist/';
module.exports = {
entry: {
app: './src/index.js',
},
output: {
publicPath: ASSET_PATH,
filename: '[name].[contenthash].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
.......... code removed for brevity