Simple webpack (4.20.2) tutorial

2018-10-12

Updated 2018-10-12 for webpack 4.20.2 (first original post on 2016-05-27 then updated on 2017-03-13 for webpack 2.x, then 2017-08-23 for webpack 3.x )

This is an article in a series to introduce Angular2 and related technologies. In this first entry i want to focus on the build tool used frequently with angular2 which is webpack. If you are not using Angular, then don't worry i won't go into any specifics for Angular. We will however use the result from this article as a base to create an angular application in an next entry.

I don't want to go into an introduction of webpack, there are some excellent articles who do just that examples: here and here. If you come from a Java background, then webpack to gulp/grunt/browserify is similar to what maven was to ant.

We will create a very simple web application and gradually add more webpack functionality as we go along.

You can find the resulting project on github webpack-intro-tutorial

Installation

I will assume you have already node installed as well as npm

Let's create a new project directory and initialise it with a package.json

$ mkdir demo && cd demo
$ npm init -y

FYI The -y option above stops npm prompting for questions and answers "yes" to them all instead.

Then install webpack (locally):

$ npm install webpack@4.20.2 webpack-cli@3.1.2 --save-dev

Hello Friend

Let's do a quick "hello friend": create a javascript file named src/index.js together with a html files named index.html (latter in the root of our demo project directory.

Contents of the index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack Hello World</title>
</head>
<body>
   <script  src="dist/main.js" ></script>
</body>
</html>

It will become clearer later on why we are loading a javascript file named main.js and not index.js ;)

As to the javascript code (inside src/index.js) we simply write some text to the html document:

document.write("hello friend");

Your structure should resemble

├── node_modules ├── src │   └── index.js ├── index.html ├── package.json └── package-lock.json

Let's now run webpack:

$ npx webpack
Hash: 41d0be40189559c9d7ee
Version: webpack 4.20.2
Time: 63ms
Built at: 10/12/2018 9:11:12 AM
  Asset       Size  Chunks             Chunk Names
main.js  960 bytes       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js 32 bytes {0} [built]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.

Note it has created a chunk named dist/main.js, which we referred to inside our html file. Now simply open your index.html file in a browser and you should see "hello friend" on the page.

A second javascript file

Let's add a second javascript and use that from the one you created above. Create a new file named src/contents.js . We will just have it export a string with the text to be printed (might be a bit silly, but it serves the purpose of this tutorial)

module.exports = "hello friend!";

Notice we have added an exclamation mark. This will make it easier to see the update once we test this version.

Change the src/index.js to use the exported string from above:

var msg = require("./contents.js");
document.write(msg);

And let's run webpack again:

$ npx webpack
Hash: 8da285d8ecf4c5a8be9e
Version: webpack 4.20.2
Time: 32ms
Built at: 10/12/2018 9:13:45 AM
  Asset        Size  Chunks             Chunk Names
main.js  1000 bytes       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js 57 bytes {0} [built]
[1] ./src/contents.js 35 bytes {0} [built]

Notice it now created the main chunk based on two javascript files. Refresh your browser and you should see the updated message (remember we added an exclamation mark for this purpose)

Using a configuration file

We have not configured webpack, this is possible since webpack 4. However for most projects you'll need webpack configuration.

Let's now create the webpack configuration file. By default webpack looks for files named webpack.config.js or webpackfile.js. We will use the former: add a file named webpack.config.js to the root of our demo directory with the following contents:

const path = require('path');

module.exports = {
    entry: "./src/index.js", // bundle's entry point
    output: {
        path: path.resolve(__dirname, 'dist'), // output directory
        filename: "[name].js" // name of the generated bundle
    }
};

Some details explained:

  1. the entry specifies the entry point for the bundle(s) (in this case we only have one entry point, hence we will only be creating a single bundle)
  2. The output.path tells webpack where to save the generated files
  3. The output.filename specifies the name of each output (where [name] refers to the name of the "chunk". We could have used a hard coded value such as "main.js", but this prepares it better for the future)

Let's rerun webpack:

$ npx webpack

It now used your configuration (you could have also passed the configuration using --config webpack.config.js, but we have used a name which is loaded by default, so no need.

So far our configuration is not adding any functional we did not already have without a configuration file. Notice we are getting a warning each time indicating The 'mode' option has not been set? We can set it to development mode, but adding the mode option to the configuration:

const path = require('path');

module.exports = {
    mode: 'development',
    entry: "./src/index.js", // bundle's entry point
    output: {
        path: path.resolve(__dirname, 'dist'), // output directory
        filename: "[name].js" // name of the generated bundle
    }
};

Run webpack again and notice the warning is gone. You now feel more confident it is indeed using your configuration file. Let's configure the webpack project further, by configuring a plugin.

Use the HtmlWebpackPlugin

We can have webpack inject links into our html for our generated chunks file by using the HtmlWebpackPlugin plugin.

As with everything, first install the HtmlWebpackPlugin using npm:

$ npm install html-webpack-plugin@3.2 --save-dev

And then update our webpack.config.js to add support for the HtmlWebpackPlugin plugin

  • In the top of the file add an import:
    const HtmlWebpackPlugin = require('html-webpack-plugin');
  • Under the module section, add a plugins section:
     plugins : [
            new HtmlWebpackPlugin({
                template: "index.html",
                inject : "body"
            })
        ]

We are telling webpack to use the plugin and we have configured it to inject the assets at the bottom of the body element using index.html as the template. By default the outcome can be found inside a file also named index.html (can be changed using filename option) inside dist directory (which is our configured output.path)

The plugin will now inject our javascript chunk, therefore make sure you remove the script tag from our index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Webpack Hello World</title>
</head>
<body>
</body>
</html>

Then run webpack again:

$ npx webpack
Hash: 553b2be8132ef1a0f493
Version: webpack 4.20.2
Time: 415ms
Built at: 10/12/2018 9:50:57 AM
     Asset       Size  Chunks             Chunk Names
   main.js   4.17 KiB    main  [emitted]  main
index.html  191 bytes          [emitted]  
Entrypoint main = main.js
[./src/contents.js] 35 bytes {main} [built]
[./src/index.js] 57 bytes {main} [built]
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
    [./node_modules/html-webpack-plugin/lib/loader.js!./index.html] 339 bytes {0} [built]
    [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 509 bytes {0} [built]
    [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 519 bytes {0} [built]
        + 1 hidden module

It has now also generated the dist/index.html based on our template with an injected script reference. Now open this! file into your browser (so the one from the dist directory, not the one from the root of your project)

It is a good idea to move the index.html also to the src folder:

$ mv index.html src

And then change the webpack configuration accordingly:

new HtmlWebpackPlugin({
    template: "src/index.html",
    inject : "body"
})

`

Using the dev server

You could have webpack "watch" your files and generate files as needed. You would just run webpack with the --watch flag:

$ npx webpack --watch
...

Now every time you make a change to one of your files webpack will process them. But there is even a neater trick: using its development web server webpack-dev-server. Let's install it locally:

$ npm install  webpack-dev-server@3.1.9 --save-dev

And run webpack-dev-server using npx:

 $ npx webpack-dev-server

By default it also uses webpack.config.js or webpackfile.js (otherwise you would need to use the --config option).

Now open your browser at http://localhost:8080/

Any change you make to the code is live reloaded in your browser, try it out!

note it does not update the files in the dist folder. All assets are in memory!

Adding other assets (css)

Add a css file named src/message.css:

#message {
    background-color: #0086b3;
    font-weight: bold;
}

Change the src/index.js and add an div#message element:

// src/index.js
var msg = require("./contents.js");

var div = document.createElement("div");
div.id="message";
var txtNode = document.createTextNode(msg);
div.appendChild(txtNode);
document.body.appendChild(div);

The code above requires a css to be present on the page. We can accomplish this by adding a required instruction to the top of the file:

// src/index.js
require("./message.css");
...

If you are still running the dev server, you'll notice an error:

ERROR in ./src/message.css 2:0
Module parse failed: Unexpected character '#' (2:0)
You may need an appropriate loader to handle this file type.

The next section explain this. You better stop your 'webpack-dev-server' first

Explanation of webpack modules

Look at this require("./message.css") again. What we are doing is really defining a module dependency, but not to a javascript file but to a css file. This is something node.js for example is not capable of. This is in fact the whole «raison d'etre» for webpack. It can treat any file in your project as a module. Plus you can use many techniques to express a dependency. We are using require (commonJs), but this also works for es6 import and even @import inside your sass files.

But by default webpack only knows how to deal with javascript modules. We therefore need to configure support for our module type (which is css).

Inside our configuration we can define how a module should be treated. This typically consists of

  • a condition (when does our module definition apply) often in the form of a test with a regular expression to match resource names
  • applying a set of loaders which describe to webpack how to process non-JavaScript modules

The above view is a simplified one, as much more is possible (nested rules, parser options and more)

So we need to add a configuration for modules that match the regular expression /\.css$/ and what loaders to use in order for webpack to be able to process css modules.

We will need two loaders: the style-loader and the css-loader. (their roles are explained further below) Install both of these using npm:

$ npm install css-loader@1.0.0 style-loader@0.23.1 --save-dev

And let's now configure our module. Add a module section to our webpack.config.js and add rules as shown below:

entry : ...,
output : {...},
module: {
    rules: [
        {
            test: /\.css$/,
            loader: ["style-loader","css-loader"]
        }
    ]
},
plugins : [...]

If you have not done so already rerun:

$ npx webpack-dev-server

And notice the style is applied.

So what is going on here? We are instructing webpack to process all files matching our test (/\.css$/) using a pipeline of loaders: first the css-loader and after that the style-loader are applied. The css-loader loads css files and returns css code, the style-loader then adds this as a style element to the DOM. Notice the DOM tree from the resulting page:

webpack style loader result

Add and use a library

Let's use a library and for the purpose of this article let's use the familiar jquery (cough).

First let's add the node packages:

$ npm install jquery@3 --save-dev

Then change the code in our index.js to use jquery:

var msg = require("./contents.js");
require("./message.css");

var $ = require("jquery");

$(function(){
    $("<div id='message'>")
        .text(msg+"!")
        .appendTo("body");
});

When you run again, you should see two exclamation marks. Wow that was easy. We did not have to change our html file or worry about loading jquery.

Separate vendor from app code

In the previous step we added jquery. The jquery code has been added to our single chunk dist/main.js. Have a look at the number of lines in the generated file: (http://localhost:8080/main.js). Also search for "jQuery" and you should find it as part of our resulting asset.

Our goal is to be able to specify imports of "vendor" modules. These vendor modules hardly change, whereas our code changes frequently. It is therefore a good idea to separate the two and place all "vendor" modules in on chunk and our code in another.

To do this we will create a second javascript file src/vendor.js:

require("jquery");

And add this as this as an entry to our webpack configuration. Do this by changing the entry from a simple string value to an object literal as shown below:

entry: {
    main: "./src/index.js",
    vendor: "./src/vendor.js"
},
...

The keys (property names main and vendor) and the chunk names, the values are references to the entry points). It is important to note that our chunk name was named main before by default. We are now explicitly naming it main

Now to configure it. Webpack 4 has replaced the 'CommonsChunkPlugin' with a more flexible and much improved SplitChunksPlugin. To configure this add a optimization section. This is to override different default optimisation techniques webpack applies. The defaults for the SplitChunksPlugin are for most modern project sufficient, however we are using something that makes it more difficult for webpack to optimise (this is mentioned further below.)

Add the following configuration

module : {
    …
}
optimization: {
  splitChunks: {
    cacheGroups: {
      vendor: {
        chunks: 'initial',
        name: 'vendor',
        test: 'vendor',
      },
    },
  }
},
plugins: [
    …
]    
…

A few key points to mention about this configuration:

  • We are using dynamic imports through the use of require as apposed to using ES2015's import. We therefore need to set the value of chunks to initial
  • The test indicates we are only interested in modules in the vendor chunk
  • We name the optimised chunk vendor, the same name as our chunk. This replaces our chunk with the created one.

See more information at split-chunks-plugin. The configuration above is based on half of the common-chunk-and-vendor-chunk pattern (we are not using common code). See (https://github.com/webpack/webpack/tree/master/examples/common-chunk-and-vendor-chunk)

Run the bare webpack (npx webpack) command and observe the contents of dist/main.js and dist/vendor.js. You should no longer find "JQuery" inside main.js

Check if your dist/index.html still behaves as before.

Using other languages (typescript)

With webpack it is fairy straightforward using various javascript compiles such as babel, typescript and coffee. This is accomplished using loaders just as we did with css earlier on.

Let's use typescript as an example (if you are interested we have a blog article explaining how to setup a simple typescript project)

Install typescript, typings for node/core-js and jquery

$ npm  install \
     typescript@3 \
     @types/node \
     @types/core-js@2.5 \
     @types/jquery@3  --save-dev

Create a tsconfig.json to configure the typescript compiler:

{
   "compilerOptions": {
       "module": "commonjs",
       "target": "es6",
       "moduleResolution": "node",
       "lib": [ "es2015", "dom" ]

   }
}

We completed configuring a typescript project, Next let's turn back to webpack. We start by installing a webpack typescript loader. We could use ts-loader, but we will use awesome-typescript-loader as it has some benefits over the former.

$ npm install awesome-typescript-loader@5.1 --save-dev

Configure webpack to use our typescript loader (awesome-typescript-loader) by adding a module.rules to our webpack.config.js:

module: {
    rules: [
        {
            test: /\.ts$/,
            loader: "awesome-typescript-loader"
        },
        {
           …
        }
    ]
},

We can now migrate our code from javascript to typescript. Let's only migrate index.js and contents.js, we will leave vendor.js alone. Rename your files:

  • index.js to index.ts
  • contents.js to contents.ts
$ mv src/index.js src/index.ts && mv src/contents.js src/contents.ts

Don't forget to update the entry references to index.ts inside the webpack.config.js:

entry: {
    main: "./src/index.ts",
    …
},

As a final step migrate your javascript code to typescript.

The migrated contents.ts (notice we are changing the text as well so that it will be easier to observe when webpack uses this source):

export default  "hello ts friend";

And rewrite index.ts as well:

import * as $ from "jquery";
import msg from "./contents";
import "./message.css";
$(() => {
    $("<div id='message'>")
        .text(msg + "!")
        .appendTo("body");
});

Notice we have removed the suffix from the import of contents (before we had contents.js). We can have webpack resolve extension-less references:

   entry : ...,
   resolve: {
           extensions: ['.js', '.ts']
   },
   output : {...},
   module: {...},
   plugins : [...]

You are ready to check your project again. Run webpack or the dev-server and observe everything is still working, but now written in typescript.

Add a pre-loader (tslint)

Now that we are using typescript, it is a good idea to also use tslint. This is to check the style of our typescript code.

First install tslint locally:

$ npm install tslint@5.11 --save-dev

TSLint comes with its own configuration in the form of a tslint.json file. It has with a command line utility to generate an initial default configuration. We have installed it locally so use npx to run it:

$ npx tslint --init

Ensure this created a default tslint.json file.

For webpack we need to install an appropriate loader for TSLint:

$ npm install tslint-loader@3.5 --save-dev

To pre-process typescript files, register a pre-loader (a loader that processes before normal processing occurs). This is accomplished using the enforce option with a value of pre.

Register a module pre-loader for our typescript files inside the webpack.config.js:

    …,
    module: {
        rules: [
            …,
            {
                test: /\.ts$/,
                enforce: "pre",
                loader: 'tslint-loader'
            },
        ]
    },
    …

If you want to see the linter at work, screw up some formatting by for example removing the space before the => in the index.ts. This should cause tslink to show a warning:

WARNING in ./src/index.ts
Module Warning (from ./node_modules/tslint-loader/index.js):
[4, 5]: missing whitespace

Use scss

Let's add support for yet another module type: sass css.

In order to use scss we need to install node-sass and a webpack loader:

$ npm install node-sass@4 sass-loader@7 --save-dev

Register the loader under our set of rules:

{
    test: /\.scss$/,
    loader: ["style-loader","css-loader","sass-loader"]
}

Rename your message.css to message.scss:

$ mv src/message.css src/message.scss 

Don't forget to update the reference to this renamed file inside our index.ts:

import "./message.scss";

And let's throw in some sassiness into the message.scss

$color: #0086b3;

#message {
  background-color: $color;
  font-weight: bold;
  border: solid darken($color, 10%);
  padding: 0.5rem;
}

Test it and see your page looks much hipper now!

As a final step let's add source maps for sass. In order to enable these we need to add the sourceMap option to both the css and sass loaders:

{
    test: /\.scss$/,
    loader: ["style-loader","css-loader?sourceMap","sass-loader?sourceMap"]
}

As you can see the the sourcemaps are working.

This completes this first tutorial. Remember the project is available on github at webpack-intro-tutorial

Next is the helloworld-angular2 article.