Introduction
This is a note on Webpack, @Angular and miscellaneous topics. In this note, I will give two examples to start an Angular application. One uses the "systemjs
", the other one uses webpack.
Background
Angular CLI can pack an Angular SPA for deployment to address its long blamed extremely large "node_modules" directory. Although "ng build
" is a simple command, it is still nice to manually set up webpack to pack an Angular application by ourselves.
- We will know what to pack in an Angular application
- We will know how to pack an Angular application by webpack and gain some insight on how Angular CLI works
I will use Node as the web server for the examples in this note.
Which package.json & Which TSC Compiler
Before looking into the examples, I will talk about a simple yet constantly overlooked topic with NPM/Node.
- How to specify the "package.json" location when we run "
npm install
" - If we have both global and project level "
devDependencies
" installed for "typescript", how can we make sure which one is used to compile the typescript code?
Attached in the zip file "which-p-which-c" has a simple "package.json" file.
{
"name": "which-p-which-c-client",
"version": "0.0.1",
"private": true,
"scripts": {
"tsc": "tsc"
},
"devDependencies": {
"typescript": "~2.4.2"
}
}
This "package.json" file is in the "which-p-which-c" -> "client" directory.
Which is the package.json
If we run "NPM" from the "which-p-which-c" directory, we can issue the following command:
npm -prefix client install
The "-prefix client
" argument tells NPM to look for the "package.json" file in the "client" directory and put the "node_modules" directory in the "client" directory.
Which is the tsc Compiler
In order to check which "tsc
" is used, we can first install "tsc
" globally.
npm install typescript@2.6.2 -g
and then issue the command to check the "tsc
" version.
tsc -v
The version is "Version 2.6.2", which tells us that we are using the global installation. If we want to use the "typescript~2.4.2" downloaded into the "node_modules" directory as a "devDependencies
", we can issue the following command:
npm -prefix client run tsc -- -v
- The "
-prefix client
" tells NPM to look for the "package.json" file in the "client" directory and run the "tsc
" script. - The "
--
" allows us to specify further parameters to the "tsc
". In this case, we just want "-v
" to print out the version.
The version for the "tsc
" now is "Version 2.4.2", which confirms that the project level installation is used.
The Good/Old "systemjs"
In the recent versions of Angular, the preferred way of managing an Angular SPA is the Angular CLI. But by my experience with the attached zip file, "the-good-old-systemjs", the "systemjs" way to start an Angular application still works for Angular 5. The reason to create an example using "systemjs" is to have a better understanding on what we need to pack for an Angular application when webpack is used.
The attached is a simple Node
application. In the "app.js", we tell Node
to serve static content from the "client" directory.
{
"name": "the-good-old-systemjs",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "4.16.2",
"errorhandler": "1.5.0"
}
var express = require('express'),
http = require('http'),
path = require('path'),
errorhandler = require('errorhandler');
var app = express();
app.set('port', 3000);
app.use(function (req, res, next) {
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
res.header('Expires', '-1');
res.header('Pragma', 'no-cache');
next();
});
app.use(express.static(path.join(__dirname, 'client')));
app.use(errorhandler());
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
The "client" directory is the Angular application that we will use to figure out the critical pieces for an Angular application to run.
This example application has 2 Angular components:
- The "
app.component
" is the top level component that will be "bootstrapped" in the "app.module
". It uses the "sub.component
" to display some text message. - The "
sub.component
" uses the "text.service
" to get some text and binds it to the UI.
Since this is a standard Angular application, I will only list the important things in this note. The following is the "sub.component.ts".
import { Component, OnInit, Inject } from '@angular/core';
import { TextService } from '../../services/text.service';
@Component({
moduleId: module.id,
providers: [TextService],
selector: 'sub-component',
templateUrl: './sub.component.html',
styleUrls: ['./sub.component.css']
})
export class SubComponent implements OnInit {
public Text: string = 'This is OK';
constructor(@Inject(TextService) private textService: TextService) { }
ngOnInit(): void {
this.Text = this.textService.getText();
}
}
and the following is the "text.service.ts" that the "sub.component.ts" uses to get the text to display.
import { Injectable } from '@angular/core';
@Injectable()
export class TextService {
constructor() { }
getText() {
return 'Hello from NG 5 ...';
}
}
This Angular application is started from the "main.ts" and "boostrapped" into the "index.html" through the "systemjs.config.js".
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
enableProdMode();
let platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular-example</title>
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<script src="/node_modules/zone.js/dist/zone.js"></script>
<script src="/systemjs.config.js"></script>
<script>
System.import('app').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<app></app>
</body>
</html
(function (global) {
System.config({
paths: { 'npm:': '/node_modules/' },
map: {
'app': '/app/',
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser':
'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic':
'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'rxjs': 'npm:rxjs'
},
packages: {
app: { main: './main.js', defaultExtension: 'js' },
rxjs: { defaultExtension: 'js' }
}
});
})(this);
To run the application, you need to issue the following commands to install the "node_modules" directory for both the server and the client directories.
npm install
npm -prefix client install
You will also need to compile the "typescript" files by the following command:
npm -prefix client run tsc
To start the Node
server, you can issue the following command:
node app.js
If you take a look at the network traffic to display this simple page, Angular actually made 56 requests to the server.
Webpack and @Angular
By going through the example to load an Angular application by "systemjs", we can find that the 56 files except the "index.html" belong to 3 categories.
- The polyfills - In my example, I only added the "zone.js". If you want to support more browsers, you may need to add more polyfill files.
- The JavaScript files from the "
node_modules
" that are used by the application code. - The application files created by ourselves. They are the JavaScript, HTML, and CSS files created by us that the "main.ts" file directly and indirectly depends on.
The attached zip file "the-webpack-ng
" is exactly the same Angular application but we will use webpack to pack and start it.
With the 3 categories of files in mind, we are ready to use webpack to bundle them. To use webpack, we need to add a few more "devDependencies
" besides the "typescript
" and "@types/node" in the "package.json".
{
"name": "the-good-old-systemjs-client",
"version": "0.0.1",
"private": true,
"scripts": {
"tsc": "tsc",
"webpack": "webpack --config ./webpack.config.js"
},
"dependencies": {
"@angular/animations": "^5.0.0",
"@angular/common": "^5.0.0",
"@angular/compiler": "^5.0.0",
"@angular/core": "^5.0.0",
"@angular/forms": "^5.0.0",
"@angular/http": "^5.0.0",
"@angular/platform-browser": "^5.0.0",
"@angular/platform-browser-dynamic": "^5.0.0",
"@angular/router": "^5.0.0",
"core-js": "^2.4.1",
"rxjs": "^5.5.2",
"zone.js": "^0.8.14",
"systemjs": "0.20.19"
},
"devDependencies": {
"typescript": "~2.4.2",
"@types/node": "~6.0.60",
"webpack": "2.2.1",
"html-webpack-plugin": "^2.16.1",
"awesome-typescript-loader": "^3.0.4",
"raw-loader": "^0.5.1",
"file-loader": "^0.9.0",
"html-loader": "^0.4.3",
"css-loader": "^0.26.1",
"style-loader": "^0.13.1",
"extract-text-webpack-plugin": "2.0.0-beta.5",
"angular2-template-loader": "^0.6.0"
}
}
The "polyfills.ts" specifies what the polyfills are. In this example, I only added the "zone.js" in it.
import 'zone.js/dist/zone';
The "vendor.ts" specifies the entry points of the files that are needed from the "node_modules" directory to run the Angular application.
import '@angular/core';
import '@angular/common';
import '@angular/platform-browser';
import '@angular/platform-browser-dynamic';
import '@angular/http';
import '@angular/router';
import '@angular/forms';
import 'rxjs';
The "webpack.config.js" file tells webpack how we want it to pack the application.
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var path = require('path');
module.exports = {
entry: {
'polyfills': path.join(__dirname, './app/webpack/polyfills.ts'),
'vendor': path.join(__dirname, './app/webpack/vendor.ts'),
'app': path.join(__dirname, './app/main.ts')
},
output: {
path: path.join(__dirname, './dist'),
filename: '[name].bundle.js'
},
resolve: {
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
loaders: [
{
loader: 'awesome-typescript-loader',
options: { configFileName: path.join(__dirname, './tsconfig.json') }
} , 'angular2-template-loader'
]
},
{
test: /\.html$/,
loader: 'html-loader'
},
{
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
loader: 'file-loader?name=assets/[name].[hash].[ext]'
},
{
test: /\.css$/,
exclude: path.join(__dirname, './app'),
loader: ExtractTextPlugin.extract({ fallbackLoader: 'style-loader',
loader: 'css-loader?sourceMap' })
}
,{
test: /\.css$/,
include: path.join(__dirname, './app'),
loader: 'raw-loader'
}
]
},
plugins: [
new webpack.ContextReplacementPlugin(
/angular(\\|\/)core(\\|\/)@angular/,
path.join(__dirname, './app'),
{}
),
new webpack.ContextReplacementPlugin(
/(.+)?angular(\\|\/)core(.+)?/,
path.join(__dirname, './app'),
{}
),
new webpack.optimize.CommonsChunkPlugin({
name: ['app', 'vendor', 'polyfills']
}),
new webpack.optimize.UglifyJsPlugin()
]
};
The "webpack.config.js" is a complex configuration file, but the important things that can get us started are the following:
- The "
entry
" attribute tells webpack what to bundle. In this example, we want webpack to bundle the polyfills, the "node_modules" files required by the application, and all the application files used by the "main.ts" file; - The "
output
" attribute tells webpack where to put the bundles and the name of the bundle files; - The "
CommonsChunkPlugin
" tells webpack to remove any files in the vendor bundle that have already packed in the "polyfill
" bundle, and to remove any files in the "app
" bundle that have already packed in the vendors bundle, although we know that the polyfills and vendors bundles do not actually share any files. - The "
UglifyJsPlugin
" tells webpack to minimize the size of the bundles.
The "index.html" file uses the bundles to start the Angular application.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular-example</title>
<script src="/dist/polyfills.bundle.js"></script>
<script src="/dist/vendor.bundle.js"></script>
</head>
<body>
<app></app>
</body>
<script src="/dist/app.bundle.js"></script>
</html>
It is important to know that we need to remove "moduleId: module.id
" from the Angular component definition. There is a slight difference between how webpack and "systemjs" start an Angular application.
To run the application, you need to issue the following commands to install the "node_modules" directory for both the server and the client directories.
npm install
npm -prefix client install
You will also need to create the bundle files by the following command:
npm -prefix client run webpack
To start the Node server, you can issue the following command.
node app.js
When you load the application into the browser, you will find that the number of files downloaded reduced to 4 from 56 and the application starts a lot faster. It is important to note that we do not have to separate the vendors bundle from the app bundle. We can simply create a single app bundle to serve the Angular application, if we have no intention to share the vendors bundle among different SPAs.
The "moduleId: module.id"
The webpack reduces the package size. But if we want the components to work in webpack, we need to remove the "moduleId: module.id
" from the "@Component
" decoration. If we want the components to work in both the webpack package and the "systemJs
" environments, we can use the "string-replace-loader" (Credit - Stackoverflow).
"string-replace-loader": "1.3.0"
We can add the "string-replace-loader
" in the package.json file as one of the "devDependencies
". In the webpack configuration file, we can use it in the "module
" section to remove the "moduleId: module.id
" from the package.
module: {
rules: [
{ test: /\.ts$/,
loader: 'string-replace-loader',
include: path.join(__dirname, './app'),
query: { search: 'moduleId: module.id,', replace: '' }
},
{
test: /\.ts$/,
loaders: [
{
loader: 'awesome-typescript-loader',
options: { configFileName: path.join(__dirname, './tsconfig.json') }
} , 'angular2-template-loader'
]
},
Other rules ...
]
}
This solution may not be absolutely the best, but it at least allows us to keep the "moduleId: module.id
" in the components.
Bundle Images in CSS
Sometimes, you may have image references in your component level CSS files. For example, if the "sub.component.css" references the "blue.jpg" file, we will need to bundle the image in the package.
.greeting {
background-image: url("./images/blue.jpg");
font-family: verdana;
font-size: 24px;
}
To bundle the image file "blue.jpg", we can use the "url-loader
". The following is the "webpack.config.js" file.
var webpack = require('webpack');
var path = require('path');
module.exports = {
entry: {
'app': path.join(__dirname, './app/main.ts')
},
output: {
path: path.join(__dirname, './dist/app'),
filename: '[name].bundle.js'
},
resolve: { extensions: ['.ts', '.js'] },
module: {
rules: [
{
test: /\.ts$/,
loaders: [
{
loader: 'awesome-typescript-loader',
options: { configFileName: path.join(__dirname, './tsconfig.json') }
} , 'angular2-template-loader'
]
},
{
test: /\.html$/,
loader: 'html-loader'
},
{
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
loader: 'url-loader'
},
{
test: /\.css$/,
include: path.join(__dirname, './app'),
use: [ 'to-string-loader', 'css-loader' ]
}
]
},
plugins: [
new webpack.ContextReplacementPlugin(
/angular(\\|\/)core(\\|\/)@angular/,
path.join(__dirname, './app'),
{}
),
new webpack.ContextReplacementPlugin(
/(.+)?angular(\\|\/)core(.+)?/,
path.join(__dirname, './app'),
{}
),
new webpack.optimize.UglifyJsPlugin()
]
};
We need the following "devDependencies
" in the "package.json".
"devDependencies": {
"typescript": "~2.4.2",
"@types/node": "~6.0.60",
"webpack": "3.10.0",
"awesome-typescript-loader": "^3.0.4",
"angular2-template-loader": "^0.6.0",
"file-loader": "^0.9.0",
"html-loader": "^0.4.3",
"css-loader": "^0.26.1",
"to-string-loader": "1.1.5",
"url-loader": "0.6.2"
}
Upon a successful run of the webpack, we can load the packed JavaScript file into the "index.html". We can see that the background image is successfully applied to the Angular component.
If you now inspect the Angular component in the browser, you can see that the image file is Base64 encoded into the CSS and packed into the bundle.
Webpack Performance
Webpack does a lot of work to pack all the required files into a bundle, so it will take some time to finish. But if you start it with a watcher, it is optimized for the speed after the first run.
webpack --config ./webpack.config.js --watch
If you still feel that the speed is not enough, you can remove the line in the configuration file and give the minification task to the release build.
new webpack.optimize.UglifyJsPlugin()
Eclipse IDE & "node_modules"
If you want to load the projects into Eclipse, you may want to exclude the "node_modules" and "dest" directories in the resource filters. You can right click on the project -> Properties -> Resource -> Resource Filters to add the filters. The "node_modules" directory can get too large for Eclipse to handle and it is very common that we will never need to look into these directories.
Points of Interest
- This is a note on Webpack, @Angular and miscellaneous topics;
- Although Angular CLI is the preferred way to manage an Angular application, it is still nice that we can set up webpack by ourselves, so we know exactly how Angular CLI builds the applications.
- I hope you like my postings and I hope this note can help you one way or the other.
History
- 12/6/2017: First revision