Technologie im Fokus

Making Angular2 a team player – multiple apps on a single page

M


Angular2/4’s toolchain is hard to use and even harder to comprehend in detail. Although powerful, the plethora of tools playing together – npm, angular-cli, typescript, webpack2 – makes it hard to fully understand the inner workings of Angular’s toolchain.

Webpack2, Angular’s bundling tool is currently in very active development, and the documentation, although improved recently, is far from comprehensive.

In a recent portal project for a customer, a development stack based on Angular2 was required, and had to be integrated into a JSR-268 portal. In such a portal environment, Angular does not own the web page exclusively – instead, it has to share it with other frameworks, and plain old JavaScript code. This setting is different of what Angular’s main use case of developing SPAs (single page applications) seems to be.

Requirements in a portal environment

More specifically,  the following requirements arise specifically in a portal environment:

  1. Angular apps live side by side with classic JS apps and frameworks
  2. Multiple angular apps may live together on a single page, sharing code and services
  3. Angular loading has to comply to a given JS loader like SystemJS, CommonJS or RequireJS
  4. Loading of multiple apps (dynamically arranged on a page) should load the Angular2 runtime only once, sharing it among all Angular2 apps on a page.

While Angular’s toolchain, especially with features like code splitting/lazy loading is already complex, the requirements above make building Angular apps for such an environment a challenge.

This article is intended to be the first in a series of articles, where I’d like to share our insights into this very complex task. In this first part, I’ll try to draft our solution to make two Angular2 apps live together on the same page while sharing the Angular runtime.

Please note that this is still work in progress and far from complete. Any feedback, improvements or other suggestions are highly welcome. You’ll find the complete code on github.

Choosing the right tools

Webpack (more exactly Webpack2) is Google’s choice for a module bundler for Angular2 apps. It basically Webpack’s plugins allow optimizations like uglification and treeshaking, reducing the size of the final JS bundle significantly. As a downside, it prohibits packaging angular2 applications into separate modules, that are loaded only once and shared among multiple angular2 apps on a page. Webpack’s CommonsChunkPlugin provides an option to carve out common chunks of code for multiple entry points in a single application and thus boost caching at the client since common chunks are only loaded once. However, this is not our use case: we want bundles/chunks to be reused among multiple separate applications, that do not necessarily know of each other. The DLLPlugin provides a different approach, that seems to be more promising. The idea is to call webpack with a special config to build a ‘DLL’ ( yes, it actually means „Dynamic Link Library“ – though this sounds anachronistic, it makes the concept of this plugin quite clear).

Crafting a simple Angular2 application

simple Angular2 app file structure
simple Angular2 app file structure

Our project file structure is that of a simple npm project:

  • the package.json file contains the main project definition for this app.
  • the config folder contains a couple of webpack config, we’ll use all of them when we build our application.
  • the scripts folder contains a script for a local http server and is irrelevant.
  • the src folder contains the TS sources for our first Angular2 app
  • the src2 folder contains the TS sources for our second Angular2 app.
  • the tsconfig.json file contains our typescript compiler settings.

Apart from the two source folders, this is quite a common setup for developing Angular2 apps.

 

 

 

We’ll start off our minimal application with a package.json file:

{
  "name": "minimalist-angular-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "npm run build:dll && npm run build:app && npm run build:app2",
    "build:dll": "webpack --config config/webpack.dll.js --progress --profile --bail",
    "build:app": "webpack --config config/webpack.app.js --progress --profile --bail",
    "build:app2": "webpack --config config/webpack.app2.js --progress --profile --bail",
    "hotdll": "webpack-dev-server --config config/webpack.dev-with-dll.js --hot --inline",
    "hotdll2": "webpack-dev-server --config config/webpack.dev2-with-dll.js --hot --inline",
    "start": "node scripts/simple-server.js",
    "clean": "rimraf dist"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@angular/animations": "~4.1.2",
    "@angular/common": "~4.1.2",
    "@angular/compiler": "~4.1.2",
    "@angular/core": "~4.1.2",
    "@angular/forms": "~4.1.2",
    "@angular/http": "~4.1.2",
    "@angular/platform-browser": "~4.1.2",
    "@angular/platform-browser-dynamic": "~4.1.2",
    "@angular/platform-server": "~4.1.2",
    "@angular/router": "~4.1.2",
    "@angularclass/hmr": "~1.2.2",
    "@angularclass/hmr-loader": "~3.0.2",
    "core-js": "^2.4.1",
    "http-server": "^0.9.0",
    "ie-shim": "^0.1.0",
    "reflect-metadata": "^0.1.10",
    "rxjs": "~5.0.2",
    "zone.js": "0.8.5"
  },
  "devDependencies": {
    "@types/core-js": "^0.9.41",
    "@types/hammerjs": "^2.0.34",
    "@types/jasmine": "2.5.45",
    "@types/node": "^7.0.13",
    "@types/source-map": "^0.5.0",
    "@types/uglify-js": "^2.6.28",
    "@types/webpack": "^2.2.15",
    "add-asset-html-webpack-plugin": "^2.0.1",
    "angular2-template-loader": "^0.6.2",
    "awesome-typescript-loader": "^3.1.3",
    "connect": "^3.6.2",
    "extract-text-webpack-plugin": "^2.1.0",
    "html-loader": "^0.4.5",
    "html-webpack-plugin": "^2.28.0",
    "raw-loader": "^0.5.1",
    "rimraf": "^2.6.1",
    "serve-static": "^1.12.3",
    "source-map-loader": "^0.2.1",
    "tslib": "^1.7.1",
    "typescript": "^2.3.4",
    "webpack": "^2.6.1",
    "webpack-dev-server": "^2.4.5",
    "webpack-merge": "^4.1.0"
  }
}

