Angular2 – Custom project & Workflow setup

An alternative to create the project with the Angular CLI. A custom way.

Create Angular applications with a Webpack based tooling.

Official doc: https://angular.io/docs/ts/latest/guide/webpack.html

Initializing the project

  • We run
    npm init

to create a package.json.

Setting up the basic project files

  • Lets make the folder structure and blank files:
    • src folder
    • src/app folder
    • src/app/main.ts: Compiles the application with the JIT compiler and bootstraps the application’s main module (AppModule) to run in the browser. The JIT compiler is a reasonable choice during the development of most projects and it’s the only viable choice for a sample running in a live-coding environment like Plunker.
    • src/app/home.component.ts
    • src/app/app.module.ts:  Defines AppModule, the root module that tells Angular how to assemble the application. Right now it declares only the AppComponent. Soon there will be more components to declare.
    • src/app/app-routing.module.ts
    • src/app/app.component.ts:  It is the root component of what will become a tree of nested components as the application evolves.
    • src/app/app.component.html
    • src/app/app.component.css
    • src/index.html
    • src/pollyfills.js
    • src/app/users/users.module.ts
    • src/app/users/users,component.ts
    • src/app/users/users.component.html
    • src/app/users/users.component.css
    • src/app/users/users-routing.module.ts

Installing the core dependencies

Lets install all the packages needed with npm install:

npm install --save @angular/core @angular/compiler @angular/common @angular/compiler-cli @angular/forms @angular/http @angular/platform-browser @angular/platform-browser-dynamic @angular/platform-server @angular/router @angular/upgrade @angular/animations
typescript rxjs zone.js core-js

Filling the Project Files with some life

  • We create some basic content for all the components.

index.html & pollyfills

./polyfills.js: 

You’ll need polyfills to run an Angular application in most browsers as explained in the Browser Support guide. Polyfills should be bundled separately from the application and vendor bundles. Add a polyfills.ts like this one to the src/folder.

import 'core-js/es6';
import 'core-js/es7/reflect';
require('zone.js/dist/zone');

./index.html

<!doctype html>
<html class="no-js" lang="">
    <head>
        <base href="/">
        <meta charset="utf-8">
        <meta http-equiv="x-ua-compatible" content="ie=edge">
        <title>Angular seed</title>
        <meta name="description" content="">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <link rel="apple-touch-icon" href="apple-touch-icon.png">
        <!-- Place favicon.ico in the root directory -->

        <link rel="stylesheet" href="css/normalize.css">
        <link rel="stylesheet" href="css/main.css">
    </head>
    <body>
      <app-root>Loading...</app-root>
    </body>
</html>

Installing development dependencies

sudo npm install --save-dev webpack webpack-dev-server angular-router-loader angular2-template-loader awesome-typescript-loader html-loader raw-loader del-cli

Setting up a development workflow: Typescript configuration

TypeScript is a primary language for Angular application development. It is a superset of JavaScript with design-time support for type safety and tooling.Browsers can’t execute TypeScript directly. Typescript must be “transpiled” into JavaScript using the tsc compiler, which requires some configuration.

Typescript wiki: http://www.typescriptlang.org/docs/handbook/tsconfig-json.html

Typescript compiler options: http://www.typescriptlang.org/docs/handbook/compiler-options.html

./tsconfig.json:TypeScript compiler configuration

{
  "compilerOptions": {
    "moduleResolution": "node",  
    "emitDecoratorMetadata": true,  
    "experimentalDecorators": true,
    "target":"es5",
    "lib": [
      "es5",
      "dom"
    ]
  }
}
  • emitDecoratorMetadata: true and experimentalDecorators:true because we are using decorators.
  • target:es5 to run in all browsers

Setting up a development workflow:Webpack

Webpack is a powerful module bundler. A bundle is a JavaScript file that incorporates assets that belong together and should be served to the client in a response to a single file request. A bundle can include JavaScript, CSS styles, HTML, and almost any other kind of file.

Webpack roams over your application source code, looking for import statements, building a dependency graph, and emitting one or more bundles. With plugins and rules, Webpack can preprocess and minify different non-JavaScript files such as TypeScript, SASS, and LESS files.

You determine what Webpack does and how it does it with a JavaScript configuration file, webpack.config.js.

  • And we create three different webpack config files: webpack.config.common.js , webpack.config.dev.js and webpack.config.prod.js
  • For merging webpack config files we need to install a new npm package:
sudo npm install --save-dev webpack-merge
  • we also need to install

    HtmlWebpackPlugin

    Webpack generates a number of js and CSS files. You could insert them into the index.html manually. That would be tedious and error-prone. Webpack can inject those scripts and links for you with the HtmlWebpackPlugin.

sudo npm install --save-dev html-webpack-plugin

./webpack.config.common.js:

The webpack.common.js configuration file does most of the heavy lifting. Create separate, environment-specific configuration files that build on webpack.common by merging into it the peculiarities particular to the target environments.

These files tend to be short and simple.

module.exports = {
  entry: './src/app/main.ts',

  resolve: {
    extensions: ['.js', '.ts']
  },

  module: {
    rules: [
      {
        test: /\.html$/,
        loaders: ['html-loader']
      },
      {
        test: /\.css$/,
        loaders: ['raw-loader']
      }
    ],
    exprContextCritical: false
  },
 plugins: [
   new HtmlWebpackPlugin({
   template: 'src/index.html'
 })
 ]
};

./webpack.config.dev.js

var webpackMerge = require('webpack-merge');
const path = require('path');

var commonConfig = require('./webpack.config.common');

module.exports = webpackMerge(commonConfig, {
  devtool: 'cheap-module-eval-source-map',
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/',
    filename: 'bundle.js',
    chunkFilename: '[id].chunk.js'
  },

  module: {
    rules: [
      {
        test: /.ts$/,
        use: [
          {loader: 'awesome-typescript-loader', options: {
            transpileOnly: true
          }},
          {loader: 'angular2-template-loader'},
          {loader: 'angular-router-loader'}
        ]

      }
    ]
  },

  devServer: {
    historyApiFallback: true,
    stats: 'minimal'
  }
});

 

Webpack config: Entries and outputs

You supply Webpack with one or more entry files and let it find and incorporate the dependencies that radiate from those entries. The one entry point file in this example is the application’s root file, src/main.ts:

entry: {
  'app': './src/main.ts'
},

Webpack inspects that file and traverses its import dependencies recursively.

Webpack config: Loaders

Webpack can bundle any kind of file: JavaScript, TypeScript, CSS, SASS, LESS, images, HTML, fonts, whatever. Webpack itself only understands JavaScript files. Teach it to transform non-JavaScript file into their JavaScript equivalents with loaders. Configure loaders for TypeScript and CSS as follows.

rules: [
  {
    test: /\.ts$/,
    loader: 'awesome-typescript-loader'
  },
  {
    test: /\.css$/,
    loaders: 'style-loader!css-loader'
  }
]

Webpack config: Plugins

Webpack has a build pipeline with well-defined phases. Tap into that pipeline with plugins such as the uglify minification plugin:

plugins: [
  new webpack.optimize.UglifyJsPlugin()
]

Finishing and using the development workflow

  • Lets modify our package.json to run the local development server:

package.json

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack-dev-server --inline --progress --port 8080 --config webpack.config.dev.js"
  },
  • Now we can run
    npm run build

and hit localhost:8080 in the browser

Setting up a production workflow

  • For make a build for production we want to add AoT (Ahead of time compilation) to optimize loading of the app.
  • Let first create versions of the config files implementing AoT.
  • ./src/app/main.aot.ts 
import '../polyfills';

import {platformBrowser} from '@angular/platform-browser';
import {enableProdMode} from '@angular/core';

import {AppModuleNgFactory} from './app.module.ngfactory';

enableProdMode();
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

