Sunday, December 18, 2016

Angular2 + Webpack + Maven + SpringMVC + Swagger

This post is to track the steps followed to integrate an Angular2 web application with a Maven Spring MVC application, that uses Swagger annotations to annotate the Model and API objects and generate angular2-typescript code.

Glossary


Term Description
Angular2 Angular2 is a javascript framework to develop web applications for desktop and mobile. More about Angular2 here.
Webpack Webpack is a module bundler for javascript applications. More about Webpack here.
Maven Maven is a dependency management and build tool. More about Maven here.
Spring MVC Spring MVC provides a framework to dispatch HTTP requests to request handlers in a highly-configurable, extensible manner. More about Spring MVC here.
Swagger Swagger is an API framework to document and consume RESTful APIs. More about Swagger here.

Step-by-step Guide

Setup Angular2 Webapp

In a maven project, create the directory where angular2 code will be present:
 
# For example, in my-service
$ cd my-service/src/main
$ mkdir my-webapp
$ cd my-webapp

Create package.json and add the following configuration (Note: Replace all the ${webapp.*} placeholders in the file.):
 

{
  "name": "${webapp.name}",
  "version": "${webapp.version}",
  "description": "${webapp.description}",
  "readme": "${webapp.readme.url}",
  "repository": {
    "type": "git",
    "url": "${webapp.repository.url}"
  },
  "scripts": {
    "start": "webpack-dev-server --inline --progress --port 8080",
    "build": "rimraf dist && webpack --config config/webpack.prod.js --progress --profile --bail"
  },
  "license": "ISC",
  "dependencies": {
    "@angular/common": "~2.2.0",
    "@angular/compiler": "~2.2.0",
    "@angular/core": "~2.2.0",
    "@angular/forms": "~2.2.0",
    "@angular/http": "~2.2.0",
    "@angular/platform-browser": "~2.2.0",
    "@angular/platform-browser-dynamic": "~2.2.0",
    "@angular/platform-server": "~2.2.0",
    "@angular/router": "~3.2.0",
    "assets-webpack-plugin": "^3.4.0",
    "bootstrap": "^3.3.6",
    "core-js": "^2.4.1",
    "font-awesome": "^4.6.3",
    "moment": "^2.14.1",
    "ng2-bootstrap": "1.1.16",
    "ng2-modal": "0.0.22",
    "rxjs": "5.0.0-beta.12",
    "zone.js": "^0.6.25"
  },
  "devDependencies": {
    "@types/core-js": "^0.9.35",
    "@types/jasmine": "^2.5.35",
    "@types/node": "^6.0.45",
    "angular2-template-loader": "^0.4.0",
    "awesome-typescript-loader": "^3.0.0-beta.13",
    "connect-history-api-fallback": "^1.2.0",
    "copy-webpack-plugin": "^4.0.0",
    "css-loader": "^0.23.0",
    "extract-text-webpack-plugin": "^1.0.1",
    "file-loader": "^0.8.5",
    "html-loader": "^0.4.3",
    "html-webpack-plugin": "^2.24.1",
    "http-proxy-middleware": "^0.17.0",
    "jasmine-core": "^2.4.1",
    "null-loader": "^0.1.1",
    "phantomjs-prebuilt": "^2.1.7",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.5.4",
    "script-ext-html-webpack-plugin": "^1.3.2",
    "style-loader": "^0.13.1",
    "to-string-loader": "^1.1.4",
    "ts-helpers": "1.1.2",
    "ts-node": "^1.7.0",
    "typescript": "^2.1.4",
    "webpack": "^1.13.3",
    "webpack-dev-middleware": "^1.6.1",
    "webpack-dev-server": "^1.16.2",
    "webpack-md5-hash": "^0.0.5",
    "webpack-merge": "^1.0.2"
  }
}

Create tsconfig.json and add the following configuration:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  }
}

Create index.html, the landing page for the webapp.

<!DOCTYPE html>
<html>
<head>
    <title>My UI Application</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="/static/ext/bootstrap/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <my-app>Loading My UI Application ...</my-app>
    <script src="/static/ext/bootstrap/js/jquery.js"></script>
    <script src="/static/ext/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

Create .npmrc file with the following contents. This is needed while compiling the angular project.

strict-ssl=false
registry=http://registry.npmjs.org/

Add the app folder that actually contains all the modules, components, services corresponding to the webapp. Create client, components, service sub folders within the app folder. Create app.module.ts file in the app folder, with the following content:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { Ng2BootstrapModule } from 'ng2-bootstrap/ng2-bootstrap';
import { RouterModule, PreloadAllModules } from '@angular/router';
import {APP_BASE_HREF, Location} from '@angular/common';
import { AppComponent } from './app.component';
 
 
@NgModule({
    bootstrap: [AppComponent],
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        Ng2BootstrapModule,
        RouterModule.forRoot([
        ])
    ],
    providers: [
        {
            provide: APP_BASE_HREF,
            useValue: window['_app_base'] || '/'
        }
    ]
})
export class AppModule {
}

Create app.component.ts with the following content:

import { Component, ViewContainerRef, OnDestroy } from '@angular/core';
@Component({
    selector: 'my-app'
})
export class AppComponent {
}

Distribute the dependencies into two different typescript files
  • polyfills.ts: this will be loaded before the tag in index.html.
  • vendor.ts: this will be loaded before the tag in index.html.

//polyfills.ts

import 'core-js/es6';
import 'core-js/es7/reflect';
require('zone.js/dist/zone');
if (process.env.ENV === 'production') {
  // Production
} else {
  // Development
  Error['stackTraceLimit'] = Infinity;
  require('zone.js/dist/long-stack-trace-zone');
}


//vendor.ts

// Angular
import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/core';
import '@angular/common';
import '@angular/http';
import '@angular/router';
// RxJS
import 'rxjs';
// Other vendors for example jQuery, Lodash or Bootstrap
// You can import js, ts, css, sass, ...

And main.ts:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { AppModule } from './app.module';
if (process.env.ENV === 'production') {
  enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);

Now the angular2 webapp directory will be as follows:

my-service
|- src
   |- main
      |- my-webapp
         |- app
            |- client -- This folder contains the swagger REST client that can be used to talk to the backend service.
               |- model
                  |- 
            |- components
               |- 
            |- service
               |- 
            |- app.module.ts -- Main module.
            |- app.component.ts -- Top level component that will load the rest of the components.
            |- main.ts -- entry point.
            |- polyfills.ts
            |- vendor.ts
         |- .npmrc
         |- index.html
         |- package.json
         |- tsconfig.json

Configure Webpack

The package.json file created above already has the webpack dependencies included, and the scripts already are configured to run webpack command. Now, create webpack.config.js in the webapp root folder, i.e. in my-webapp.

module.exports = require('./config/webpack.dev.js');

Create config directory under my-webapp. Add the following files:

helpers.js


var path = require('path');
var _root = path.resolve(__dirname, '..');
function root(args) {
  args = Array.prototype.slice.call(arguments, 0);
  return path.join.apply(path, [_root].concat(args));
}
exports.root = root;

webpack.common.js


var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var helpers = require('./helpers');
const { CheckerPlugin } = require('awesome-typescript-loader')
module.exports = {
  entry: {
    'app': './app/main.ts',
    'polyfills': './app/polyfills.ts',
    'vendor': './app/vendor.ts'
  },
  resolve: {
    extensions: ['', '.ts', '.js']
  },
  module: {
    loaders: [
      {
        test: /\.ts$/,
        loaders: ['awesome-typescript-loader', 'angular2-template-loader']
      },
      {
        test: /\.html$/,
        loader: 'html'
      },
      {
        test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
        loader: 'file?name=assets/[name].[hash].[ext]'
      },
      {
        test: /\.css$/,
        exclude: helpers.root('app'),
        loader: ExtractTextPlugin.extract('style', 'css?sourceMap')
      },
      {
        test: /\.css$/,
        include: helpers.root('app'),
        loader: 'raw'
      }
    ]
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: ['app', 'polyfills', 'vendor']
    }),
    new HtmlWebpackPlugin({
      template: './index.html',
      baseUrl: '/'
    }),
    new CheckerPlugin()
]
};

webpack.dev.js


var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');
module.exports = webpackMerge(commonConfig, {
  devtool: 'cheap-module-eval-source-map',
  output: {
    path: helpers.root('dist'),
    publicPath: 'http://localhost:8080/',
    filename: '[name].js',
    chunkFilename: '[id].chunk.js'
  },
  plugins: [
    new ExtractTextPlugin('[name].css')
  ],
  devServer: {
    historyApiFallback: {
      index: "/",
      baseUrl: "/"
    },
    stats: 'minimal',
    proxy: [
      {
        context: [
          
        ],
        target: '',
        secure: false
      }
    ]
  }
});

webpack.prod.js


var webpack = require('webpack');
var webpackMerge = require('webpack-merge');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var commonConfig = require('./webpack.common.js');
var helpers = require('./helpers');
const ENV = process.env.NODE_ENV = process.env.ENV = 'production';
module.exports = webpackMerge(commonConfig, {
  devtool: 'source-map',
  output: {
    path: helpers.root('dist'),
    publicPath: '/assets/',
    filename: '[name].[hash].js',
    chunkFilename: '[id].[hash].chunk.js'
  },
  htmlLoader: {
    minimize: false // workaround for ng2
  },
  plugins: [
    new webpack.NoErrorsPlugin(),
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin({ // https://github.com/angular/angular/issues/10618
      mangle: {
        keep_fnames: true
      }
    }),
    new ExtractTextPlugin('[name].[hash].css'),
    new webpack.DefinePlugin({
      'process.env': {
        'ENV': JSON.stringify(ENV)
      }
    })
  ]
});

At the end of this, the my-webapp folder would be as follows:

my-service
|- src
   |- main
      |- my-webapp
         |- app
            |- client -- This folder contains the swagger REST client that can be used to talk to the backend service.
               |- model
                  |- 
            |- components
               |- 
            |- service
               |- 
            |- app.module.ts -- Main module.
            |- app.component.ts -- Top level component that will load the rest of the components.
            |- main.ts -- entry point.
            |- polyfills.ts
            |- vendor.ts
         |- config
            |- helpers.js
            |- webpack.common.js
            |- webpack.dev.js
            |- webpack.prod.js
         |- .npmrc
         |- index.html
         |- package.json
         |- tsconfig.json

Now, the set up is ready to bring up the dev instance of the angular app with the following commands:


# Run npm install, to install all the dependencies under the node_modules directory.
$ npm install
 
 
# A sample output would be as follows:
 
