Advertisement

#2 Webpack: Transpiling and Bundling

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.

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");
(a) Importing the node specific variables and modules as well as webpack and its plugins.

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"
}
(b) Helper functions and entry object that define the entry points for webpack.
  • WebUi
    • Source
      • app
        • main.site.ts
      • sass
        • 2-base
          • _variables.scss
        • main.site.scss

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!");
(c) The beginning contents of our main.site.ts file.

main.site.scss

@import "../../node_modules/normalize.css/normalize.css";

@import "2-base/_base.scss";
@import "2-base/_media-queries.scss";
(d) The beginning contents of our main.site.scss file.

_variables.scss (add)

@import "../_bourbon-neat.scss";
(e) Importing statement that needs to be placed at the top of the _variables.scss file.

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;
}
(f) The output object used to generate our bundles depending on environment and entry point.

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());
}
(g) The webpack plugins that we will need to start our project.
Advertisement

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"
                    ]
                })
            }
        ]
    }
}
(h) The webpack configuration object.

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": {}
    }
}
(i) The configuration file for post css loader.

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"
}
(j) Updated package.json script object.

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).

Console output of the webpack development build.
(k) Webpack development build output.
Console output of the webpack production build.
(l) Webpack production build information.

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).

(m) Visualization of the imports contained within our vendor.bundle.min.js file.
  • WebUi
    • Source
      • app
        • vendor.ts

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";
(n) Modified vendor.ts showing the importing of individual packages for lodash.

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

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);
});
(o) Modified contents of our main.site.ts file in order test if we still have access to the each function from lodash.

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.

Exciton Interactive LLC
Advertisement