You may have noticed the bunch of webpack configs in the config/ folder. The first one we need is the webpack.dll.js which contains the webpack settings to create a DLL for 1) the polyfills and 2) the Angular2 runtime. Here it is:

const webpack = require('webpack');
const helpers = require('./helpers');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const ENV = process.env.NODE_ENV = process.env.ENV = 'production';

module.exports = {
    entry: {
        'polyfills': [ './config/polyfills.ts' ],
        'vendor': [ './config/vendor.ts' ]
    },
    devtool: "source-map",
    output: {
        filename: '[name].dll.js',
        path: helpers.root('dist'),
        // The name of the global variable which the library's
        // require() function will be assigned to
        library: '[name]'
    },
    plugins: [
        new webpack.NoEmitOnErrorsPlugin(),
        new webpack.optimize.UglifyJsPlugin({ 
            mangle: {
                keep_fnames: true
            },
            sourceMap: true,
            compress: true
        }),
        new ExtractTextPlugin('[name].[hash].css'),
        new webpack.DefinePlugin({
            'process.env': {
                'ENV': JSON.stringify(ENV)
            }
        }),
        new webpack.LoaderOptionsPlugin({
            htmlLoader: {
                minimize: false // workaround for ng2
            }
        }),
        new webpack.DllPlugin({
            path: 'dist/[name]-manifest.json', 
            name: '[name]'
        })
    ]
};

There are two “entry” elements that define the names of the two libraries we’ll be creating here:

  • polyfills will contain the polyfills we want to package as DLL
  • vendor will contain the actual angular2 framework code

The webpack.DLLPlugin does the DLL packaging magic:
The ‘name’ attribute sets the library name (that will be replaced by the keys in the “entry” map, i.e. “polyfills” and “vendor”). Additionally, it defines the path and name of a “manifest file” that will contain all the information needed for the library to be used by other applications. So packaging this library, probably as NPM module, will need to take care of providing this manifest file to applications using it.
Both “polyfills.ts” and “vendor.ts” are regular typescript files as you would use them in a regular angular2 app:

import 'reflect-metadata';

import 'core-js/es6/symbol';
import 'core-js/es6/object';
import 'core-js/es6/function';
import 'core-js/es6/parse-int';
import 'core-js/es6/parse-float';
import 'core-js/es6/number';
import 'core-js/es6/math';
import 'core-js/es6/string';
import 'core-js/es6/date';
import 'core-js/es6/array';
import 'core-js/es6/regexp';
import 'core-js/es6/map';
import 'core-js/es6/set';
import 'core-js/es6/weak-map';
import 'core-js/es6/weak-set';
import 'core-js/es6/typed';
import 'core-js/es6/reflect';
// see issue https://github.com/AngularClass/angular2-webpack-starter/issues/709
// import 'core-js/es6/promise';

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');
}

The ‚vendor.ts‘ file defining what will go into the ‚vendor‘ (=angular) package.

import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/core';
import '@angular/common';
import '@angular/http';
import '@angular/router';
import '@angular/forms';
// RxJS
import 'rxjs';

Note that both files are in the config folder because they are there only to define the contents of our two DLLs. We can now build our DLL using npm by calling ’npm run build:dll‘ (see above for what this npm script does).

So, we’ve just build out DLLs (i.e. libraries) for polyfills and the angular runtime. You’ll find the compilations results in the dist/ folder.

Using the DLLs in an Angular2 app

Next, we’ll use our newly created DLLs in a client app (for example the one in the src folder). Again, we’ll use a special webpack.config file to build our app while using the DLLs:

var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
var helpers = require('./helpers');

