A Guide to Managing Webpack Dependencies

The concept of modularization is an inherent part of most modern programming languages. JavaScript, though, has lacked any formal approach to modularization until the arrival of the latest version of ECMAScript ES6.

In Node.js, one of today’s most popular JavaScript frameworks, module bundlers allow loading NPM modules in web browsers, and component-oriented libraries (like React) encourage and facilitate modularization of JavaScript code.

Webpack is one of the available module bundlers that processes JavaScript code, as well as all static assets, such as stylesheets, images, and fonts, into a bundled file. Processing can include all the necessary tasks for managing and optimizing code dependencies, such as compilation, concatenation, minification, and compression.

Webpack: A Beginner's Tutorial

However, configuring Webpack and its dependencies can be stressful and is not always a straightforward process, especially for beginners.

This blog post provides guidelines, with examples, of how to configure Webpack for different scenarios, and points out the most common pitfalls related to bundling of project dependencies using Webpack.

The first part of this blog post explains how to simplify the definition of dependencies in a project. Next, we discuss and demonstrate configuration for code splitting of multiple and single page applications. Finally, we discuss how to configure Webpack, if we want to include third-party libraries in our project.

Configuring Aliases and Relative Paths

Relative paths are not directly related to dependencies, but we use them when we define dependencies. If a project file structure is complex, it can be hard to resolve relevant module paths. One of the most fundamental benefits of Webpack configuration is that it helps simplify the definition of relative paths in a project.

Let’s say we have the following project structure:

- Project
    - node_modules
    - bower_modules
    - src
        - script
        - components	
            - Modal.js
            - Navigation.js
        - containers
            - Home.js
            - Admin.js

We can reference dependencies by relative paths to the files we need, and if we want to import components into containers in our source code, it looks like the following:

Home.js

Import Modal from ‘../components/Modal’;
Import Navigation from ‘../components/Navigation’;

Modal.js

import {datepicker} from '../../../../bower_modules/datepicker/dist/js/datepicker';

Every time we want to import a script or a module, we need to know the location of the current directory and find the relative path to what we want to import. We can imagine how this issue can escalate in complexity if we have a big project with a nested file structure, or we want to refactor some parts of a complex project structure.

We can easily handle this issue with Webpack’s resolve.alias option. We can declare so-called aliases – name of a directory or module with its location, and we don’t rely on relative paths in the project’s source code.

webpack.config.js

resolve: {
    alias: {
        'node_modules': path.join(__dirname, 'node_modules'),
        'bower_modules': path.join(__dirname, 'bower_modules'),
    }
}

In the Modal.js file, we can now import datepicker much simpler:

import {datepicker} from 'bower_modules/datepicker/dist/js/datepicker';

Code Splitting

We can have scenarios where we need to append a script into the final bundle, or split the final bundle, or we want to load separate bundles on demand. Setting up our project and Webpack configuration for these scenarios might not be straightforward.

In the Webpack configuration, the Entry option tells Webpack where the starting point is for the final bundle. An entry point can have three different data types: String, Array, or Object.

If we have a single starting point, we can use any of these formats and get the same result.

If we want to append multiple files, and they don’t depend on each other, we can use an Array format. For example, we can append analytics.js to the end of the bundle.js:

webpack.config.js

module.exports = {
    // creates a bundle out of index.js and then append analytics.js
    entry: ['./src/script/index.jsx', './src/script/analytics.js'],
    output: {
        path: './build',
        filename: bundle.js '  
   }
};

Managing Multiple Entry Points

Let’s say we have a multi-page application with multiple HTML files, such as index.html and admin.html. We can generate multiple bundles by using the entry point as an Object type. The configuration below generates two JavaScript bundles:

webpack.config.js

module.exports = {
   entry: {
       index: './src/script/index.jsx',
       admin: './src/script/admin.jsx'
   },
   output: {
       path: './build',
       filename: '[name].js' // template based on keys in entry above (index.js & admin.js)
   }
};

index.html

<script src=”build/index.js”></script>

admin.html

<script src=”build/admin.js”></script>

Both JavaScript bundles can share common libraries and components. For that, we can use CommonsChunkPlugin, which finds modules that occur in multiple entry chunks and creates a shared bundle that can be cached among multiple pages.

webpack.config.js

var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');
 
module.exports = {
   entry: {
       index: './src/script/index.jsx',
       admin: './src/script/admin.jsx'
   },
   output: {
       path: './build',
       filename: '[name].js' // template based on keys in entry above (index.js & admin.js)
   },
   plugins: [commonsPlugin]
};

Now, we must not forget to to add <script src="build/common.js"></script> before bundled scripts.

Enabling Lazy Loading

Webpack can split up static assets into smaller chunks, and this approach is more flexible than standard concatenation. If we have a big single page application (SPA), simple concatenation into one bundle is not a good approach because loading one huge bundle can be slow, and users usually don’t need all of the dependencies on each view.

We explained earlier how to split up an application into multiple bundles, concatenate common dependencies, and benefit from browser caching behavior. This approach works very well for multi-page applications, but not for single-page applications.

For the SPA, we should only provide those static assets that are required to render the current view. The client-side router in the SPA architecture is a perfect place to handle code splitting. When the user enters a route, we can load only those needed dependencies for the resulting view. Alternatively, we can load dependencies as the user scrolls down a page.

For this purpose, we can use require.ensure or System.import functions, which Webpack can detect statically. Webpack can generate a separate bundle based on this split point and call it on demand.

In this example, we have two React containers; an admin view and a dashboard view.

admin.jsx

import React, {Component} from 'react';
 
export default class Admin extends Component {
   render() {
       return <div > Admin < /div>;
   }
}

dashboard.jsx

import React, {Component} from 'react';
 
export default class Dashboard extends Component {
   render() {
       return <div > Dashboard < /div>;
   }
}

If the user enters either the /dashboard or /admin URL, only the corresponding required JavaScript bundle is loaded. Below we can see examples with and without the client-side router.

index.jsx

if (window.location.pathname === '/dashboard') {
   require.ensure([], function() {
       require('./containers/dashboard').default;
   });
} else if (window.location.pathname === '/admin') {
   require.ensure([], function() {
       require('./containers/admin').default;
   });
}

index.jsx

ReactDOM.render(
   <Router>
       <Route path="/" component={props => <div>{props.children}</div>}>
           <IndexRoute component={Home} />
           <Route path="dashboard" getComponent={(nextState, cb) => {
               require.ensure([], function (require) {
                   cb(null, require('./containers/dashboard').default)
               }, "dashboard")}}
           />
           <Route path="admin" getComponent={(nextState, cb) => {
               require.ensure([], function (require) {
                   cb(null, require('./containers/admin').default)
               }, "admin")}}
           />
       </Route>
   </Router>
   , document.getElementById('content')
);

Extracting Styles Into Separate Bundles

In Webpack, loaders, like style-loader and css-loader, pre-process the stylesheets and embed them into the output JavaScript bundle, but in some cases, they can cause the Flash of unstyled content (FOUC).

We can avoid the FOUC with ExtractTextWebpackPlugin that allows generating of all styles into separate CSS bundles instead of having them embedded in the final JavaScript bundle.

webpack.config.js

var ExtractTextPlugin = require('extract-text-webpack-plugin');
 
module.exports = {
   module: {
       loaders: [{
           test: /\.css/,
           loader: ExtractTextPlugin.extract('style', 'css’)'
       }],
   },
   plugins: [
       // output extracted CSS to a file
       new ExtractTextPlugin('[name].[chunkhash].css')
   ]
}

Handling Third-party Libraries and Plugins

Many times, we need to use third-party libraries, various plugins, or additional scripts, because we don’t want to spend time developing the same components from scratch. There are many legacy libraries and plugins available that are not actively maintained, don’t understand JavaScript modules, and assume the presence of dependencies globally under predefined names.

Below are some examples with jQuery plugins, with an explanation of how to configure Webpack properly to be able to generate the final bundle.

 
 
 
 
 
  •  

ProvidePlugin

Most third-party plugins rely on the presence of specific global dependencies. In the case of jQuery, plugins rely on the $ or jQuery variable being defined, and we can use jQuery plugins by calling $(‘div.content’).pluginFunc() in our code.

We can use Webpack plugin ProvidePlugin to prepend var $ = require("jquery") every time it encounters the global $ identifier.

webpack.config.js

webpack.ProvidePlugin({
   ‘$’: ‘jquery’,
})

When Webpack processes the code, it looks for presence $, and provides a reference to global dependencies without importing the module specified by the require function.

Imports-loader

Some jQuery plugins assume $ in the global namespace or rely on this being the window object. For this purpose, we can use imports-loader which injects global variables into modules.

example.js

$(‘div.content’).pluginFunc();

Then, we can inject the $ variable into the module by configuring the imports-loader:

require("imports?$=jquery!./example.js");

This simply prepends var $ = require("jquery"); to example.js.

In the second use case:

webpack.config.js

module: {
   loaders: [{
       test: /jquery-plugin/,
       loader: 'imports?jQuery=jquery,$=jquery,this=>window'
   }]
}

By using the => symbol (not to be confused with the ES6 Arrow functions), we can set arbitrary variables. The last value redefines the global variable this to point to the window object. It is the same as wrapping the whole content of the file with the (function () { ... }).call(window); and calling this function with window as an argument.

We can also require libraries using the CommonJS or AMD module format:

// CommonJS
var $ = require("jquery");  
// jquery is available

// AMD
define([‘jquery’], function($) {  
// jquery is available
});

Some libraries and modules can support different module formats.

In the next example, we have a jQuery plugin which uses the AMD and CommonJS module format and has a jQuery dependency:

jquery-plugin.js

(function(factory) {
   if (typeof define === 'function' && define.amd) {
       // AMD format is used
       define(['jquery'], factory);
   } else if (typeof exports === 'object') {
       // CommonJS format is used
       module.exports = factory(require('jquery'));
   } else {
       // Neither AMD nor CommonJS used. Use global variables.
   }
});

webpack.config.js

module: {
   loaders: [{
       test: /jquery-plugin/,
       loader: "imports?define=>false,exports=>false"
   }]
}

We can choose what module format we want to use for the specific library. If we declare define to equal false, Webpack doesn’t parse the module in the AMD module format, and if we declare variable exports to equal false, Webpack doesn’t parse the module in the CommonJS module format.

Expose-loader

If we need to expose a module to the global context, we can use expose-loader. This can be helpful, for example, if we have external scripts that are not part of Webpack configuration and rely on the symbol in the global namespace, or we use browser plugins that need to access a symbol in the browser’s console.

webpack.config.js

module: {
   loaders: [
       test: require.resolve('jquery'),
       loader: 'expose-loader?jQuery!expose-loader?$'
   ]
}

The jQuery library is now available in the global namespace for other scripts on the web page.

window.$
window.jQuery

Configuring External Dependencies

If we want to include modules from externally hosted scripts, we need to define them in the configuration. Otherwise, Webpack cannot generate the final bundle.

We can configure external scripts by using the externals option in the Webpack configuration. For example, we can use a library from a CDN via a separate <script> tag, while still explicitly declaring it as a module dependency in our project.

webpack.config.js

externals: {
   react: 'React',
   'react-dom': 'ReactDOM'
}

Supporting Multiple Instances of a Library

It’s great to use the NPM package manager in front-end development for managing third-party libraries and dependencies. However, sometimes we can have multiple instances of the same library with different versions, and they don’t play together well in one environment.

This could happen, for example, with the React library, where we can install React from NPM and later a different version of React can become available with some additional package or plugin. Our project structure can look like the following:

project
|
|-- node_modules
    |
    |-- react
    |-- react-plugin
        |
        |--node_modules
            |
            |--react

Components coming from the react-plugin have a different React instance than the rest of the components in the project. Now we have two separate copies of React, and they can be different versions. In our application, this scenario can mess our global mutable DOM, and we can see error messages in the web console log. The solution to this problem is to have the same version of React throughout the whole project. We can solve it by Webpack aliases.

webpack.config.js

module.exports = {
   resolve: {
       alias: {
           'react': path.join(__dirname, './node_modules/react'),
           'react/addons': path.join(__dirname, '/node_modules/react/addons'),
       }
   }
}

When react-plugin attempts to require React, it uses the version in project’s node_modules. If we want to find out which version of React we use, we can add console.log(React.version) in the source code.

Focus on Development, Not Webpack Configuration

This post just scratches the surface of the power and utility of Webpack.

There are many other Webpack loaders and plugins that will help you optimize and streamline JavaScript bundling.

Even if you’re a beginner, this guide gives you a solid ground to start using Webpack, which will enable you to focus more on development less on bundling configuration.

 
Tags: