Chapter 14. Deploying and Scaling the SPA

Having built the core functionality of the application, now it's time to move the SPA into a production-like environment that is accessible from the Internet. For this, we will be using Platform as a Service (PaaS).

PaaS is a type of a cloud-based service that allows developers to launch applications on managed infrastructure. Before PaaS, developers or operations engineers had to perform a lot of setup and maintenance tasks, such as provisioning hardware, installing operating software, and insuring uptime.

There are a number of PaaS providers, but I have chosen Heroku. One reason for this is that you can stand up an application for free on a sandbox, which will allow you to experiment on the app and scale up when you're ready. Deploying an app to Heroku is also quite easy, as, you'll see, Heroku uses Git to deploy.

We will also set up a production database in the cloud. We will use MongoLab, which also has a free sandbox tier with enough memory to get started.

We'll finish this chapter by briefly discussing the following concerns for scaling your application:

  • Packaging the application with the Grunt task runner
  • Setting up a production database online
  • Moving the SPA into the cloud
  • Considerations for scaling

Packaging for deployment

Our application is still quite small and not complicated, but we will begin by setting up an automated process for packaging our application up for deployment.

Setting up Grunt for deployment

We will use the Grunt JavaScript task runner to set up some automated tasks to package up our files for deployment. There's not a lot for us to do here, but you'll get a sense of what can be done and be able to explore the rich selection of Grunt plugins to further customize your automated tasks.

Installing Grunt

If you haven't already, install the grunt CLI using NPM:

$ npm install -g grunt-cli
[email protected] /usr/local/lib/node_modules/grunt-cli
|- [email protected]
|- [email protected] ([email protected])
|_ [email protected] ([email protected], [email protected])

For Grunt to run correctly, you'll need two files in your project root directory. The first one is a package.json file to declare dependencies. You already have one in your root directory. The next file you need is Gruntfile.js, where you will load grunt modules and configure the tasks that Grunt can run. Go ahead and create this file in your root directory and add the following code to it:

module.exports = function(grunt) { 
 
grunt.initConfig({ 
pkg: grunt.file.readJSON('package.json'), 
 
    }); 
 
}; 

This is the framework for Gruntfile. We export a function that expects to receive a reference to the grunt object as its argument. Inside that function, we call the grunt.initConfig() function, passing it a configuration object. Currently, that configuration object has a single property, that is, a reference to the package.json file.

The power of Grunt comes from employing any number of the thousands of plugins made available by its active community. At the time of writing this book, there were over 5,000 Grunt plugins listed at http://gruntjs.com/plugins. If there's some automated task you want to run, chances are that somebody's already created a plugin to support it.

Note

Grunt plugins, which are officially maintained, are always named grunt-contrib-X. You can generally trust the quality of these plugins, although there are many great unofficially maintained plugins.

Installing Grunt plugins

A nice feature of Grunt is that plugins are installed using NPM. Let's install a few useful plugins that we will use:

$ npm install grunt-contrib-clean--save-dev
[email protected]_modules/grunt-contrib-clean
|- [email protected]
|_ [email protected] ([email protected])
$ sudonpm install grunt-contrib-uglify--save-dev
[email protected]_modules/grunt-contrib-uglify
|- [email protected]
|- [email protected] ([email protected], [email protected], [email protected])
|- [email protected] ([email protected], [email protected], [email protected], [email protected], [email protected])
|- [email protected] ([email protected], [email protected], [email protected], [email protected])
|_ [email protected]
$ sudonpm install grunt-contrib-htmlmin--save-dev
[email protected]_modules/grunt-contrib-htmlmin
|- [email protected] ([email protected], [email protected], [email protected], [email protected], [email protected])
|- [email protected] ([email protected], [email protected], [email protected])
|_ [email protected] ([email protected], [email protected], [email protected], [email protected], [email protected], [email protected])
$ sudonpm install grunt-contrib-copy--save-dev
[email protected]_modules/grunt-contrib-copy
|- [email protected]
|_ [email protected] ([email protected], [email protected], [email protected], [email protected], [email protected])

We installed Grunt plugins for clean, uglify, htmlmin, and copy tasks. Clean will clean files out of a directory. Uglify minimizes JavaScript files. Htmlmin minifies HTML files. The Copy task copies files. The --save-dev flag will add these modules to your package.json file as devdependecies. You need these packages only in your development environment, not in your production environment.

Before we go any further, let's create a dist folder in the root of our project. This is where our production-ready assets will be found.

Configuring the Gruntfile

Now, we need to modify our Gruntfile to load the plugins:

module.exports = function(grunt) { 
 
grunt.initConfig({ 
pkg: grunt.file.readJSON('package.json'), 
 
    }); 
 
    //load the task plugins
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-htmlmin');
grunt.loadNpmTasks('grunt-contrib-clean'); 
 
}; 

Here, we use a call to grunt.loadNPMTasks() for each Grunt plugin we want to load, passing it the name of the module to be loaded.

Next, we need to configure each of our tasks inside our Gruntfile. Note that every plugin will have its own configuration properties. Consult the documentation for each plugin you use to see how it is configured. Open up your Gruntfile.js and make the following edits:

module.exports = function(grunt) { 
 
grunt.initConfig({ 
pkg: grunt.file.readJSON('package.json'), 
        clean: ['dist/**'],

        copy: {

            main: {

                files: [

                    {expand: true, src: ['*'], dest: 'dist/',
                     filter: 'isFile'},
 
                    {expand: true, src: ['bin/**'], dest:
                    'dist/', filter:
                     'isFile'},

                    {expand: true, src: ['config/**'], dest:
                    'dist/', filter:  
                     'isFile'},

                    {expand: true, src: ['models/**'], dest:
                    'dist/', filter:
                     'isFile'},

                    {expand: true, src: ['passport/**'], dest:
                    'dist/', filter:'isFile'},

                    {expand: true, src: ['public/**'], dest: 
                   'dist/', filter:'isFile'},

                    {expand: true, src: ['routes/**'], dest:
                    'dist/', filter: 'isFile'},

                    {expand: true, src: ['scripts/**'], dest:
                    'dist/', filter: 'isFile'},

                    {expand: true, src: ['utils/**'], dest:
                    'dist/', filter:'isFile'},

                    {expand: true, src: ['views/**'], dest: 
                     'dist/', filter: 
                     'isFile'}

 
               ]

            }

        },

uglify: {
 
           options: {

                mangle: false

            },

my_target: {

                files: {

'dist/public/javascripts/giftapp.js': ['dist/public/javascripts/giftapp.js'],

'dist/public/javascripts/controllers/dashMainController.js': ['dist/public/javascripts/controllers/dashMainController.js'],

'dist/public/javascripts/controllers/giftappFormController.js': ['dist/public/javascripts/controllers/giftappFormController.js'],

'dist/public/javascripts/services/giftlistFactory.js': ['dist/public/javascripts/services/giftlistFactory.js']

                }

            }

        },

htmlmin:{

            options: {

removeComments: true,

colapseWhitespace: true

            },

dist: {

                files: {

'dist/public/templates/dash-add.tpl.html': 'dist/public/templates/dash-add.tpl.html',

'dist/public/templates/dash-main.tpl.html': 'dist/public/templates/dash-main.tpl.html'

                }

            }

        } 
    }); 
 
    //load the task plugins

grunt.loadNpmTasks('grunt-contrib-uglify');

grunt.loadNpmTasks('grunt-contrib-copy');

grunt.loadNpmTasks('grunt-contrib-htmlmin');

grunt.loadNpmTasks('grunt-contrib-clean');

    //register the default task

grunt.registerTask('default', ['clean','copy','uglify','htmlmin']); 
 
}; 

The first change we made was adding a number of task configuration properties inside the grunt.InitConfig() function. Each of these properties decorates the grunt object when grunt is run and tells the various tasks how to execute.

The first task configuration is for clean. This task is configured to delete all the files and folders in the dist folder. The clean configuration takes an array of paths; the syntax of the path definition is pretty standard for the grunt-contrib plugins. For more information on Grunt's URL globbing, refer to http://gruntjs.com/configuring-tasks#globbing-patterns.

The other task configurations are similar, but take objects, and can include some options, a target, and lists of files to operate on. For configuration options for grunt plugins, find the plugin you're interested in at http://gruntjs.com/plugins and click on the name of the plugin to get the documentation.

The next section after the configuration is where we load each plugin that will be used by this Gruntfile. We do this by passing the name of the plugin as an argument to the grunt.loadNPMTasks() function. Grunt will look for these plugins in our node_modules folder. If we were to use custom tasks, such as the one we wrote ourselves, we could load them using calls to grunt.loadTasks(), passing in a path.

The last thing we did was to register a task. We did this by calling grunt.registerTask(). This took two arguments. The first is a string, the name of the task. In our case, this is the default task. Grunt requires that all Gruntfiles register a default task. The next argument is an array of strings containing the name of any tasks and targets required to run as part of this task.

Right now, we are just running tasks without listing any individual targets. If we had targets we wished to run on the tasks, the syntax would be task:target. For example, if we defined a test target for our uglify task, we would register it in our array as ['uglify:test'].

Running Grunt

Running grunt couldn't be simpler.

First, ensure that the grunt CLI is installed, as shown in the following:

$ npm install -g grunt-cli
Password:
/usr/local/bin/grunt -> /usr/local/lib/node_modules/grunt-cli/bin/grunt
[email protected] /usr/local/lib/node_modules/grunt-cli
|- [email protected]
|- [email protected] ([email protected])
|- [email protected]
|_ [email protected] ([email protected])

From the directory where your Gruntfile lives, simply run grunt followed by the name of the task you wish to run. To run the default task, you can omit the taskname. Now, let's try running grunt:

$ grunt
Running "clean:0" (clean) task
>> 53 paths cleaned.
Running "copy:main" (copy) task
Copied 35 files
Running "uglify:my_target" (uglify) task
>> 4 files created.
Running "htmlmin:dist" (htmlmin) task
Minified 2 files

If you look in your dist folder now, you'll notice it's no longer empty. Grunt has cleaned it, moved a bunch of files in, and minified some things. Note that the first time you run this with an empty dist folder, the clean task will report 0 paths cleaned. When you run this subsequently, you should see the number of files in the dist folder actually being cleaned.

Another thing you might notice is that each task is running a target. Copy is running main, uglify is running my_target. By default, if no target is specified, Grunt will run the first-defined target.

If you open up your dist/public/javascripts/giftapp.js file, you should see that it has been minified:

angular.module("giftapp",["ui.router","giftappControllers"]).config(["$stateProvider","$urlRouterProvider",function($stateProvider,$urlRouterProvider){$urlRouterProvider.otherwise("/dash"),$stateProvider.state("dash",{url:"/dash",templateUrl:"/templates/dash-main.tpl.html",controller:"DashMainController"}).state("add",{url:"/add",templateUrl:"/templates/dash-add.tpl.html",controller:"GiftappFormController"})}]); 

Code minification makes our files smaller and somewhat harder to read. It can improve the files' performance on the web significantly. For a more significant performance improvement, we might have to look into concatenating script files and use tools such as the Closure compiler to make them even more efficient.

Note

There is no need to minify server-side JavaScript code. The main reason for minification is reduced data transfer with a client.

Setting up our config for production

One issue we're going to run into when moving our application into a production environment is that there will be a difference between our development and our production environment. Right now, all our database references point to our local MongoDB database.

We're going to use Git to push our files to production later on, and we also don't want to store configuration variables in Git repositories. We also don't want to store node_modules in Git or push them to production environment since they can be fetched on the fly using our package.json file.

Create a .gitignore file

In the root of your project, create a file called .gitignore. This file contains a list of files and paths that we don't want Git to store or track:

node_modules 
config 
.idea 
dist/config 

Line by line we just list the files and folders we want Git to ignore. The first is node_modules. Again, there's no reason to store these. I then want to ignore anything in my config folder, which contains sensitive information.

In here, I ignore .idea. You may or may not have this folder. This is a folder created by my development environment to store project information. I'm using JetBrains IDE for JavaScript called Webstorm. Whatever you're using, you'll want to exclude your IDE files, if any. Finally, I explicitly exclude dist/config, which will be a copy of config.

Create an environment-based configuration module

What we want is for the configuration to be handled dynamically. If you're in the development environment, use our configuration for your local machine. If you're in the production environment, you would want to use appropriate configuration variables for that environment.

The safest way to do that in the production environment setting up environment variables that can be read in the application. We will set them up when we set up our deployment environment, but we can set the stage now.

In your root giftapp folder, create a new file called appconfig.js, using the following code:

module.exports = function(){ 
    if(process.env.NODE_ENV&&process.env.NODE_ENV === 'production'){ 
        return{ 
db: process.env.DB, 
facebookAuth : { 
clientID: process.env.facebookClientID, 
clientSecret: process.env.facebookClientSecret, 
callbackURL: process.env.facebookCallbackURL, 
            }, 
 
twitterAuth : { 
'consumerKey': process.env.twitterConsumerKey, 
'consumerSecret': process.env.twitterConsumerSecret, 
'callbackURL': process.env.twitterCallbackURL 
            } 
        } 
    } else { 
varauth = require('./config/authorization'); 
        return { 
db: 'localhost:27017/giftapp', 
facebookAuth : { 
clientID: auth.facebookAuth.clientID, 
clientSecret: auth.facebookAuth.clientSecret, 
callbackURL: auth.facebookAuth.callbackURL 
            }, 
 
twitterAuth : { 
'consumerKey': auth.twitterAuth.consumerKey, 
'consumerSecret': auth.twitterAuth.consumerSecret, 
'callbackURL': auth.twitterAuth.callbackURL 
            } 
        } 
    } 
 
}; 

We first check to see whether there is a NODE_ENV environment variable and whether it is set to production. If it is, we will have to grab our database and our Facebook and Twitter authorization information from environment variables. We will set up our environment variables later on when we set up our deployment environment.

If our test fails, we assume we're in our development environment and then manually set our database. We grab our authorization.js file out of the config directory and use that to set up our Twitter and Facebook authorization variables.

Using the new config file

Now, we need to employ our configuration file. Open up your main app.js file and make a couple of edits:

var express = require('express'); 
var path = require('path'); 
var favicon = require('serve-favicon'); 
var logger = require('morgan'); 
varcookieParser = require('cookie-parser'); 
varbodyParser = require('body-parser'); 
varisJSON = require('./utils/json'); 
var routing = require('resource-routing'); 
var controllers = path.resolve('./controllers'); 
var helmet = require('helmet'); 
varcsrf = require('csurf'); 
varappconfig = require('./appconfig');
varconfig = appconfig(); 
 
//Database stuff 
varmongodb = require('mongodb'); 
var monk = require('monk'); 
vardb = monk(config.db); 
 
var mongoose = require('mongoose'); 
mongoose.connect(config.db); 
 
var routes = require('./routes/index'); 
var users = require('./routes/users'); 
var dashboard = require('./routes/dashboard'); 
varauth = require('./routes/auth') 
 
var app = express(); 
 
// view engine setup 
app.set('views', path.join(__dirname, 'views')); 
app.set('view engine', 'ejs'); 
 
app.set('x-powered-by', false); 
 
app.locals.appName = "My Gift App"; 
 
// uncomment after placing your favicon in /public 
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 
app.use(logger('dev')); 
app.use(bodyParser.json()); 
app.use(bodyParser.urlencoded({ extended: false })); 
app.use(cookieParser()); 
app.use(express.static(path.join(__dirname, 'public'))); 
app.use(isJSON); 
 
var flash = require('connect-flash'); 
app.use(flash()); 
 
var passport = require('passport'); 
varexpressSession = require('express-session'); 
app.use(expressSession({secret: 'mySecretKey'})); 
app.use(passport.initialize()); 
app.use(passport.session()); 
 
varinitializePassport = require('./passport/init'); 
initializePassport(passport); 
 
//Database middleware 
app.use(function(req,res,next){ 
req.db = db; 
    next(); 
}); 
 
app.use(helmet()); 
app.use(csrf()); 
 
app.use('/', routes); 
app.use('/users', users); 
app.use('/dash', dashboard); 
app.use('/auth', auth); 
 
 
var login = require('./routes/login')(passport); 
app.use('/login', login); 
 