module.exports = {
    entry: [
        './config/polyfills.ts',
        './src/main.ts'
    ],

    output: {
        path: helpers.root('dist'),
        publicPath: 'http://localhost:8080/',
        filename: 'bundle.js'
    },
    resolve: {
        // resolve module file requests by looking for explicit extensions
        // or look for matching files with .js or .ts extensions
        extensions: ['*', '.js', '.ts']
    },
    module: {
        loaders: [
            {
                test: /\.ts$/,
                loaders: ['awesome-typescript-loader', '@angularclass/hmr-loader', 'angular2-template-loader'],
            },
            {
                test: /\.html$/,
                loader: 'raw-loader'
            },
            // handle component-scoped styles specified with styleUrls
            {
                test: /\.css$/,
                include: helpers.root('src'),
                loader: 'raw-loader'
            }
        ]
    },

    plugins: [
        new webpack.DllReferencePlugin({
            context: '.',
            manifest: require(helpers.root('dist', 'vendor-manifest.json'))
        }),
        new webpack.DllReferencePlugin({
            context: '.',
            manifest: require(helpers.root('dist', 'polyfills-manifest.json'))
        }),
        new HtmlWebpackPlugin({
            template: 'src/index.html',
            filename: 'index.html'
        }),
        new AddAssetHtmlPlugin([
            { filepath: 'dist/polyfills.dll.js', includeSourcemap: false },
            { filepath: 'dist/vendor.dll.js', includeSourcemap: false }
        ])
    ],

    devServer: {
        host: '0.0.0.0',
        port: 8080
    }
};

Note lines 54-61 where we use a complementary webpack plugin, the DLLReferencePlugin to use the DLL (more specific: the DLL manifest files) to reference the DLLs we built previously.

Building the DLLs and everything else

Now we can build the DLLs with npm run build – which in turn builds the DLLs and the two apps (don’t forget to run ’npm install‘ after checking out the repo). After this, everything is built in the dist folder:

drwxr-xr-x 12 developer developer     408 Jun  6  2017 .
drwxr-xr-x 15 developer developer     510 Jun  6  2017 ..
-rw-r--r--  1 developer developer   17753 Jun  6  2017 bundle.js
-rw-r--r--  1 developer developer   17820 Jun  6  2017 bundle2.js
-rw-r--r--  1 developer developer    1069 Jun  6  2017 index.html
-rw-r--r--  1 developer developer    1391 Jun  6  2017 index2.html
-rw-r--r--  1 developer developer   20041 Jun  6  2017 polyfills-manifest.json
-rw-r--r--  1 developer developer  116925 Jun  6  2017 polyfills.dll.js
-rw-r--r--  1 developer developer  896957 Jun  6  2017 polyfills.dll.js.map
-rw-r--r--  1 developer developer   34154 Jun  6  2017 vendor-manifest.json
-rw-r--r--  1 developer developer 1076939 Jun  6  2017 vendor.dll.js
-rw-r--r--  1 developer developer 7346433 Jun  6  2017 vendor.dll.js.map

The bundle.js and bundle2.js contain the code from the applications only. The polyfills-dll.js and vendor.dll.js files contain the code for the polyfills and vendor (=angular) runtime.

Now it’s time to checkout the results: run ’npm run start‘ and point your browser to http://localhost:8080 to see one of the applications in action. Point it to http://localhost:8080/index2.html to see both applications play side-by-side on a single page.

For demonstration purposes, we still keep the libraries and both apps in a single npm project. In any real usecase, we’ll separate out the DLLs into a separate project, publish it via NPM and use it in our own, separate applications.

Summary

In this first article of our series, we sketched out the first step into the journey of developing separate Angular2 apps using common modules to effectively share code.
In the parts to come we’ll cover the topic of letting this setup play together nicely with JS module loaders to fit into the portal environment that is our use case.

This article is heavily based on the great work of Minko Gechev  who does extraordinary work in the Angular2 tooling area, and the also great work of Jason Watmore who has created huge amounts of incredibly valuable Angular2/4 Tutorials.

Über den Autor

Michael Hager
Michael Hager

Michael Hager ist Gründer und Geschäftsführer von artindustrial informationstechnologien GmbH in Wien, Österreich.

Seine Tätigkeitsgebiete umfassen Cloud Computing, Angular2 Applikationsentwicklung und Java-Applikationsentwicklung.

Michael Hager Von Michael Hager
Technologie im Fokus

Hier finden Sie uns

Adresse
Mariahilferstrasse 111/6-7
1060 Wien
Kartenansicht

Öffnungszeiten
Montag – Freitag: 9–17 Uhr

Kontakt
Mail: it@artindustrial.com
Tel.: +43 1 5954023

Folgen Sie uns