Mariusz Rajczakowski
Software Engineer
18 min read | 1 year ago

Wrap your head around webpack and start bundling now!

What is webpack?

Webpack is a module bundler for modern JavaScript applications.

It processes your app starting from an entry point(s), then any time an entry file depends on another, webpack treats it as a dependency.

Process of reading dependencies and creating dependency graph of every module is carried on recursively, then webpack packages those dependencies into a small number of bundles (in many cases just one).

What are wepback features helping you with development process?

  • loading modules in specific order
  • including dependencies only once rather than many times
  • using bleeding edge features which thanks to loaders will be compiled to to js/css
  • making your single-page apps loading faster thanks to code splitting
  • optimizing assets delivery
  • solving scope issues in js and css
  • speeding up development when using webpack-dev-server and hot-module-reloading

Webpack API

You can use webpack via:

  • CLI (command line interface) - comes as a default, used for dev/production
  • Node.js API (used in webpack-dev-server - can be installed separately - dev only)

How to get started?

You can install webpack globally via yarn or npm:

yarn global add webpack
//or
npm install -g webpack

However it is more recommended to install webpack on per project basis as a devDependency.

Let's then start from scratch with a mini app:

//go to your terminal and create a new directory
mkdir webpack-basics
cd webpack-basics

//init your package.json
yarn init -y
//or
npm init -y

//then install webpack using yarn or npm
yarn add webpack --dev
//or
npm install webpack --save-dev

//create a src dir where we will store our files and dist
//where bundle file will be outputed
mkdir src
mkdir dist

//create a two files
touch src/app.js src/coffee.js

//then edit both files:
//coffee.js
module.exports = function(){
return 'I am making a strong expresso...';
};

//app.js
const coffee = require('./coffee.js');
console.log(coffee()); // will print out 'I am making a strong expresso...'

//the easiest way to bundle this entry point will be using CLI.
//you could simply type:
node_modules/.bin/webpack src/app.js dist/bundle.js
//where src/app.js is an entry point where webpack starts it's journey
//and dist/bundle.js is an output file which contains bundled files

//after that you should see in your terminal something like this:
Hash: b89e8abcbe2ab003de48
Version: webpack 3.8.1
Time: 60ms
  Asset     Size  Chunks             Chunk Names
bundle.js  2.73 kB       0  [emitted]  main
 [0] ./src/app.js 110 bytes {0} [built]
 [1] ./src/coffee.js 71 bytes {0} [built]

//those two files were bundled together
//into one (seemingly bigger) bundle.js file
//if you actually open up bundle.js, you will notice
// that vast majority of a code is a  webpack bootstrap code
// which allows itself to load dependencies
//in a right order in a runtime, your code can be found
//at the bottom and it could be wrapped with something like this:

/***/ (function(module, exports, __webpack_require__) {
const coffee = __webpack_require__(1);
console.log(coffee()); // will print out 'I am making a strong expresso...'
/***/ }),
/* 1 */
/***/ (function(module, exports) {
module.exports = function(){
return 'I am making a strong expresso...';
};
/***/ })
/******/ ]);

Using webpack like in the example above can be fine for learning. However for your custom application, you might find it better having custom npm script and webpack config instructing your webpack what to do. Let's then apply those changes.

//let's create a scripts section in your package.json like so:
{
"name": "webpack-basics",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
  "webpack": "^3.8.1"
},
"scripts" : {
  "start": "webpack"
}
}

//then in your root dir create a config file named:
//webpack.config.js (default name)
touch webpack.config.js
//as you may notice the extension of the file is .js (not json) so we can use
//java script to i.e. load other modules, conditionally load some configuration etc.

//webpack.config.js
const path = require('path');
//path is build in node module which helps translating relative paths, regardless
//of operating system we use to absolute equivalents
module.exports = {
   entry: './src/app.js', //entry points of application could be a string, array, or object
   output: {
       //path to the output folder where __dirname is a current directory
       path: path.resolve(__dirname, 'dist'),
       //actual bundle name
       filename: 'bundle.js'
   }
};
//after that you can just type to run webpack:
yarn start
//or
npm start

You can test whether webpack is still working by running your build again

yarn start
//or
npm start

//then you can test it first with node:
node dist/bundle.js //that will print 'I am making a strong expresso...' in your terminal

If you want to test it in a browser then follow the steps:

//create a simple page in dist folder:
touch dist/index.html

Edit this file and insert a simple skeleton:


<html>
  <head>
     <title>Hello Webpack</title>
  </head>
  <body>
    <h2>Hello Webpack</h2>
    <script src="bundle.js"></script>
  </body>
</html>

When you go open index.html in your browser and open up dev tools you will see:

 width=

Understanding the core concepts

Examples above vere not very impressive, but they were just to start with webpack.

Let's break down webpack core elements into pieces: entry, output, loaders, plugins.

Entry

Webpack defines multiples ways of providing entry points for the app.

We have already seen string type, from the example above ('./src/app.js'), however you might sometimes specify entries as an object or array

//webpack.config.js
module.exports = {
//object syntax - might be useful in single-page apps
//when you can separate vendors libraries from you actual code
//webpack will create a separate dependency graph for each entry
//and each produced bundle will have its own webpack bootstrap
// by separating your app code from vendors (using CommonsChunkPlugin) you can
// leverage long-term vendor-caching (as those modules are not updated as often
// as app itself)
entry: {
  app: './src/app.js',
  vendors: './src/vendors.js'
},
 //...other config
};

Consider also other scenario when you have multi-page application

//webpack.config.js
module.exports = {
//each of pages might have different modules, and when you are i.e in blog section
//you might not need any of the code on dashboard and vice versa
//to benefit of code sharing you might use CommonsChunkPlugin to create a shared
//bundle between each pages
entry: {
  main: './src/main/index.js',
  dashboard: './src/dashboard/index.js',
  blog: './src/blog/index.js'
},
//...other config
};

Another possibility to specify entry point is an array.

It is useful when you have multiple files which not depend on each other and you want to append them to the bundle i.e. discus or google analytics scripts

//webpack.config.js
module.exports = {
entry: [
  './src/app.js',
  './src/discus.js',
  './src/ga.js'
],
//...other config
};

Output

In your config you can specify an output property (which you define as an object) which instructs webpack where to store the compiled bundle on your filesystem.

To specify correctly output property on webpack config you have to provide path where the build will be store and filename or substitutions (for multiple entry points)

//webpack.config.js
//single entry point
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
  filename: 'bundle.js',
  path: path.resolve(__dirname, 'dist')
}
};
//multiple entry points
module.exports = {
entry: {
  app: './src/app.js',
  vendor: './src/vendor.js'
},
output: {
  filename: '[name].js', // this is called substitution which gives each bundle a unique name
  path: path.resolve(__dirname, 'dist')
}
};

Output config contains of filename which not only can be a name of the produced bundle, but also you can specify subdirectory as well:

{
 //...
 output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'js/bundle.js' // this will actually create a bundle.js in a dist/js
 }

Output has an additional option - publicPath which specifes the public URL for the output directory, when refrenced in a browser.

Relative url is resolved relatively to the HTML page (or tag) or when required absolute i.e. when hosting assets on CDN

The value of the option will be prefixed to every URL created by the runtime or loaders. That's why in most cases in ends with '/'

//usage with cdn
{
//...
output: {
  path: path.resolve(__dirname, 'public/assets'),
  publicPath: 'https://cdn.example.com/assets/'
}
}

//other examples:
publicPath: '/assets/', // server-relative
publicPath: 'assets/', // relative to HTML page
publicPath: '../assets/', // relative to HTML page
publicPath: ', // relative to HTML page (same directory, default value)

Loaders

Loaders are similar like tasks in other build tools like gulp. They allow you to pre-process files as you load them.

Loaders can be use for various tasks like transforming different language like sass to css or typescript to java script, or inline images as data urls

You can install loaders using npm or yarn like so:

npm install css-loader --save-dev
npm install style-loader --save-dev
npm install ts-loader --save-dev
or
yarn add css-loader --dev
yarn add style-loader --dev
yarn add ts-loader --dev

How to use loaders?

//specify them via webpack.config.js file (recommended)
//webpack.config.js
module.exports = {
//...other config
module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        { loader: 'style-loader' },
        {
          loader: 'css-loader',
          options: {
            modules: true // turns on the CSS modules mode
          }
        }
      ]
    }
  ]
}
};

//specify them explicitly in each import statement
import styles from 'style-loader!css-loader?modules!./app.css';

//specify them via CLI
webpack --module-bind 'css=style-loader!css-loader'
//this uses style-loader and css loader for .css files

Resources can be applied in a pipeline to the resource

Output from first loader is passed to the next one, at the end webpack wants to receive javascript file unless you are using some plugins to extract the content to separate file.

It is important to know loader evaluation order of webpack

{
test: /\.css$/,
use: ['style-loader', 'css-loader']
//first css-loader will process any .css file then output will be processed
//by style-loader which will result in js code
},

Plugins

Plugins allow you doing anything else that a loader cannot do.

Moreover webpack is build on the same plugin system that you use in webpack configuration

You can install external plugins via npm/yarn:

npm install html-webpack-plugin --save-dev
//or
yarn add html-webpack-plugin --dev

You can also write your custom ones

Plugins are objects which have an apply method. This apply method is called by webpack compiler, which gives you access to the entire compilation lifecycle

//CoffeeBreakPlugin.js
function CoffeeBreakPlugin(options){
 const defaults = {
    size: 'large',
    type: 'latte'
 };
 this.settings = Object.assign({}, defaults, options);
}

CoffeeBreakPlugin.prototype.apply = function(webpackCompiler){
 webpackCompiler.plugin('run', function(webpackCompiler, callback){
    console.log('Would like a coffee?');
    console.log('Can I have a '+ this.settings.size + ' ' + this.settings.type ' please');
    callback(); // tells compiler that you have finished your job
 }.bind(this));
}
module.exports = CoffeeBreakPlugin;

Plugins can take arguments/options so you must instantiate them as objects in plugins property (which is an array) in your webpack config.

const CoffeeBreakPlugin = require('./CoffeeBreakPlugin'); // internal plugin
const HtmlWebpackPlugin = require('html-webpack-plugin'); //external plugin
module.exports = {
entry: './src/app.js',
output: {
  path: path.resolve(__dirname, 'dist'),
  filename: 'bundle.js'
},
plugins: [
  new CoffeeBreakPlugin({size: 'medium', type: 'cappuccino' }),
  new HtmlWebpackPlugin({template: './src/index.html'})
]
};

//then if you run you webpack:
npm start
//or
yarn start

//you should see
Would like a coffee?
Can I have a medium cappuccino please

Sometimes you have to use both loaders and plugins to achieve your goal

Imagine a common scenario: an app has styles written in sass/scss and you want to extract them to separate file, rather than treat them as js modules

//first install extract-text-webpack-plugin and essential loaders
npm install node-sass sass-loader css-loader style-loader extract-text-webpack-plugin --save-dev
//or
yarn add node-sass sass-loader css-loader style-loader extract-text-webpack-plugin --dev

//webpack.config.js
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
 entry: [
   './src/app.js',
   './src/app.scss'
 ],
 output: {
   path: path.resolve(__dirname, 'dist'),
   filename: 'js/bundle.js' //all js modules will be bundled to dist/js/app.js
 },
 module: {
  rules: [
    {
      test: /\.sa|css$/,
      use: ExtractTextPlugin.extract({
        fallback: 'style-loader',
        use: ['css-loader', 'sass-loader'] //using loaders from right to left scss->css
      })
    }
  ]
},
plugins: [
  //extract text pluging will extract all chunks to subdirectory dist/styles/app.css
  new ExtractTextPlugin({
    filename: 'styles/app.css',
    allChunks: true,
  })
]
};

Then in your markup you can use it like that:


<html>
  <head>
    <title>Webpack Test</title>
    <link rel="stylesheet" href="css/styles.css">
  </head>
  <body>
    <h2>Hello Webpack <h2>
    <script src="js/bundle.js"></script>
  </body>
</html>

Will webpack survive in HTTP2 era?

You might wonder if flagship feature - multiplexing will kill webpack and idea of bundling.

Prior 2015, the actual recomendation for heavy js apps was to bundle all your files into smallest possible number of js files (preferably one or two) which you included into page.

However as it often happens in web development, what was a best practice a couple of years ago, now might be an anti-pattern.

The answer for that question is... probably not, as even that HTTP2 has been implemented in majority of modern browsers, it is still not widely used yet in development, means that current best practices for http1.1 connection are still alive.

Moreover, since introduction of code splitting in webpack 2 and some other facilities such as aggressiveSplittingPlugin you can actually keep your current workflow which still fit into http2 world.

Start using webpack now!

Using webpack nowdays is pretty much standard for web development.

There are plenty of advantages of choosing this bundler over the competitors (like browserify.js etc..) - growing community of helpful people, extremely flexible config (which allow you to do anything which is available in node.js), infinitive possibilities of extending functionality via plugins, and loaders which let you load literally anything (including bleeding edge javascript)!

If you looking for solution for solving most of your front-end problems - look no further! https://webpack.js.org

References
  1. https://webpack.js.org

Share:



Warning! This site uses cookies
By continuing to browse the site, you are agreeing to our use of cookies. Read our privacy policy