This article demonstrates how to set up an Angular2 project with Webpack. The setup also supports Typescript, Less, CSS packages like Twitter Bootstrap and Fonts e.g. FontAwesome.
It describes how to set up all the different plugins, loaders etc. I needed for a real Angular2 application. Most articles I read so far only concentrate on one small part of a complete setup. If you think I forgot to mention an elementary part of the setup, please let me know.
Required NPM Packages
All required dependencies for Webpack are put into package.json as devDependencies, so that NPM will automatically install them.
package.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
{ "version": "1.0.0", "name": "faces", "dependencies": { "@angular/common": "^2.0.0-rc.1", "@angular/compiler": "^2.0.0-rc.1", "@angular/core": "^2.0.0-rc.1", "@angular/http": "^2.0.0-rc.1", "@angular/router": "^2.0.0-rc.1", "@angular/platform-browser": "^2.0.0-rc.1", "@angular/platform-browser-dynamic": "^2.0.0-rc.1", "core-js": "2.2.2", "rxjs": "5.0.0-beta.6", "reflect-metadata": "0.1.3", "zone.js": "0.6.12" "jquery": "2.2.3", "bootstrap": "3.3.6", "font-awesome": "4.6.1" }, "devDependencies": { "ts-loader": "0.8.2", "typescript": "1.8.10", "typings": "0.7.12", "webpack": "1.12.15", "es6-promise": "3.1.2", "es6-shim": "0.35.0", "extract-text-webpack-plugin": "1.0.1", "file-loader": "0.8.5", "gulp": "3.9.1", "less": "^2.6.1", "less-loader": "2.2.3", "style-loader": "0.13.1", "url-loader": "0.5.7", "webpack-merge": "0.8.4", "webpack-stream": "3.1.0" } } |
After all the development dependencies are installed using NPM, we can start to configure Webpack itself.
Webpack Configuration
webpack.config.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
var webpack = require('webpack'); var path = require('path'); var ExtractTextPlugin = require("extract-text-webpack-plugin"); // Webpack Config var webpackConfig = { entry: { 'polyfills': './wwwroot/polyfills.ts', 'vendor': './wwwroot/vendor.ts', 'app': './wwwroot/app.ts', 'vendor-style': './wwwroot/style/vendor-style.ts', 'app-style': './wwwroot/style/app-style.ts', }, devtool: 'source-map', cache: true, output: { path: './wwwroot/dist', filename: '[name].bundle.js', sourceMapFilename: '[name].map', chunkFilename: '[id].chunk.js' }, resolve: { extensions: ['', '.ts', '.js'] }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ name: ['app', 'vendor', 'polyfills'], minChunks: Infinity }), new ExtractTextPlugin("[name].css"), new webpack.optimize.DedupePlugin() ], module: { loaders: [ // .ts files for TypeScript { test: /\.ts$/, loader: 'ts-loader' }, { test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader") }, { test: /\.less$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader!less-loader") }, { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file" }, { test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, loader:"url?prefix=font/&limit=5000" }, { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=application/octet-stream" }, { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&mimetype=image/svg+xml" } ] } }; var webpackMerge = require('webpack-merge'); module.exports = webpackMerge(webpackConfig); |
A couple of explanations for this configuration file. At least in my example Angular2 application, packing all the files up takes around 30 seconds. If you have to wait for 20 seconds to pack all files after each change would be very time consuming. By the way, all the configuration files below should be places in the root folder of your Angular2 application.
Splitting the App into Chunks
So a better approach is to split up the application into polyfills, vendor files and the application itself. This way only your application has to be re-packed after each change, which saves a lot of time.
Therefore the Webpack configuration file contains the following line:
1 |
new webpack.optimize.CommonsChunkPlugin({ name: ['app', 'vendor', 'polyfills'], |
Webpack builds the graph of dependencies for each of these entry points. Dependencies that are already included in polyfills will not be put into the vendor file again. Dependencies that are included in the vendor or polyfills files will not be additionally put into the app file.
So how do the polyfills file and the vendor file look like:
polyfills.ts
1 2 |
import 'core-js'; import 'zone.js/dist/zone'; |
vendor.ts
1 2 3 4 5 6 7 8 9 |
import "@angular/common"; import "@angular/compiler"; import "@angular/core"; import "@angular/http"; import "@angular/router"; import "@angular/platform-browser"; import "@angular/platform-browser-dynamic"; import 'rxjs/Rx'; |
In order to be able to compile TypeScript with Webpack you also have to include a special loader to cope with .ts files. This is done by the following line with in loader section of the Webpack configuration.
1 |
{ test: /\.ts$/, loader: 'ts-loader' }, |
These loaders are contained within separate NPM packages listed in the devDependencies section in the package.json file
Support for LESS and CSS
The configuration file also contains a couple of other loader for less and css files like:
1 2 3 4 5 6 7 8 |
{ test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader") }, { test: /\.less$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader!less-loader") }, |
In order to support packaging of font files mentioned within less or css files, I also added a couple of additional loaders for these files e.g.
1 |
{ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file" }, |
These contain a regular expression for a version that is often times appended to the filename of a font file e.g. within bootstrap.
In addition to the loaders, I also added two additional entry points for vendor styles and app styles.
vendor-style.ts
1 2 |
import 'bootstrap/dist/css/bootstrap.css'; import 'font-awesome/less/font-awesome.less'; |
app-style.ts
1 |
import './app.less'; |
TypeScript Configuration
For the matter of completeness, I’m also including my TypeScript configuration file tsconfig.json here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{ "compilerOptions": { "module": "commonjs", "target": "es5", "outDir": ".", "rootDir": ".", "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "moduleResolution": "node" }, "exclude": [ "typings/main.d.ts", "typings/main", "node_modules" ], "compileOnSave": false, "buildOnSave": false, "atom": { "rewriteTsconfig": false } } |
Typings Configuration
Typings is the successor of tsd. It is used to get TypeScript definitions files for libraries that are coded in pure JavaScript. You can use the typings command line tool to pull in different typing definitions for additional libraries. Afterwards you will have a typings configuration file within your project that looks like this:
typings.json
1 2 3 4 5 6 7 8 9 10 |
{ "name": "myapp", "dependencies": { }, "ambientDependencies": { "es6-collections": "registry:dt/es6-collections#0.5.1+20160215162030", "es6-promise": "registry:dt/es6-promise#0.0.0+20160221190517", "jquery": "registry:dt/jquery#1.10.0+20160316155526" } } |
This will give you support e.g. for jQuery within TypeScript (autocompletion, syntax checking etc.). Little tip here… often times you have to append a parameter –ambient to the typings command line tool to pull in a definition file for a dependency. Otherwise the dependency cannot even be found. Please take a look at
https://github.com/Microsoft/TypeScript/blob/master/doc/spec.md#1.1
to read more about what ambient declarations actually are.
Packing using Webpack
First install Webpack using NPM. After that you can start packing by entering the command:
1 |
webpack -d |
This will compile the export packages in development mode. This also includes the generation of map files for all the output files. That way, the code can later on be nicely debugged in your browser. If you use the command line parameter -p for production mode, the files are minified and the output does not contain map files.
Output:
The bundles, font files etc. are all exported into the destination folder specified in the Webpack configuration file.
You will notice that compiling still takes a lot of time if you try this example. So why did we split up the files into chunks in the first place? Because you can start Webpack in watch mode.
1 |
webpack -d -w |
This will first do a full packing of all packages. But after the full compilation is done Webpack remains in watch mode. Every time a file is changed on the file system, all affected bundles are rebuild. Since we have splitted up the code into polyfills, vendor files and the app itself, most of the time only the app bundle itself will be rebuild. This can be done quite fast. For me in about a second.
So during development I recommend to keep Webpack in watch mode.
Consuming the Output in the Html Page
The bundles then simply have to be included in your Html page as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<html> <head> <title>MyApp</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0, maximum-scale=1.0" /> <meta name="apple-mobile-web-app-capable" content="yes"> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <link rel="stylesheet" href="dist/vendor-style.css" /> <link rel="stylesheet" href="dist/app-style.css" /> </head> <body> <myapp> Loading... </myapp> <script src="dist/polyfills.bundle.js"></script> <script src="dist/vendor.bundle.js"></script> <script async src="dist/app.bundle.js"></script> </body> </html> |
There are also additional plugins for Webpack, so that you do not have to manually include the bundles within an HTML page yourself. Since these files probably will never in my setup, I don’t mind including these manually once.
Remarks About the Output Size
In my example the output sizes of the bundles are still quite big. In development mode ca. 4Mb and in production mode 1MB. However, after these few files are initially loaded, your single page applications does not need any further dependencies. If someone can point me into the right direction how to further reduce the size of the output, this would be great!
Summary
This article shows how to bundle an Angular2 project. It took me quite some time to put the different pieces together. Hopefully this will save time for other developers during the setup of an Angular2 project. If you have any questions or remarks, please leave a comment below!
Thanks, good article.
I’m also interested in reducing the size of the bundle. It’s 2.4 MB in my case.
In my application the bundle also has a size of ca. 2.4MB in develop mode (with comments etc.) and ca. 850k in production mode (webpack -p, uglified and stripped of comments) .
I looked inside the file and I don’t think you can compress it any further with the given tools. Apparently all the angular2 and RxJS code needs at least the 850k.
Thank you @lindner. This a great article. I like the way it’s broken down. I started the dive into Angular 2 a few months ago. This really helps. Thank you.
Concerning the bungle size: I read http://www.netinstructions.com/100-days-of-angular-2/ where gzip was discussed to really minimized the bungle size. I’m definitely do that.