Getting started with webpack 4

Face it, it’s over…

It’s over the time when you just added a simple “script” element to your page in order to use a library, it does not work like that anymore… Now your application probably uses a “JavaScript” framework (Angular, React, etc…), it also plays with DOM (jQuery) and as you’re a cool guy, you’re using top of the notch technologies such as ES2015, but you want your application to work on old browsers such as IE11 (yes, IE11 is old, period!), so you’re using Babel… And just because you can, you’re using observables. Man, you’re cool…

With all this dependencies, it’s just unthinkable to just manage them by adding “script” elements to your page and update them when a new version of a library is released. That’s why tools like “webpack” have been created; take every part of your application and bundle them together. As it is a pretty reductive explanation of what “webpack” actually is, we’ll look a bit deeper into it.

Get webpack

Since its version 4, “webpack” comes as two “node” packages: “webpack” and “webpack-cli”. Start you project by running the following commands:

yarn init -y
yarn add webpack webpack-cli -D

The first line simply initializes the project while the second one install “webpack”. If everything went well, your project architecture should be the following:

Untitled

Now, we are going to create a folder called “src” that is going to contain the code of our application, so the “css”, the “html” and the “javascript”. We don’t put these files directly in the root of our folder in order not to mixe them with configuration files (such as the “package.json” file).

In the previous version of “webpack” (< 4), it was pretty cumbersome to configure it. Indeed, everything needed to be defined and most of the time, we ended with big configuration files. In the latest version of “webpack”, this issue has been handled by providing default values for some critical configuration parts. For example: the entry point. Long story short, “webpack” works by analyzing the dependencies in our code source. So you give it an entry point (which is a file), then “webpack” will analyze it, check if this file depends on other ones (using “import” as we’ll see later in this post) and if so, analyze them as well until every dependency is handled. The default value of the entry point is “./src/index.js”, so let’s create this file and put the following content in it:

Then, run “webpack”… How? Simply by defining the following script in your “package.json”:

You’re now able to run “webpack” by typing “npm run build” in your terminal.

Untitled

Don’t mind the big yellow message, we’ll see how to treat him later in this post.

The first noticable change is the apparition of a folder called “dist” in our project. Indeed, by default, once “webpack” is done bundling all of our source code, it will output the result in the “dist” folder of our project. In our case, we just have a single “JavaScript” file. However, if you open the “./dist/main.js” file, you’ll see that our code is surrounded by some other code. This additional code is the “webpack” one and is used by it to locate the different modules that belong to our application. Don’t worry if it’s not very clear so far, we’ll get to it.

Big webpack is watching you

As you can imagine, it would be pretty cumbersome to run the command for every change you make to your application, which is why you can use the “watch” parameter like this:

You can now run the command once again in your terminal and this time, the process won’t stop, it will wait for changes. To prove that I’m not saying random stuff, change something in your JavaScript to see “webpack” running again.

Production vs development mode

As the yellow message explained it, the default configuration of “webpack” bundles your source code in “production” mode. This means that besides of bundling your source code, “webpack” will also apply some optimization such as tree-shaking or minification, which is why when you open the “./dist/main.js” file, this one contains just one long line of code without comment or new line. If you want to set the mode to “development”, you can do it either by appeding “–mode=development” to the webpack command:

Or by specifying it in the…

webpack.config.js

Even though “webpack” 4 is focusing on reducing the configuration to a minimum, you’ll see that we can’t get rid of it totally, that’s why you have the possibility to create a file called “webpack.config.js” to configure how “webpack” bundles your code. So let’s create such a file in the root of your directory and put the following content in it:

When starting, “webpack” will automatically looks for a file called “webpack.config.js” and if it finds it, it reads and applies the configuration. If you prefer giving it another name, you can specify the “–config” parameter of the “webpack” command. Note that the content we put in this file does exactly the same as specifying “–mode=development”. As it’s better to create two scripts (for example, a “build” and a “release”), let’s remove the “mode” property from the “webpack.config.js” and add a “release” script in your “package.json” file:

Can I ES2015?

For those who are not aware, ES2015 is the upcoming specification followed by JavaScript. It describes a bunch of new functionalities that JavaScript will soon support. However, all browsers don’t support yet all these features, so we have to make sure that even though we write our JavaScript code using ES2015 features, it will run even on browsers that don’t support them yet. To do so, we’ll use “Babel”.

“Babel” is an “node” package that is going to analyze your code and translate the ES2015 functionality in ES5 compatible JavaScript code. Let’s see how to setup “Babel”.

First you have to install it:

yarn add babel-loader babel-core babel-preset-env -D

Once it’s done, you’ll need to add a loader in your “webpack.config.js”. Basically, a “loader” specifies how “webpack” is supposed to “read” a certain type of file. For example, on our example, we want that every time “webpack” reads a “JavaScript”, it calls “Babel” in order to compile it into ES5 compatible “JavaScript” code. To do so, just define your “webpack.config.js” like this:

So, the “loaders” are defined in the “module.rules” array. Each object defines a specific loader. Use the “test” property to define what file is going to be handled by the loader you define. Most of the time, we’ll do the selection by extension. For example, here, the loader will handle all the files with a name that ends by “.js”, in other words, all “JavaScript” files. The “exclude” property allows to say that we won’t use “Babel” for the “JavaScript” files located in the “node_modules” folder. Then, “use.loader” defines the name of the loader that will be used to handle the matching files. This name must identity a node package previously installed as we did for “babel-loader”. The options allow us to define presets. Basically, presets represent some rules telling “Babel” what needs to be converted and what does not. While you can define such rules by yourself, “Babel” already did a bit of work for you by creating presets. The “env” one simply compiles all ES2015 features in ES5.

So now that we have configured this, let’s test it. Replace the code of your “./src/index.js” file by this:

Here we are using 3 majors new functionality of ES2015:

  • The “let” operator used to defines scoped variables.
  • The spread operator (“…”) used to spread array.
  • The “for … of” loop.

If you were to run this in IE11, you’ll get a “syntax error” as it does not know the “let” operator of ES2015, however, IE11 would be able to execute the file generated by “webpack”. Why is that? Well, simply because “webpack” compiled your code in ES5 compatible one thanks to “Babel”. If you don’t trust me (how dare you?!), you can check it by yourself by opening the “./dist/main.js” file and look at the bottom of it:

This very long line of code is the result of the compilation of your code in ES5 compatible one. You’ll notice that the “let” operators have been replace by “var”, the spread operators have been replace by the call to the “concat” function and finally, the “for … of” loop has been replaced by a bunch of weird lines of code.

What about my HTML page?

Of course, an application does not consist only of “JavaScript” code, you need at least an “index.html” to present it. That’s where you’ll need to use “html-webpack-plugin”. Start by installing it this way:

yarn add html-webpack-plugin -D

“html-webpack-plugin” can be used in a lot of ways, I’ll only describe one here, however if you’re interested, I’d advice you to read the official documentation. Here, we are going to provide a template and “html-webpack-plugin” is going to insert “script” references to the files generated by “webpack” in it before outputting it in the “./dist” directory. So, let’s create the file “./src/index.html” with the following content:

Then modify you “webpack.config.js” for it to look like this:

We’re simply adding a new plugin and specifying some options for it. The “template” one defines the template used to generate the “./dist/index.html” file. Indeed, if you open this file, you’ll see that it’s simply your template with a script that reference the “./dist/main.js” file:

This is so awesome, but there is an issue…

Caching

The first time “webpack” bundles everything, it works like a charm, you’re happy and you’re already thinking that the sky looks more blue (bluer ?). Then, you change your “JavaScript” file, you look at “webpack” that fires itself to re-bundle everything, then you refresh your page and… WTF? Nothing changed?

Well, that’s actually normal. The “./dist/index.html” file references “./dist/main.js”, just like that, this means that the first time “Chrome” will load your page, it will put “main.js” file in his cache, so even though “webpack” re-bundles it, “Chrome” will still take it from the cache. You can disable it from the “Chrome developer console” but you can’t ask that to all your visitors 😉

Therefore, the idea is to ask “webpack” to append a unique “ID” to the name of the generated file. This “ID” would change for every compilation and therefore ensure that the version will never be taken from the cache. To do so, apply the following modification to your “webpack.config.js”:

“output” can be used to define how the bundle will be emitted. We’re defining the “filename” property to define the name of the “JavaScript” file. We’re using two tokens that “webpack” will replace during the compilation phase:

  • [name] is the name of the chunk. In a basic configuration such as ours, the default chunk name is “main”.
  • [chunkhash] is a unique ID corresponding to our chunk.

The “path” property defines the folder in which the bundle will be emitted. “path.resolve” can be used to generate an absolute path to a folder by taking care of the operating system node is running on. Indeed, if you’re running “node” on “Windows”, the path parts will be separated by “\” while they will be separated by “/” on “Unix” operating systems. “__dirname” is a variable known by “webpack” that contains the current directory “webpack” is running in. We pass “dist” to the second parameter of the function to concatenate the two parts and obtain a full path to the “dist” directory.

Start your script again and you’ll notice now a file called “./dist/main.something.js” in your project. The “something” is actually a unique ID such as “8faac814c5d03791be81”. And even more awesome, this is also this name that is used in the “script” reference in the generated “html” file. Now, you can change your source code to see that a new file will be generated every time.

Wow, what a mess…

You’re right, this is all very cool but if I do fifty modifications, I’ll end up with fifty different versions of my “main.js” file… That’s true but you can clean this a bit thanks for the “clean-webpack-plugin” plugin.

yarn add clean-webpack-plugin -D

And modify you “webpack.config.js” file like this to use it:

Yeah, it is as simple as that. We just add a new plugin telling “webpack” to delete the “dist” folder when it starts. Note that this cleaning will only be done when launching the “webpack” command, not every time that “webpack” detects a change. But it is still cool, you just have to stop/execute the npm script once in a while to clean up the “dist” folder.

Cool but ugly. What about CSS?

What would be a good web site without some “CSS” to make it beautiful? Nothing, I agree with you, this is why we need to know how to handle “CSS” with “webpack”. Before “webpack” 4, we had to use the package “extract-text-webpack-plugin” but their official github page now advices us to use “mini-css-extract-plugin” instead. So let’s install it:

yarn add mini-css-extract-plugin css-loader -D

Wait a minute! What’s the “css-loader” that discretely tries to get installed along with “mini-css-extract-plugin”? Well, the later package is actually responsible for gathering all the “CSS” and bundle them in one single file while the former one is used to read “CSS” files and analyze them to find external resources such as images. So it’s “css-loader” that will notify “webpack” if other files needs to be loaded (we will cover that a bit later). We also need to modify our “webpack.config.js” file to use these packages:

The first thing we did was to configure a new loader for “CSS” files. As you can see, we defined two loaders in the “use” array: “MiniCssExtractPlugin.loader” (that only reference a variable that contains the name of the loader) and “css-loader”. This means that “CSS” files will actually be loaded by the first loader, then the outputted result will be passed to the second loader. Note that the weird thing is that this array is used backward. This means that the first loader used will be “css-loader” and not “MiniCssExtractPlugin.loader”. So “css-loader” will read the file, analyze it and look for external resources, then it will pass the content of the “CSS” file to “MiniCssExtractPlugin.loader” that will organize it in memory in order to eventually merge all the “CSS” files of our project.

The second thing we did was to define a new plugin in order to pass options to “MiniCssExtractPlugin”. Here, we use the strict minimum by defining the filename of the bundled file.

Now, create a new file in “./src/assets/css” called “global.css” that contains:

Finally, add the following line in your “JavaScript” file in order to reference the “CSS” one:

Let’s run the npm script once again. This time, a “css” folder gets created in the “dist” folder of your project and contains a “CSS” file that contains the code of all “CSS” files of your project (for now, there is only one but you just have to create and reference new ones to add them to the bundled file).

Watch is cool but there is cooler

The “–watch” parameter of “webpack” is cool but it’s not optimal. Indeed, the biggest issue with how we’re testing what we’re doing right now is that we simply use the file in the browser. Therefore, the protocol is not “http://” but “file://” which will create issues in the future when we’ll try to load resources such as images. The second issue is that we have to manually refresh the page after each change. Well… It’s not dramatic but remember, we’re IT guys so we’re lazy… Let’s use “webpack-dev-server” to kill these two birds with one package 😉

yarn add webpack-dev-server -D

Once the package is installed, the only thing you have to do is define a new npm script. Lets add it and modify the existing ones like this:

Finally, just run:

npm run dev

On the top of bundling your application in development mode, this command will also create a tiny http server and render your application with it. So now, your application is using “http://” protocol making the loading of the resources possible.

The other cool thing is that if you edit a file in your project, the page automatically reloads itself for you to see your changes.

But I don’t see my images

As such, the configuration does not allow us to reference images. Well, nothing prevents you to but they won’t get displayed. For example, add the following image in “./src/assets/img/ng.png”.

ng

Add the following class in your “CSS” file:

And replace the content of the “body” tag of your “HTML” template by:

Just doing this won’t be enough to be able to use this image. Indeed, if you take a look at your terminal, you’ll see that “webpack” is complaining about the fact that it does not find an appropriate loader to load “png” files. That’s why we need to install the “file-loader” package that is used to load binary files:

yarn add file-loader -D

If you understood all you just read, you’ll be able to guess that we now need to add a loader for image files:

This configuration tells “webpack” that for “png”, “jpg” and “gif” files, the loader to use is “file-loader”. We also defined a “name” query string to the loader name to specify that the loaded file will have to be copied in the folder “img” of the “dist” folder. The tokens used for the name are:

  • [name]: name of the file without the extension.
  • [ext]: extension of the file.

So basically, we just copy images from their original location to the “./dist/img” folder. If you try to display your page right now, it won’t work because we’re missing a crucial configuration option for “webpack”.

Right now, the “.ng” class of your “CSS” is trying to load the file “img/ng.png”. However, it’s trying to load it from the current location of the “CSS” file, so “./dist/css” while the real location of the image is one level below. To fix this issue, you just need to define the “publicPath” property of “webpack” that allows you to define the base path for all assets of your project. Basically, it will just prepend this value to the assets path. So modify your “webpack.config.js” like this:

Now, the “CSS” will try to load the image from “/img/” and not “img”, so from the root of the project and not relatively to the “css” folder. Run the “npm” script again and this time, you’ll see your picture.

Note that without “webpack-dev-server”, “/img” would point to “C:\” and not the root of your application, which is why we needed the “http://” protocol instead of the “file://” one.

My project has more than one “JavaScript” file

Of course, most of the time, your project won’t be composed of just an “index.js” file, so “webpack” must be able to find all the files from your project to bundled them in the final file. You’ll do that by using the ES2015 “import”/”export” keywords.

Actually, with “webpack”, “JavaScript” files are no longer just “JavaScript” files that you insert with “script” elements on the page, they are modules. Modules have public and private elements (variables, functions, etc…) and can be imported in other modules. That’s how “webpack” knows what is used and what is not.

Say that we want to create a module containing some mathematical functions. Add the file “./src/math.js” with the following content:

In order to make elements visible in other modules, you have to export them. In the code above, we export everything but the “log” function that we use in the “add” and “subtract” ones. This means that all functions but “log” will be available in other modules. Replace the code of “./src/index.js” by this one:

Besides importing our “CSS” file, we also import everything (*) from the module “./math”. Note that we don’t need to add the “.js” extension as “webpack” can figure it out by itself. We won’t go into further details about that syntax but just know that everything that is exported in the “./math.js” module will be available through the “math” element in “./src/index.js”. That’s why the code above will produce the following result:

ng

The two first log comes from the “log” method called by the “add” and “subtract” functions. Then we just log the results of these two functions and the value of the two variables exported by the “math” module. The interesting thing is that if you try to call the “log” function from the developer console:

ng

So even though we declared the “log” function as a global one via the “function” keyword, this one is not accessible. This comes from the fact that “webpack” bundles everything as modules, so the “log” function is only accessible from inside the “./math.js” module and not from outside as it was not exported, unlike the other functions.

That’s all folks

“webpack” is a complex but useful tool that can be configured in a lot of ways. Here, we just saw the basics to get an application running but there are still a lot of options that we didn’t explore and topics we didn’t cover. For example, how would you handle extra fonts?

 

 

2 Responses

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.