routing.resources(app, controllers, "giftlist"); 
routing.expose_routing_table(app, { at: "/my-routes" }); 
 
// catch 404 and forward to error handler 
app.use(function(req, res, next) { 
var err = new Error('Not Found'); 
err.status = 404; 
  next(err); 
}); 
 
// error handlers 
 
// development error handler 
// will print stacktrace 
if (app.get('env') === 'development') { 
app.use(function(err, req, res, next) { 
res.status(err.status || 500); 
res.render('error', { 
      message: err.message, 
      error: err 
    }); 
  }); 
} 
 
// production error handler 
// no stacktraces leaked to user 
app.use(function(err, req, res, next) { 
res.status(err.status || 500); 
res.render('error', { 
    message: err.message, 
    error: {} 
  }); 
}); 
 
 
module.exports = app; 

First, we load our appconfig.js file and assign it to the variable appconfig. Remember, our appconfig module exports a function. We need to invoke that function to run the code and get access to the dynamically set properties. So, we invoke appconnfig() and assign the returned object to the variable config.

Finally, we use config.db in the call to monk() to create the database object. You should now be able to start up your database and server, and there should be no difference in the functionalities.

Next, we need to use the appconfig in our passport OAuth strategies. Let's start with passport/facebook.js:

varFacebookStrategy = require('passport-facebook').Strategy; 
var User = require('../models/user'); 
varappconfig = require('../appconfig')
varauth = appconfig(); 
 
module.exports = function(passport){ 
 
passport.use('facebook', new FacebookStrategy({ 
clientID: auth.facebookAuth.clientID, 
clientSecret: auth.facebookAuth.clientSecret, 
callbackURL: auth.facebookAuth.callbackURL, 
profileFields: ['id', 'displayName', 'email'] 
        }, 
        function(accessToken, refreshToken, profile, cb) { 
User.findOne({ 'facebook.id': profile.id }, function (err, user) { 
                if(err){ 
                    return cb(err) 
                } else if (user) { 
                    return cb(null, user); 
                } else { 
                    for(key in profile){ 
                        if(profile.hasOwnProperty(key)){ 
console.log(key + " ->" + profile[key]); 
                        } 
                    } 
var newUser = new User(); 
newUser.facebook.id = profile.id; 
newUser.facebook.token = accessToken; 
newUser.facebook.name = profile.displayName; 
                    if(profile.emails){ 
newUser.email = profile.emails[0].value; 
                    } 
 
newUser.save(function(err){ 
                        if(err){ 
                            throw err; 
                        }else{ 
                            return cb(null, newUser); 
                        } 
                    }); 
                } 
            }); 
        } 
    )); 
} 

Once again, we require appconfig.js from the root of our application. We then invoke the returned function and assign it to the variable auth. We should require no additional changes, and restarting our server should show that our changes have worked.

Finally, let's do the same thing to our passport/twitter.js file:

varTwitterStrategy = require('passport-twitter').Strategy; 
var User = require('../models/user'); 
varappconfig = require('../appconfig')
varauth = appconfig(); 
 
module.exports = function(passport){ 
 
passport.use('twitter', new TwitterStrategy({ 
consumerKey     : auth.twitterAuth.consumerKey, 
consumerSecret  : auth.twitterAuth.consumerSecret, 
callbackURL     : auth.twitterAuth.callbackURL 
        }, 
        function(token, tokenSecret, profile, cb) { 
User.findOne({ 'twitter.id': profile.id }, function (err, user) { 
                if(err){ 
                    return cb(err) 
                } else if (user) { 
                    return cb(null, user); 
                } else { 
                    // if there is no user, create them 
var newUser                 = new User(); 
 
                    // set all of the user data that we need 
newUser.twitter.id          = profile.id; 
newUser.twitter.token       = token; 
newUser.twitter.username    = profile.username; 
newUser.twitter.displayName = profile.displayName; 
 
newUser.save(function(err){ 
                        if(err){ 
                            throw err; 
                        }else{ 
                            return cb(null, newUser); 
                        } 
                    }); 
                } 
            }); 
        } 
    )); 
} 

As you can see, we've made exactly the same change to the Twitter authorization strategy file. Once again, give it a test and it should work exactly the same way.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.144.216