piatok 5. apríla 2013

Mobile webapps with nodejs & typescript - Part I

Overview

With more and more powerful mobile devices spreading into population we can see that mobile web applications are coming to boom also on mobile devices. This brings in new set of people working in this segment looking into this way of development in new way.

Traditionally web applications require desktop computer which is usually by magnitude better than average mobile device; in combination with known issues of web development (scattered platform, Javascript etc.) and the mobile-world limitations like limited available memory and external events like low battery, incoming calls etc. this can lead into quick frustration and dismissing the platform.

Webapps definitely have their place in current ecosystems as tool for quick prototyping and showcasing features but looking forward into heavy investments in Tizen, Firefox OS and Ubuntu Mobile promises that in short time frame this can become viable option also for production-quality application. Still, from the nature of tools used in this area it quickly becomes pain to maintain and add new features.

One of the major contributor to this fact is that Javascript as main language is designed with other purposes (simplicity, easy learning curve) while more traditional languages like Java, C# or C++ focuses more on larger scale project maintainability. In this article I’ll try to target following issues:

  • Static typing in Javascript
  • Module loading in larger projects
  • Packaging for mobile platform
  • Infrastructure

As main tool in this article I’ll be using Node.js and created setup that can be easily deployed to Continuous Integration system of your choice. Choice of Node.js is driven by fact that is server-side Javascript engine which easily integrates with our web app (no need of special XML files etc.) and provides good toolset for web app development.

Assuming installed Node.js (current version is v0.10.3) we start by laying down some infrastructure and creating new package in new directory (e.g. helloworld):

npm init 

Now we configure our new project by answering couple of questions (name, version, entry point etc. - defaults are fine) and we will be granted with newly created package.json file which serves as descriptor of our package.

Now to install Grunt (current version is v0.4.1) that we will be using to chain our toolset and save us some headaches (and of course create whole set of new ones). We will need to install it globally to get new grunt command by invoking:

npm install -g grunt-cli 

Main driver of Grunt system is Gruntfile.js file that can be created from template or by hand. Since templates are quite complex we will start by very simple Gruntfile that will be extended:

module.exports = function(grunt) {
    // Project configuration.
    grunt.initConfig({
      pkg: grunt.file.readJSON('package.json'),
      typescript: {
        base: {
          src: ['src/**/*.ts'],
          dest: './dist/www/js',
          options: {
            module: 'amd', //or commonjs
            target: 'es5', //or es3
            base_path: 'src'
          }
        }
      }
    });
    grunt.loadNpmTasks('grunt-typescript');
    grunt.registerTask('default', ['typescript']);
}; 

We still need to install local copy of Grunt + tasks into our project:

npm install grunt --save-dev 

Option --save-dev tells NPM that we will need to have this package only for development - our final codes will don’t need that. And now we should be able to invoke Grunt command from command line to run our builds system:

grunt 

Success! By invoking Grunt without parameters we run default task. Now to prepare some folder structure:

  • dist - all output JS, CSS & HTML pages, platform packages etc.
  • lib - external libraries
  • images - images for our project
  • platform - platform specific files
  • styles - Stylus or CSS styles
  • src - Typescript files
  • views - Jade or HTML pages

Typescript

Typescript is new kid on the block from Microsoft which aims to help with most critical issues of Javascript for larger projects - module loading, classes & interfaces and static typing. Great thing about Typescript is that it’s superset of JS - this means that every JS file is automatically Typescript and Typescript follows proposed features for ECMAScript 6 which in future should allow Typescript files without need of compilation.
For now we still need to compile Typescript into Javascript so we will install new Grunt task

npm install grunt-typescript --save-dev 

Now we need to tell Grunt to load new tasks and tell it how to translate Typescript files into Javascript by updating Gruntfile.js:

module.exports = function(grunt) {
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    typescript: {
      base: {
        src: ['src/**/*.ts'],
        dest: './dist/www/js',
        options: {
          module: 'amd', //or commonjs
          target: 'es5', //or es3
          base_path: 'src'
        }
      }
    }
  });

  grunt.loadNpmTasks('grunt-typescript');

  grunt.registerTask('default', ['typescript']);
}; 

And create our main Typescript file (into src/main.ts):

export class Main  {
    constructor() {
        console.log("main.init()");
    }

    run() { 
     var self = this;
        console.log("main.run()");
    }
} 

Now we can run grunt command again and we should get compiled main.js in dist/www. Reason for adding specific www folder is that it’s usually faster to do first check in desktop browser like Chrome rather than building it for specific platform - this desktop version will be our default target that will be later incorporated into target for building for specific platform.

File that we got is build for dynamic module loading via AMD mechanism - unfortunately no current browser supports this so we need to add support for this.

Dynamic module loading

