Any developer worth their salt will of course be playing around with Visual Studio 2015, and those of us that are interested in web development would be of course trying out the new ASP vNext capabilities.
One thing that hit me straight away was that Microsoft have indeed stuck by their promises to embrace more "open source" works with ASP vNext. As such any new ASP vNext you do you will likely have to learn these things
- NPM (Node package management system)
- Bower (Client library package management system (requires node,js NPM))
- Either Gulp / Grunt, for automating repetitive tasks
- NuGet (for managing .NET package dependencies)
NPM and Bower are extremely easy to work with, so you should not fear them. As for NuGet, well if you are a .NET developer you have more than likely used NuGet before, nothing new there.
Me personally I had used Node and NPM before, I had not used Bower, but was comfortable with that within a day. As for Gulp / Grunt, I had obviously heard of these tools but had not used them. I looked at both and actually preferred the syntax of Grunt over Gulp. Both of these are essentially trying to do the same thing, which is they are both task runners. Where a task may be something that you want to do all the time, such as watch some files, and if they change do some action, minify content, move content from one folder to another etc etc. These are mundane tasks that can be automated. This is what Gulp / Grunt are all about, that is what a task runner is.
One thing that Gulp claims to have done better is to stream files, such that they can be done asynchronously. That said I just felt more at home working with Grunt, so I decided to use that. One thing of note is that with ASP vNext you may use either Grunt or Gulp.
So decided to learn about Grunt, and just thought I would share my findings here with any of you that may be in the same ship as me (ie a beginner with a JavaScript task runner).
The best place to get started with Grunt, is by actually going to the Grunt web site, where they are many useful guidelines to get you started. However the following sections should hopefully at least get you a little more familiar with Grunt, and by reading this and the Grunt resources at the Grunt web site you should be able to get up and running with Grunt pretty easily.
As usual you can grab the code from github account : https://github.com/sachabarber/SmallGruntDemo
You should ensure that you do this before you carry on with the rest of the article
Installing Grunt
In order to install Grunt there are a couple of things you MUST do. These are outlined below.
- Install Node (as we need NPM to run Grunt. So we need Node as NPM needs Node)
- From the "Node.js command line" run this command "npm install -g grunt". This installs Grunt itself globally
- From the "Node.js command line" run this command "npm install -g grunt-cli". This installs the Grunt client globally
As I have already state Grunt is a task runner. It is written in JavaScript and has a configuration file (gruntfile.js) that details the tasks that need to be run. It is a bit like MSBuild / NANT in the fact that there is a default task.
Tasks can also call other tasks. Grunt also relies on NPM to install certain Grunt plugins.
Huh, what is a Grunt plugin? Well a Grunt plugin, is simple a prebuilt Grunt task that has either been authored by the Grunt team or the Grunt community.
The official Grunt team plugins all follow the naming conventions of "grunt-contrib-XXXXXX". That way you can tell the "Official" packages from the non official packages.
These Grunt plugins (that are actually installed by using NPM) need to get loaded into the Grunt context. This is done using the following sort of line:
grunt.loadNpmTasks('grunt-contrib-clean');
Once a plugin is loaded it may be used by Grunt. Here is an example of how to use the loaded Grunt plugin, where it can be seen that the plugin has a name, and is a JSON object. Each plugin has its own API, so you will need to check the API documenation for the Grunt plugin you choose to use.
clean: {
dist: [
'dist/**/*'
],
tmp: [
'tmp/**/*'
]
},
Ok so now on to what the demo app does. The demo app is a very simple VS2013 solution that does the following things:
- Will use the Typescript Greeter (hello world example in TypeScript) example
- Will use jquery as an external module, and we will use the ambient type declarations for jquery for TypeScript in order to get the require/export package working as we need
- Willl use grunt-browserify
- Will use grunt-contrib-clean to clean certain directories
- Will use grunt-ts to compile TypeScript files (Note that Visual Studio has excellent support for TypeScript compilation, but I just wanted to do everything I could using Grunt to prove something to myself)
- Will use grunt-browserify to package the javascript source files to a single output file
- Will use grunt-contrib-watch to watch for changes in source files, and run tasks when these files change
- Will use grunt-contrib-uglify to minify the javascript source files (if the Grunt file task was the "
prod
" task) to a single output file
You need to carry out these steps if you want to work with the demo code.
As is the case with NuGet where you would not submit the packages themselves to the source control repository (github for example). So it is with NPM, you would check in a configuration file (package.json
) that tell NPM what packages are needed, from there each end user would be expected to install the packages locally using NPM.
You should now ensure you do this:
From Node.js command line, Change to directory which has package.json in it, and run the following command line
"npm install". This will fetch all the packages (from the internet, so make sure you have a connection) using the information contained within your package.json
file. You should end up with something like this, where the "node_modules" directory will contain all the downloaded packages.
The project structure that we are using for this small demo app looks like this
- The src folder holds our source files (which for this demo app are all TypeScript)
- The tmp folder will hold the transpiled TypeScript -> JavaScript files
- The dist folder will hold the final (perhaps minified) distributable single file app.js, which will be referenced by Index.html
It can also be seen that this is where the NPM package.json
and Grunt gruntfile.js
files live.
Here is the complete demo app Grunt gruntfile.js
file.
module.exports = function (grunt) {
'use strict';
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-ts');
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
clean: {
dist: [
'dist/**/*'
],
tmp: [
'tmp/**/*'
]
},
ts: {
options: {
fast: 'never',
target: 'es5',
module: 'commonjs',
sourceMap: true
},
index: {
src: ['./src/**/*.ts'],
outDir: './tmp'
}
},
browserify: {
'./dist/app.js': ['./tmp/*.js', './src/Jquery/jquery.js']
},
uglify: {
my_target: {
options: {
sourceMap: 'dist/<%= pkg.name %/>.map',
sourceMapRoot: '..',
sourceMappingURL: '/<%= pkg.name %/>.map'
},
files: {
'./dist/app.js': ['./dist/app.js']
}
}
},
watch: {
scripts: {
files: ['./src/**/*.ts'],
tasks: ['dev'],
options: {
event: ['all'],
}
}
}
});
grunt.registerTask('default', ['dev']);
grunt.registerTask('prod', [
'clean',
'ts:index',
'browserify',
'uglify'
]);
grunt.registerTask('dev', [
'clean',
'ts:index',
'browserify'
]);
};
As you can see the gruntfile.js
is made up of a number of tasks, each with its own section. We will look at each of these in turn below.
This section will dive into each of the demo apps Grunt tasks in more detail.
The "default" task is what would be called if you ran the "grunt" command line from the directory that contains the gruntfile.js
file. It simply runs the following other tasks in order
- clean
- ts:index
- browserify
We will talk more about these tasks later
The "dev" task is what would be called if you ran the "grunt dev" command line from the directory that contains the gruntfile.js
file. It is intended to be run when you are developing an app, as such no minification of the code is performed. It simply runs the following other tasks in order
- clean
- ts:index
- browserify
We will talk more about these tasks later
The "prod" task is what would be called if you ran the "grunt prod" command line from the directory that contains the gruntfile.js
file. It is intended to be run when you are about to deploy an app, as such minification of the code IS performed. It simply runs the following other tasks in order
- clean
- ts:index
- browserify
- Uglify
We will talk more about these tasks later
The "watch" task makes use of the following NPM package, and its sole job is to monitor a set of input files for changes, and then run x-many other tasks.
In order to initiate this watching of the files you MUST issue the following Grunt command line "grunt watch". Here is the relevant section of the Grunt gruntfile.js
module.exports = function (grunt) {
'use strict';
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
watch: {
scripts: {
files: ['./src/**/*.ts'],
tasks: ['dev'],
options: {
event: ['all'],
},
},
},
});
};
It can seen that this task monitors the files in the src folder and will run the "dev" task when any event (such as add, change, delete, rename) of these files occur.
The "clean" task makes use of the following NPM package, and its sole job is to clean directories
Here is the relevant section of the Grunt gruntfile.js
module.exports = function (grunt) {
'use strict';
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
clean: {
dist: [
'dist/**/*'
],
tmp: [
'tmp/**/*'
]
},
});
};
It can seen that this task cleans a set of files contained with the targets specified by the JSON objects properties. Like I said EVERY Grunt task will be different, so you MUST consult the Grunt tasks API docs.
As I have stated the demo app uses TypeScript. Now Visual Studio has excellent support for TypeScript right in the box. You can see that in the following screen shot.
Thing was I wanted to try out Grunt, so I decided to use a Grunt task for doing the TypeScript stuff too. The TypeScript files are pretty simple, but this is what we are looking to achieve:
- Use TypeScript
- Use external modules (which for the demo app is jQuery). In order to use jQuery as an external module you need to get the ambient TypeScript type definitions for it. Luckily there is a great project for all sorts of JavaScript libraries (that have been done by the community) to expose the TypeScript typings. You can check that out here : http://definitelytyped.org/
So once we have some TypeScript in place and we are able to use jQuery as an external module as follows:
import Foo = require('./foo');
import $ = require('jquery');
$(document).ready(function () {
.....
.....
});
We are then able to look at the actual Grunt task. As I say Visual Studio and the TypeScript compiler (tsc.exe) can do everything you are about to see in the Grunt task, but I just wanted to test my Grunt know how, so decided to use Grunt tasks to do the TypeScript compilation too.
The "ts" task makes use of the following NPM package, which is responsible for transpiling the TypeScript into JavaScript
Here is the relevant section of the Grunt gruntfile.js
module.exports = function (grunt) {
'use strict';
grunt.loadNpmTasks('grunt-ts');
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
ts: {
options: {
fast: 'never',
target: 'es5',
module: 'commonjs',
sourceMap: true
},
index: {
src: ['./src/**/*.ts'],
outDir: './tmp'
}
},
});
};
It can be see that the grunt-ts module (Grunt task really) allows us to specify exactly the same sort of things as we did using the TypeScript tab in Visual Studio. For example the above Grunt task does the following:
- Tells the TypeScript to target ecmascript 5
- Tells the TypeScript to use Common.js style packages (not AMD)
- Tells the TypeScript to create source maps
- Tells the TypeScript to what source file to use for the transpiling
- Tells the TypeScript where to place the output files
The "browserify" task makes use of the following NPM package, and its sole job is to deal with packaging up the source files into a distributable package.
Here is the relevant section of the Grunt gruntfile.js
module.exports = function (grunt) {
'use strict';
grunt.loadNpmTasks('grunt-browserify');
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
browserify: {
'./dist/app.js': ['./tmp/*.js', './src/Jquery/jquery.js']
},
});
};
It can be seen that this task takes several disparate source(s) and produced a final output file in the dist folder.
The "uglify" task makes use of the following NPM package, and its sole job is to minify the source file(s) and produce a single minified output file
Here is the relevant section of the Grunt gruntfile.js
module.exports = function (grunt) {
'use strict';
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
uglify: {
my_target: {
options: {
sourceMap: 'dist/<%= pkg.name %>.map',
sourceMapRoot: '..',
sourceMappingURL: '<%= pkg.name %>.map'
},
files: {
'./dist/app.js': ['./dist/app.js']
}
}
},
});
}
The <%= pkg.name %>
will be replaced by the name of the current file. So app in this case.
Here is what the resulting app.js file looks like when it is finished being outputted by the "uglify" task. See how it is also minified.
Anyway that is all I wanted to say this time. As always if you liked what you saw, you can always leave a vote/comment they are always appreciated