If you have been primarily a .Net developer (with not much exposure to Javascript technologies, other than perhaps JQuery) and are now in the process of embracing ASP.Net Core, it is very likely that you would be very quickly pushed into the direction of popular front-end frameworks such as React or Angular. The obvious reason for this is that even though ASP.Net core is a compelling server platform, it does not come with a large selection of UI controls that you can program both from the server and the front-end. Inevitably you would have to make choices for your front-end code; do I simply use JQuery with some JS template library? do I purchase an expensive ASP.Net core “controls” library (typically components in these libraries emit JS / HTML code)? or do I look at integrating technologies such as React or Angular?
This article assumes that you have made a choice to integrate React into your ASP.Net core application. And typically, this is where the fun(?) begins. You start by heading over to the React Website and start encountering terms like JSX, ES6, npm, yarn, Webpack, Babel, etc. All of this can become quickly overwhelming. As a first attempt, you create a new project from the Visual Studio 2017 using the React template. If you are lucky, the solution works out of the box; but it is likely that you may struggle to create another project with just the stuff that you need. The Visual Studio template solution uses a number of advanced concepts which may be way beyond the comprehension of beginner or even intermediate Javascript developers.
In this (long)post, I will start from a basic Visual Studio 2017 template and take you through the steps to the point where you can start adding React code to your solution (like a Pro, well, almost). It is important to have a clear understanding of several concepts before getting to our end goal. I will explain concepts as we go along.
Server Side Javascript; Understanding and Installing Node
By now, most developers (other than the most insulated), know (or have heard) about Javascript on the server. Node has been around for a while now and provides a Javascript runtime. Node works on all the most popular Operating systems and allows us to run complete (stand-alone)Javascript applications.
For our purposes, if you haven’t already, please head to the Node download page and install it “globally” (meaning simply typing “node” on the command prompt should work from any location).
While knowing how to write your own Javascript application on Node is not required to follow this post, it may be worthwhile to spend some time reading the docs and writing a basic application or two to get the hang of things.
When you install Node, it also installs a utility called “npm” (node package manager). npm is used to install packages (libraries) that you can use in your own (or along with other developers’) Javascript applications. Simply typing “npm” on the command prompt should provide you with usage hints at the same time confirming that you have actually installed it.
Setting up a blank Visual Studio 2017 ASP.Net core Web solution
I am assuming you have already installed Visual Studio 2017 (community or other versions) and I don’t cover it here.
We will be using MVC, but will start off with an “Empty” ASP.Net core Web project template and MVC support manually, so that our project is not littered with the default MVC project files. I added a new project named “ASPNetCoreReact” using File → New →Project and then selecting “ASP.Net Core Web Application” and choosing “Empty” in the next step (No Docker, No Authentication support).
If you run the application at this time, it will simply print a “Hello World!” message on a new Browser window.
Note: I have also added this project to a Github repo. The commit at this point is “Initial Commit — Empty ASP.Net Core Solution Template”.
Enabling MVC to the application
In this post I have assumed that you are already familiar with MVC and do not explain how the framework works. Regardless, if you follow the steps below you should get the starer project working with minimal fuss.
Add the MVC Service to your Startup class (ConfigureServices method)
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); }
Update the Startup class Configure method
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); }
app.UseStaticFiles();
app.UseMvc(routes => { routes.MapRoute( name: "default", template: " {controller=Home}/{action=Index}/{id?}"); } ); }
Note that the “Hello World!” console write has been removed. I have also added the “UseStaticFiles” call, as that would be required later (to serve static content like CSS, JS, etc).
Add a new folder named “Controllers” and add a “Home” controller
(At this point, Visual Studio may also add the “Microsoft.VisualStudio.Web.CodeGeneration.Design” package to your solution if you use the scaffolding option to add the Home controller.)
The (default / scaffolded) Home Controller code should look like this:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc;
namespace ASPNetCoreReact.Controllers { public class HomeController : Controller { public IActionResult Index() { return View(); } } }
Add a View corresponding to the Home / Index Action
Once the View is added (with the “No Layout” option — if you are using scaffolding), add a single “h1” tag inside the body with text : “This is the entry page.” The View code should look like this:
@{ Layout = null; }
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /><title>Index</title> </head> <body>
<h1>This is the entry page.</h1>
</body> </html>
Your solution folder should at this point look similar to this:
When you run the application, a browser page should open up with output that looks similar to this:
Note that the “h1” tag has default styling applied by the browser (in the above case, it is Chrome).
The solution at this stage is available on this Github repo with the commit label: MVC Enabled.
Introducing Javascript and the concept of dependenices
Let us now add some basic Javascript to our application.
Create a new folder named “Source” under the “wwwroot” folder and two Javascript files in the source folder: app.js and lib.js.
In app.js, enter code as below:
document.getElementById("fillthis").innerHTML = getText();
In lib.js, enter code as below:
getText = function () { return "Data from getText function in dep.js"; }
Modify the “body” section of the Index View code as below:
<body>
<h1>This is the entry page.</h1>
<div id="fillthis"></div>
<script src="~/Source/lib.js"></script> <script src="~/Source/app.js"></script>
</body>
We have now added a new “div” element with id, “fillthis”, and included “Script” references to the two Javascript files we added.
If you run the application now, you should see output as below (with the “div” displaying data from the new Javascript code:
Note that we have added a reference to the “lib.js” file first and then the reference to “app.js” in the Index View code.
If instead, we were to reverse the order of references in the Index View code, thus:
... <script src="~/Source/app.js"></script> <script src="~/Source/lib.js"></script> ...
the browser would complain about the “getText” function not defined (and of course the “div” element would not get populated):
The “app.js” is said to be implicitly dependent on “lib.js” and it is up to the developer to include these Javascript files in the correct order where referenced.
The solution folder now looks like this:
The solution at this stage (with the correct order of JS file references) is available on this Github repo with the commit label: Basic Javascript introduced
Introducing Javascript modules / bundles / Webpack
In the previous section, we saw how implicit dependencies need to be handled appropriately by the developer using functionality from various Javascript files. Well, there is a better way. Javascript “modules” allow programmers to write independent units of code (of course named as modules) that other programmers can explicitly include (or mark as “required”) in their Javascript code.
For example, using the concepts of modules, we can now modify the app.js files thus:
require('./lib');
document.getElementById("fillthis").innerHTML = getText();
app.js now explicitly marks the lib file as a dependency.
Javascript modules by itself is an extensive topic — but for our purposes, it is sufficient to appreciate that JS modules are a way to share code using explicit dependencies. For a detailed explanation of Javascript modules, you can read Preethi’s post.
Imagine now that you could “bundle” all your Javascript modules into a single file, so that you can add a reference to that file alone in your HTML ! This is where the Webpack tool comes in.
Let’s now install Webpack. Open a new command prompt window and navigate to the folder that has your csharp project file (.csproj file). Before we actually install Webpack, let us first create a package.json file. The package.json file holds metadata information and is used to give information to npm about a project’s (module)dependencies. To create a “default” package.json file, type the “npm init” command:
npm init -y
The “-y” option simply uses default options.
If the command was successful, it should have created a package.json file with contents similar to this:
{ "name": "ASPNetCoreReact", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
To now install Webpack (which is also a “module”), type the following command:
npm install webpack --save-dev
If the command ran successfully, it should have created a new folder named “node_modules” in your project folder and downloaded a number of modules into this folder (into various folders), of course including the Webpack package as well. (Note: There are some differences between modules and packages — you can read about that here.)
The “ — save-dev” option adds a “dev dependency” to the package.json file, instead of a run-time dependency. This indicates that the Webpack package is only useful during development. That would make sense, as once we have created the “bundles” as we require, we would use them directly in production. The package.json file should look similar to this:
{
"name": "ASPNetCoreReact",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^3.8.1"
}
}
Note the “devDepencies” section above. It indicates a dependency on webpack as well as the version information. Other developers can use this package.json file and install the modules specified in “devDepencies” section using npm install. (npm may also create a package-lock.json file which contains information about dependency trees.)
Now that we have installed Webpack, let us use it to create our first bundle! Note that we have installed Webpack “locally” (meaning within the project folder) and not globally (as we did node). So, we would not be able to simply type “webpack” on the command prompt.
There are multiple ways to run webpack from a local installation, but I found it easiest to run it using node after adding an entry in the Scripts section in the package.json file. Modify your package.json file like this:
{
"name": "ASPNetCoreReact",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"wbp" : "webpack wwwroot/source/app.js wwwroot/dist/bundle.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^3.8.1"
}
}
We have added a “script” by name “wpb” as an alias for the command: “webpack wwwroot/source/app.js wwwroot/dist/bundle.js” — We can now use this script to run our actual command using npm. The command passes the “app.js” as the “entry” file to webpack and instructs it to generate a new bundle file named bundle.js (after traversing all explicit dependencies in the entry file). Webpack treats (converts) every Javascript file as a module.
Before running the Webpack commad, ensure that your app.js is updated to use the “require” for lib like this:
require('./lib');
document.getElementById("fillthis").innerHTML = getText();
To run webpack, type the following at the command prompt:
npm run wbp
This will run the command that corresponds to the “script” name wbp. If the command ran successfully, it should have created a file called bundle.js in the wwwroot/dist folder. This is your first bundle file! Take some time to inspect the contents of bundle.js. You will notice that it has included the code in app.js as well as lib.js in bundle.js (other than the logic to handle modules).
To test our new bundle, modify the Index View code as below (only the “body” element is shown here):
... <body>
<h1>This is the entry page.</h1> <div id="fillthis"></div>
<script src="~/dist/bundle.js"></script>
</body>
...
Note that references to app.js and lib.js are removed. The only file we are referencing here is the newly created bundle.js.
If you run the application now, the output should be identical to the results above (when app.js and lib.js were included as separate references).
Now that we have run our first Webpack command, let us get a bit adventurous and create a “config” file for the Webpack command. Webpack has various capabilities and to use its full power we can setup a “webpack.config.js” file. “webpack.config.js” is the default file name that webpack uses to read “instructions” for processing files.
In our case, we had specified a single “entry” file and a single “bundle” (output) file. Let us now create a webpack.config.js file. Use Visual studio to create a new Javascript file in the main project folder and name it webpack.config.js. Enter the following code into this file:
const path = require('path');
module.exports = { entry: './wwwroot/source/app.js', output: { path: path.resolve(__dirname, 'wwwroot/dist'), filename: 'bundle.js' } };
We need to “export” a config object from within the config file (which by the way is a normal Javascript file where you can use code from other modules). Our config object has an entry and an output specified. As expected, the entry file is app.js and the output path is specified to be the wwwroot/dist folder and the output (bundle) “filename” is specified as bundle.js. The inbuilt node module “path” is used to resolve the folder paths.
Now that we have created a config file for webpack, we can remove the arguments from the webpack command in the Scripts section in the package.json file, like so:
{
"name": "ASPNetCoreReact",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"wbp" : "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^3.8.1"
}
}
The “entry” and the “output” parameters are removed from the webpack command. Now it will use the new config file we have created.
To run the command, type the following in the command prompt (as before):
npm run wbp
If all’s well, it will have an identical effect to running the Webpack command with arguments supplied.
The solution at this stage (with the correct order of JS file references) is available on this Github repo with the commit label: Webpack and webpack.config.js added.
The solution folder should look similar to this:
Introducing JQuery to the solution
Let us now introduce JQuery to the mix.
Modify the Index View to include JQuery from a CDN, add a new div to hold contents to be filled by JQuery, and (temporarily)revert to using the app.js and lib.js as independent files, like so:
... <head> <meta name="viewport" content="width=device-width" /><title>Index</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" ></script>
</head> <body>
<h1>This is the entry page.</h1>
<div id="fillthis"></div>
<div id="fillthiswithjquery"></div>
<script src="~/Source/lib.js"></script>
<script src="~/Source/app.js"></script>
</body>
...
Modify app.js to make a JQuery call to populate the new div with id, “fillthiswithjquery”, and also comment out the line with the “require” call, like so:
//require('./lib');
document.getElementById("fillthis").innerHTML = getText();
$('#fillthiswithjquery').html('Filled by Jquery!');
If you run the application now, you should see the following result:
As you can see, the new div has been populated with the text: “Filled by Jquery!” as expected.
Let us now un-comment the “require” line from app.js , like so:
require('./lib');
document.getElementById("fillthis").innerHTML = getText();
$('#fillthiswithjquery').html('Filled by Jquery!');
and recreate the bundle.js, by running the same command as before:
npm run wbp
Modify the Index View code to include the bundle created and also position the JQuery Script reference line below the bundle.js reference line.
... <head> <meta name="viewport" content="width=device-width" /><title>Index</title>
</head> <body>
<h1>This is the entry page.</h1>
<div id="fillthis"></div>
<div id="fillthiswithjquery"></div>
<script src="~/dist/bundle.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" ></script>
</body>
...
If you run the application now, you will receive the following error:
The browser reports that “$” is not defined. The “implicit” JQuery dependency has not been included in the correct order.
Let us now remove the implicit dependency on JQuery and make it an explicit dependency in app.js. But before we do that, let us install the JQuery package using npm, like so (run the command from the Project folder):
npm install jquery --save-dev
This should install the JQuery package in the node_modules and modify the package.json file, like so:
{
"name": "ASPNetCoreReact",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"wbp" : "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jquery": "^3.2.1",
"webpack": "^3.8.1"
}
}
Modify the app.js file to “require” Jquery and map it to “$”, like so:
$ = require('jquery');
require('./lib');
document.getElementById("fillthis").innerHTML = getText();
$('#fillthiswithjquery').html('Filled by Jquery!');
Note: you can also use “import $ from ‘jquery’;” (ES6 module syntax instead of the require statement.)
Remove the reference to the JQuery CDN from the Index View code. like so:
... <head> <meta name="viewport" content="width=device-width" /><title>Index</title>
</head> <body>
<h1>This is the entry page.</h1>
<div id="fillthis"></div>
<div id="fillthiswithjquery"></div>
<script src="~/dist/bundle.js"></script>
</body>
...
Build the bundle again using the following command as before:
npm run wbp
If you run the application, you should now see the expected result as below:
This time however, JQuery is included within bundle.js ! You will note that bundle.js has grown in size.
The solution at this stage is available on this Github repo with the commit label: JQuery added as package.
Introducing Webpack loaders & plugins, ES6 (ES2015)
Webpack gets its power from what are known as loaders and plugins.
“Loaders are transformations that are applied on the source code of a module.” “Plugins are the backbone of webpack. webpack itself is built on the same plugin system that you use in your webpack configuration! They also serve the purpose of doing anything else that a loader cannot do.”
Let us now use a plugin, named “ProvidePlugin”. This plugin automatically loads modules instead of having to import or require them everywhere.
Let us use this plugin to automatically load the JQuery module and make it available everywhere.
Modify the webpack.config.js like so:
const path = require('path');
const webpack = require('webpack');
module.exports = { entry: './wwwroot/source/app.js', output: { path: path.resolve(__dirname, 'wwwroot/dist'), filename: 'bundle.js' }, plugins: [ new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', 'window.jQuery': 'jquery' })
]
};
We add a new “plugins” element as an array of the plugins that webpack will use to process the source files. In this case, we have instructed to make “$”, “Jquery”, “window.jquery” available everywhere without having to explicitly importing them in our files. Note that we are “requiring” the webpack module itself and using it to create the Plugin.
As a result of using the plugin, we can now comment out the “require” statement from app.js, like so:
//$ = require('jquery');
require('./lib');
document.getElementById("fillthis").innerHTML = getText();
$('#fillthiswithjquery').html('Filled by Jquery!');
If you now regenerate the bundle.js using the command below, the results will be unchanged and as expected.
npm run wbp
ES6 (ES2015)
ECMAScript 6 (now known predominantly as ES2015) is a specification for the Javascript language. ES6 introduces quite a few new features not implemented in all browsers (especially older browsers like IE 11/IE 10, which may still be widely in use). In fact, there are already newer features introduced that correspond to ES2016/ES2017. One of the new features in ES6 is classes.
Let us introduce an ES6 feature in a new Javascript file and include it as a dependency in app.js. Create a new file named “es6codelib.js” in the “Source” folder and enter the following code in that file:
export default class ES6Lib {
constructor() { this.text = "Data from ES6 class"; }
getData() { return this.text; } }
This does not do much other than initialize a “text” property which is returned from the getData() “method”. It marks the ES6Lib class as the default “export”.
Modify the Index View code to include a new “div” with id: fillthiswithes6lib, like so:
...
<h1>This is the entry page.</h1>
<div id="fillthis"></div>
<div id="fillthiswithjquery"></div>
<div id="fillthiswithes6lib"></div>
...
Modify app.js to use the new ES6 code, like so:
//$ = require('jquery');
require('./lib');
import ES6Lib from './es6codelib';
document.getElementById("fillthis").innerHTML = getText();
$('#fillthiswithjquery').html('Filled by Jquery!');
let myES6Object = new ES6Lib(); $('#fillthiswithes6lib').html(myES6Object.getData());
Now, if we re-create the bundle and run the application (make sure that your browsers are not loading files from Cache), modern browsers like Chrome will probably run the code without any issues. However, if you run the code on an older browser like IE 11, you are likely to encounter the issue as below:
IE 11 is not recognizing the “class” syntax.
The workaround for these issues is to use a Transpiler, such as Babel. The transpiler transforms ES6 code to use ES5 syntax that older browsers can understand. While Babel itself is very feature-rich and can be used independently, for our purposes, we can use the corresponding babel “loaders” to do the job.
Before we can use any loaders, we need to first install them. For our purpose, we will have to install the relevant babel loaders. Use the following command to install the babel loaders along with the “presets”.
npm install babel-loader@8.0.0-beta.0 @babel/core@next @babel/preset-env@next --save-dev
If the command ran successfully, these loaders would be installed in the node_modules folder and the package.json would be modified, like so:
{
"name": "ASPNetCoreReact",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"wbp" : "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.0.0-beta.31",
"@babel/preset-env": "^7.0.0-beta.31",
"babel-loader": "^8.0.0-beta.0",
"jquery": "^3.2.1",
"webpack": "^3.8.1"
}
}
Modify the webpack.config.js like so:
const path = require('path'); const webpack = require('webpack');
module.exports = {
entry: './wwwroot/source/app.js',
output: {
path: path.resolve(__dirname, 'wwwroot/dist'),
filename: 'bundle.js'
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
'window.jQuery': 'jquery'
})
],
module: {
rules: [ { test: /\.js?$/,
use: { loader: 'babel-loader', options: { presets:
['@babel/preset-env'] } } },
]
}
};
We have now added a “module” property to the config object which in turn has a “rules” array. The rules instruct webpack to use the loader “babel-loader” (for the .js files, as specified in the Regex “test”) passing it the options of “preset-env” . This preset instructs the loader to recognize all the latest ES features (2015/2016/2017) and transform code using these features into code that can be understood by older browsers.
Now, recreate the bundle as always with:
npm run wbp
Run the application and it should now display correctly in IE 11:
It may be worthwhile to inspect the code in bundle.js to see the transformations at work!
The solution at this stage is available on this Github repo with the commit label: ProvidePlugin and babel-loader added
The Solution folder at this stage looks like this:
Introducing Bootstrap and custom CSS to the solution
As Bootstrap is so widely used, I have decided to include it in this solution.
Incredible as it may seem, CSS files can actually be included as “modules” by using webpack, which gets help from a loader to do its job. The loader as you may have guessed, is named css-loader.
Before using Bootstrap in our solution, we would of course have to install the Bootstrap package. Bootstrap (JS components) itself depends on a package called Popper.js (other than being dependent on JQuery), so let us install that first, like so:
npm install popper.js --save-dev
We will also install the latest version of Bootstrap (currently V4.0 /Beta 2) with the command:
npm install bootstrap@4.0.0-beta.2 --save-dev
Also install the CSS-loader, like so:
npm install css-loader --save-dev
After these commands, Bootstrap and Popper packages as well as the css-loader should be installed in the node_modules folder and the package.json file should look like:
{ "name": "ASPNetCoreReact", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "wbp" : "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.0.0-beta.31", "@babel/preset-env": "^7.0.0-beta.31", "babel-loader": "^8.0.0-beta.0", "bootstrap": "^4.0.0-beta.2", "css-loader": "^0.28.7", "jquery": "^3.2.1", "popper.js": "^1.12.6", "webpack": "^3.8.1" } }
Of course we can include the source Bootstrap precompiled Sass files and use appropriate loaders for transforming the Sass files, but we will use the Bootstrap min CSS file instead to keep things simple.
Let us also add a custom CSS file, site.css under a new folder named: CSS under the wwwroot folder.
Modify the contents of the site.css file thus:
h1 { color: red; }
Modify the app.js file to include the bootstrap min CSS file:
//$ = require('jquery');
require('./lib');
import 'bootstrap/dist/css/bootstrap.min.css';
import '../css/site.css';
import ES6Lib from './es6codelib';
document.getElementById("fillthis").innerHTML = getText();
$('#fillthiswithjquery').html('Filled by Jquery!');
let myES6Object = new ES6Lib(); $('#fillthiswithes6lib').html(myES6Object.getData());
Modify the webpack.config.js, like so:
const path = require('path'); const webpack = require('webpack');
module.exports = { entry: './wwwroot/source/app.js', output: { path: path.resolve(__dirname, 'wwwroot/dist'), filename: 'bundle.js' }, plugins: [ new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', 'window.jQuery': 'jquery', Popper: ['popper.js', 'default'] }) ],
module: { rules: [ {test: /\.css$/, use: [{ loader: "css-loader" }]}, { test: /\.js?$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } },
]
}
};
We have added the “Popper” reference to the ProvidePlugin (making it available everywhere) as well as added a “test” rule for processing css files using the css-loader.
Recreate the bundle now using the same command as before and run the application.
You will find that there is no change to the display and Bootstrap styles have not been applied. Why? Because all that the css-loader has done is transform the CSS file into a Javascript module and included in bundle.js. It has not actually rendered the “style” element. Inspect the bundle.js code to find references to Bootstrap (Bootstrap v4.0.0-beta.2) inside the code.
Let us now use another loader, named style-loader, to actually render the styles element. We need to pass the output of css-loader to the style-loader.
First, let us install the style-loader, like so:
npm install style-loader --save-dev
This installs the style-loader in the node_modules folder and modifies the package.json file thus:
{
"name": "ASPNetCoreReact",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"wbp" : "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.0.0-beta.31",
"@babel/preset-env": "^7.0.0-beta.31",
"babel-loader": "^8.0.0-beta.0",
"bootstrap": "^4.0.0-beta.2",
"css-loader": "^0.28.7",
"jquery": "^3.2.1",
"popper.js": "^1.12.6",
"style-loader": "^0.19.0", "webpack": "^3.8.1"
}
}
Modify the webpack.config.js, like so:
const path = require('path'); const webpack = require('webpack');
module.exports = { entry: './wwwroot/source/app.js', output: { path: path.resolve(__dirname, 'wwwroot/dist'), filename: 'bundle.js' }, plugins: [ new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', 'window.jQuery': 'jquery', Popper: ['popper.js', 'default'] }) ],
module: { rules: [ {test: /\.css$/, use: [{ loader: "style-loader" }, { loader: "css-loader" }]}, { test: /\.js?$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } },
]
} };
The loaders are applied from right to left; in this case, the css-loader is applied first and then the style-loader.
Recreate the bundle.js using the same command as before. When you run the application now, you will see the Bootstrap styles applied as well as the “red” color we applied to the “h1” element using our custom CSS file:
If you inspect the DOM on this page, you will see that the Style element has been rendered inline:
You should find a line of code: “addStylesToDom(styles, options);” inside the bundle.js file. If you comment out that line and re-load the page, you will see that the style element is no longer available. This is the code added by the style-loader.
The solution at this stage is available on this Github repo with the commit label: Bootstrap and custom CSS added
The solution folder now looks like this:
Extract-text-webpack-plugin and uglifyjs-webpack-plugin
We saw in the previous section how all the CSS was rendered inline on the page. We may not want that. We may want to extract / combine / minify all the CSS into a file and then add a reference to it. This is where the Extract-text-webpack-plugin comes in. We can use this plug-in to extract the styles code as generated by the css-loader and extract it to a physical CSS file.
The uglifyjs-webpack-plugin is used to minify JavaScript code.
Install both plugins as below:
npm install extract-text-webpack-plugin --save-dev
npm install uglifyjs-webpack-plugin --save-dev
The package.json file now looks like:
{ "name": "ASPNetCoreReact", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "wbp" : "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.0.0-beta.31", "@babel/preset-env": "^7.0.0-beta.31", "babel-loader": "^8.0.0-beta.0", "bootstrap": "^4.0.0-beta.2", "css-loader": "^0.28.7", "extract-text-webpack-plugin": "^3.0.2", "jquery": "^3.2.1", "popper.js": "^1.12.6", "style-loader": "^0.19.0", "uglifyjs-webpack-plugin": "^1.0.1", "webpack": "^3.8.1" } }
By now, you should be familiar with adding plugins; Modify the webpack.config.js to add these plugins, like so:
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractCSS = new ExtractTextPlugin('allstyles.css');
module.exports = {
entry: './wwwroot/source/app.js', output: { path: path.resolve(__dirname, 'wwwroot/dist'), filename: 'bundle.js' },
plugins: [ extractCSS, new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', 'window.jQuery': 'jquery', Popper: ['popper.js', 'default'] }), new webpack.optimize.UglifyJsPlugin() ], module: { rules: [ { test: /\.css$/, use: extractCSS.extract(['css-loader? minimize']) },
{ test: /\.js?$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } }, ] } };
Note that the extract Text CSS plugin uses the output directly from the css-loader (the style-loader is not required).
Re-run Webpack with this config; you should now see an “allstyles.css” file in your “dist” folder. The newly generated bundle.js should be significantly smaller owing to the use of uglifyjs-webpack-plugin.
Now you should include a link to the newly generated allstyles.css in the Index view code, like so:
<html> <head> <meta name="viewport" content="width=device-width" /><title>Index</title>
<link rel="stylesheet" href="~/dist/allstyles.css" />
</head>
<body>
<h1>This is the entry page.</h1>
<div id="fillthis"></div> <div id="fillthiswithjquery"></div> <div id="fillthiswithes6lib"></div>
<script src="~/dist/bundle.js"></script>
</body>
</html>
Running the application would now produce the same results — but with a smaller bundle.js file and a newly generated CSS file.
The solution at this stage is available on this Github repo with the commit label: Extract Text and Uglify Plugins added
The solution folder now looks like this:
Hotmodule replacement
The final functionality we will be including before adding React to the project is Hotmodule replacement.
While developing a Javascript application that requires a build step before we see the changes on the browser, it becomes quite inconvenient to perform these steps every time we make a change to a Javascript file. Here is where a couple of packages come to our help (by allowing updates to Javascript files to be automatically propagated to the browser without manual intervention).
Add the webpack-hot-middleware and aspnet-webpack packages, like so:
npm install webpack-hot-middleware --save-dev npm install aspnet-webpack --save-dev
Once these packages are installed, the package.json file looks like this:
{ "name": "ASPNetCoreReact", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "wbp" : "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.0.0-beta.31", "@babel/preset-env": "^7.0.0-beta.31", "aspnet-webpack": "^2.0.1", "babel-loader": "^8.0.0-beta.0", "bootstrap": "^4.0.0-beta.2", "css-loader": "^0.28.7", "extract-text-webpack-plugin": "^3.0.2", "jquery": "^3.2.1", "popper.js": "^1.12.6", "style-loader": "^0.19.0", "uglifyjs-webpack-plugin": "^1.0.1", "webpack": "^3.8.1", "webpack-hot-middleware": "^2.20.0" } }
Modify the startup class, Configure method to use Webpack dev middleware, like so (you also need to add a using statement for Microsoft.AspNetCore.SpaServices.Webpack at the beginning of the file):
... using Microsoft.AspNetCore.SpaServices.Webpack; ...
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { HotModuleReplacement = true }); } ...
Modify the webpack.config.js, like so:
const path = require('path'); const webpack = require('webpack'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const extractCSS = new ExtractTextPlugin('allstyles.css');
module.exports = {
entry: { 'main': './wwwroot/source/app.js' },
output: {
path: path.resolve(__dirname, 'wwwroot/dist'),
filename: 'bundle.js',
publicPath: 'dist/'
},
plugins: [ extractCSS, new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery', 'window.jQuery': 'jquery', Popper: ['popper.js', 'default'] }), new webpack.optimize.UglifyJsPlugin() ],
module: { rules: [ { test: /\.css$/, use: extractCSS.extract(['css-loader?minimize']) }, { test: /\.js?$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } }, ]
} };
Note the minor change in the “entry” element — Hotmodule replacement requires an entry with an explicitly named “main” entry point. It also requires a “publicPath” specified in the “output” element.
We also need to let HMR know that we are ready to “accept” Hotmodule updates from within our module.
Modify the app.js file and add an “accept” call, like so:
//$ = require('jquery'); require('./lib'); import 'bootstrap/dist/css/bootstrap.min.css'; import '../css/site.css';
import ES6Lib from './es6codelib';
document.getElementById("fillthis").innerHTML = getText(); $('#fillthiswithjquery').html('Filled by Jquery??');
let myES6Object = new ES6Lib(); $('#fillthiswithes6lib').html(myES6Object.getData());
module.hot.accept();
Build and run the application now and monitor the console in your browser window. Once the initial page has loaded, make a change in app.js and note that the browser picks up the change automatically upon save, and updates the UI accordingly.
The solution at this stage is available on this Github repo with the commit label: Hotmodule replacement
We are now ready to add React to the solution !
Adding React to the solution
Let us first install the React packages (react and react-dom) to our solution using npm, like so:
npm install react react-dom --save-dev
We also need a babel (React) “preset” to process React JSX code. Let us install the corresponding preset, like so:
npm install @babel/preset-react --save-dev
The package.json file will now look similar to:
{ "name": "ASPNetCoreReact", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "wbp" : "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.0.0-beta.31", "@babel/preset-env": "^7.0.0-beta.31", "@babel/preset-react": "^7.0.0-beta.31", "aspnet-webpack": "^2.0.1", "babel-loader": "^8.0.0-beta.0", "bootstrap": "^4.0.0-beta.2", "css-loader": "^0.28.7", "extract-text-webpack-plugin": "^3.0.2", "jquery": "^3.2.1", "popper.js": "^1.12.6", "react": "^16.0.0", "react-dom": "^16.0.0", "style-loader": "^0.19.0", "uglifyjs-webpack-plugin": "^1.0.1", "webpack": "^3.8.1", "webpack-hot-middleware": "^2.20.0" } }
Add the @babel/preset-react to the babel-loader in webpack.config.js, like so:
...
{ test: /\.js?$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-react' ,'@babel/preset-env'] } } }
...
Remove unnecessary content (“div”s) from the Index view code, assign the Bootstrap CSS class name “container” to the “body” element and add placeholders for two React components; a basic React component and a React component that makes calls to an api controller to fetch data, like so:
... <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title>
<link rel="stylesheet" href="~/dist/allstyles.css" />
</head>
<body class="container">
<h1>This is the entry page.</h1>
<br/><br /><br />
<div id="basicreactcomponent"></div>
<br /><br /><br />
<div id="reactcomponentwithapidata"></div>
<script src="~/dist/bundle.js"></script>
</body> </html>
Add a new Javascript file, reactcomponent.js, in the Source folder and type the following code:
import * as React from 'react'; import * as ReactDOM from 'react-dom';
export default class Counter extends React.Component {
constructor() { super(); this.state = { currentCount: 0 }; }
render() { return <div> <h1>Counter</h1> <p>This is a simple example of a React component.</p> <p>Current count: <strong>{this.state.currentCount}</strong></p>
<button onClick={() => { this.incrementCounter() }}>Increment </button>
</div>; }
incrementCounter() { this.setState({ currentCount: this.state.currentCount + 1 }); } }
This creates a basic “Counter” component; it is a JSX adaptation of the Typescript component in the Visual Studio 2017 React sample solution.
Modify the app.js to import React and this component and render it, like so:
//$ = require('jquery');
require('./lib'); import 'bootstrap/dist/css/bootstrap.min.css'; import '../css/site.css';
import React from 'react';
import ReactDOM from 'react-dom';
import Counter from './reactcomponent';
import ES6Lib from './es6codelib';
ReactDOM.render(
<Counter />,
document.getElementById('basicreactcomponent')
);
//document.getElementById("fillthis").innerHTML = getText();
//$('#fillthiswithjquery').html('Filled by Jquery??');
//let myES6Object = new ES6Lib();
//$('#fillthiswithes6lib').html(myES6Object.getData());
module.hot.accept();
Re-generate the bundle.js and run the application. You should now see the Counter component displayed on the page, like so:
Hitting the button increments the counter.
Let us now add the next component to display data fetched from an api controller.
Add a new controller named “SampleDataController” in the Controllers folder and enter the following code:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc;
namespace ASPNetCoreReact.Controllers { [Route("api/[controller]")] public class SampleDataController : Controller { private static string[] Summaries = new[] {
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
[HttpGet("[action]")] public IEnumerable<WeatherForecast> WeatherForecasts() {
var rng = new Random();
return Enumerable.Range(1, 5) .Select(index => new WeatherForecast { DateFormatted = DateTime.Now.AddDays(index).ToString("d"), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }); }
public class WeatherForecast { public string DateFormatted { get; set; } public int TemperatureC { get; set; } public string Summary { get; set; } public int TemperatureF { get { return 32 + (int)(TemperatureC / 0.5556); }
} } } }
This controller is used ASIS from the Visual Studio 2017 sample React solution template.
Create a new Javascript file named fetchdata.js under the Source folder and enter the following code:
import * as React from 'react'; import 'es6-promise'; import 'isomorphic-fetch';
export default class FetchData extends React.Component{
constructor() { super(); this.state = { forecasts: [], loading: true };
fetch('api/SampleData/WeatherForecasts') .then(response => response.json()) .then(data => { this.setState({ forecasts: data, loading: false }); }); }
render() { let contents = this.state.loading? <p><em>Loading...</em></p> : FetchData.renderForecastsTable(this.state.forecasts); return <div> <h1>Weather forecast</h1> <button onClick={() => { this.refreshData() }}>Refresh</button>
<p>This component demonstrates fetching data from the server.</p> {contents} </div>; }
refreshData() { fetch('api/SampleData/WeatherForecasts') .then(response => response.json()) .then(data => { this.setState({ forecasts: data, loading: false }); }); }
static renderForecastsTable(forecasts) { return <table className='table'> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> {forecasts.map(forecast => <tr key={forecast.dateFormatted}> <td>{forecast.dateFormatted}</td> <td>{forecast.temperatureC}</td> <td>{forecast.temperatureF}</td> <td>{forecast.summary}</td> </tr> )} </tbody> </table>; } }
This is a JSX adaptation of the same Typescript component in the Visual Studio 2017 React template solution. The refreshData() method has been added to respond to the “Refresh” button.
Modify the app.js file to import this component and render it accordingly, like so:
...
import React from 'react'; import ReactDOM from 'react-dom';
import Counter from './reactcomponent';
import FetchData from './fetchdata';
import ES6Lib from './es6codelib';
ReactDOM.render( <Counter />, document.getElementById('basicreactcomponent') );
ReactDOM.render(
<FetchData />,
document.getElementById('reactcomponentwithapidata')
);
....
The FetchData component requires the isomorphic-fetch package, so let us install it, like so:
npm install isomorphic-fetch --save-dev
The package.json file now looks like:
{ "name": "ASPNetCoreReact", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "wbp" : "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.0.0-beta.31", "@babel/preset-env": "^7.0.0-beta.31", "@babel/preset-react": "^7.0.0-beta.31", "aspnet-webpack": "^2.0.1", "babel-loader": "^8.0.0-beta.0", "bootstrap": "^4.0.0-beta.2", "css-loader": "^0.28.7", "extract-text-webpack-plugin": "^3.0.2", "isomorphic-fetch": "^2.2.1", "jquery": "^3.2.1", "popper.js": "^1.12.6", "react": "^16.0.0", "react-dom": "^16.0.0", "style-loader": "^0.19.0", "uglifyjs-webpack-plugin": "^1.0.1", "webpack": "^3.8.1", "webpack-hot-middleware": "^2.20.0" } }
Re-generate the bundle.js and run the application. It should now display the dynamic data table component as well. Clicking on the Refresh button should display a new set of data:
The final solution is available on this Github repo with the commit label: React components integrated
With this, we come to the end of this very long post ! We have covered a lot of topics, but there are still possible developments listed below.
Further development
Server Side rendering: This was not covered, and is left as an exercise for the reader.
“Vendor” files separation / other useful plugins: This solution clubs “vendor” solutions (such as Bootstrap CSS) along with custom implementation (site.css). Separate “bundles” should ideally be created to keep the vendor files and the custom files separate. The Visual Studio 2017 React solution demonstrates this nicely by having a separate webpack.config.vendor.js file that includes all vendor files, separate from the webpack.config.js file that includes custom implementations. That solution further uses the DllReferencePlugin and DllPlugin plugins to implement the separation.
Build integration : While we have stuck to using the command prompt for all our installs and configurations, the Visual Studio 2017 template solution has a couple of Build steps integrated into its Project file. Right click on the project file and select the Edit .csproj project file option to review the contents of the project file. You will see entries similar to the following:
<Target Name="DebugRunWebpack" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('wwwroot\dist') ">
<!-- 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." />
<!-- In development, the dist files won't exist on the first run or when cloning to a different machine, so rebuild them if not already present. -->
<Message Importance="high" Text="Performing first-run Webpack build..." />
<Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js" />
<Exec Command="node node_modules/webpack/bin/webpack.js" />
</Target>
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
<Exec Command="npm install" /> <Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" />
<Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" />
<!-- 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> </ResolvedFileToPublish>
</ItemGroup> </Target>
It defines two cases, a “DebugRunWebpack” and a “PublishRunWebpack”. In the Debug workflow, it checks for the existence of the “wwwroot/dist” folder and if not uses Webpack to run against the “vendor” and the “default” webpack config files — essentially building the bundles as per the specification in these config files (after running npm install on the package.json file). If it finds that “node” is not installed, it stops the build notifying the user to install “node” before proceeding further.
In the “Publish” path, it includes the “distribution” files in the build (after initiating a “fresh” build using npm). Note that in this path, it passes the “ — env.prod” argument to webpack. The config files have code to make sense of this parameter (the “vendor” config file uses the “DefinePlugin” plugin to configure whether webpack should run a “development” build or “production” build). For the “dev” build, it also uses the SourceMapDevToolPlugin to generate source maps.
Developers can optionally modify their build files according to their needs. MSBuild docs can be handy.
No comments:
Post a Comment