Usual problem with larger web projects is separation of code modules and code dependencies - in order to keep project maintainable we need to split functionality into multiple JS files, however there is no way to tell that this JS module requires other JS module directly in JS file since this needs to be specified in HTML file by script tag (and usually also in correct order).

To resolve this issue script loaders like RequireJS uses AMD format to specify which modules are required for script. Fortunately Typescript can export module dependencies in AMD format as well which makes it suitable for our purposes.

We will start by creating src/config.ts (which will serve as main entry point into application and in future will hold also RequireJS shim configuration for external libraries) but for now it will be pretty simple:

import main = module("main");

var Main = new main.Main();
Main.run(); 

This code will import main.ts file, instantiate Main class and runs Main.run() method. Now we need to create sample web page that will demonstrate module loading (views/index.html):

<html>
 <head>    
  <script data-main="js/config" src="js/require.js">
 </script>
</head>
<body>
 <div id="corpus">
  Hello world!
 </div>
 </body>
</html> 

Additionaly, we download RequireJS file into libs/require.js and tell Grunt to copy both HTML and JS files into their respective locations. For this we will need contrib-copy task:

npm install grunt-contrib-copy --save-dev 

And update Gruntfile.js:

module.exports = function(grunt) {
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    typescript: {
      base: {
        src: ['src/**/*.ts'],
        dest: './dist/www/js',
        options: {
          module: 'amd', //or commonjs
          target: 'es5', //or es3
          base_path: 'src'
        }
      }
    },
    copy: {
      libs: {
        files: [
          { flatten: true, expand: true, src: ['lib/require.js'], dest: 'dist/www/js/'}
        ]
      },
      html: {
        files: [
          { flatten: true, expand: true, src: ['views/index.html'], dest: 'dist/www'}          
        ]
      }
    },
  });

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

  // Default task(s).
  grunt.registerTask('default', ['copy:libs', 'typescript', 'copy:html']);
}; 

We now have 2 subtasks of copy task (libs and html) - this way we can create build steps of same task that can be used for different targets or purposes. By running grunt from command line we should get new structure in dist/www folder and opening dist/www/index.html should write two messages into development console:

Packaging for mobile platform

In this example we will focus on Tizen platform since it has native support for web applications but with few modifications it’s possible to integrate PhoneGap (Apache Cordova) framework as well and target Android, iOS, Windows Phone and other platforms.

Tizen works with HTML5 packaged web app files which is basically zip file with config.xml defining package metadata. We will start by creating this file into platform/tizen:

<?xml version="1.0" encoding="UTF-8"?>
<widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets" id="http://org.example/helloWorld" version="1.0" viewmodes="fullscreen">
    <icon src="icon.png"/>
    <content src="index.html"/>
 <name>helloWorld</name>
 <tizen:application id="c8ETUJghqu" required_version="1.0"/>
</widget>

Additionally we will need application icon which will be displayed in main menu (platform/tizen/icon.png):

Last bit is zip task for Grunt:

npm install grunt-zip --save-dev 

And we can update Gruntfile.js to pull it together:

module.exports = function(grunt) {
  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    typescript: {
      base: {
        src: ['src/**/*.ts'],
        dest: './dist/www/js',
        options: {
          module: 'amd', //or commonjs
          target: 'es5', //or es3
          base_path: 'src'
        }
      }
    },
    copy: {
      libs: {
        files: [
          { flatten: true, expand: true, src: ['lib/require.js'], dest: 'dist/www/js/'}
        ]
      },
      html: {
        files: [
          { flatten: true, expand: true, src: ['views/index.html'], dest: 'dist/www'}
        ]
      },
      tizen: {
        files: [
          { flatten: true, expand: true, src: ['platform/tizen/**'], dest: 'dist/tizen'},          
          { flatten: true, expand: true, src: ['dist/www/index.html'], dest: 'dist/tizen'},
          { flatten: true, expand: true, src: ['dist/www/js/*'], dest: 'dist/tizen/js'}          
        ]
      }
    },
    zip: {
      tizen: {
        src: 'dist/tizen/*',
        cwd: 'dist/tizen',
        dest: 'dist/helloWorld.wgt'
      }
    }
  });

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

  // Default task(s).
  grunt.registerTask('default', ['copy:libs', 'typescript', 'copy:html']);
  grunt.registerTask('tizen', ['default', 'copy:tizen', 'zip:tizen']);
};

Now we can invoke either grunt command without parameters or, if we wish to build Tizen target, grunt with parameter tizen:

grunt tizen

In next part we will take a look how to tie together additional template engines Jade and Stylus and how to build MVVC application using Knockout.js.

Complete project can be downloaded here without node_modules folder so it is necessary to run following command from project directory to load dependencies:

npm install .

Continue to Part II.

Žiadne komentáre:

Zverejnenie komentára