Bundling

Note: If you skipped ahead to this section, click here to download a zip of the tutorial at this point.

In this tutorial we will write our app using ecmascript6 javascript, which is the latest version of javascript updated with the new ecmascript standards (From now on we will refer it as 'es6'). So before we dive into building our app, we need to make an initial setup inorder to achieve that.

Iorder to write some es6 code we will need a pre-processor. babel plays a perfect roll for that. But that's not all. One of the most powerful tools in es6 is the module system. It uses relative paths inorder to load different modules we implement. babel can't do that alone because it can't load relative modules using sytax only. We will need some sort of a module bundler.

That's where Webpack kicks in. Webpack is just a module bundler, but it can also use pre-processors on the way and it can be easily configured by whatever rules we specify, and is a very powerful tool and is being used very commonly.

Meteor also uses the same techniques to implement es6, and load npm modules into client side code, but since we're using Ionic cli and not Meteor, we will implement our own Webpack configuration, using our own rules!

Great, now that we have the idea of what Webpack is all about, let's setup our initial config:

1.1 Add webpack config webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
var camelCase = require('lodash.camelcase');
var upperFirst = require('lodash.upperfirst');
 
module.exports = {
  entry: [
    './src/index.js'
  ],
  output: {
    path: __dirname + '/www/js',
    filename: 'app.bundle.js'
  },
  externals: [
    {
      'angular': 'angular',
      'cordova': 'cordova',
      'ionic': 'ionic'
    },
    resolveExternals
  ],
  target: 'web',
  devtool: 'source-map',
  babel: {
    presets: ['es2015', 'stage-0'],
    plugins: ['add-module-exports']
  },
  module: {
    loaders: [{
      test: /\.js$/,
      exclude: /(node_modules|www)/,
      loader: 'babel'
    }]
  },
  resolve: {
    extensions: ['', '.js'],
    alias: {
      lib: __dirname + '/www/lib'
    }
  }
};
 
function resolveExternals(context, request, callback) {
  return cordovaPlugin(request, callback) ||
         callback();
}
 
function cordovaPlugin(request, callback) {
  var match = request.match(/^cordova\/(.+)$/);
  var plugin = match && match[1];
 
  if (plugin) {
    plugin = camelCase(plugin);
    plugin = upperFirst(plugin);
    callback(null, 'this.cordova && cordova.plugins && cordova.plugins.' + plugin);
    return true;
  }
}

NOTE: Since we don't want to digress from this tutorial's subject, we won't go into details about Webpack's config. For more information, see reference.

We would also like to initiate Webpack once we build our app. All our tasks are defined in one file called gulpfile.js, which uses gulp's API to perform and chain them.

Let's edit our gulpfile.js accordingly:

1.2 Add webpack task to gulpfile gulpfile.js
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 
45
46
47
48
49
50
51
var minifyCss = require('gulp-minify-css');
var rename = require('gulp-rename');
var sh = require('shelljs');
var webpack = require('webpack');
 
var webpackConfig = require('./webpack.config');
 
var paths = {
  webpack: ['./src/**/*.js', '!./www/lib/**/*'],
  sass: ['./scss/**/*.scss']
};
 
gulp.task('default', ['webpack', 'sass']);
 
gulp.task('webpack', function(done) {
  webpack(webpackConfig, function(err, stats) {
    if (err) {
      throw new gutil.PluginError('webpack', err);
    }
 
    gutil.log('[webpack]', stats.toString({
      colors: true
    }));
 
    done();
  });
});
 
gulp.task('sass', function(done) {
  gulp.src('./scss/ionic.app.scss')
...some lines skipped...
});
 
gulp.task('watch', function() {
  gulp.watch(paths.webpack, ['webpack']);
  gulp.watch(paths.sass, ['sass']);
});
 

From now on all our client code will be written in the ./src folder, and Gulp should automatically detect changes in our files and re-build them once our app is running.

NOTE: Again, we would like to focus on building our app rather than expalining about 3rd party libraties. For more information about tasks in Gulp see reference.

And last but not least, let's install the necessary dependencies inorder to make our setup work. Run:

$ npm install babel --save
$ npm install babel-core --save
$ npm install babel-loader --save
$ npm install babel-plugin-add-module-exports --save
$ npm install babel-preset-es2015 --save
$ npm install babel-preset-stage-0 --save
$ npm install expose-loader --save
$ npm install lodash.camelcase --save
$ npm install lodash.upperfirst --save
$ npm install script-loader --save
$ npm install webpack --save

TIP: You can also write it as a single line using npm i <package1> <package2> ... --save.

Our package.json should look like this:

1.3 Install webpack dependencies package.json
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 
34
35
36
37
  "version": "1.1.1",
  "description": "whatsapp: An Ionic project",
  "dependencies": {
    "babel": "^6.5.2",
    "babel-core": "^6.7.6",
    "babel-loader": "^6.2.4",
    "babel-plugin-add-module-exports": "^0.1.2",
    "babel-preset-es2015": "^6.6.0",
    "babel-preset-stage-0": "^6.5.0",
    "expose-loader": "^0.7.1",
    "gulp": "^3.5.6",
    "gulp-concat": "^2.2.0",
    "gulp-minify-css": "^0.3.0",
    "gulp-rename": "^1.2.0",
    "gulp-sass": "^2.0.4",
    "lodash.camelcase": "^4.1.1",
    "lodash.upperfirst": "^4.2.0",
    "script-loader": "^0.7.0",
    "webpack": "^1.13.0"
  },
  "devDependencies": {
    "bower": "^1.3.3",
...some lines skipped...
    "ionic-plugin-keyboard"
  ],
  "cordovaPlatforms": []
}

Ionic provides us with a very nice skelton for building our app. But we would like to use a different method which is a little more advanced which will help us write some es6 code properly.

Thus, we shall clean up some files from our project, just run:

$ cd ./www
$ rm -rf ./css
$ rm -rf ./img
$ rm -rf ./js
$ rm -rf ./templates

Next, we will setup our index.html:

1.5 Setup index.html for app www/index.html
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
    <title>whatapp</title>
 
    <!-- compiled css output -->
    <link href="css/ionic.app.css" rel="stylesheet">
 
    <!-- cordova script (this will be a 404 during development) -->
    <script src="cordova.js"></script>
 
    <!-- your app's js -->
    <script src="js/app.bundle.js"></script>
  </head>
 
  <body>
    <!--
      The nav bar that will be updated as we navigate between views.
    -->
  • We named our app Whatsapp, since that's what it represents.
  • We removed all css files accept for one, since they are all pre-processed and imported using a library called SASS into one file called ionic.app.css. All our scss files should be defined in scss folder.
  • Same goes for javascript files, they will all be bundled into one file called app.bundle.js using our Webpack config we've just defined.
  • We removed the ng-app attribute which will then take place in our javascript code.

Now that we have an initial setup, let's define our entry point for our code. Create a file called index.js in our src folder with the following contents:

1.6 Add index js file src/index.js
1
2
3
4
5
6
7
8
9
// libs
import 'script!lib/angular/angular';
import 'script!lib/angular-animate/angular-animate';
import 'script!lib/angular-sanitize/angular-sanitize';
import 'script!lib/angular-ui-router/release/angular-ui-router';
import 'script!lib/ionic/js/ionic';
import 'script!lib/ionic/js/ionic-angular';
// app
import './app';

This is simply a file where all our desired scripts are loaded. Note that libraries are being loaded with the script! pre-fix, which is braught to us by the script-loader npm package. This pre-fix is called a loader, and we actually have many types of it, but in this case it tells Webpack that the files specified afterwards should be loaded as-is, without handling any module requirements or any pre-processors.

NOTE: We can also specify the script loader as a general rule for all our libraries, but this way it won't be clear that the files we just imported are being imported as scripts. Both approaches are good, but we will stick with the direct and simple approach of specifying the script loader for every library module imported, because it's more declerative.

As you can see there is also an app.js file being imported at the bottom. This file should be our main app file. Let's write it:

1.7 Add base app file src/app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Angular from 'angular';
import Ionic from 'ionic';
import Keyboard from 'cordova/keyboard';
import StatusBar from 'cordova/status-bar';
 
const App = 'whatsapp';
 
Angular.module(App, [
  'ionic'
]);
 
Ionic.Platform.ready(() => {
  if (Keyboard) {
    Keyboard.hideKeyboardAccessoryBar(true);
    Keyboard.disableScroll(true);
  }
 
  if (StatusBar) {
    StatusBar.styleLightContent();
  }
 
  Angular.bootstrap(document, [App]);
});

As you can see, we define our app's module and we bootstrap it. Bootstraping is when we initialize primary logic in our application, and is done automatically by Angular. Ofcourse, there is some additional code related to cordova enviroment, like hiding the keyboard on startup.

We'de now like to build our app and watch for file changes as we run our app. To do so, just edit the ionic.project file and add Gulp files to run on startup.

1.8 Add gulp startup tasks to ionic.project ionic.project
1
2
3
4
5
6
7
8
{
  "name": "whatsapp",
  "gulpStartupTasks": [
    "webpack",
    "sass",
    "watch"
  ]
}

Also, since we use pre-processors for both our .js and .css files, they are not relevant anymore. Let's make sure they won't be included in our next commits by adding them to the .gitignore file:

1.9 Add js and css dirs to .gitignore .gitignore
6
7
8
9
10
plugins/
.idea
www/lib/
www/css/
www/js/