NOTE 1: app.module.ngfactory will be generated in the time of building for production with aot.

NOTE 2: For bootstrapping angular this time we are using platform-browser instead of platform-browser-dynamic. The difference between platform-browser-dynamic and platform-browser is the way your angular app will be compiled.
Using the dynamic platform makes angular sending the Just-in-Time compiler to the front-end as well as your application. Which means your application is being compiled on client-side.
On the other hand, using platform-browser leads to an Ahead-of-Time pre-compiled version of your applicatiion being sent to the browser. Which usually means a significantally smaller package being sent to the browser.
The angular2-documentation for bootstrapping at https://angular.io/docs/ts/latest/guide/ngmodule.html#!#bootstrap explains it in more detail.

./tsconfig.aot.json

{
  "compilerOptions": {
    "module": "es2015",
    "outDir":"./dist",
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target":"es5",
    "lib": [
      "es5",
      "dom"
    ]
  },
  "angularCompilerOptions": {
    "skipMetadataEmit":true
  }
}

./webpack.config.prod.js

var webpackMerge = require('webpack-merge');
const path = require('path');
var webpack = require('webpack');

var commonConfig = require('./webpack.config.common');

module.exports = webpackMerge(commonConfig, {
  entry: './src/app/main.aot.ts',
  devtool: 'cheap-module-eval-source-map',
  output: {
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/',
    filename: '[hash].js',
    chunkFilename: '[id].[hash].chunk.js'
  },

  module: {
    rules: [
      {
        test: /.ts$/,
        use: [
          {loader: 'awesome-typescript-loader'},
          {loader: 'angular2-template-loader'},
          {loader: 'angular-router-loader?aot=true'}
        ]

      }
    ]
  },

  plugins: [
    new webpack.optimize.UglifyJsPlugin()
  ]
});

package.json

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack-dev-server --inline --progress --port 8080 --config webpack.config.dev.js",
    "build:prod":"del-cli dist && ngc -p tsconfig.aot.json && ngc -p tsconfig.aot.json && webpack --config webpack.config.prod.js --progress --profile --bail && del-cli 'src/app/**/*.js' 'src/app/**/*.ngfactory.ts' 'src/app/**/*.js.map' 'src/app/**/*.shim.ts' 'src/app/**/*.ngsummary.json'"
  },
  • In the build:prod script , we first clean the dist folder , then we compiler typescripts files with angular offline compiler (ngc), then we make the bundle with webpack and finally we clean files not needed.

Adding types and fixing bugs

  • For resolving a bug in the compilation we need to install two more packages: @types/core-js and @types/node. NOTE : be sure to fit to these versions:
    "@types/core-js": "^0.9.36",
    "@types/node": "^6.0.45",

and modify tsconfig.aot.json to :

    "target":"es5",
    "typeRoots": [
      "node_modules/@types"
    ],
  • Now we are ready to run
    npm run build:prod

this will generate a dist folder with a build release for production.

Finishing touches

  • Let finally create a lite server for testing our build release for production.
  • Lets install npm package lite-server
npm install --save-dev lite-server
  • And create a config file for that server:

bs-config.js

module.exports = {
  server: {
    baseDir: './dist',
    middleWare: {
      1: require('connect-history-api-fallback')({ index:'/index.html', verbose:true })
    }
  }
};

and a new script “serve” in the package.json:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack-dev-server --inline --progress --port 8080 --config webpack.config.dev.js",
    "build:prod": "del-cli dist && ngc -p tsconfig.aot.json && ngc -p tsconfig.aot.json && webpack --config webpack.config.prod.js --progress --profile --bail && del-cli 'src/app/**/*.js' 'src/app/**/*.ngfactory.ts' 'src/app/**/*.js.map' 'src/app/**/*.shim.ts' 'src/app/**/*.ngsummary.json' 'src/app/**/*.ngstyle.ts' 'dist/app'",
    "serve": "lite-server"
  },

and we are ready to run

 npm run serve

and see our prod release live!