Module is an idea that cannot be taken for granted when discussing the modern JavaScript. As JavaScript files became modularized according to functionality, the bundler, a tool used to group and manage these modules, became increasingly important. Bundlers can be used to modularly program source codes and easily manage intermodular or external library dependency. This guide explains the JavaScript development through webpack.
Before discussing the development straight away, the following sections will briefly explain modules and bundlers.
Traditional JavaScript demanded developers to separate each file according to functionality and then to load them onto the HTML with a <script>
tag. However, this method made it difficult for developers to ensure the internal logical sequence among dependencies, and there always was a possibility that the entire program would not run just because of one error in a single file. In order to address these kinds of problems, more developers started to use modular programming. Also, with the help of continued efforts of browser environments that do not support module level scope to develop modular programming, various types of module formats were created. Module is the core reason behind the appearance of bundlers, and for more detailed information on modules, refer to [FE Guide] Dependency Management.
Bundler is a tool that combines dependent module codes into one (or more) file. In browser environments, codes written with CommonJS or parts of the ES6 Module (Chrome and other latest browsers support ES6 Modules) cannot be rendered immediately, and must be analyzed and modified into a new code to conform to the specifications of a JavaScript module (Ex: https://rollupjs.org/repl). The main objective of the bundler is to modify the code so that it may run smoothly on browser environments. Most iconic bundlers include RequireJS, Browserify, Rollup, and Parcel, and currently, webpack stands firmly in the middle of the spotlight.
This guide is written based on webpack 4, the currently most widely used JavaScript bundler. webpack supports CommonJS, AMD, and ES6 Module (v2 and above) formats, and also manages dependencies of resources including not only JavaScript, but also CSS, image files, and more. For example, webpack manages all of @import
and url(…)
statements used in CSS/Sass/Less and <img src = …>
tag used in the HTML. Also, webpack acts as a Task Runner, automating tasks like transpiling, minifying/uglifying, banner generation, and CSS preprocessing. Furthermore, it also provides useful functionalities like Code Spliting, Dynamic imports(Lazy Loading), Tree Shaking, and Dev Server(Node.js Express Web Server) for more efficient JavaScript development.
Installing and using webpack is simple. Execute the following commands to install. However, it is worth mentioning that different projects may require different versions of webpack, so it is recommended to install it locally instead of globally. When installing locally, execute by using the file in the .bin
folder or by using the npx
command. Open npm and install webpack by following these steps.
1. Initialize the package.json
file
npm init
2. Install webpack
npm install --save-dev webpack webpack-cli
3. Create src/index.js
4. Run webpack
node_modules/.bin/webpack --mode development
or
npx webpack --mode development
If webpack was installed and executed properly, the following message will be printed on the terminal.
Now, let’s take a look at bundling a module with an example.
Modules will be added to the bundle one by one. First, the entry file, starting point of all modules, must be added. The src/index.js
file mentioned above is an example of an entry file. The following code will import the sayHello
module into the entry file.
import sayHello from './js/sayHello';
console.log(sayHello());
sayHello
module exports the sayHello
function.
export default function sayHello() {
return 'HELLO WEBPACK4';
}
Add the following lines of codes to package.json file. bundle
and production
scripts both combine module files into one bundle, but behaves differently depending on the value of mode
option when it is bundled.
{
"scripts": {
"bundle": "webpack --mode=development",
"production": "webpack --mode=production"
}
}
mode is a built in configuration, newly introduced in webpack 4, and it has development
, production
(default), and none
(warning, falls back to production) as possible values.
Run the scripts added above in the following sequence, and compare the two files.
npm run bundle
(development)dist/main.js
file checknpm run production
(production)Upon inspecting the bundle file created using the production mode, it is clear to see that the codes have been uglified. Also, compared to the file created using the development mode, the file created using the production mode is significantly smaller (5 kb -> 981 bytes).
Created bundle files are loaded in HTML with a <script>
tag as shown below. It is unnecessary to include individual module files one by one; instead, simply include a single modular file created using the webpack bundling. Creating the HTML file and including the bundle file can be done automatically using the webpack configuration, and will be discussed in greater detail in later sections.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>demo</title>
</head>
<body>
<!-- Include the bundle file -->
<script src="./dist/main.js"></script>
</body>
</html>
So far, the guide discussed only the very basics of bundling. Now, the following sections will explain how to import and bundle external modules, or libraries. For this section, Lodash will be used as an example.
In the same directory used to run previous examples, run the following command. If Lodash was installed properly, it will appear on package.json
as one of the dependencies
.
npm install --save lodash
When running a webpack build, watch
option can be given. In watch
mode, when a file is changed, the bundle file recompiles automatically. This resolves the issue of having to compile the bundling script every time a change is made to a file. Add the option to the script added to package.json
, and run npm run bundle
. Next, inspect whether the file auto-bundling is enabled when a change is made to the index.js
file.
"scripts": {
"bundle": "webpack --watch --mode development",
...
},
Now, let’s use the Lodash API to create and export a function that checks valid string length. Create a src/js/name.js
file and add the code presented below. In this example, every module created within the project will be imported through a relative path, and external packages are accessible by the package name using the npm. The code below imports Lodash entirely. (To only use parts of Lodash, refer to Installation section of the official Lodash website).
import * as _ from 'lodash';
let minLen = 2;
function isValid() {
return _.trim(name).length >= minLen;
}
export default {
isValid
};
Print the module from the entry file (src/index.js
) built with Lodash. Other external modules can be imported by following the same procedure.
import sayHello from './js/sayHello';
import name from './js/name';
console.log(sayHello());
console.log(name.isValid('Rich'));
Let’s compare the bundle files created before and after using Lodash. When using an external library, it can be observed that the code from the library is included in the project. The problem occurs when a massive library is included, and the size of the bundle file becomes too big. The following sections will address this particular issue.
A configuration file provided by webpack can be used to customize necessary options during the bundling process. The configuration file is written in the form of a JavaScript module, and exports the customized configuration as a modular object. Let’s see how this is used with example options.
At the root of the project folder, create a webpack.config.js
file. This file will export the options as configurations are added as shown below. Add the option, and inspect how different options enable the project to behave differently using the npm run bundle
command.
// webpack.config.js
module.exports = {};
entry
Entry is the starting point used by the module to create the dependency graph. The previously created src/index.js
file will be set as default entry point, and the bundle containing related modules and libraries will be created around the designated file.
module.exports = {
entry: './src/index.js'
};
The entry file can be customized by changing the value of the entry option as presented below.
module.exports = {
entry: './path/to/my/entry/file.js'
};
output
Output option determines the location and the name of the resulting bundle file. When the bundling starts, dist
folder is created and at the end, the bundle file is created under the dist
folder. The default value is './dist/main.js'
, and it is recommended to use the absolute path (path.resolve
).
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
}
};
module.rules
By default, webpack can only understand JavaScript. However, loader is a tool that enables webpack to process other types of files. Following is a list of module types the webpack can process.
import
from ES2015require()
from CommonJSdefine
and require
from AMD@import
from CSS/Sass/Lessurl(…)
from Stylesheet<img src=…>
tag from HTMLCSS-loader
modularizes the CSS files like @import
and url()
by processing them. The process of modularization of CSS files is further explained below.
npm install --save-dev css-loader
Adds the src/css/styles.css
to be modularized.
body {
background-color: lime;
}
Go back to the entry file (src/index.js
) and at the top, use the import
statement to include the CSS file that corresponds to the directory. Other types of files still have to be included in the dependency graph through the entry file to be modularized.
...
import './css/styles.css'
...
module.rules
included in the webpack configuration file defines the rules for useable loaders. The loader name is added to the use
attribute, and the test
attribute takes the official format of the file extension of the target file as its value. The example below applies the css-loader
only to a CSS file, so the loader would not function properly with other style files like .scss
and .less
.
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
use: 'css-loader'
}
]
}
};
After bundling, search for style.css
in the dist/main.js
file, and check to see if the CSS file is included in the bundle.
Note : Different Loaders - https://webpack.js.org/loaders/
plugins
Plugins allow webpack to do much more than just bundling. For example, there are plugins that allow bundle optimization and asset management. The new
keyword is used to create a new plugin instance, and it is passed on to the option to be used.
Let’s install the following plugins. HtmlWebpackPlugin
creates the HTML file and inserts every bundled files into the HTML file using the <script>
tag. CleanWebpackPlugin
is a plugin that deletes a certain folder before the bundling begins, and it is mainly used to organize the build folder.
npm install --save-dev html-webpack-plugin clean-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
...
plugins: [
new HtmlWebpackPlugin(),
new CleanWebpackPlugin(['dist'])
]
};
When the bundling process begins, the dist
folder will be deleted and recreated by the CleanWebpackPlugin
, and can see that the dist/index.html
file has been added. The main.js
file is, then, loaded in recently created index.html
.
Note: Different plugins - https://webpack.js.org/plugins/
mode
Let’s manually configure the options of previous scripts
and mode
. The reason the mode distinguishes the production mode from the development mode is that the bundler must operate differently when it is bundling. For a simple example, for distribution, the file needs to be compressed to make the file size smaller, but for development environment, compression is not necessary for debugging. mode
option does exactly this, and webpack automatically undertakes the appropriate procedure according to the value of mode
. The script
option can also be set with development
, production
, and none
values. The following example uses the configuration file to better suit to different environments.
So far, this document only used one configuration file. When the webpack runs, it will automatically execute the default webpack.config.js
, but to use other configuration files, the config
option can be used as presented below.
webpack --config webpack.custom.config.js
Create separate config files for development and production environment.
webpack.dev.config.js
module.exports = {
mode: 'development',
...
};
webpack.config.js
module.exports = {
mode: 'production',
...
};
"scripts": {
"bundle": "webpack --watch --config webpack.dev.config.js",
"production": "webpack" // default for webpack.config.js
},
Check if the files created are identical to the files created using the webpack –mode
option.
The following section will now deal with options that are being used very effectively in the real-world development.
Ultimately, there is only one file that webpack bundles. However, there are definitely cases where parts of the modules have to be split. For example, loading a heavy multi-moduled bundle for a web application can significantly slow down the initial loading speed. In this case, it is better to simply separate the service code and the library code. Separation of modules happens mainly due to performance issues and maintenance issues. Let’s use options provided by webpack to separate files.
optimization.splitChunks
SplitChunksPlugin is a new option released with webpack 4, and it is a built-in version of CommonChunkPlugin
, which was supported up to webpack 3. With this option, webpack splits the bundle into chunks by grouping modules and libraries that are commonly used. By default, it groups the modules in node_modules
into a chunk. If the code is executed with the name
option in splitChunks
customized, as shown below, a new file with the customized name is created.
module.exports = {
...
optimization: {
splitChunks: {
chunks: 'all',
name: 'vendors'
}
}
};
When bundled, the vendor.js
(library code) file is created in the dist
folder as well as the main.js
(service code) file. The Lodash has been separated into a vendor.js
file.
So far, the entry file has been defined using the entry
option. Although the entry
option uses single entry syntax as default, it can also use multi entry syntax. With the following settings, the name of the separated service code bundle is chaged from main.js
to app.js
.
module.exports = {
...
entry: {
app: './src/index.js'
},
...
};
The name of the resulting file can also be changed using the output
option. When the bundle file is created, the resulting file is set as the value of output.filename
, and the [name]
corresponds to respective names of the bundle files. After running the code, it can be seen that the custom-app.js
and custom-vendor.js
files have been created.
module.exports = {
...
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'custom-[name].js'
},
...
};
Creating smaller bundle files for each page has numerous benefits over creating a single bundle file for the whole page. Let’s create page-level bundle files. First, add the src/sub/index.js
which will only be used at a sub-page.
import * as _ from 'lodash';
console.log(_.toLength('sub page'));
Bundle the files with the entry
and splitChunk
settings from previous sections.
module.exports = {
...
entry: {
app: './src/index.js',
subPage: './src/sub/index.js'
},
optimization: {
splitChunks: {
chunks: 'all',
name: 'vendors'
}
},
...
};
Write the following codes in the HTML file.
// main page
...
<script src="./dist/vendors.js"></script>
<script src="./dist/app.js"></script>
...
// sub page
...
<script src="./dist/vendors.js"></script>
<script src="./dist/subPage.js"></script>
...
In the previous example, separate settings were used to handle the development and production environments. The webpack-dev-server
provides a feature that allows developers to host the server on a local PC and to directly check the bundle files on the browser. This feature enables developers to forgo complicated yet tedious process of server construction or building an HTML page that includes the bundle file, and easily check the current project.
Run the following commands to install webpack-dev-server
.
npm install --save-dev webpack-dev-server
Add the script to host the server, and execute. When using the mode
option, the server will run according to the current value of the mode
option. The example below renders a development
mode.
// package.json
{
"scripts": {
...
"dev": "webpack-dev-server --open --mode=development"
}
}
Now, let’s consider how to check and build projects by changing the configuration of the example bundle file over browsers. First, make sure that the webpack.dev.config.js
, the configuration file that runs in development environment, has the following options defined. HtmlWebpackPlugin
automatically creates dist/index.html
file and this file loads the main.js
bundle file. When connected to the server, the index.html
file that was created will open.
// webpack.dev.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
mode: 'development',
module: {
rules: [
{
test: /\.css$/,
use: 'css-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin(),
new CleanWebpackPlugin(['dist'])
]
};
webpack creates a feature that allows developers to replace modules and update the bundle file in real-time. Using HMR(Hot Module Replacement), only the parts that were edited becomes updated without having to load the entire website again. Let’s add HotModuleReplacementPlugin
and configure the devServer
option.
...
const webpack = require('webpack');
module.exports = {
...
plugins: [
...
new webpack.HotModuleReplacementPlugin()
],
devServer: {
hot: true,
inline: true
}
};
host
: Since the default value is localhost
, configure this option to enable outside access.port
: Configure this option to change to port number from default value of 8080
.module.exports = {
...
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: {
hot: true,
inline: true,
host: '0.0.0.0',
port: 8080
}
};
If the reader paid proper attention throughout the guide, there may have been something off putting in previous examples. The index.html
did not have the appropriate styles even if the style module was imported. The modularized CSS style can only be interpreted when added to the DOM. That means, <style>
tag has to be added to the DOM, and style-loader
does exactly that. Install the style-loader
as presented below. style-loader
must be loaded before css-loader
, so the use
attribute must be specified as so.
npm install --save-dev style-loader
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
...
};
When importing a CSS file as a module, all of CSS style is included in the bundle file. To use the style in the service page, configure the plugin as shown below.
extract-text-webpack-plugin
, commonly used in the previous version, is still configuring itself to the webpack 4 (to use the prerelease 4.0.0-beta.0, use extract-text-webpack-plugin/@next
version), so use mini-css-extract-plugin
.
npm install --save-dev mini-css-extract-plugin
...
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
...
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
...
new MiniCssExtractPlugin()
],
...
};
As the bundling starts, app.css
file is created in the dist
folder, and a tag loading the CSS file in index.html
appears.
When the bundling process is finished, the asset file is created in the dist
folder. Also, the file using the asset must include the changed path. The file-loader
executes the series of steps necessary to load image files.
npm install --save-dev file-loader
Create an images
folder, and add the images to use in the styles. Then, make the following changes to styles.css.
body {
background-image: url('../images/image.png');
}
Add loader configurations to webpack.dev.config.js
and webpack.config.js
to deal with images.
module.exports = {
...
module: {
rules: [
...
{
test: /\.(gif|png|jpe?g|svg)$/i,
use: [
'file-loader'
]
}
]
},
...
};
When the bundling process executes with npm run production
, the dist/[hash].png
is created. Also, the image path has been changed on the app.css
file as well.
Fonts, too, have to be modularized and imported like images. file-loader
can be used to deal with fonts as well.
First, create a fonts
folder, and add font to the folder. Then, add the font style to the src/css/styles.css
like the following.
@font-face {
font-family: 'noto-sans';
src: url('../fonts/NotoSans-Black.ttf');
}
body {
font-size: 12px;
font-family: 'noto-sans';
background-image: url('../images/image.png');
}
When the webpack-dev-server
is compiled, index.html
file will be created in dist
folder and the bundled modules will be inserted into the HTML file. To visually see the font-loader example, <h1 />
tag, or other font style related tags, must be in the HTML file, so create a new custom HTML file, and replace the index.html
file with it. For example, create a demo/index.html
file as presented below, and assign the demo/index.html
file to the template
option of HtmlWebpackPlugin
.
// demo/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>demo</title>
</head>
<body>
<h1>Hello Wabpack!<h1>
</body>
</html>
// webpack.dev.config.js
module.exports = {
...
plugins: {
new HtmlWebpackPlugin({
template: 'demo/index.html'
}),
...
},
...
};
Add loader configurations to webpack.dev.config.js
and webpack.config.js
to deal with images.
module.exports = {
...
module: {
rules: [
...
{
test: /\.(woff|woff2|eot|ttf|otf|)$/,
use: [
'file-loader'
]
}
]
},
...
};
First, run the webpack-dev-server
to see if the font has been applied correctly to the <h1>Hello Webpack!</h1>
text. Then, when bundled, the font file has also been created in the dist
folder along with the image file.
Latest JavaScript codes are usually written with ES6. However, because the legacy browsers cannot interpret ES6 codes, developers must build the program in ES6, and go through the process of transpiling the code to ES5. A babel-loader
is frequently used in these situations. The example will be explained using Babel7.
npm install --save-dev babel-loader @babel/core @babel/preset-env
.babelrc
is a file that determines how much of the JavaScript file has to be changed using Babel according to the latest specifications. For more detail on how to use the .babelrc
file, refer to this document.
{
"presets": ["@babel/preset-env"]
}
The babel loader is added to the config file using the same methods used to add other loaders. However, since the files under node_modules
folder do not need to be transpiled, it is possible to exclude
the entire folder. Since the transpiled bundle file is needed in the production environment, configure the webpack.config.js
file accordingly.
// webpack.config.js
module.exports = {
...
module: {
rules: [
...
{
test: /\.js$/,
exclude: /node_modules/,
use: [
'babel-loader'
]
}
]
},
...
};
Create a src/index.js
file to briefly test transpiling. The following browsers support the let
keyword, and the bundle file must be transpiled so that it can run on browsers that are Internet Explorer 10 or below.
let foo;
if (true) {
foo = 1;
} else {
foo = 2;
}
console.log(foo);
When the files creating using the babel-loader
and the files created without is are compared,
... var r;r=1,console.log(r) ...
... let r;r=1,console.log(r) ...
When the loader was used, the let
keyword has changed to var
. As such, with the Babel loader, JavaScript code written in the latest specification can be transpiled to be backwards compatible to older browsers.
Since JavaScript does not have a compile stage, the program cannot be tested for error until it is executed. Large portion of the errors occur because of typos, syntax errors, and other minor mistakes. To address this issue, JavaScript requires help from the static analysis tools. Static analysis tools will be explained in greater detail in [FE Guide] Static Analysis. The most widely used static analysis tool is ESLint, and the examples will be based on it.
npm install -save-dev eslint eslint-loader
node_modules/.bin/eslint --init
or npx eslint --init
.eslintrc.js
file is createdmodule.exports = {
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2015,
"sourceType": "module"
},
"rules": { ... }
};
Add the eslint-loader
option to the configuration so that the linter analyzes the JavaScript file for errors as the file is bundled. It is recommended to configure both webpack.dev.config.js
and webpack.config.js
.
// webpack.config.js
module.exports = {
...
module: {
rules: [
...
{
test: /\.js$/,
exclude: /node_modules/,
use: [
'babel-loader',
'eslint-loader'
]
}
]
},
...
};
eslint-config-tui allows developers to check for ESLint conventions used at FE Development Lab.
npm install --save-dev eslint-config-tui
Install the module, and configure the .eslintrc.js
file as such.
module.exports = {
...
"extends": "tui/es6",
"parserOptions": {
"sourceType": "module"
}
}
As the file is bundling, if there is a syntax error upon inspection, error message will appear as did below.
Source map is a piece of technology that converts compressed/uglified codes into the actual codes. Although it does not strictly follow the specifications, most latest browsers (Internet Explorer 11+) supports it. (Current specifications : Google document) It is a file in JSON format, and stores the mapping data in base64. It is also supported in many tools including webpack.
Add the source-map to the configuration to allow source map of compressed/uglified codes.
// webpack.config.js
module.exports = {
...
devtool: 'source-map'
};
When bundled, //#sourceMappingURL=app.js.map
comment will be added to the end of the bundled file. Aslo, [bundle-filename].js.map]
is created as a file for mapping. With this pair of files, compressed codes can be debugged.
Chrome developer extension allows tracking of the actual code through a source map. In order to debug using the source map, “Enable JavaScript source maps” must be enabled.
Below pictures show the differences between a file ran with and without a source map. When using a source map, source code and the actual code are both shown. Even if the error occurs in a compressed file, using the source map, developer can easily track down the cause of it. Let’s try debugging with the source map.
This document served to help developers become comfortable with webpack as well as to illustrate the role of bundlers. As numerous examples clearly demonstrate, webpack facilitates the development process by taking care of intricate items that require the developers’ attention (like development environments and file sizes) by providing numerous options and plugins. It is the author’s hope that this document helps developers to modular programming and bundlers in the workplace.
The employee training provided at FE Development Lab related to this document are as listed below. It is recommended to take these courses as well.
This document is an official Web Front-End development guide written and maintained by NHN Cloud FE Development Lab. Any errors, questions, and points of improvement pertaining to this document should be addressed to the official support channel (dl_javascript@nhn.com).
Last Modified |
---|
2019. 05. 13 |