+-- @angular/common@2.2.4
+-- @angular/compiler@2.2.4
+-- @angular/core@2.2.4
+-- @angular/forms@2.2.4
+-- @angular/http@2.2.4
+-- @angular/platform-browser@2.2.4
+-- @angular/platform-browser-dynamic@2.2.4
+-- @angular/platform-server@2.2.4
| `-- parse5@2.2.3
+-- @angular/router@3.2.4
+-- @types/core-js@0.9.35
+-- @types/jasmine@2.5.38
+-- @types/node@6.0.51
+-- angular2-template-loader@0.4.0
| `-- loader-utils@0.2.16
|   +-- big.js@3.1.3
|   +-- emojis-list@2.1.0
|   `-- json5@0.5.1
+-- assets-webpack-plugin@3.5.0
.....
.....
.....
.....
.....
+-- webpack-md5-hash@0.0.5
| `-- md5@2.2.1
|   +-- charenc@0.0.1
|   +-- crypt@0.0.1
|   `-- is-buffer@1.1.4
+-- webpack-merge@1.1.1
| +-- lodash.clonedeep@4.5.0
| +-- lodash.differencewith@4.5.0
| +-- lodash.isequal@4.4.0
| +-- lodash.isfunction@3.0.8
| +-- lodash.isplainobject@4.0.6
| +-- lodash.mergewith@4.6.0
| `-- lodash.unionwith@4.6.0
`-- zone.js@0.6.26
npm WARN optional Skipping failed optional dependency /chokidar/fsevents:
npm WARN notsup Not compatible with your operating system or architecture: fsevents@1.0.15
$
 
# Run npm start to start the webpack's dev server
$ npm start
 
# A sample output would be as follows:
[at-loader] Checking started in a separate process...
[at-loader] Ok, 0.874 sec.                                                                                                                           chunk    {0} app.js (app) 959 kB {1} [rendered]
chunk    {1} polyfills.js (polyfills) 246 kB {2} [rendered]
chunk    {2} vendor.js (vendor) 2.6 MB [rendered]
Child html-webpack-plugin for "index.html":
    chunk    {0} index.html 542 bytes [rendered]
webpack: bundle is now VALID.
 
 
 
# Run npm run build to generate the artifacts that will be used when the angular webapp is hosted from the backend server
$ npm run build
 
 < some output here...>

Upon npm run build, dist directory will be created with the following files:

my-service
|- src
   |- main
      |- my-webapp
         |- dist
            |- app.*.js
            |- app.*.js.map
            |- index.html
            |- pollyfills.*.js
            |- pollyfills.*.js.map
            |- vendor.*.js
            |- vendor.*.js.map

Spring MVC Configuration

As part of Spring MVC configuration, we will add the following:
  1. Mapping /assets/* to the dist folder created above, that gets copied to WEB-INF/my-webapp/dist, more about that in the pom.xml section below.
  2. Exclude /assets/* from Authentication.

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/**" />
        <mvc:exclude-mapping path="/assets/**" />
    </mvc:interceptor>
</mvc:interceptors>
<mvc:resources mapping="/assets/**" location="/WEB-INF/my-webapp/dist/" />

In the Spring MVC controller, load the index.html file for all the paths starting with "/ui", assuming that the Angular2 Webapp will be rendered at this path. If you would like to render the webapp at the root, then the controller should load index.html for "", "/", "/**".

// If the angular2 webapp should be rendered in the root context.
@RequestMapping(value = { "", "/**" }, method = RequestMethod.GET)
// If the angular2 webapp should be rendered in the /ui context.
@RequestMapping(value = {"/ui", "/ui/**" }, method = RequestMethod.GET)
@ResponseBody
public String loadAngular2IndexHtml() throws IOException
{
    return ;
}

Maven Build Configuration - pom.xml

As part of pom.xml, we would like to
  1. Compile and build the angular2 webapp using webpack
  2. Copy the angular2 webapp artifacts generated by webpack's npm run build method, to WEB-INF directory, during the processing of preparing the war file.
  3. Clean up the angular2 build artifacts as part of project clean up step.
  4. (Optional) Generate the swagger client module for angular2 webapp.

<properties>
    <nodeVersion>v6.1.0</nodeVersion>
    <npmVersion>3.8.6</npmVersion>
</properties>
 
<build>
    <plugins>
        <!-- Step 1: Compile and build the angular2 webapp -->
        <plugin>
            <groupId>com.github.eirslett</groupId>
            <artifactId>frontend-maven-plugin</artifactId>
            <version>1.0</version>
            <configuration>
                <workingDirectory>src/main/my-webapp/</workingDirectory>
                <installDirectory>target/temp</installDirectory>
            </configuration>
            <executions>
                <!-- It will install nodejs and npm -->
                <execution>
                    <id>install node and npm</id>
                    <goals>
                        <goal>install-node-and-npm</goal>
                    </goals>
                    <configuration>
                        <workingDirectory>src/main/my-webapp/</workingDirectory>
                        <installDirectory>target/temp</installDirectory>
                        <nodeVersion>${nodeVersion}</nodeVersion>
                        <npmVersion>${npmVersion}</npmVersion>
                    </configuration>
                </execution>
                <!-- It will execute command "npm install" inside "/angular" directory -->
                <execution>
                    <id>npm install</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                    <configuration>
                        <workingDirectory>src/main/my-webapp/</workingDirectory>
                        <installDirectory>target/temp</installDirectory>
                        <arguments>install</arguments>
                    </configuration>
                </execution>
                <execution>
                    <id>npm build</id>
                    <goals>
                        <goal>npm</goal>
                    </goals>
                    <configuration>
                        <workingDirectory>src/main/my-webapp/</workingDirectory>
                        <installDirectory>target/temp</installDirectory>
                        <arguments>run build</arguments>
                    </configuration>
                </execution>
            </executions>
        </plugin>
 
 
        <!-- Step 2: Copy the angular2 build artifacts to the war generated for the backend webapp -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>2.6</version>
            <configuration>
                <webResources>
                    <resource>
                        <directory>src/main/my-webapp/</directory>
                        <targetPath>WEB-INF/my-webapp</targetPath>
                        <excludes>
                            <exclude>**/node_modules/**</exclude>
                            <exclude>**/node/**</exclude>
                            <exclude>**/typings/**</exclude>
                        </excludes>
                    </resource>
                </webResources>
            </configuration>
        </plugin>
 
 
        <!-- Step 3: Clean up the build artifacts -->
        <plugin>
            <artifactId>maven-clean-plugin</artifactId>
            <version>3.0.0</version>
            <configuration>
                <filesets>
                    <fileset>
                        <directory>src/main/my-webapp/dist</directory>
                        <includes>
                            <include>**/*</include>
                        </includes>
                        <followSymlinks>false</followSymlinks>
                    </fileset>
                    <fileset>
                        <directory>src/main/my-webapp/node_modules</directory>
                        <includes>
                            <include>**/*</include>
                        </includes>
                        <followSymlinks>false</followSymlinks>
                    </fileset>
                    <fileset>
                        <directory>src/main/my-webapp/node</directory>
                        <includes>
                            <include>**/*</include>
                        </includes>
                        <followSymlinks>false</followSymlinks>
                    </fileset>
                </filesets>
            </configuration>
        </plugin>
 
        <!-- Step 4: (Optional) Generate the Angular2-TypeScript client module for the backend REST services annotated with Swagger annotations.-->
        <plugin>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-codegen-maven-plugin</artifactId>
            <version>2.2.1</version>
            <executions>
                <execution>
                    <phase>prepare-package</phase>
                    <goals>
                        <goal>generate</goal>
                    </goals>
                    <configuration>
                        <inputSpec>${project.basedir}/src/main/webapp/my-service.swagger.json</inputSpec>
                        <language>typescript-angular2</language>
                        <output>src/main/my-webapp/app/client/</output>
                        <configOptions>
                            <supportsES6>true</supportsES6>
                        </configOptions>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>