#2 Webpack: Transpiling and Bundling
Monday, October 16, 2017
In this article we configure Webpack to handle all of our transpiling and bundling requirements. When this is done we will be able to transpile all of our .scss files for both the pages of our web application as well as for our Angular components. We will also be able to process all of our .pug (formerly known as jade) and typescript files. The results will be added to bundles that can be served with our page to drastically reduce the number of calls our pages need to make to the server. We will also configure the output of our bundles to perform minification for use in production.
Parts
- Part 30: User Database
- Part 29: Canonical Url
- Part 28: Razor Page Handlers
- Part 27: Send Screen Finished
- Part 26: Stroke Dashoffset
- Part 25: Send Screen
- Part 24: Forgot Link Behaviour
- Part 23: Animating Controls (cont.)
- Part 22: Animating Controls
- Part 21: Hiding Control Groups
- Part 20: ModelState Errors (cont.)
- Part 19: ModelState Errors
- Part 18: Animating Info Pages (cont.)
- Part 17: Animating Info Pages
- Part 16: Keyboard Navigation
- Part 15: Accessing the DOM
- Part 14: All About the Username
- Part 13: CSRF Attacks
- Part 12: Http Requests
- Part 11: HMR
- Part 10: Color Inputs And Buttons
- Part 9: Animating Sub-Actions
- Part 8: Form Validation (cont.)
- Part 7: Form Validation
- Part 6: Form Group
- Part 5: Authenticator Validators
- Part 4: Authenticator Inputs
- Part 3: First Angular Component
- Part 2: Webpack
- Part 1: Beginning
Correction
Depending on how you chose to install the npm packages from the previous article we may need to update the uglifyjs-webpack-plugin package.
First you will want to open the package.json file and check the version number of the package. If the version says '1.0.0-beta.3' then
everything is fine. If on the other hand it says something different, for example 0.4.6, then we need to update the package. We do this
by navigating a console to the root of our project and typing in npm uninstall uglifyjs-webpack-plugin --save
and pressing enter to uninstall the package and then typing in npm install uglifyjs-webpack-plugin@1.0.0-beta.3 --save
and pressing enter to install the version we need.
Importing
-
WebUi
- webpack.config.js
Now we turn to setting up our webpack.config.js file. We will use webpack to not only bundle our javascript and css but also transpile it from typescript and sass respectively using the appropriate loaders. The first thing we do is create a file named 'webpack.config.js' at the root of our project. Inside this file we start by importing the node specific variables and modules that we need (a). The environment variable will allow us to modify the configuration depending on whether we are running a development or production build and autoprefixer will take care of adding any vendor specific prefixes that we might need in our css. We also need to import webpack itself and its associated plugins. We can use the bundle analyzer plugin to see a visual representation of our bundles in order to determine if there are any opportunities to decrease their size, the extract text plugin will be used to remove the css styles, generated from our sass files, from our javascript bundles to create css bundles and we will use the uglify plugin when we create a production build in order to minify our output files.
webpack.config.js (environment and imports)
/*********************************
* Environment and imports
*********************************/
const environment = process.env.NODE_ENV || "development";
const autoprefixer = require("autoprefixer");
const webpack = require("webpack");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
Entry
Next we will create a couple of functions and our entry object (b) that will be used to specify the entry points of our application and thus specify the bundles that should be created. As has already been stated before we will use the Source folder to hold all of our front-end files. The sass folder, that we created previously, will contain all the style files that are not related to an Angular component and the app folder, that we also created previously, will likewise contain all of our typescript files not related again to an Angular component. It is important to note that the '.site' suffix of the scss files is important since we will use it to indicate that the file requires the special processing, via the extract text plugin, mentioned above.
webpack.config.js (entry)
/*********************************
* Entry
*********************************/
function siteScripts(name) {
return `./Source/app/${name}.site.ts`;
}
function siteStyles(name) {
return `./Source/sass/${name}.site.scss`;
}
function siteScriptsAndStyles(name) {
return [
siteScripts(name),
siteStyles(name)
];
}
const entry = {
"main": siteScriptsAndStyles("main"),
"vendor": "./Source/app/vendor.ts"
}
-
WebUi
-
Source
-
app
- main.site.ts
-
sass
-
2-base
- _variables.scss
- main.site.scss
-
2-base
-
app
-
Source
As you can see our entry object defines a couple of files, 'main.site.ts' (c) and 'main.site.scss' (d), that
we will create now. To begin with we will just add some dummy code to the main.site.ts so that we can see that our bundles are
being created correctly (I couldn't resist the urge to add the 'Hello World!' statement). And in the main.site.scss file we will import
'normalize.css' from our node_modules folder, the _bourbon-neat.scss file,
and '2-base/_base.scss' file which exports the contents of the 2-base folder. I realized at this point that I forgot to include the
_media-queries.scss file, that we created in the last article, to the _base.scss file. This will be the only time that we import
the _base.scss file anywhere so we will just import the media queries file here. You can of course place this import statement
inside the _base.scss file if you like. Next if we open the '2-base/_variables.scss' file we will see a
squiggle line under $font-stack-system
telling us that this variable has not been declared.
It is defined in a file contained within
the 0-bourbon folder. If we tried to build our bundles at this point the build would fail due to this undeclared variable so to fix it
we simply need to import our _bourbon-neat.scss file into the _variables.scss file (e).
main.site.ts
console.log("Hello World!");
main.site.scss
@import "../../node_modules/normalize.css/normalize.css";
@import "2-base/_base.scss";
@import "2-base/_media-queries.scss";
_variables.scss (add)
@import "../_bourbon-neat.scss";
Javascript Output
The following code (f) sets up the output object for our javascript bundles depending on the environment we are currently running. The result of using this object is our javascript files are output to the wwwroot/js folder where the template variable 'name' is extracted from the specified entry point for that particular set of files. And of course if we are running a production build the output will have the '.min' addition to the filename automatically. The path info option adds information, in the form of comments, in the bundles for the webpack modules that are created.
webpack.config.js (javascript output)
/*********************************
* Javascript Output
*********************************/
const output = {
filename: "./wwwroot/js/[name].bundle.js",
pathinfo: true
};
if (environment === "production") {
output.filename = "./wwwroot/js/[name].bundle.min.js";
output.pathinfo = false;
}
Plugins
Next we configure the webpack plugins (g) that we will be using. First we will use the commons chunk plugin to create our third party 'vendor' bundle. Once that is added we configure the extract text and the loader options plugin. Again the extract text plugin will output our css bundles with the appropriate name depending on environment and the loader options plugin will run autoprefixer on any css files that we create. In almost all, if not all, the projects I have done I have needed to inject some information from the node environment into the bundle created by webpack. In order to do this we will use the define plugin. For now we are just creating an object with the property 'process.env.NODE_ENV' and settings its value to the string value of the node variable of the same name. You can of course use this method to define whatever properties you like for use within your bundles. Finally if this is a production run we will use the uglify plugin to minify all of our files and the bundle analyzer plugin in order to analyze the size of our bundles.
webpack.config.js (plugins)
/*********************************
* Plugins
*********************************/
const plugins = [
new webpack.optimize.CommonsChunkPlugin({ name: "vendor", minChunks: Infinity }),
new ExtractTextPlugin({
filename: environment === "production" ? "./wwwroot/css/[name].bundle.min.css" : "./wwwroot/css/[name].bundle.css",
disable: false,
allChunks: false
}),
new webpack.LoaderOptionsPlugin({
options: {
postcss: [
autoprefixer()
]
}
}),
new webpack.DefinePlugin({
"process.env": {
"NODE_ENV": JSON.stringify(process.env.NODE_ENV)
}
})
];
if (environment === "production") {
plugins.push(
new UglifyJsPlugin({
uglifyOptions: {
mangle: {
keep_fnames: true
},
compress: {
warnings: false
},
output: {
comments: false
}
}
}),
new BundleAnalyzerPlugin());
}
Exports
Finally we have the configuration object (h) that is being exported from our webpack.config.js. We are configuring webpack to be able to handle: typescript, pug (formerly jade),
and scss (both for the site and angular components) files currently. If we need to support other types in the future we will modify this object accordingly.
To reiterate the note that was mentioned before, in the rules array we have set
up a test for /\.site.scss$/
to specify styles that are to be used in our html pages and not associated with an angular
component. A convention that I have adopted for my angular components is for their scss files to end with '.component.scss' which is handled by the
/\.component.scss$/
test.
webpack.config.js (exports)
/*********************************
* Exports
*********************************/
module.exports = {
entry: entry,
output: output,
plugins: plugins,
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
"ts-loader"
]
},
{
test: /\.pug$/,
exclude: /node_modules/,
use: [
"raw-loader",
"pug-html-loader"
]
},
{
test: /\.component.scss$/,
exclude: ["node_modules", "0-bourbon", "1-neat", "2-base"],
use: [
"raw-loader",
"postcss-loader",
"sass-loader"
]
},
{
test: /\.site.scss$/,
exclude: ["node_modules", "0-bourbon", "1-neat", "2-base"],
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: [
"css-loader",
"postcss-loader",
"sass-loader"
]
})
}
]
}
}
PostCss Setup
-
WebUi
- postcss.config.js
Next we need to create a small configuration file (i), again at the root of the project, named 'postcss.config.js' with the following content. A detailed description of the contents can be found at https://github.com/postcss/postcss-loader.
postcss.config.js
module.exports = {
plugins: {
"autoprefixer": {}
}
}
Compiling Our Bundles
-
WebUi
- package.json
Before we can test to see if our configuration is working we need to modify our package.json file by replacing the scripts object with
code shown in (j). We are defining three different commands (build, build:production and watch) that can be executed by typing
npm run <command>
in a console window.
package.json (scripts)
"scripts": {
"build": "set NODE_ENV=development&&webpack --progress",
"build:production": "set NODE_ENV=production&&webpack --progress -p",
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "set NODE_ENV=development&&webpack --progress --watch"
}
Finally we can test to make sure that our configuration is working. First we will create our development bundles by typing into our
console, making sure we are in the root of the project, npm run build
which should output something
close to image (k). And now we can create our production build bundles using npm run build:production
,
shown in image (l).
As we can see in (l) the production size of our vendor bundle comes out to 1.17 MB which of course is large and we would very much like to reduce it. Our first step in reducing the size of the bundle is to analyze the size of each of the imports in our vendor.ts and thanks to the bundle analyzer that we included in our webpack.config.js we can do just that. Once the production build is complete a new window/tab will open in your browser showing a visualization of our bundle sizes (m).
-
WebUi
-
Source
-
app
- vendor.ts
-
app
-
Source
By mousing over the bundle analyzer window that was opened in our browser we can see some of the module sizes that we are dealing with are angular ~700kb, rxjs ~200kb, lodash ~70kb, corejs ~70kb, web animations ~50kb, zonejs ~40kb and a other smaller miscellaneous modules. Of course minimizing the size of our bundles is an on going effort and our first step in doing it is to return to our vendor.ts file and modifying the importing of lodash. Instead of importing all of its contents, like we were doing initially, we will only import the parts that we need (n).
vendor.ts (modified)
import "core-js/es6";
import "core-js/es7/reflect";
import "zone.js/dist/zone";
if (process.env.ENV !== "production") {
Error["stackTraceLimit"] = Infinity;
require("zone.js/dist/long-stack-trace-zone");
}
import "@angular/platform-browser";
import "@angular/platform-browser-dynamic";
import "@angular/core";
import "@angular/common";
import "@angular/http";
import "@angular/router";
import "@angular/forms";
import "classlist-polyfill";
import "lodash/each";
import "rxjs";
import "web-animations-js";
As you can see we are now just importing the each function from lodash. I know we will need this function latter on and as our needs change we will return here and add any additional imports required. This change will have a relatively modest effect on the size of our production bundle but once we have some angular code written, so that we can test our changes, we will return to our vendor.ts file and see what other changes we can make.
-
WebUi
-
Source
-
app
- main.site.ts
-
app
-
Source
Now that we are importing only the each function from lodash we want to test to make sure it is working and to that we will modify our main.site.ts to make sure that we have access to it. We do this by importing the function and including another console statement shown in (o).
main.site.ts (modified)
import each = require("lodash/each");
console.log("Hello World!");
each(["test1", "test2"], (str: string) => {
console.log(str);
});
Does everything work?
The last thing for us to do is test that everything is working and to do that we simply need to start our web server and navigate to the appropriate address. If you are developing this in Visual Studio we just need to go the Debug menu located in the menubar and choose 'Start Without Debugging' or press Ctrl+F5 otherwise you can navigate a console to the WebUi project and enter 'dotnet run' and press enter. Once the project is built, if you are starting the web server through the console you will need build the project yourself, the website will open in your default browser. The page will be almost blank except for the footer and if we look in the developer tools we should see the output from our main.bundle.js file. In the next article we will actually get to start doing some programming.