The first thing we have to do is to set up our workstation: it won't be difficult because we only need a small set of essential tools. These include Visual Studio 2015, the web platform installer, .NET Framework 4.5 or higher, a web server such as IIS or IIS Express, and a decent source code control system such as Git, Mercurial, or Team Foundation, which we will take for granted as you most likely already have them in place. Just make sure you're all set with these before going further.
IMPORTANT
If you haven't already, be sure to download and install Visual Studio 2015 Update 3 and the .NET Core for Visual Studio Tools Preview 2. These are the latest updates at the time of writing, but will be updated in the near future.
These updates will address some important issues for web development tools, as well as adding improved support for TypeScript, NPM, and other components that we'll be using later on:
The first thing we need to do is to create a new ASP.NET Core web application project:
/Controller/
folder, a ValuesController.cs
sample, and some useful references in the Startup.cs
file.Now that our project has been created, it's time to add the resources we're going to use. As we already mentioned, the layout of a standard ASP.NET Core solution is quite different from what it used to be. The main differences are:
/src/
folder by default. This is just a convention, though, as they can be placed anywhere.wwwroot
folder, which will contain the compiled, ready-to-publish contents of our application, while everything else will be the project source code.Other things worth noting are a Controller
folder, containing a sample ValueController.cs
class, a Startup.cs
file containing the application class and a couple of other files we'll address in a while.
The first thing we need to do is to add a folder called Scripts
to the root of our project. We'll use this folder to place all of our JavaScript files, and then we'll combine and minify these using a JavaScript Task Runner such as Gulp, this way we'll make sure that the resulting file will be added to the wwwroot
folder automatically after each build.
Now it's time to make a good use of the three package managers natively supported by ASP.NET, namely NuGet, NPM, and Bower. These tools will allow you to gather all the resources you need to build your application: they will download all the resources and their dependencies automatically, so you needn't do it manually, thus saving a lot of time. In case you're wondering why we need three of them, it can be useful to briefly recap their scope:
project.json
file so they can be retrieved and/or checked for updates at any time.package.json
file.git
. Its configuration file is called bower.json
. Notice that, since the Angular 2 team is pushing their code using NPM rather than Bower, we won't be using it in our project.ASP.NET Core gives us at least four different ways to add NuGet packages to our project:
Install-Package
command followed by the package name and build version.project.json
.The first three methods, although being absolutely viable, are basically shortcuts for populating the fourth one; the latter has the advantage of being the less opaque one, so we'll just use it.
Open the project.json
file, find the dependencies
section and add the following packages to the list (new lines are highlighted):
"dependencies": { "Microsoft.AspNetCore.Mvc": "1.0.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0", "Microsoft.Extensions.Configuration.Json": "1.0.0", "Microsoft.Extensions.Logging": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.Extensions.Logging.Debug": "1.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", "Microsoft.AspNetCore.Diagnostics": "1.0.0", "Microsoft.AspNetCore.Routing": "1.0.0", "Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0", "Microsoft.AspNetCore.StaticFiles": "1.0.0", "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0" }
We have added a space to visually separate the default dependencies required by all ASP.NET Core projects from our newly added ones.
The listed builds are the latest at the time of writing, but they won't last forever: as soon as ASP.NET Core passes its initial release (1.0.0 at the time of writing), these numbers will gradually increase over time, whenever a new version comes out. To check the latest version of each package, just place the cursor between the quotes and delete the version number, a dynamic drop-down list will be shown containing all the latest versions for that given module.
While we're here, it can be useful to check which version of the .NET Framework we are targeting by looking at the frameworks
key. Since we choose the .NET Framework template, we should find something like this:
"frameworks": { "net461": { } },
This will most likely change in the future, so be sure to target a version compatible with the packages you want to use. For the purpose of this book, the .NET Framework 4.6.1 will do the job.
As soon as we save the project.json
file, Visual Studio will start to retrieve the missing packages from the web. Wait for it to finish, then proceed with opening the Startup.cs
file, which is also in the project root.
If you're a seasoned .NET developer you might already be familiar with the Startup.cs
file, introduced in OWIN-based applications to replace most of the tasks previously handled by the good old Global.asax
file. In ASP.NET Core, the Startup.cs
file serves the same purpose as the OWIN
startup class, being nothing less than the application main entry point, it is the place where we can add services, choose which application modules and middleware functions to load, handle dependency injection tasks, and configure the pipeline.
However, the similarities end here, the class has been completely rewritten to be as pluggable and lightweight as possible, meaning that it will include and load only what's strictly necessary to fulfill our application's tasks.
To better understand this, let's take a look at the following lines taken from the Startup.cs
source code shipped with the ASP.NET Core Web API project template we chose:
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); app.UseMvc(); }
Notice how empty our application's HTTP request pipeline is, it won't ever serve static files, since there is nothing telling it to do so. To better understand it, let's perform a quick test before proceeding.
In order to check that the ASP.NET pipeline is properly working, click on the Start Debugging option in the Debug menu, or just hit the
F5
hotkey. If everything is OK, your default web browser will open pointing to the following URL: http://localhost:14600/api/values
.
The page content will show the following:
If we're seeing the preceding screenshot, it means that the request pipeline is working fine, the MVC module has been added within the Startup.cs
file and it's working as expected. That's because there's a sample ValueController.cs
class in the /Controllers/
folder, conveniently added by the Web API project template we chose a while ago, that behaves exactly like this.
Now let's try to request the static Project_Readme.html
file, also added by our chosen template in the project root. In order to reach it, we need to move it inside the /wwwroot/
folder. Once done, it should be reachable by pointing at the following URL: http://localhost:14600/Project_Readme.html
.
However, if we try to do that, and then issue that request using the same browser we used before, we would get the following response:
This HTTP 404 error clearly demonstrates what we've just said, the HTTP request pipeline won't serve static files, simply because we didn't tell it to. However, we can easily fix that behavior by adding them to the pipeline within the Startup.cs
file (new lines highlighted):
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); // Configure a rewrite rule to auto-lookup for standard default files such as index.html. app.UseDefaultFiles(); // Serve static files (html, css, js, images & more). See also the following URL: // https://docs.asp.net/en/latest/fundamentals/static-files.html for further reference. app.UseStaticFiles(); // Add MVC to the pipeline app.UseMvc(); }
While we are here, we also added the following:
index.htm
, index.html
, and so on), which will be automatically served without the user having to fully qualify the URI.If we run our application again, we should now be welcomed with the following:
That's it. We have enabled static file support, so that we'll be able to serve not only HTML but also CSS, JS files, and so on. Delete the Project_Readme.html
and get ready to install an important third-party NuGet package that we'll be using a lot in the following chapters.
If you're working with ASP.NET and you've never heard about Newtonsoft.Json (formerly Json.NET), you've almost certainly missed something that could've eased your job, big time. We're talking about one of the finest libraries - and most useful tools, ever developed for .NET, at least for the writer. It's a very efficient (and thus very popular), high-performance JSON serializer, deserializer, and all-around framework for .NET, which also happens to be completely open source.
We won't dig into it anymore here, as we'll be using it soon enough. For now, let's just install it by right-clicking on our solution's node in the Solution Explorer, then choosing Manage NuGet Packages for this Solution... to open up the following panel:
Search for Newtonsoft.Json
to make it appear if it isn't there already, then left-click to select it. Ensure to select the latest stable version (9.0.1 at the time of writing), click on Install, and then wait for the installer to complete its job.
That's it for now: if we open the project.json
file now, we can see that the Newtonsoft.Json
package reference has been added at the end of the dependencies
section, together with the other project-specific dependencies we've manually added before. We'll be installing other packages using the GUI in the following chapters, as soon as we need them, now that we know how easy it is to do that.
Now it's time to choose the client programming language to adopt. Given the fact we're planning to use Angular 2, our choices are basically the following three: good old JavaScript, its Microsoft superset known as TypeScript, or the Google growing beast known as Dart.
In this project, we're going to use TypeScript for a number of good reasons, the most important of them are as follows:
We're not the only ones praising TypeScript: it's something acknowledged by the Angular team itself, considering the fact that the Angular 2 source code has been written using TypeScript, as proudly announced by Microsoft in the following MDSN blog post: https://blogs.msdn.microsoft.com/typescript/2015/03/05/angular-2-built-on-typescript/.
The first thing we hav to do to set up TypeScript is to add a tsconfig.json
file to our root project.
In the e to do to set up TypeScript is to add a tsconfig.json file to our root project. In the Solution Explorer, right-click on the root project node and select Add, then New Item; switch to the client-side from the top left tree view, then select TypeScript Configuration File, and add the tsconfig.json
file to the project root.
The tsconfig.json
file will be generated with its default set of options, which are good for most scenarios but not ideal for what we're going to do. That's why we need to add/change some settings until it will look like the following:
{ "compileOnSave": false, "compilerOptions": { "emitDecoratorMetadata": true, "experimentalDecorators": true, "module": "system", "moduleResolution": "node", "noImplicitAny": false, "noEmitOnError": false, "removeComments": false, "target": "es5" }, "exclude": [ "node_modules", "wwwroot" ] }
These instructions will influence how Intellisense and our external TypeScript compiler will work; two things that will help us, big time.
Wait a minute, did we just say external TypeScript compiler? What about the built-in, integrated TypeScript compile feature provided by the Visual Studio IDE?
Well, we just said that one of TypeScript's biggest advantages is that we can use it anywhere a plain JavaScript file would run, that's because any .ts
file can be compiled into a .js
file without any problem. The compile task is handled by the TypeScript compiler itself, also known as tsc, which can be run directly from the Visual Studio IDE with the help of the tsconfig.json
we added in the preceding paragraph. That file, as we've just seen, contains a wide set of compiling options such as creating source maps, creating definition files, concatenating everything into a single output file, and so on. Looking great, isn't it?
Unfortunately, not so much. Although Visual Studio 2015 does a pretty decent job of acting as a bridge between our TypeScript sources and tsc, it still lacks an important key feature, it doesn't give the option to minify/uglify the resulting JS files, thus leaving them uncompressed after the compile task.
Such feature can be trivial during development, yet it happens to be quite important in production because we'll definitely want to cloak our source code before publishing it. That's why the best thing we can do is to stop the Visual Studio IDE from compiling TypeScript files and implement a custom alternative that does support minify/uglify.
In case you're wondering about what the terms minify and/or uglify actually mean, we strongly suggest that you read the following Wikipedia page: https://en.wikipedia.org/wiki/Minification_(programming).
Luckily, we're going to use something we would need anyway, a dedicated, streamlined, and modern task runner that goes by the name of Gulp. In order to install it, though, we need to set up the appropriate package manager.
NPM is the tool we will use to add some important packages to our project, the most relevant ones being Gulp and Angular 2. To install it, do the following:
package.json
file to the project root.
Change the contents of the automatically-generated package.json
file to match the following:
{ "version": "1.0.0", "name": "opengamelistwebapp", "private": true, "dependencies": { }, "devDependencies": { "gulp": "^3.9.1", "gulp-clean": "^0.3.2", "gulp-concat": "^2.6.0", "gulp-sourcemaps": "^1.6.0", "gulp-typescript": "^2.13.6", "gulp-uglify": "^2.0.0", "typescript": "^1.8.10" } }
Please notice that, at the time of writing, Gulp 3.9.1 is the latest version. You can check the most recent build by checking the Visual Studio Intellisense, which is able to perform impressive real-time update checks upon each package's latest build just like it does within the project.json
file:
As an alternative, you can always visit the Gulp project NPM page at https://www.npmjs.com/package/gulp and change the build version numbers accordingly.
Notice that you aren't forced to input precise build numbers, as you can also use the standard npmjs syntax to specify auto-update rules bound to custom version ranges using the supported prefixes, such as the following:
This is another scenario where Intellisense will come in handy, as it will also suggest how to do it.
For an extensive list of available npmjs
commands and prefixes, you can also check out the official npmjs documentation at https://docs.npmjs.com/files/package.json.
As you most likely already know, Gulp is a powerful task runner toolkit that we will use to automate some time-consuming tasks in our development workflow. If you never worked with a task runner, think of it as a batch file or, even better, a PowerShell script powered with some useful web development tools such as file concatenation, js/css shrinking, and more.
You will notice that, together with Gulp, we're also installing some Gulp-related plugins:
gulp-clean
: This deletes the contents of the destination foldersgulp-concat
: This merges multiple files into onegulp-sourcemaps
: This generates the Source Mapsgulp-typescript
: This compiles TypeScript files into JSgulp-uglify
: This minifies JavaScript filesConfiguring Gulp is as simple as adding the Gulp Configuration file to your project. You can do that just like you did with the NPM Configuration file, right-click on the root project node in the Solution Explorer and select Add, then New Item. From the client-side tree panel, locate and select the Gulp Configuration file and add a gulpfile.js
file to the project root.
The Gulp configuration file, generally known as Gulpfile, is basically a list of tasks and commands that Gulp will execute, written using a mostly readable JavaScript syntax. Working with it can be tricky at first, especially if you want to use it to handle complex tasks. Digging too much into it would take us out of the scope of this book, so we'll just see how we can tell it to do a rather simple set of jobs, merge all the JavaScript files in the /Scripts/
folder, minus those in a directory for third-party scripts/libraries, into a single, minified all.min.js
file which will be generated into the wwwroot
folder. This can be obtained with the following code:
var gulp = require('gulp'), gp_clean = require('gulp-clean'), gp_concat = require('gulp-concat'), gp_sourcemaps = require('gulp-sourcemaps'), gp_typescript = require('gulp-typescript'), gp_uglify = require('gulp-uglify'); /// Define paths var srcPaths = { app: ['Scripts/app/main.ts', 'Scripts/app/**/*.ts'], js: ['Scripts/js/**/*.js'] }; var destPaths = { app: 'wwwroot/app/', js: 'wwwroot/js/' }; // Compile, minify and create sourcemaps all TypeScript files and place them to wwwroot/app, together with their js.map files. gulp.task('app', function () { return gulp.src(srcPaths.app) .pipe(gp_sourcemaps.init()) .pipe(gp_typescript(require('./tsconfig.json').compilerOptions)) .pipe(gp_uglify({ mangle: false })) .pipe(gp_sourcemaps.write('/')) .pipe(gulp.dest(destPaths.app)); }); // Delete wwwroot/app contents gulp.task('app_clean', function () { return gulp.src(destPaths.app + "*", { read: false }) .pipe(gp_clean({ force: true })); }); // Copy all JS files from external libraries to wwwroot/js gulp.task('js', function () { return gulp.src(srcPaths.js) // .pipe(gp_uglify({ mangle: false })) // disable uglify // .pipe(gp_concat('all-js.min.js')) // disable concat .pipe(gulp.dest(destPaths.js)); }); // Delete wwwroot/js contents gulp.task('js_clean', function () { return gulp.src(destPaths.js + "*", { read: false }) .pipe(gp_clean({ force: true })); }); // Watch specified files and define what to do upon file changes gulp.task('watch', function () { gulp.watch([srcPaths.app, srcPaths.js], ['app', 'js']); }); // Global cleanup task gulp.task('cleanup', ['app_clean', 'js_clean']); // Define the default task so it will launch all other tasks gulp.task('default', ['app', 'js', 'watch']);
Here's a brief explanation of what these commands actually do:
app
task, which is the most important and complex one. This task makes use of three different plugins: sourcemaps, typescript, and uglify. Notice how each line is bound to a specific plugin action, which takes care of a single job, following the "It should do one thing" paradigm Gulp is all about. Line 21 defines the source files, line 22 initializes the sourcemaps plugin, line 23 instructs the plugin to fetch the TypeScript compiler options from the tsconfig.json
file, and so on.app_clean
task, which will erase every file and subfolder within the wwwroot/app
target path.js
task, which is meant to publish external JavaScript libraries manually added to the project. This is a rather simple task, as it performs a mere copy of the .js
files from a source folder into its production counterpart. Notice how we could easily choose to also minify and/or concatenate the source files into a single one by uncommenting a few lines of code. However, since this task is meant to handle third-party JavaScript libraries, it would be wise not to manipulate their code.js_clean
task that will erase everything within the wwwroot/js
target path.watch
task. This is basically a file watcher/monitoring task that will execute the app
and js
tasks if one or more of their source files will change. cleanup
task that will erase the content of all destination paths by launching the app_clean
and js_clean
tasks. It can be useful to reset these folders to their initial state.default
task that will execute all the other tasks, including watch
, within a single call. This is the one we will launch.It's worth noting that the uglify plugin requires the mangle
option to be set to false
: this is a workaround for a bug in the Angular 2 RC5 that will hopefully be fixed in future releases. For more info regarding this topic, check out the following URL from the official Angular 2 GitHub repository:
https://github.com/angular/angular/issues/10618.
To execute the Gulp file, right-click on it from the Solution Explorer and choose Task Runner Explorer, or open it manually by selecting it from File, Other Windows in the Visual Studio 2015 main menu bar. Once opened, click the refresh button, then right-click the default
task and select Run to activate it.
Once you do that, the watch
task we just created will silently run in background, keeping tracks of our changes and act accordingly. Notice the default (running) word within the tab label, which will remind us that there is still an ongoing task: closing that console window will immediately shut it down.
That's everything we'll ever need from Gulp for this project; there's no need for us to go further. If you want to learn more about Gulp and its configuration file syntax, you will find a lot of resources and documentation, together with a lot of useful samples, in the official website at the following address: http://gulpjs.com/.
Since we're going to do a lot of modifications to the files contained in the /Scripts/app/
folder within the following chapters, it would be great if the app_clean
task could run automatically before the app
task: such behavior would ensure that we'll always get rid of outdated and orphaned files without having to manually perform the cleanup
. Luckily enough, Gulp allows us to easily do that by passing an optional array of dependent tasks that will be launched before the main one.
Let's go back to the line where we defined the app
task and add the following (updated code are highlighted):
gulp.task('app', ['app_clean'], function () {
That's it. From now on the app
task will launch the app_clean
dependent task and wait for its completion before executing itself, meaning that the /wwwroot/app/
folder contents will be erased before the arrival of the new file, this is precisely what we wanted.
Before Gulp was widely welcomed by the web application development community, Grunt used to be the king of the hill. Does that mean the former killed the latter? Well, most certainly not. Grunt is still an excellent tool and can be a great alternative to Gulp, especially if you already know how to use it. Unless you do, though, we suggest starting with Gulp because we think that it has a fair edge on code flexibility, and also a more streamlined approach. However, if you don't feel like using it, sticking to Grunt and its renowned plugins is just as good, you won't ever get fired for such a choice.
Using Grunt instead of Gulp is as easy as doing the following:
package.json
file, replace the Gulp references - together with the relevant plugins, with the Grunt packages.gruntfile.js
file instead of a gulpfile.js
.gruntfile.js
, rewrite the same tasks defined previously using the Grunt syntax.This is a good example of the Grunt-powered package.json
:
{ "version": "1.0.0", "name": "opengamelistwebapp", "private": true, "dependencies": { }, "devDependencies": { "grunt": "^0.4.5", "grunt-contrib-clean": "^1.0.0", "grunt-contrib-copy": "^1.0.0", "grunt-contrib-uglify": "^1.0.0", "grunt-contrib-watch": "^0.6.1", "grunt-ts": "^5.3.2", } }
And this is how the gruntfile.js
would look after porting all the Gulp tasks defined previously to Grunt syntax:
module.exports = function (grunt) { grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-ts'); grunt.initConfig({ clean: [ 'Scripts/app/*', 'Scripts/js/*' ], ts: { base: { src: [ 'Scripts/app/main.ts', 'Scripts/app/**/*.ts' ], outDir: 'wwwroot/ app', tsconfig: './tsconfig.json' } }, uglify: { my_target: { files: [{ expand: true, cwd: 'wwwroot/ app', src: ['**/*.js'], dest: 'wwwroot/ app' }] }, options: { sourceMap: true } }, // Copy all JS files from external libraries and required NPM packages to wwwroot/js copy: { main: { files: [{ expand: true, flatten: true, src: [ 'Scripts/js/**/*.js' ], dest: 'wwwroot/js/', filter: 'isFile' }] } }, // Watch specified files and define what to do upon file changes watch: { scripts: { files: [ 'Scripts/**/*.ts', 'Scripts/**/*.js', ], tasks: ['clean', 'ts ', 'uglify', 'copy'] } } }); // Global cleanup task grunt.registerTask('cleanup', ['clean']); // Define the default task so it will launch all other tasks grunt.registerTask('default', ['clean', 'ts', 'uglify', 'copy', 'watch']); };
As we can see, the syntax is very different but the underlying logic and features are quite similar, we still have a bunch of dedicated tasks to handle TypeScript files, minify actions, copy operations, and monitor content change, and then the default one to wrap everything up. Notice how, just like we did with Gulp, the TypeScript compiler options are fetched from the tsconfig.json
file so we don't have to write them a second time.
To learn more about Grunt and its configuration file syntax you can visit the official website at the following address: http://gruntjs.com/.
That's basically all we need to know about task runners. Too bad we don't yet have any TS and/or JS files to properly test what we just did! Don't worry, though, we'll get there soon enough. Before that, let's finish our NPM journey by bringing the last (but not least) of our players into the loop.
There are basically two ways to get Angular 2, both with their pros and cons, using NPM, which implies fetching the latest build and hosting the code within our project, or by a dedicated set of links to a suitable CDN.
To install Angular 2 using NPM we need to open the package.json
file and add a bunch of packages under the dependencies
node. This is how the file should look like at the end (Angular lines are highlighted):
{ "version": "1.0.0", "name": "OpenGameListWebApp", "private": true, "dependencies": { "@angular/common": "2.0.0-rc.5", "@angular/compiler": "2.0.0-rc.5", "@angular/core": "2.0.0-rc.5", "@angular/http": "2.0.0-rc.5", "@angular/platform-browser": "2.0.0-rc.5", "@angular/platform-browser-dynamic": "2.0.0-rc.5", "@angular/upgrade": "2.0.0-rc.5", "core-js": "^2.4.1", "reflect-metadata": "^0.1.3", "rxjs": "5.0.0-beta.6", "systemjs": "^0.19.37", "zone.js": "^0.6.12" }, "devDependencies": { "gulp": "^3.9.1", "gulp-clean": "^0.3.2", "gulp-concat": "^2.6.0", "gulp-sourcemaps": "^1.6.0", "gulp-typescript": "^2.13.6", "gulp-uglify": "^2.0.0", "typescript": "^1.8.10" } }
The packages starting with the @
symbol are part of the Angular 2 bundle, which, at the time of writing, reached its Release Candidate 5 (RC5) development stage. The other ones are a set of ES6 shims (core-js
), polyfills (reflect-metadata
), loading libraries (systemjs
), dependencies (rxjs
), and helper tools (zone.js
).
All of these packages are required for a number of good reasons:
As usual, all of these packages will be downloaded in the background by the IDE as soon as you Save the file. Eventually, you will have a local instance of Angular 2 available under the following folder:
<project_root>/node_modules/@angular/
It's worth noting that we have also added a new script
node that will execute a post-install command against the typings
package we just added.
Now we need to move these files to the wwwroot
folder. We can achieve this by adding a dedicated task to our Gulp configuration file as follows (new lines are highlighted):
/* This file in the main entry point for defining Gulp tasks and using Gulp plugins. Click here to learn more. http://go.microsoft.com/fwlink/?LinkId=518007 */ var gulp = require('gulp'), gp_clean = require('gulp-clean), gp_concat = require('gulp-concat'), gp_sourcemaps = require('gulp-sourcemaps'), gp_typescript = require('gulp-typescript'), gp_uglify = require('gulp-uglify'); /// Define paths var srcPaths = { app: ['Scripts/app/main.ts', 'Scripts/app/**/*.ts'], js: [ 'Scripts/js/**/*.js', 'node_modules/core-js/client/shim.min.js', 'node_modules/zone.js/dist/zone.js', 'node_modules/reflect-metadata/Reflect.js', 'node_modules/systemjs/dist/system.src.js', 'node_modules/typescript/lib/typescript.js' ], js_angular: [ 'node_modules/@angular/**' ], js_rxjs: [ 'node_modules/rxjs/**' ] }; var destPaths = { app: 'wwwroot/app/', js: 'wwwroot/js/', js_angular: 'wwwroot/js/@angular/', js_rxjs: 'wwwroot/js/rxjs/' }; // Compile, minify and create sourcemaps all TypeScript files and place them to wwwroot/app, together with their js.map files. gulp.task('app', ['app_clean'], function () { return gulp.src(srcPaths.app) .pipe(gp_sourcemaps.init()) .pipe(gp_typescript(require('./tsconfig.json').compilerOptions)) .pipe(gp_uglify({ mangle: false })) .pipe(gp_sourcemaps.write('/')) .pipe(gulp.dest(destPaths.app)); }); // Delete wwwroot/app contents gulp.task('app_clean', function () { return gulp.src(destPaths.app + "*", { read: false }) .pipe(gp_clean({ force: true })); }); // Copy all JS files from external libraries to wwwroot/js gulp.task('js', function () { gulp.src(srcPaths.js_angular) .pipe(gulp.dest(destPaths.js_angular)); gulp.src(srcPaths.js_rxjs) .pipe(gulp.dest(destPaths.js_rxjs)); return gulp.src(srcPaths.js) // .pipe(gp_uglify({ mangle: false })) // disable uglify // .pipe(gp_concat('all-js.min.js')) // disable concat .pipe(gulp.dest(destPaths.js)); }); // Delete wwwroot/js contents gulp.task('js_clean', function () { return gulp.src(destPaths.js + "*", { read: false }) .pipe(gp_clean({ force: true })); }); // Watch specified files and define what to do upon file changes gulp.task('watch', function () { gulp.watch([srcPaths.app, srcPaths.js], ['app', 'js']); }); // Global cleanup task gulp.task('cleanup', ['app_clean', 'js_clean']); // Define the default task so it will launch all other tasks gulp.task('default', ['app', 'js', 'watch']);
As you can see, it hasn't changed much, we just added a bunch of JS files that we need to copy from the /node_modules/
folder into the wwwroot/js/
folder, the same one we were already using to host third-party libraries. There's nothing odd there, NPM package files are external libraries, after all. For this very reason, it is also preferable to restrain ourselves from uglifying and/or concatenating them. It's also worth noting that we defined a separate, dedicated folder for Angular 2 and Rxjs packages, since both of them are spanned into multiple files.
Before moving forward, there is another thing we need to take care of. Since we plan to transpile our TypeScript code into ECMAScript5, we have added the core-js
NPM module into our package.json
file. In case you've never heard of it, let's just say that it happens to be a standard JavaScript library providing a great set of polyfills for ES6, which is precisely what we need.
If you would like to know more about the core-js
, here's the URL to the project's official GitHub repository:
https://github.com/zloirock/core-js.
The only problem is that it doesn't come with a proper TypeScript definition file, meaning that both of our TypeScript compilers, either Visual Studio's or Gulp's, won't be aware of its existence, thus throwing a fair amount of TS2304 (type not found) exceptions upon each build attempt.
The best thing we can do in order to fix that is to add the proper type definitions to our project. To do that, open the package.json
file again and add the following (new lines are highlighted):
{ "version": "1.0.0", "name": "opengamelistwebapp", "private": true, "dependencies": { "@angular/common": "2.0.0-rc.5", "@angular/compiler": "2.0.0-rc.5", "@angular/core": "2.0.0-rc.5", "@angular/http": "2.0.0-rc.5", "@angular/platform-browser": "2.0.0-rc.5", "@angular/platform-browser-dynamic": "2.0.0-rc.5", "@angular/upgrade": "2.0.0-rc.5", "core-js": "^2.4.1", "reflect-metadata": "^0.1.8", "rxjs": "5.0.0-beta.6", "systemjs": "^0.19.37", "typings": "^1.3.2", "zone.js": "^0.6.12" }, "devDependencies": { "gulp": "^3.9.1", "gulp-concat": "^2.6.0", "gulp-sourcemaps": "^1.6.0", "gulp-typescript": "^2.13.6", "gulp-uglify": "^2.0.0", "typescript": "^1.8.10" }, "scripts": { "postinstall": "typings install dt~core-js --global" } }
We can see that there are two new things here:
typings
, which is a TypeScript type definition manager. In other words, a tool we can use to retrieve type definitions from the web.script
key containing a small command that will be executed during the post-install phase. This basically means that the script will trigger every time we change something within the package.json
file, right after all the NPM modules are retrieved and installed (or removed).As soon as we Save our package.json
file, a new typings
folder will be added to our OpenGameListWebApp project's root, containing the type definition file we need.
If typings didn't install successfully during the Save, try to use the Restore Package option by right-clicking on the project's Dependencies node. Another way is to use the command line to install the typings explicitly. To do this, navigate to the root folder of your app and press CTRL+SHIFT, then select Open command window here. In the command line, type the following command: > npm run typings install That should do the trick.
At the time of writing, adding Angular 2 via NPM would produce the following TypeScript compilation error:
Invalid module name in augmentation, module '../../Observable' cannot be found.
This is due to a known bug in the TypeScript version currently shipped with Visual Studio 2015. The most effective way to fix that is to replace this local VS2015 file:
C:Program Files (x86)Microsoft Visual Studio 14.0Common7IDECommonExtensions
MicrosoftTypeScript ypescriptServices.js
.
With the following remote file:
https://raw.githubusercontent.com/Microsoft/TypeScript/Fix8518/lib/typescriptServices.js
.
Doing that will also fix a couple more errors we will most likely get in Chapter 3, Angular 2 Components and Client-Side Routing. Needless to say, it's highly advisable to make a backup of the original file before replacing it.
Additional information regarding the bug is available through the following URLs: https://github.com/Microsoft/TypeScript/issues/8518 and https://github.com/Microsoft/TypeScript/pull/7507.
It's time to lay down the first bricks of our app, a working skeleton, used as a starting point for what we're going to do in the next chapters. It will consist of:
Scripts/app/app.component.ts
Scripts/app/app.module.ts
Scripts/app/main.ts
wwwroot/systemjs.config.js
wwwroot/index.html
The component is the most basic and fundamental concept in Angular 2. Think of it like a class that controls a specific piece of a web page where we can either display some data to each user and/or respond to their feedbacks. We can say that our Angular 2 app will be almost entirely built upon multiple Components serving specific purposes-most of them will be reusable, others will be only used once. They can also either be as small as a few lines or they can result in a ridiculously long piece of code.
Luckily, our first component is quite simple. In the Solution Explorer, right-click on the /Scripts/app/
folder and add a new app.component.ts
file:
Once created, feed it with the following code:
import {Component} from "@angular/core"; @Component({ selector: "opengamelist", template: `<h1>OpenGameList</h1><div>Work in progress...</div>` }) export class AppComponent { }
That's it. Let's see what we just did here in detail:
@angular/core
. The Component function is what we need to define a Component's metadata for a class, which basically means being able to make Components, this is a required step for what we'll do next.@
symbol and invoking it just above the component class. In other words, the @Component
instruction tells Angular that this class is an Angular component. Notice that the selector
and template
fields are passed as a configuration object, as we will analyze them soon enough.AppComponent
class. Notice the export
keyword, which will allow us to import
it from other components. The act of exporting makes our app.component.js
file a module.Angular 2 is a modular framework: this means that Angular 2 apps are also modular, as they consist of many files dedicated to a specific purpose. Typically, most application files export a single Component
class, which is a class bundled with component metadata. Our app.component.js
file, for example, exports the AppComponent
class decorated with its component metadata, thus being a Module file exporting a Component class. We could also say that the Component is the content, while the Module is the file itself. We'll see how to import Components in the next chapter.
Angular Modules, also known as NgModules
, have been introduced in Angular 2 RC5, which is the latest version at the time of writing, and are a great and powerful way to organize and bootstrap any Angular 2 application. They help developers to consolidate their own set of components, directives, and pipes into reusable blocks.
Every Angular 2 application since RC5 must have at least one module, which is conventionally called the root module and given the AppModule
class name.
From the Solution Explorer, right-click on the /Scripts/app/
folder, add a new app.module.ts
file, and fill it with the following code:
///<reference path="../../typings/index.d.ts"/> import {NgModule} from "@angular/core"; import {BrowserModule} from "@angular/platform-browser"; import {HttpModule} from "@angular/http"; import "rxjs/Rx"; import {AppComponent} from "./app.component"; @NgModule({ // directives, components, and pipes declarations: [ AppComponent ], // modules imports: [ BrowserModule, HttpModule ], // providers providers: [ ], bootstrap: [ AppComponent ] }) export class AppModule { }
Again, let's take a look at what we just wrote:
rxjs
library definition file(s), which will be useful to compile the Angular 2 libraries.NgModule
. As we can see it consists in an array of named arrays, each one containing a set of Angular 2 objects that serves a common purpose: directives, components, pipes, modules, and providers. The last one of them contains the component(s) we want to bootstrap, which in our case is the AppComponent
one.Working with the root module only is a very viable approach until the Angular 2 app grows to a certain size. When it becomes bigger, it will be more practical to refactor it into a number of feature modules, each one of them grouping together a set of related tasks.
Now that we have our root module, we need to bootstrap it. From the Solution Explorer, right-click on the /Scripts/app/
folder and add a new main.ts
file, then fill it with the following code:
import {platformBrowserDynamic} from "@angular/platform-browser-dynamic"; import {AppModule} from "./app.module"; platformBrowserDynamic().bootstrapModule(AppModule);
Now we're just missing an entry point to load with the browser. Let's add it right now.
In this application, we're going to use the SystemJS module loader library to load our application and all the required Angular 2 modules and dependencies. In order to do that, we have to add a systemjs.config.js
file to the wwwroot
folder and define a number of configuration rules within it, as follows:
(function (global) { // map tells the System loader where to look for things var map = { 'app': 'app', // our application files '@angular': 'js/@angular', // angular2 packages 'rxjs': 'js/rxjs' // Rxjs package }; // packages tells the System loader which filename and/or extensions to look for by default (when none are specified) var packages = { 'app': { main: 'main.js', defaultExtension: 'js' }, 'rxjs': { defaultExtension: 'js' } }; // configure @angular packages var ngPackageNames = [ 'common', 'compiler', 'core', 'http', 'platform-browser', 'platform-browser-dynamic', 'upgrade', ]; function packIndex(pkgName) { packages['@angular/' + pkgName] = { main: 'index.js', defaultExtension: 'js' }; } function packUmd(pkgName) { packages['@angular/' + pkgName] = { main: '/bundles/' + pkgName + '.umd.js', defaultExtension: 'js' }; }; var setPackageConfig = System.packageWithIndex ? packIndex : packUmd; ngPackageNames.forEach(setPackageConfig); var config = { map: map, packages: packages } System.config(config); })(this);
The code is pretty much self-documented with inline comments, yet it could be useful to highlight the most relevant tasks:
/wwwroot/
: app
for our application; js/@angular
for Angular 2 and js/rxjs
for Rxjs.import
statement without specifying them.If we want to use a CDN instead of relying upon the local JS folder, we only need to perform a minor update within the previous code, in the map section, as follows:
var map = {
'app': 'app', // our application files
'@angular': 'js/@angular', // angular2 packages
'rxjs': 'https://npmcdn.com/[email protected]' // Rxjs package (CDN)
};
And also within the two module loader functions:
function packIndex(pkgName) { packages['https://npmcdn.com/' + pkgName] = { main: 'index.js', defaultExtension: 'js' }; }
function packUmd(pkgName) { packages['https://npmcdn.com/' + pkgName] = { main: '/bundles/' + pkgName + '.umd.js', defaultExtension: 'js' }; };
For further info regarding SystemJS and its Configuration API, including advanced options, we strongly suggest reading the official documentation on the project's GitHub page:
https://github.com/systemjs/systemjs and https://github.com/systemjs/systemjs/blob/master/docs/config-api.md.
Before going further, it might be useful to explain why we worked so hard with a module loader instead of adding all the relevant JS files into the index.html
file right from the start.
To keep it simple, we did it because it's the only way to efficiently handle any modern JavaScript modular system such as Angular 2
, Rxjs
and also all applications based upon them, including the one we're working on right now.
What's a modular system exactly? It's nothing more than a package, library, or application split into a series of smaller files which depend on each other using reference statements such as import
, require
, and so on. ASP.NET, Java, Python, and most compilation-based languages have it. That's not the case with script-based languages such as PHP and JavaScript: they that are doomed to pre-load everything in the memory before being able to determine whether they'll be using it or not. All of these changes, with the introduction of ECMAScript 6 (ES6), bring a fully-featured module and dependency management solution for JavaScript. SystemJS basically acts as an ES6-polyfill for browsers that don't support it already, allowing us to get that module system working in modern browsers. Since both Angular 2
and Rxjs
leverage that dynamic-loading approach, implementing it within our project will result in a huge performance gain.
The HTML file serves two main purposes: being an entry point for the browser so it can load the client-script files and execute the application, and laying out the DOM structure used by Angular 2 to display it. In the Solution Explorer, right-click on the wwwroot
folder and add a new index.html
file, then fill it with the following code:
<!DOCTYPE html> <html> <head> <base href="/"> <title>OpenGameList</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- Step 1. Load libraries --> <!-- Polyfill(s) for older browsers --> <script src="js/shim.min.js"></script> <script src="js/zone.js"></script> <script src="js/Reflect.js"></script> <script src="js/system.src.js"></script> <!-- Step 2. Configure SystemJS --> <script src="systemjs.config.js"></script> <script> System.import('app').catch(function(err){ console.error(err); }); </script> </head> <!-- Step 3. Display the application --> <body> <!-- Application PlaceHolder --> <opengamelist>Loading...</opengamelist> </body> </html>
After adding the index.html
file, we need to set it as the main entry point when executing our app in the Debug mode. In the Solution Explorer, right-click on the project node and select Properties, then switch to the Debug tab and change the Launch URL parameter accordingly.
If we want to use a CDN instead of local JS files, we can replace the <script>
elements right below the Step 1 comment with the following:
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.4.1/shim.min.js"></script> <script src="https://npmcdn.com/[email protected]"></script> <script src="https://npmcdn.com/[email protected]"></script> <script src="https://cdnjs.cloudflare.com/ajax/ libs/systemjs/0.19.37/system.js"></script>
These are the latest versions at the time of writing. Be sure to replace them with the most recent Angular 2-compatible versions.