A Guide to Managing Webpack Dependencies
The Webpack module bundler processes JavaScript code and all static assets, such as stylesheets, images, and fonts. However, configuring Webpack and its dependencies can be cumbersome and not always a straightforward process, especially for beginners.
In this article, Toptal Software Engineer Andrej Gajdos provides a guide with examples on how to configure Webpack for different scenarios and points out the most common pitfalls connected to project dependencies and their bundling when using Webpack.
The Webpack module bundler processes JavaScript code and all static assets, such as stylesheets, images, and fonts. However, configuring Webpack and its dependencies can be cumbersome and not always a straightforward process, especially for beginners.
In this article, Toptal Software Engineer Andrej Gajdos provides a guide with examples on how to configure Webpack for different scenarios and points out the most common pitfalls connected to project dependencies and their bundling when using Webpack.
With a Master’s degree in Service Science, Management, and Engineering, Andrej works on projects of all sizes for clients around the world.
Expertise
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 Webpack bundle dependencies, such as compilation, concatenation, minification, and compression.
![Webpack: A Beginner's Tutorial](https://assets.toptal.io/images?url=https%3A%2F%2Fuploads.toptal.io%2Fblog%2Fimage%2F121840%2Ftoptal-blog-image-1484069520643-3fe07c68c08902f11a88beaad08c0e6e.png)
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 a project’s definition of its Webpack dependencies. 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 Webpack 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.
Note: Webpack shared dependencies best practices have evolved in newer versions of Webpack. It’s worth reading up on SplitChunksPlugin and ModuleFederationPlugin and comparing their benefits unless you’re stuck on a legacy version of Webpack.
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 Webpack 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 Webpack externals
configuration option. 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. So how should Webpack, node_modules
, and React fit together properly in this case?
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.
Prague, Czech Republic
Member since August 31, 2016
About the author
With a Master’s degree in Service Science, Management, and Engineering, Andrej works on projects of all sizes for clients around the world.