Angular Build with Webpack from Scratch – Part 2

Angular Build with Webpack Hero

Derived from photo by Yann Caradec / flickr.com, CC BY-SA

This is the second post in a two-part series getting started with webpack from scratch. The first part covered how to configure the Angular-specific portion of a webpack build. This post walks through how to configure the HTML, CSS, and development server. It builds upon the configuration started in part one.

If you want to jump straight to the code, the completed demo is on GitHub.

Webpack Context

As webpack begins to handle more of the application, it becomes necessary to configure multiple entry points for the build. Part one configures the full path for the single entry file. You could follow the same pattern with any new entry files. You could instead set the context configuration property.

The context property sets a directory as the location where file paths resolve. Instead of combining the paths of each entry file, you configure the base path and then specify the individual file names. This is how part one configured the single entry for webpack:

const path = require('path');
const source = path.resolve(__dirname, 'src', 'index.ts');

module.exports = {
    entry: source,
    // ...
};

To prepare for multiple entries, refactor the entry to use the context property as well:

const path = require('path');
const source = path.resolve(__dirname, 'src');

module.exports = {
    context: source,
    entry: [
        './index.ts'
    ],
    // ...
};

Note: The __dirname variable is a string from Node.js providing the current module's directory.

Using the context property is optional but it helps to remove repetitive path combining logic from your configuration.

HTML Build

Single-page applications are named this way because they typically contain one HTML document that loads the scripts to bootstrap the application. Angular is no exception and there is a handy plugin for webpack called HtmlWebpackPlugin to dynamically configure the HTML page.

The initial configuration for HtmlWebpackPlugin is straightforward. Import the module from its npm package, html-webpack-plugin, and then add it to the plugins property of the webpack configuration.

const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    // ...
    plugins: [
        new HtmlWebpackPlugin()
    ]
};

By default, the plug-in gives you a boilerplate HTML file for your application. You can however, supply it with a template through the module's constructor arguments. Keep in mind that when using a template, the HtmlWebpackPlugin automatically adds a script element referencing the output of the webpack build so ensure you haven't added the script reference multiple times. This is how to specify the template for HtmlWebpackPlugin:

module.exports = {
    // ...
    plugins: [
        new HtmlWebpackPlugin({
            template: './index.html'
        })
    ]
};

Now you have a webpack build that includes the JavaScript and HTML for your application where the bundle is automatically referenced in the output index.html file. Try it by running npm run build at the command line and inspect the output in the dist directory. The HtmlWebpackPlugin has many great extensions. You can find some of them listed in the project's README file.

CSS Build

The final portion of the Tour of Heroes to address is the global CSS. In the first part of this series, you configured webpack to load any CSS files that are referenced by Angular components. However, the application has global CSS defined in the styles.css file. This file includes style rules relevant to the overall look and feel of the application so it makes sense to keep this outside of the individual components` CSS.

The configuration already processes CSS for the components but this configuration won't work for the global CSS. The first thing is to define the boundary between when webpack should process CSS for components and when webpack should process the global CSS. Looking at the application, everything in the directory .\src\app\ needs to be configured for the angular2-template-loader where other CSS in the .\src\ directory should be bundled and included as a linkelement in the index.html file.

Define a variable for the .\src\app\ path and use the include property on the raw-loader configuration:

const source = path.resolve(__dirname, 'src');
const appDirectory = path.resolve(source, 'app');

module.exports = {
    // ...
    module: {
        rules: [
            // ...
            {
                test: /\.(css|html)$/, 
                include: appDirectory,
                loader: 'raw-loader'
            },
            // ...
        ]
    },
    // ...
};

To process the non-component CSS, use the ExtractTextPlugin. This plugin is essentially a wrapper that provides bundling functionality. It leverages other loaders' import logic to load the CSS content. In the end, you get a combined CSS file. Import the plugin from its npm package, extract-text-webpack-plugin. You also use the style-loader and css-loader npm packages for their respective CSS loaders.

Add the following to the rules array of the webpack configuration:

const ExtractTextPlugin = require('extract-text-webpack-plugin');

const source = path.resolve(__dirname, 'src');
const appDirectory = path.resolve(source, 'app');

module.exports = {
    // ...
    module: {
        rules: [
            // ...
            {
                test: /\.css$/,
                exclude: appDirectory,
                loader: ExtractTextPlugin.extract({ 
                    fallback: 'style-loader', 
                    use: 'css-loader?sourceMap' 
                })
            },
            // ...
        ]
    },
    // ...
};

You see this configuration loads files ending in the .css extension and excludes the appDirectory CSS. Remember, the appDirectory path contains your Angular code and undergoes a different build process than global styles.

The ExtractTextPlugin is configured as both a loader and a plug-in. To configure the plug-in portion of ExtractTextPlugin, add it to the plugins array. Provide a name for the final output bundle, for example styles.css.

module.exports = {
    // ...
    plugins: [
        // ...
        new ExtractTextPlugin('styles.css')
    ]    
};

Note: This tutorial uses styles.css as both the name of the input file and the output file. Feel free to customize this to help ensure you understand which piece of the configuration is related to inputs and which is related to outputs.

Finally, webpack needs to know about the existing Tour of Heroes styles.css file so add it as an entry in the configuration:

module.exports = {
    context: source,
    entry: [
        './index.ts',
        './styles.css'
    ],
    // ...
);

Now that the CSS is auto-generated, the HtmlWebpackPlugin and ExtractTextPlugin work together to add a link to the index.html template file. Remove any existing links to styles.css from the index.html file.

The ExtractTextPlugin has several different options and uses so it's worth taking a look at its documentation.

At this point, your webpack build is complete. Try it with npm run build to see the output. The final HTML template and webpack configuration should have the following contents:

<html>
<head>
  <base href="/">
  <title>Angular 2 Tour of Heroes</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
  <my-app>Loading...</my-app>
</body>

</html>
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

const source = path.resolve(__dirname, 'src');
const destination = path.resolve(__dirname, 'dist');
const appDirectory = path.resolve(source, 'app');

module.exports = {
    context: source,
    entry: [
        './index.ts',
        './styles.css'
    ],
    output: {
        filename: 'index.js',
        path: destination
    },
    resolve: {
        extensions: ['.ts', '.js']
    },
    module: {
        rules: [
            {
                test: /\.ts$/,
                loaders: [
                    'awesome-typescript-loader',
                    'angular2-template-loader'
                ]
            }, 
            {
                test: /\.ts$/,
                loader: 'webpack-replace',
                query: {
                    search: 'moduleId: module.id,',
                    replace: ''
                }
            },
            {
                test: /\.css$/,
                exclude: appDirectory,
                loader: ExtractTextPlugin.extract({ 
                    fallback: 'style-loader', 
                    use: 'css-loader?sourceMap' 
                })
            },
            {
                test: /\.(css|html)$/, 
                include: appDirectory,
                loader: 'raw-loader'
            },
        ]
    },
    plugins: [        
        new HtmlWebpackPlugin({
            template: './index.html'
        }),
        new ExtractTextPlugin('styles.css')
    ]
};

Development Server

The webpack development server integrates with webpack to provide many great features to aid your development. This functionality is installed with the webpack-dev-server npm package. The server automatically watches for changed files and refreshes the browser once a new compilation is complete. To improve performance, the webpack development server keeps the generated contents in memory as opposed to writing it to disk.

To setup a project for the webpack development server, create an npm script. For this tutorial, use the npm start command. There are several command line options available to configure the development server. Use the --open option to have the development server automatically open the default browser to the server's port on localhost:

"scripts": {
    "start": "webpack-dev-server --open",
    ...
}

While the development server has many configuration options, this tutorial covers two that are relevant to Angular developers: devtool and historyApiFallback. The devtool property specifies a source map type to aid in debugging at runtime. Go ahead and set the property to 'source-map'.

Then under the devTool configuration property, set the historyApiFallback property to true. This property tells the server to always return the index.html contents when a route is not available on the server. This is very useful when using client-side routing so that the server defers routing responsibility to Angular's router. Define these properties in your webpack configuration:

// ...
module.exports = {
    // ...
    devtool: 'source-map',
    devServer: {
        historyApiFallback: true,
    }
};

Save your configuration and type npm start at the command line to see webpack build and serve the application.

Final Thoughts

Now you have a starting point for your webpack build and more importantly, you understand how these pieces work together. Many of the more advanced webpack examples you see online use these pieces as their foundation so you can more easily add additional features to the build as you need them.

If you want to see this build in action, look at the repository on GitHub.

What are the next steps on your webpack journey? Which aspects of the build are still causing issues for you? Please sound off in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *