Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bundle Angular 2 app using UMD bundles (not building vendor bundle)

I'm currently bundling my Angular 2 app with WebPack. We are still spinning rapid cycles, so rather than adding delays to our build and application load process, we want to include the rarely-changing Angular 2 UMD CDN prepared bundles, e.g.:

<script src="https://npmcdn.com/@angular/[email protected]/bundles/core.umd.min.js"></script>
<script src="https://npmcdn.com/@angular/[email protected]/bundles/common.umd.min.js"></script>
<script src="https://npmcdn.com/@angular/[email protected]/bundles/compiler.umd.min.js"></script>
  • When I just let WebPack do its thing, the package runs fine but is a few MB, because it doesn't leverage pre-built bundles or separate out Angular 2 "vendor" code.
  • When I use the Angular 2 WebPack Recommendations, e.g.: plugins: [ new webpack.optimize.CommonsChunkPlugin("vendor", "vendor.bundle.js") ], my app bundle is small, but I manually build a separate, unique 1MB bundle containing most of the Angular 2 framework in it, every build. This file changes slightly each build depending on my application, and is not portable between versions of my applications or various applications, and doesn't have the benefit of "CDN". Of course I have to include this file for my app to run.
  • When I use the IgnorePlugin to filter @angular|rxjs, e.g. plugins: [ new webpack.IgnorePlugin(/\@angular|rxjs/) ], it excludes the vendor files, but inserts hard-coded exceptions / throws errors at the top of my application bundle.
  • When I use WebPack externals, e.g. externals: ['@angular/core', ..., I get function(module, exports) { module.exports = @angular/core; }, output in my app bundle, which obviously isn't functional. The WebPack documentation isn't terribly forthcoming, but I think I may be able to either specify a libraryTarget or quoted resolve function, which would instruct WebPack to compile in module loading.
  • When I ditch WebPack altogether and use instead the TypeScript compiler bundler (as per this guide, which uses the UMD bundles), I get System.register() calls referring to the NPM namespaces I expected, e.g. System.register("myapp/boot", ['@angular/core', ..., but I'm still working on SystemJS configuration to call the UMDs. As a side note, this file is an extra 25% in size relative to what WebPack is generating.
  • If I use SystemJS as in the prior item, I want this compilation to occur during build, or as a parallel process, rather than as part of file save. I guess SystemJS-Builder (see related questions here and here) would be the way to do this? Perhaps this also would produce smaller file sizes that the Typescript-integrated "bundler".

How can I build an application bundle that doesn't depend on a uniquely-repackaged Angular 2 bundle?

I'm currently building against RC3. My process is currently WebPack, as mentioned above, but I would move to another toolset if that makes it easier.

Doing some more research, I think I've been misled by WebPack's "Loader" terminology. I have to use a module loader, and it doesn't look like WebPack has one that will work for this.

To assign UMD bundles module namespaces (and wire up dependents) they can't be loaded in script tags. Instead they have to be evaluated with a given this context to act as the module reference. That means that even if I want them all loaded synchronously, I still have to configure something else like SystemJS to load them over the wire, so their context is controlled/wrapped.

This Angular 2 plunker is near what I'm looking for. It uses the Angular 2 UMD bundles, but doesn't use an RxJs bundle, although that looks easy enough to change if I want the entire RxJs library.

like image 360
shannon Avatar asked Jul 01 '16 21:07

shannon


2 Answers

More than one of the approaches in my question will work. Some will not, some do not currently due to defects in Angular 2. Here's the approach I'm currently using:


WebPack + require.js

angular2-webpack-config.js

var config = {
    entry: {
        app: inputFile
    },
    externals: [
        /^@angular\//,
        /^rxjs\//
    ],
    output: {
        libraryTarget: "amd",
        path: __dirname,
        filename: './' + outputName
    },
    plugins: [
        new require('webpack').optimize.UglifyJsPlugin()
    ]
};

I only tell it what are externals and what pseudo-standard mechanism will load them at runtime via libraryTarget (AMD/RequireJS, CommonJs/node, UMD). My setting simply causes external library references to be wrapped in define().

Note that I don't do anything with paths in WebPack. In my software, anything in the node_modules folder has a similar reference mechanism both in my software and in third party modules, internally. Both my code and third-party libraries expect to find RxJS at rxjs (e.g. rather than ../rxjs or 'node_modules/rxjs`). At runtime, both need to be mapped, but since we aren't allowing WebPack to reach into third party modules (we're using pre-built UMD's), WebPack isn't the place to do that mapping. It would only map my code. Instead, we should do that in our runtime loader:

index.htm

<script src="https://npmcdn.com/core-js/client/shim.min.js"></script>
<script src="https://npmcdn.com/[email protected]/dist/zone.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reflect-metadata/0.1.3/Reflect.min.js"></script>
<script>
    var require = (function(){
        var versions = {
            'router-deprecated': '@@2.0.0-rc.2',
            'forms': '@@0.1.1',
            'angular': '@@2.0.0-rc.4',
            'rxjs': '@@5.0.0-beta.10'
        }

        var paths = {
            'rxjs': "https://npmcdn.com/rxjs" + versions.rxjs + "/bundles/Rx.umd.min"
        };
        [
            'core',
            'http',
            'common',
            'compiler',
            'platform-browser',
            'router-deprecated',
            'platform-browser-dynamic'
        ].forEach(function (submodule) {
            var module = '@@angular/' + submodule
            paths[module] = 'https://npmcdn.com/' + module + (versions[submodule] || versions.angular) + '/bundles/' + submodule + '.umd.min';
        });

        var rxmap = {};
        [
            'Rx',
            'Observable',
            'Subject',
            'observable/PromiseObservable',
            'operator/toPromise'
        ].forEach(function (submodule) {
            rxmap['rxjs/' + submodule] = 'rxjs';
        })

        return {
            paths: paths,
            map: {
                '*': rxmap
            }
        };
    })();
</script>
<script data-main="../assets/compiled/a2.webpack.js" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.2.0/require.min.js"></script>

Also, since if you are using WebPack and UMDs you probably care about resulting file sizes and times. This sub-app's Angular 2 build process went from about 24 seconds to 1 second. Its publish changed-size went from over 2MB to about 100k.

Here are wire load sizes of the cached dependencies for reference. Strangely they are currently a few KB smaller in the UMD version than the wire-size increase of an integrated, WebPack pruned, bundle.

 KB
27.5 shim
 6.8 zone
 8.0 require
 3.3 platform-browser-dynamic
36.8 http
 8.7 core
20.8 common
16.5 router
98.5 compiler
27.9 platform-browser
39.0 Rx

Obviously my public site load times after updates are drastically reduced (down to 1 second from about 10-20 seconds), but those numbers are pretty variable based on connection.

like image 180
shannon Avatar answered Oct 11 '22 08:10

shannon


I have been trying to achieve the same result without success so far.

As a first attempt, I managed to have webpack loading the pre-built UMD bundles using resolve configurations, as in the example below:

var webpack = require('webpack');

module.exports = {
  entry: './src/main',
  output: {
    filename: 'bundle.js',
    path: 'bundles/app',
  },

  resolve: {
    extensions: ['', '.ts', '.js'],
    alias: {
        '@angular/common' : '@angular/common/common.umd.js',
        '@angular/compiler' : '@angular/compiler/compiler.umd.js',
        '@angular/core' : '@angular/core/core.umd.js',
        '@angular/http' : '@angular/http/http.umd.js',
        '@angular/platform-browser' : '@angular/platform-browser/platform-browser.umd.js',
        '@angular/platform-browser-dynamic' : '@angular/platform-browser-dynamic/platform-browser-dynamic.umd.js',
        '@angular/router' : '@angular/router/router.umd.js',
        '@angular/router-deprecated' : '@angular/router-deprecated/router-deprecated.umd.js',
        '@angular/upgrade' : '@angular/upgrade/upgrade.umd.js',
        'rxjs/Observable' : 'rxjs/bundles/Rx.umd.min.js',
        'rxjs/add/operator/map' : 'rxjs/bundles/Rx.umd.min.js',
        'rxjs/add/observable/fromEvent' : 'rxjs/bundles/Rx.umd.min.js',
        rxjs : 'rxjs/bundles/Rx.umd.min.js'
    }
  },

  module: {
    loaders: [
      {test: /\.ts$/, exclude: /node_modules/, loader: 'ts'},
    ],
    noParse: [/@angular/, /rxjs/]
  },

};

The key configurations here are noParse and resolve/alias. I am telling webpack to load the UMD pre-built bundles whenever it finds the requires such as @angular/common and etc.

This configuration does make webpack include the UMD files instead of processing each angular and rxjs file. My goal here was to improve the build time. However, on my case, my application failed at runtime. I guess I was using a library that requires angular files on its src folders and it made it fail. I'd give this setup a try to check if it works on your scenario.

As a second attempt I tried a feature called Webpack DLL as described at http://engineering.invisionapp.com/post/optimizing-webpack/. It created a bundle that may be reused across builds, thus avoiding re-bundling angular2 everytime. Unfortunatelly my application fails with dependency injection exceptions with this scenario.

I did manage to load the UMD bundles using SystemJS instead of Webpack, with the following setup

(function(global) {

  var paths = {
    'jquery' : '../node_modules/jquery/dist/jquery.js',
    'bootstrap' : '../node_modules/bootstrap/dist/js/bootstrap.js',
    //'rxjs/*' : '../node_modules/rxjs/bundles/Rx.umd.min.js', //FAILS with router 3.0
  }

  var map = {
    'app':                        'app', // 'dist',
    'angular2-in-memory-web-api': '../node_modules/angular2-in-memory-web-api',
    '@angular':                   '../node_modules/@angular',
    'lodash':                     '../node_modules/lodash',
    'rxjs':                       '../node_modules/rxjs'    
  };

  var packages = {
    'app':                        { main: 'main.js',  defaultExtension: 'js' },
    'angular2-in-memory-web-api': { defaultExtension: 'js' },
    'lodash':                     { main: 'lodash.js', defaultExtension: 'js' },
    'rxjs':                       { defaultExtension: 'js' },
  };

  var ngPackageNames = [
    'common',
    'compiler',
    'core',
    'http',
    'platform-browser',
    'platform-browser-dynamic',
    'router',
    'router-deprecated',
    'upgrade',
  ];
  ngPackageNames.forEach(function(pkgName) {
    packages['@angular/'+pkgName] = { main: '/bundles/' + pkgName + '.umd.js', defaultExtension: 'js' };
  });

  var config = {
    map: map,
    packages: packages,
    paths : paths,
  };

  // filterSystemConfig - index.html's chance to modify config before we register it.
  if (global.filterSystemConfig) { global.filterSystemConfig(config); }

  System.config(config);

})(this);

This is pretty mych the setup proposed by the official Angular2 docs with the addition of path section. It used to be able to load RxJS as a single UMD bundle but with the recent introduction of router 3.0 this setup fails with exceptions on router code. I had to revert and load RxJS as individual requests again.

like image 43
Daniel Melo Avatar answered Oct 11 '22 08:10

Daniel Melo