My history with JS Build tools

I remember the first time I came across a build system for my JS programs, it was quite a revelation... Wait, I can compile scss, minify my js code, lint, and run unit tests seamlessly with no extra effort? Add to that the ability to livereload my page and see the css\js changes I make without the hassle of reloading my webapp every time.

The first build tool I used was Grunt. I created my first build tasks, it was simply made to compile my scss files, run some lints and live reloading the web page on changes. It was all great, but when my app grown in size, my Grunt tasks started to get slow. The main reason for this was mainly because Grunt requires opening each file in the task flow, make the changes and write the file back to disk for the next step in the process.

Gulp to the rescue, when gulp came out it was kind of a game changer for me, since I was just started working with nodejs and the support of streams that allowed gulp to run my tasks wayyyy faster then Grunt. Gulp reads the file once, and then using node streams transforms the files in memory, and return the processed output to the next step.

Module bundling

When I started to get interested in SPA frameworks, and mainly with angularjs, it came to a point when my index.html file was looking something like:

<script src="3rdpartyplugins1.js">
// 10 plugins later...
<script src="app.js">
<script src="ctrl1.js">
<script src="ctrl2.js>
// 20 controllers later....
<script src="service1.js">
<script src="service2.js">
// 10 services later....
// Well you get the point... And I haven't showed you my <head> tag with all the css files there :)

Really fast it was a mess, so I started to look in to better ways to write my applications.

Module Bundlers, I was looking the web for solutions for this js files mess. Browserify and Requirejs was the first solid options I found and suddenly I could create only a single entry file for my entire Angular app, I could require files and plugins as needed from my main entry point and write modular webapps. Hurray!

Webpack

So it's all sounds good, why replacing Gulp + Browserify workflow ?

There are few things about webpack you need to understand, the first one is that webpack is much more opinionated. It assumes that your intent is to have some sort of entry file or files and you want to transform them and output to another location. This statement alone made my webpack.config much smaller in size compared to my Gulpfile.

Then, when it comes to bundling, webpack allows you to bundle not only js files, but also bundle your scss, hbs\jade and image files using the same bundle file. Webpack is smart enough to determine if its a small file it will simply inject the html with an inline style block. Or if the file is large it will minify it and serve it as normal css file. For images, it will transfer your images to a Base64 String and inject them directly.

Another cool feature from webpack is the ability to split bundles, then when you need them you can require them as if they exist and webpack will automatically load them during runtime. You can use ocLazyLoader with angular to avoid loading huge bundle files upfront.

The last thing I really loved about webpack is the webpack-dev-server, it allows really cool feature called hot module reloading, which means that when you make changes to a file, webpack will replace the changed module in memory and inject it to your app without the need to fully reload the page. Recently I was rewriting big module inside our app using react, with react-hot-loader by Dan Abramov. And I won't ever code again without it.

And the most amazing part, besides configuring webpack the only thing you need to added to your index.html as an asset is:

<script src="/dist/bundle.js"></script>

Webpack and Ionic workflow

So after hopefully you understand how webpack can improve your productivity and code scalability while improving performance, let's start to configure our Ionic app.

This guide assuming you already familiar with ionic basics and already installed the ionic cli.

We will create a blank ionic app using the ionic:

ionic start ionic-webpack blank
cd ionic-webpack

We will install webpack and webpack-dev-server, for the purposes of this tutorial I will install them globally, but you can install them locally to the project.

npm install webpack webpack-dev-server -g

Now we will install some webpack loaders, they are responsible for transforming our code. We will install babel-loader that will allow us to use ES6 syntax inside our ionic code.

npm install webpack babel-loader babel-preset-es2016 babel-preset-stage-2 --save-dev

The reason we install webpack to the project aswell is because extract-text-webpack-plugin requires some webpack modules, and it will fail saying: 'Cannot find module webpack/lib/removeAndDo', or other errors without installing webpack locally.

Error: Cannot find module 'webpack/lib/removeAndDo'
    at Function.Module._resolveFilename (module.js:440:15)
    at Function.Module._load (module.js:388:25)
    at Module.require (module.js:468:17)
    at require (internal/module.js:20:19)

Now when we got js covered, let's handle ionic scss file so we can use scss in our app.

npm install node-sass extract-text-webpack-plugin sass-loader css-loader url-loader file-loader --save-dev

