Blog

Logo Webpack

Webpack und AngularJS

Trotz des Hypes um TypeScript und auch Angular 2 gibt es noch viele AngularJS 1.x-Anwendungen. Oft wurden letztere noch mit ng-min oder anderen Werkzeugen in einem Build-Prozess mit Grunt oder Gulp paketiert. Angular 2, die neue Major-Version des Frameworks, setzt jedoch standardmäßig auf Webpack. Daher bietet es sich an, bei einer Migration von AngularJS 1.x in Richtung Angular 2 zunächst auf Webpack umzustellen.

Webpack ist ein „Bundler“, der aus einer Reihe von JavaScript-Dateien bzw. -Modulen eine einzige „vollständige“ JavaScript-Datei (ein „Bundle“) erzeugt. Dabei unterstützt Webpack auch das Nachladen von Modulen zur Laufzeit. Deswegen wird bei Webpack auch von einem „Loader“ gesprochen.

Für die Entwicklungszeit stellt das Programm außerdem den sog. Webpack Dev Server zur Verfügung. Dabei handelt es sich um einen einfachen Webserver, über den ihr eure Anwendung zum Testen ausliefern könnt. Ein großer Mehrwert dieses Tools kommt durch den „Hot“-Mode des Servers: in diesem Modus erkennt der Server Änderungen an euren Quelldateien (sei es JavaScript, CSS, …), transformiert diese automatisch und lädt eure Webseite neu. Das geht sehr schnell, weil Webpack nicht die ganze Anwendung, sondern nur die veränderten Dateien neu übersetzt.

Zusammengefasst hilft Webpack mit seinen Features eine Reihe von Problemen im Frontend zu lösen, wobei verschiedene Modulsysteme unterstützend tätig sind. Außerdem können Abhängigkeiten unabhängig vom Quellcode aufgespalten und gemeinsame Abhängigkeiten über mehrere Module nur einmal geladen werden. Zusätzlich ist es möglich, beliebige Assets als Modul nachzuladen.

Aber was genau ist Webpack ?

  • Module-Bundler für CommonJS and AMD
  • Erstellt ein oder mehrere Bundles
  • Jedes Asset kann prinzipiell ein Modul sein
  • Stellt Hooks für die Modultransformation bereit
  • Stellt Hooks für das Bundlen bereit
  • Effizient:
    • Webpack: 243b + 20b pro Modul + 4b pro Abhängigkeit
    • Browserify: 14.7kb + 0b pro Modul + (3b + X) pro Abhängigkeit
    • RequireJS: 415b + 25b pro Modul + (6b + 2X) pro Abhängigkeit

Eine gute Schritt-für-Schritt-Anleitung für AngularJS ist das Webpack-AngularJS-Cookbook. Darin wird sehr genau auf die einzelnen Einstellungen und Auswirkungen eingegangen.

Wo aber liegen die Stärken von Webpack?

Die flexible Architektur mit Plugins und Loadern hat ein großes Ökosystem geschaffen. Außerdem kann man ohne Änderung am Code erzeugte Artefakte anders schneiden. Das wird gerade mit HTTP2 interessant. Man kann für z.B. zwei verschieden Bundles (Module im Webpack) das Commons Chunk-Plugin nutzen, um den gemeinsamen Code in ein drittes Artefakt auszulagern. Dazu definiert man lediglich seine Bundle-Entries in der webpack.config.js und aktiviert das Plugin:

module.exports = {
  entry: {
    entry1: './entry1',
    entry2: './entry2'
  },
 
  output: {
    path: 'output',
    filename: 'bundle-[name].js'
  },
 
  plugins: [
    new CommonsChunkPlugin('common', 'bundle-[name].js')
  ]
};

$ webpack
Hash: 75ef3309e9d1f1110c46
Version: webpack 1.4.15
Time: 30ms
           Asset  Size  Chunks             Chunk Names
bundle-entry2.js   172       0  [emitted]  entry2
bundle-entry1.js   172       1  [emitted]  entry1
bundle-common.js  3842       2  [emitted]  common
   [0] ./entry1.js 72 {1} [built]
   [0] ./entry2.js 72 {0} [built]
   [1] ./greeter.js 81 {2} [built]

Dabei zeigt sich die ganze Macht von Webpack beim Optimieren: Neben dem Dedupe-Plugin zum Vermeiden von dupliziertem Code kann der jeweilige Modul-Kontext manipuliert werden. Nehmen wir das Beispiel von moment.js, einer bekannten JavaScript-Bibliothek:

var moment = require('moment');
console.log(moment().format('dddd'));
$ webpack
Hash: 3fa34cb738076f531876
Version: webpack 1.4.15
Time: 396ms
    Asset    Size  Chunks             Chunk Names
bundle.js  393777       0  [emitted]  main
   [0] ./entry.js 70 {0} [built]
    + 81 hidden modules

Wie gewohnt wird das Modul per require eingebunden. Das Problem dabei ist, dass alle Sprachen-Pakete von moment.js ins Bundle gepackt werden (aka „hidden modules“). Gerade bei 3rd-Party-Bibliotheken will man diese nicht ändern, nur um die diese Dateien mit zu packen, vgl. moment.js:

function loadLocale(name) {
  var oldLocale = null;
  if (!locales[name] && hasModule) {
    try {
      oldLocale = moment.locale();
      require('./locale/' + name);
      moment.locale(oldLocale);
    } catch (e) { }
  }
  return locales[name];
}

Hier hilft das Context Replacement-Plugin. Dabei kann ich den jeweiligen Modul-Kontext verändern. Wir wollen nur die deutschen und englischen Locales benutzen, deswegen sieht unsere webpack.config wie folgt aus:

module.exports = {
  entry: './entry',
 
  output: { path: 'output', filename: 'bundle.js' },
 
  plugins: [
    new ContextReplacementPlugin(
    /moment[\/\\]locale$/,
    /de|en/
    )
  ]
};
$ webpack
Hash: d6a652b194a14ca3d0a6
Version: webpack 1.4.15
Time: 141ms
Asset Size Chunks Chunk Names
bundle.js 101653 0 [emitted] main
[0] ./entry.js 70 {0} [built]
+ 3 hidden modules

Die Dateigröße ist damit deutlich kleiner geworden.

Um Webpack mit AngularJS nutzen zu können, sind am Code Änderungen nötig. Die Services, Controller und Direktiven müssen als ES6-Module gebaut sein:

/* @ngInject */
function UserService($resource, CrmService) {
  'use strict';
 
  return $resource('https://holisticon.centralstationcrm.net/api/users/:user_id.:format?active=true&apikey=' + CrmService.getApiKey(), {format: 'json'}, {
    get: {
      method: 'GET',
      isArray: true,
      interceptor: {
        'response': function (response) {
          response.data = response.data.map(function (x) {
            return x.user;
 
          });
          return response;
        }
      }
    }
  });
}
export default UserService;

Dabei sollte man die Annotation @ngInject bereits als Kommentar nutzen, damit die Injection später vom ngAnnotate-Plugin vorgenommen werden kann. Nun können wir ES6-Module in der app.js nutzen.

import angular from "angular";
import NavController from "./controllers/nav.controller";
import PreventDefaultDirective from "./directives/preventdefault.directive";
import UserService from "./services/user.service";
 
/* @ngInject */
(function (angular) {
  'use strict';
 
  angular.module('my App', [
    'ngResource',
    'ngSanitize',
    'ngRoute',
    'ngTouch',
  ])
    .constant('ENDPOINT_URI', '/rest')
    .controller('NavController', NavController)
    .directive('preventDefault', PreventDefaultDirective)
    .service('UserService', UserService);
}(angular));

Dabei sollte man keine Factory mehr nutzen, siehe dazu hier mehr.

Man kann auch seinen Build so weit auslagern, dass man lediglich ein NPM-Modul publiziert und dies in den jeweiligen Projekten als Abhängigkeit nutzt. Ein Beispiel dafür ist das AngularJS Common Build Module.

Über den Autor

Avatar

Martin Reinhardt arbeitet als Architekt bei der Management- und IT-Unternehmensberatung Holisticon AG. Er beschäftigt sich dort mit der Architektur von komplexen verteilten Systemen, modernden Webarchitekturen und Build Management. Martin engagiert sich in der Software-Craftsmanship-Bewegung. Er ist seit mehreren Jahren im Bereich der Java- und Webentwicklung tätig. Außerdem setzt er sich neben der Architektur vor allem für die Testautomatisierung in verschiedenen Technologien ein und ist auch in verschieden OpenSource-Projekten zu dem Thema wie z.B. dem Galen Framework tätig.

2 Kommentare

  1. Hey, danke fuer den Artikel. Kam grade zur richtigen Zeit, da wir einer aehnlichen Situation sind.
    Jetzt muessen wir noch das Karma coverage Problem angehen und dann passt es hoffentlich.

Antwort hinterlassen