This article shows how Angular SPA apps can be built using Visual Studio and ASP.NET Core which can be used in production. Lots of articles, blogs templates exist for ASP.NET Core and Angular but very few support Angular production builds.
Although Angular 2 is not so old, many different seeds and build templates already exist, so care should be taken when choosing the infrastructure for the Angular application. Any Angular template, seed which does not support AoT or treeshaking should NOT be used, and also any third party Angular component which does not support AoT should not be used.
This example uses webpack 2 to build and bundle the Angular application. In the package.json, npm scripts are used to configure the different builds and can be used inside Visual Studio using the npm task runner.
Code:
VS2015: https://github.com/damienbod/Angular2WebpackVisualStudio
VS2017: https://github.com/damienbod/Angular2WebpackVisualStudio/tree/VisualStudio2017
Short introduction to AoT and treeshaking
AoT
AoT stands for Ahead of Time compilation. As per definition from the Angular docs:
“With AOT, the browser downloads a pre-compiled version of the application. The browser loads executable code so it can render the application immediately, without waiting to compile the app first.”
With AoT, you have smaller packages sizes, fewer asynchronous requests and better security. All is explained very well in the Angular Docs:
https://angular.io/docs/ts/latest/cookbook/aot-compiler.html
The AoT uses the platformBrowser to bootstrap and not platformBrowserDynamic which is used for JIT, Just in Time.
// Entry point for AoT compilation. export * from './polyfills'; import { platformBrowser } from '@angular/platform-browser'; import { enableProdMode } from '@angular/core'; import { AppModuleNgFactory } from '../aot/angular2App/app/app.module.ngfactory'; enableProdMode(); platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
treeshaking
Treeshaking removes the unused portions of the libraries from the application, reducing the size of the application.
https://angular.io/docs/ts/latest/cookbook/aot-compiler.html
npm task runner
npm scripts can be used easily inside Visual Studio by using the npm task runner. Once installed, this needs to be configured correctly.
VS2015: Go to Tools –> Options –> Projects and Solutions –> External Web Tools and select all the checkboxes. More infomation can be found here.
In VS2017, this is slightly different:
Go to Tools –> Options –> Projects and Solutions –> Web Package Management –> External Web Tools and select all checkboxes:
npm scripts
ngc
ngc is the angular compiler which is used to do the AoT build using the tsconfig-aot.json configuration.
"ngc": "ngc -p ./tsconfig-aot.json",
The tsconfig-aot.json file builds to the aot folder.
{ "compilerOptions": { "target": "es5", "module": "es2015", "moduleResolution": "node", "sourceMap": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, "removeComments": true, "noImplicitAny": true, "suppressImplicitAnyIndexErrors": true, "skipLibCheck": true, "lib": [ "es2015", "dom" ] }, "files": [ "angular2App/app/app.module.ts", "angular2App/main-aot.ts" ], "angularCompilerOptions": { "genDir": "aot", "skipMetadataEmit": true }, "compileOnSave": false, "buildOnSave": false }
build-production
The build-production npm script is used for the production build and can be used for the publish or the CI as required. The npm script used the ngc script and the webpack-production build.
"build-production": "npm run ngc && npm run webpack-production",
webpack-production npm script:
"webpack-production": "set NODE_ENV=production&& webpack",
watch-webpack-dev
The watch build monitors the source files and builds if any file changes.
"watch-webpack-dev": "set NODE_ENV=development&& webpack --watch --color",
start (webpack-dev-server)
The start script runs the webpack-dev-server client application and also the ASPNET Core server application.
"start": "concurrently \"webpack-dev-server --inline --progress --port 8080\" \"dotnet run\" ",
Any of these npm scripts can be run from the npm task runner.
Deployment
When deploying the application for an IIS, the build-production needs to be run, then the dotnet publish command, and then the contents can be copied to the IIS server. The publish-for-iis npm script can be used to publish. The command can be started from a build server without problem.
"publish-for-iis": "npm run build-production && dotnet publish -c Release"
https://docs.microsoft.com/en-us/dotnet/articles/core/tools/dotnet-publish
When deploying to an IIS, you need to install the DotNetCore.1.1.0-WindowsHosting.exe for the IIS. Setting up the server IIS docs:
https://docs.microsoft.com/en-us/aspnet/core/publishing/iis
Why not webpack task runner?
The Webpack task runner cannot be used for Webpack Angular applications because it does not support the required commands for Angular Webpack builds, either dev or production. The webpack -d build causes map errors in IE and the ngc compiler cannot be used, hence no production builds can be started from the Webpack Task Runner. For Angular Webpack projects, do not use the Webpack Task Runner, use the npm task runner.
Full package.json
{ "version": "1.0.0", "description": "", "main": "wwwroot/index.html", "author": "", "license": "ISC", "scripts": { "ngc": "ngc -p ./tsconfig-aot.json", "start": "concurrently \"webpack-dev-server --inline --progress --port 8080\" \"dotnet run\" ", "webpack-dev": "set NODE_ENV=development&& webpack", "webpack-production": "set NODE_ENV=production&& webpack", "build-dev": "npm run webpack-dev", "build-production": "npm run ngc && npm run webpack-production", "watch-webpack-dev": "set NODE_ENV=development&& webpack --watch --color", "watch-webpack-production": "npm run build-production --watch --color" }, "dependencies": { "@angular/common": "~2.4.1", "@angular/compiler": "~2.4.1", "@angular/core": "~2.4.1", "@angular/forms": "~2.4.1", "@angular/http": "~2.4.1", "@angular/platform-browser": "~2.4.1", "@angular/platform-browser-dynamic": "~2.4.1", "@angular/router": "~3.4.1", "@angular/upgrade": "~2.4.1", "angular-in-memory-web-api": "~0.2.3", "core-js": "^2.4.1", "reflect-metadata": "^0.1.8", "rxjs": "5.0.1", "zone.js": "^0.7.4", "@angular/compiler-cli": "2.4.1", "@angular/platform-server": "~2.4.1", "bootstrap": "^3.3.7", "ie-shim": "^0.1.0" }, "devDependencies": { "@types/node": "^6.0.52", "angular2-template-loader": "^0.5.0", "awesome-typescript-loader": "^2.2.4", "clean-webpack-plugin": "^0.1.9", "concurrently": "^3.1.0", "copy-webpack-plugin": "^2.1.3", "css-loader": "^0.23.0", "file-loader": "^0.8.4", "html-webpack-plugin": "^2.8.1", "jquery": "^2.2.0", "json-loader": "^0.5.3", "node-sass": "^3.10.1", "raw-loader": "^0.5.1", "rimraf": "^2.5.2", "sass-loader": "^4.0.2", "source-map-loader": "^0.1.5", "style-loader": "^0.13.0", "ts-helpers": "^1.1.1", "typescript": "2.0.3", "url-loader": "^0.5.6", "webpack": "^2.2.0-rc.3", "webpack-dev-server": "^1.16.2" }, "-vs-binding": { "ProjectOpened": [ "watch-webpack-dev" ] } }
Full webpack.prod.js
var path = require('path'); var webpack = require('webpack'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var CopyWebpackPlugin = require('copy-webpack-plugin'); var CleanWebpackPlugin = require('clean-webpack-plugin'); var helpers = require('./webpack.helpers'); console.log("@@@@@@@@@ USING PRODUCTION @@@@@@@@@@@@@@@"); module.exports = { entry: { 'vendor': './angular2App/vendor.ts', 'app': './angular2App/main-aot.ts' // AoT compilation }, output: { path: "./wwwroot/", filename: 'dist/[name].[hash].bundle.js', publicPath: "/" }, resolve: { extensions: ['.ts', '.js', '.json', '.css', '.scss', '.html'] }, devServer: { historyApiFallback: true, stats: 'minimal', outputPath: path.join(__dirname, 'wwwroot/') }, module: { rules: [ { test: /\.ts$/, loaders: [ 'awesome-typescript-loader' ] }, { test: /\.(png|jpg|gif|woff|woff2|ttf|svg|eot)$/, loader: "file-loader?name=assets/[name]-[hash:6].[ext]", }, { test: /favicon.ico$/, loader: "file-loader?name=/[name].[ext]", }, { test: /\.css$/, loader: "style-loader!css-loader" }, { test: /\.scss$/, exclude: /node_modules/, loaders: ["style-loader", "css-loader", "sass-loader"] }, { test: /\.html$/, loader: 'raw-loader' } ], exprContextCritical: false }, plugins: [ new CleanWebpackPlugin( [ './wwwroot/dist', './wwwroot/assets' ] ), new webpack.NoErrorsPlugin(), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false }, output: { comments: false }, sourceMap: false }), new webpack.optimize.CommonsChunkPlugin( { name: ['vendor'] }), new HtmlWebpackPlugin({ filename: 'index.html', inject: 'body', chunksSortMode: helpers.packageSort(['vendor', 'app']), template: 'angular2App/index.html' }), new CopyWebpackPlugin([ { from: './angular2App/images/*.*', to: "assets/", flatten: true } ]) ] };
Links:
https://damienbod.com/2016/06/12/asp-net-core-angular2-with-webpack-and-visual-studio/
https://github.com/preboot/angular2-webpack
https://webpack.github.io/docs/
https://github.com/jtangelder/sass-loader
https://github.com/petehunt/webpack-howto/blob/master/README.md
https://marketplace.visualstudio.com/items?itemName=MadsKristensen.NPMTaskRunner
http://blog.thoughtram.io/angular/2016/06/08/component-relative-paths-in-angular-2.html
https://angular.io/docs/ts/latest/guide/webpack.html
http://blog.mgechev.com/2016/06/26/tree-shaking-angular2-production-build-rollup-javascript/
https://angular.io/docs/ts/latest/tutorial/toh-pt5.html
http://angularjs.blogspot.ch/2016/06/improvements-coming-for-routing-in.html?platform=hootsuite
https://angular.io/docs/ts/latest/cookbook/aot-compiler.html
https://docs.microsoft.com/en-us/aspnet/core/publishing/iis