So after installing all the dependencies and plugins needed we can create our webpack.config.js file on the project root folder:

// webpack.config.js

var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    // Our entry file for the app,
    // You can specify multiple entry files using array.
    entry: "./www/js/app.js",

    // will output to our source files after budnled, very useful during development
    // Suggested to turn it of for production bundling
    devtool: 'source-map',

    // here we will output the bundled files
    output: {
        path: __dirname + '/www',
        filename: "bundle.js"
    },
    module: {
        // loaders allow you to preproccess file when your require them
        // you can manipulate manipulate and transform them
        loaders: [
            // we will start with babel-loader,
            // used to transpile es6 code to es5 code.
            {
                //will affect all .js files
                test: /.js$/,
                loader: 'babel-loader',
                // we will not transpile node_modules and bower files for performance
                exclude: [/www\/lib/, /node_modules/],
                // presets for the babel loader, supporting stage-2 es6 funcitonality
                query: {
                  presets: ['es2015', 'stage-2']
                }
            },
            {
                // we will proccess scss files when required
               test: /\.scss$/,
               // we will extract the output file to a different location and will inject it manualy to the page
               // used for performance boost and good for large apps.
               // we will also generate sourcemaps, disable in production
               loader: ExtractTextPlugin.extract('css?sourceMap!sass?sourceMap', {
                   allChunks: true
               })
            },
            {
                test   : /\.woff/,
                loader : 'url?prefix=font/&limit=10000&mimetype=application/font-woff&name=assets/[hash].[ext]'
            },
            {
                test   : /\.ttf/,
                loader : 'file?prefix=font/&name=assets/[hash].[ext]'
            },
            {
                test   : /\.eot/,
                loader : 'file?prefix=font/&name=assets/[hash].[ext]'
            },
            {
                test   : /\.svg/,
                loader : 'file?prefix=font/&name=assets/[hash].[ext]'
            },
            // we will load all html files raw, it will allow your to require html files as string templates
            // inside your components, directives
            {
              test: /\.html$/,
              loader: "raw-loader"
            }
        ]
    },
    plugins: [
        // here we will specify the output css file name when etracted with all the scss compiled
       new ExtractTextPlugin('ionic.app.css', {
           allChunks: true
       })
   ],
   // When requiring modules in your code, require and import will first look in this directories
   // so you won't need to wrie relative paths to them
    resolve: {
        root: __dirname + "/www/",
        modulesDirectories: ["node_modules", "lib", "scss"],
        // you can add here scss extension if you want. 
        // this will allow you to require('somejsfile') with out the .js extension
        extensions: ['', '.js']
    }
};

Awesome, now we can run webpack-dev-server from cli:

webpack-dev-server --hot --inline --colors --content-base www/

If everything run smooth you should be able to access your app on http://localhost:8080.

Let's break the webpack server command:

  • --hot - will allow hot reloading of our modules
  • --inline - will inject small webpack-dev-server code that will manage auto reload.
  • --colors - Well, who doesn't want colors in their output???
  • --content-base - will be the start directory for the server, which will serve index.html by default.

It's getting quite annoying typing this long dev server line everytime we want to start. So we will add a small script to our package.json file.

Just add scripts object to your package.json file, if one already exists you can simply modify\add another line:

  "scripts": {
    "start": "webpack-dev-server --hot --inline --colors --content-base www/"
  },

Now turn off your previous server launch and type:

npm start

Your server will start now the same as before but with less typing :)

So, we done with all the boilerplate, let's start coding with webpack!

Writing modular ionic code

We will start with index.html file, we will remove 'app.js' script tag and add a single file named bundle.js

<script src="bundle.js"></script>

Then, we will remove the app.css link on index page and added ionic.app.css which will be the compiled version of our scss files.

<link href="ionic.app.css" rel="stylesheet">

We will leave ionic bundle files in the header, as well as the cordova.js import.
For this tutorial we will use angular as global variable, you can further modify your build and inject angular whenever needed.

To start, open www\js\app.js file with your favourite code editor.

We will start by importing our main scss file to the project in the top of the document, so that webpack will be able to process it:

import 'ionic.app.scss';

Now we can export our routing file to a different file. Create a new file named app.router.js in the main js directory, and add the next code:

import tabView      from './views/tabs.html';
import firstTabView from './views/first.html';

function AppRouter($stateProvider, $urlRouterProvider) {
  $stateProvider
    .state('app', {
        url: "/app",
        abstract: true,
        template: tabView
    })
    .state('tabs.home', {
        url: "/home",
        views: {
            'home-tab': {
                template: firstTabView
            }
        }
    });
    $urlRouterProvider.otherwise('/app/home');
}

export default AppRouter;

Note the we are importing our views using import, of course you can still use relative templateUrl path but for now we will stick to template syntax.

After creating the file we will create 2 simple view files, inside views folder:

tabs.html
   <ion-tabs class="tabs-icon-top tabs-positive" >
      <ion-tab title="Home" icon="ion-home" href="#/app/home">
          <ion-nav-view name="home-tab"></ion-nav-view>
       </ion-tab>
      <ion-tab title="Profile" icon="ion-person" href="#/app/profile">
       <ion-nav-view name="profile-tab"></ion-nav-view>
    </ion-tab>
   </ion-tabs>
first.html
<ion-view>
    <ion-nav-title>
        Home
    </ion-nav-title>
    <ion-content>
        <h1>Header Text</h1>
    </ion-content>
</ion-view>

our index.html body tag:

<ion-nav-bar class="nav-title-slide-ios7 bar-positive">
           <ion-nav-back-button class="button-icon ion-arrow-left-c">
           </ion-nav-back-button>
    </ion-nav-bar>
    <ion-nav-view animation="slide-left-right"></ion-nav-view>

We finished creating all of our views, now we can use them inside our app.js file. we will import the config function using:

import router from './app.router.js`

angular
    .module('starter', ['ionic'])
    .run(//Ionic default code..)
    .config(router)

let's open the browser at localhost:8080 and see if we have anything visible.

ionic default app

Creating angular module

We will start by creating a folder for our modules to live in, let's name it components and inside it we will place our 'profile' module folder.

Now, create an index.js file. Qe are using this convention so later when importing we can specify the folder name, and webpack will automatically search for index.js file to import.

//index.js
import ProfileCtrl  from './profile.ctrl';
import routes       from './profile.routes';

const ProfileModule = angular.module('profile', []);

angular.module('profile')
    .config(routes)
    .controller('ProfileCtrl', ProfileCtrl);

export default ProfileModule;

Our router for the module:

// profile.routes.js
import profileTabView from './profile.view.html';

function ProfileRoutes($stateProvider) {
    $stateProvider
        .state('app.profile', {
            url: "/profile",
            views: {
                'profile-tab': {
                    template: profileTabView,
                    controller: 'ProfileCtrl',
                    controllerAs: 'profile'
                }
            }
        });
}

export default ProfileRoutes;

and finally our controller:

// profile.ctrl
class ProfileCtrl {
    constructor() {
        this.userName = 'John Doe';
    }
}

export default ProfileCtrl;

We used here the es6 class syntax. You can inject additional services, factories inside the constructor function, and create additional methods on the class to use inside your view.

We will also use the controllerAs syntax, so we won't use $scope in our controllers and instead bind everything to this.

Next, let's write our profile.view.html file:

<ion-view>
    <ion-nav-title>
        Profile
    </ion-nav-title>
    <ion-content>
        <h1>This is profile page</h1>
        Username: {{profile.userName}}
    </ion-content>
</ion-view>

Now when navigation to profile tab we can see:

Conclusion

Webpack allows us to bundle multiple js, html, and css files to a single bundle file.

It allows us to write es6 syntax with angular 1.X, that will later help you to migrate more easily to angular 2.

There are multiple strategies to build and organise your modules in angular, here I showed a small example of what you can do with webpack. You can take it much further with your apps.

Lately I've been looking into angular components architecture with the angular 1.5 .component() method, which helps you to define directives in a faster way. For me writing angular directives before was quite tedious work, so I used it mostly when I needed it. When started playing with React, creating components was so easy and it really made my code much more flexible and reusable. Taking this approach to angular is now much straightforward for me using .component(). Angular 2 embraces components as its core, writing your angular 1.x apps with this strategy in mind will help you transfer your existing code to angular 2 later.

You can read more about components in angular on Todd motto's blog and watch the Pete Darwing talk at Ngconf.

Github Repo for this post can be found here