Angular, webpack and gulp for an SPA: Part I

The latest hotness in front-end packing has arrived. Nope, it’s not browserify. I’m talking about Webpack.

I was first alerted to Webpack by Pete Hunt’s very cool talk on optimizing frontend performance at Instagram. Highly recommended.

Anyways, inspired, I decided to refactor an existing app of mine. This is a moderately-complex Angular Single Page App (“SPA”) (check it out here at crosslinks3.mit.edu).  I wanted to:

  1. Make the codebase more modular, because good architecture makes me happy
  2. Have the ability to lazy load modules
  3. Roll with a sane build system

Enter Webpack. It’s still relatively young in the game and there isn’t much good documentation. Perhaps I’m just dense, but Webpack’s docs reminds me of Angular’s in the early days, and those could (and did) make grown men weep.

This series takes you through the process of getting set up with Webpack and Gulp (Part I, what you’re reading now), modifying your Angular files to be webpack’able (Part 2) and optimizing fun (Part 3). I assume you know Angular, appreciate the advantages of modularizing, and already use a build system. I’m also focusing on SPAs.

Let’s jump right in. First off, starting webpack. You can use webpack’s own watch and build process, but I went with Gulp because 1) I love Gulp and 2) it’s so simple. I’ve simplified the gulpfile.js below, but the core concept is this: you use Gulp to monitor and do all your typical build tasks — webpack’ing being of them.

Let’s suppose your directory structure looks like this:

StarTrekApp
  - app/
    - bower_components/
    - modules/
       - index.js (this is your "entry" file referenced in the gulpfile.js)
    - index.html
  - dist/
  - node_modules/
  - package.json
  - gulpfile.js

 You’ll need the gulp-webpack plugin:

npm install --save-dev gulp-webpack

Set up your gulpfile.js file like so:

var gulp = require('gulp');
var path = require('path');
var gulpWebpack = require('gulp-webpack');
var $ = require('gulp-load-plugins');

// This is a relatively simple config. We'll add more stuff later.
var webpackConfig = {
    debug: true,
    watch: true,
    // this is the "entry" file of your app. Think of it as your Angular "app.js" file.
    entry: "./app/modules/index.js",     
    // this is the will-be-outputted file you'll reference in your index.html file
    output: {
        filename: "bundle.js",          
    },
    module: {
        loaders: [
           // nothing here yet! We'll add more stuff in Part 2
        ]
    },
    resolve: {
        // for illustration purposes, let's use lodash. 
        // this tells Webpack where actually to find lodash because you'll need it in the ProvidePlugin
        alias: {
            lodash: path.resolve( __dirname, './node_modules/lodash-node/modern'),   
        }
    },
    plugins: [
        // this tells Webpack to provide the "_" variable globally in all your app files as lodash.
        new webpack.ProvidePlugin({         
            _: "lodash",
        })
    ]
};

// this tells gulp to take the index.js file and send it to Webpack along with the config and put the resulting files in dist/
gulp.task("webpack", function() {
    return gulp.src('app/modules/index.js')
    .pipe( gulpWebpack(webpackConfig, webpack) )
    .pipe(gulp.dest('dist/'))
});

gulp.task("copyIndex", function() {
   return gulp.src('app/index.html')
   .pipe(gulp.dest('dist/'));
});

// this should look familiar: start the server
gulp.task('serve', ['connect'], function () {
        require('opn')('http://localhost:9000');
});

// this is a somewhat fancy bit of URL rewriting to make the SPA 
// basically, it rewrites all requests so that the server sends the index page
// and lets the angular client-side routing take over
gulp.task('connect', function () {
        var connect = require('connect');
        var app = connect()
                .use(require('connect-livereload')({ port: 35729 }))
                .use(require('connect-modrewrite')([
                        '!(\\..+)$ / [L]',
                ]))
                .use(connect.static('dist'))
                .use(connect.directory('dist'));

        require('http').createServer(app)
                .listen(9000)
                .on('listening', function () {
                        console.log('Started connect web server on http://localhost:9000');
                });
});

// another familiar task: gulp is watching the dist/ folder for changes - whenever the dist/ changes, gulp reloads the page
gulp.task('watch', ['connect', 'serve'], function () {
        var server = $.livereload();

        // watch for changes
        gulp.watch([
                'dist/bundle.js',
                'dist/index.html'
        ]).on('change', function (file) {
                server.changed(file.path);
        });

        // run webpack whenever the source files changes
        // this next set of watches tells gulp to run webpack 
        // whenever the source files change and copy the new index html over
        gulp.watch('app/modules/**/*', ['webpack']);
        gulp.watch('app/index.html', ['copyIndex']);
});

// this tells gulp to combine my Angular dependencies and to output the vendor.js file into the dist/ folder
gulp.task("vendor", function() {
        return gulp.src([
                'app/bower_components/angular-ui-router/release/angular-ui-router.min.js',
                'app/bower_components/angular/angular.min.js',
                'app/bower_components/angular-animate/angular-animate.min.js',
        ])
        .pipe( $.order([
                'angular/angular.min.js',
                'angular-ui-router/release/angular-ui-router.min.js',
                'angular-animate/angular-animate.min.js',
        ], {base: './app/bower_components'}))
        .pipe( $.concat('vendor.js'))
        .pipe( $.size() )
        .pipe(gulp.dest('dist/'))
});

You may asking yourself why we don’t just do the same thing for Angular with what we did for lodash – tell Webpack to provide it in the ProvidePlugin. The answer is that Angular doesn’t come in CommonJS modules, so it’ll break and you’ll get an “angular function undefined” error. Oh well.

Let’s make a simple index.js file for now. Remember, this is like your Angular app.js file.

// define our app here. 
var startrekApp = angular.module("startrekApp", []);

Finally, your index.html file will need to reference the webpack’ed bundle:

<html>
   <body ng-app="startrekApp">
 
       <h1> Welcome to the Star Trek angular app </h1>

       <script src="vendor.js"></script>
       <script src="bundle.js"></script>
   </body>
</html>

So there we go. We have a build system set up that’s running on Gulp, watching any changes in our source files, feeding our Javascript through webpack when our files change, and serving the dist/ on localhost.

In Part 2, we’ll actually write our “Angular” code and set up views and routing. Stay tuned. I’m happy to answer any questions in the comments!

Angular, webpack and gulp for an SPA: Part I

2 thoughts on “Angular, webpack and gulp for an SPA: Part I

Leave a comment