Introduction to Webpack
npm init -y
npm install webpack --save-dev
npm install webpack-cli --save-dev
Run Webpack by calling webpack
from the command line.
npm install --save lodash
Run webpack
and view results in the browser.
Create a webpack.config.js
file.
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
Run webpack
or optionally webpack --config webpack.config.js
Configure package.json
to run webpack
"scripts": {
"build": "webpack --mode production"
}
Run npm run build
to run webpack.
Let's add some CSS to our demo! Start by installing the loaders...
npm install style-loader css-loader --save-dev
Update webpack.config.js
to use the loaders for stylesheet files:
// After output: { ... }, add the following
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
Add stylesheet src\style.css
:
.hello {
color: red;
}
Update our src\index.js
file to import the stylesheet.
import _ from 'lodash';
+ import './style.css';
function component() {
var element = document.createElement('h1');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack!'], ' ');
+ element.classList.add('hello');
return element;
}
document.body.appendChild(component());
Let's now add some images to our demo. Start by installing the file-loader.
npm install file-loader --save-dev
Add additional module rules to cover images. See webpack.config.js
.
// Under module : { rules: [ ..., ]} add the following
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
}
Update the code to add the logo image.
import _ from 'lodash';
import './style.css';
+ import Logo from './logo.svg';
function component() {
var element = document.createElement('h1');
// Lodash, now imported by this script
element.innerHTML = _.join(['Hello', 'webpack!'], ' ');
element.classList.add('hello');
+ // Add the logo
+ var myLogo = new Image(200);
+ myLogo.classList.add('logo');
+ myLogo.src = Logo;
+
+ element.appendChild(myLogo);
return element;
}
document.body.appendChild(component());
Adding fonts now. Just cause we can...
Add the font rules in webpack.config.js
.
// Under module : { rules: [ ..., ]} add the following
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader'
]
}
Update the ./src/style.css
file:
@font-face {
font-family: 'Raleway';
src: url('./Raleway-Regular.ttf') format('truetype');
font-style: normal;
}
.hello {
color: red;
font-family: 'Raleway';
}
Let's create another HTML page: ./dist/index2.html
<!doctype html>
<html>
<head>
<title>Managing Output</title>
<script src="./print.bundle.js"></script>
</head>
<body>
<script src="./app.bundle.js"></script>
</body>
</html>
... and another JS file: ./src/index2.js
import _ from 'lodash';
import './style.css';
import printMe from './print.js';
function component() {
var element = document.createElement('h1');
var btn = document.createElement('button');
element.innerHTML = _.join(['Hello', 'webpack', '<br/>'], ' ');
btn.innerHTML = 'Click me and check the console!';
btn.onclick = printMe;
element.appendChild(btn);
return element;
}
document.body.appendChild(component());
Notice that this ./src/index2.js
references another file: ./print.js
.
Let's create it now:
export default function printMe() {
console.log('I get called from print.js!');
}
Now let's modify webpack.config.js
to output multiple files:
const path = require('path');
module.exports = {
- entry: './src/index.js',
+ entry: {
+ // main: './src/index.js',
+ app: './src/index2.js',
+ print: './src/print.js'
+ },
output: {
- filename: 'bundle.js',
+ filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
This will output
app.bundle.js
andprint.bundle.js
files.
If entry-points change that requires us to update the html pages they are used on. Let's fix that.
npm install --save-dev html-webpack-plugin
Now let's update the webpack.config.js
file again to use the HTMLWebpackPlugin:
const path = require('path');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
// main: './src/index.js',
app: './src/index2.js',
print: './src/print.js'
},
+ plugins: [
+ // Generates an HTML file
+ new HtmlWebpackPlugin({
+ title: 'Managing Output',
+ filename: 'index2.html'
+ })
+ ],
output: {
./dist
folderInstall the clean-webpack-plugin
:
npm install clean-webpack-plugin --save-dev
Add the plugin to the webpack.config.js
file:
Including source maps helps track down errors:
entry: {
// main: './src/index.js',
app: './src/index2.js',
print: './src/print.js'
},
+ devtool: 'inline-source-map',
plugins: [
Now create a bug in your code by mispelling
console
in./src/print.js
to see the impact.
Instant compilation & live reloading when code changes
npm install --save-dev webpack-dev-server
Update the webpack.config.js
to use webpack-dev-server.
devtool: 'inline-source-map',
+ devServer: {
+ contentBase: './dist',
+ openPage: 'index2.html'
+ },
And update the `package.json file to assist in running webpack-dev-server:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack --mode development",
+ "start": "webpack-dev-server --open chrome --mode development",
"build": "webpack --mode production"
},
run npm start
to kick off the new web server.
Now correct the console
error, add some additional code and refresh the browser!
export default function printMe() {
console.log('I get called from print.js!');
// cosnole.log('I get called from print.js!'); // Correct the console error!
var questionElement = document.getElementsByClassName('question')[0];
questionElement.insertAdjacentHTML('beforebegin', '<p>I get called from print.js!</p>');
}
Let's set up hot module replacement and lessen a developer's burdens even more.
...
const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const webpack = require('webpack');
module.exports = {
entry: {
// main: './src/index.js',
app: './src/index2.js',
- print: './src/print.js'
+ // print: './src/print.js'
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
openPage: 'index2.html',
+ hot: true
},
plugins: [
new CleanWebpackPlugin(['dist']),
// Generates an HTML file
new HtmlWebpackPlugin({
title: 'Managing Output',
filename: 'index2.html'
}),
+ new webpack.NamedModulesPlugin(),
+ new webpack.HotModuleReplacementPlugin()
],
webpack.NamedModulesPlugin
makes it easier to see which dependencies are being patched.
Update ./src/index2.js
to enable HMR:
- document.body.appendChild(component());
- document.body.appendChild(addNote());
+ let element = component(); // Store the element to re-render on print.js changes
+ document.body.appendChild(element);
+ document.body.appendChild(addNote());
+
+ if (module.hot) {
+ module.hot.accept('./print.js', function() {
+ console.log('Accepting the updated printMe module!');
+ printMe();
+ // document.body.removeChild(element);
+ // element = component(); // Re-render the "component" to update the click handler
+ // document.body.appendChild(element);
+ })
+ }
Update the ./src/print.js
file to see the HMR in action...
Note the browser's console when updates are saved, it immediate detects and swaps the module!
However, there's a devil in the details...
You must re-establish event handlers for HMR to work trouble-free.
if (module.hot) {
module.hot.accept('./print.js', function() {
console.log('Accepting the updated printMe module!');
- printMe();
+ document.body.removeChild(element);
+ element = component(); // Re-render the "component" to update the click handler
+ document.body.appendChild(element);
})
Now updates to ./src/print.js
will be swapped and the events re-wired.
Tree shaking, aka dead-code elimination aids in reducing the code foot-print by putting only the code that is necessary into your packages.
Create a new ./src/math.js
file:
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
Now update ./src/index2.js
to use the cube function...
- // import _ from 'lodash';
import './style.css';
import printMe from './print.js';
+ import { cube } from './math.js';
function component() {
var element = document.createElement('h1');
var btn = document.createElement('button');
+ element.innerHTML = ['Hello webpack!', '5 cubed is equal to ' + cube(5) ].join('\n\n');
- element.innerHTML = _.join(['Hello', 'webpack', '<br/>'], ' ');
- btn.innerHTML = 'Click me and check the console!';
- btn.onclick = printMe;
- element.appendChild(btn);
return element;
}
Compile using npm run dev
and inspect the package contents.
Now re-compile using npm run build
(which uses production mode) and look for the square function (e*e).
Can you find the square function in the code?
Tree shaking can yield a significant decrease in bundle size when working on larger applications with complex dependency trees.
Here's a demonstration showing two modules ./src/mod1.js
and ./src/mod2.js
using lodash
.
Initially they are both compiled with their own copy of lodash
, a third-party script.
Note the module sizes in the terminal.
Afterward we use SplitChunksPlugin
to combine duplicate code from modules (aka lodash
).
Note the new module sizes.
Update webpack.config.js
to add the chunkFilename
attribute:
output: {
filename: '[name].bundle.js',
+ chunkFilename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
Create a new index file (index3.js
):
// import _ from 'lodash';
function getComponent() {
// Lodash, now imported by this script
// element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return import(/* webpackChunkName: "lodash" */ 'lodash').then(_ => {
var element = document.createElement('div');
var _ = _.default;
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}).catch(error => 'An error occurred while loading the component');
}
// document.body.appendChild(component());
getComponent().then(component => {
document.body.appendChild(component);
})
Did you Remember? Add
index3.js
to the entry settings of thewebpack.config.js
file.
Note the use of webpackChunkName in the comment. This will cause our separate bundle to be named lodash.bundle.js instead of just [id].bundle.js
Compile and inspect the output using npm run build
Replace ./src/print.js
with the following code:
console.log('The print.js module has loaded! See the network tab in dev tools...');
export default () => {
console.log('Button Clicked: Here\'s "some text"!');
};
Create ./src/index3.js
using the following code:
import _ from 'lodash';
function component() {
var element = document.createElement('div');
var button = document.createElement('button');
var br = document.createElement('br');
button.innerHTML = 'Click me and look at the console!';
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
element.appendChild(br);
element.appendChild(button);
// Note that because a network request is involved, some indication
// of loading would need to be shown in a production-level site/app.
button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
var print = module.default;
print();
});
return element;
}
document.body.appendChild(component());
Finally update the webpack.config.js
file:
entry: {
- // main: './src/index.js',
- // app: './src/index2.js',
+ app3: './src/index3.js'
- // print: './src/print.js'
- // mod1: './src/mod1.js',
- // mod2: './src/mod2.js',
},
devtool: 'inline-source-map',
devServer: {
contentBase: './dist',
+ openPage: 'index3.html',
hot: true
},
plugins: [
new CleanWebpackPlugin(['dist']),
// Generates an HTML file
new HtmlWebpackPlugin({
+ title: 'Webpack',
+ filename: 'index3.html'
}),
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin()
],
This when compiled will launch ./index3.html
in the browser.
app3.bundle.js
is loaded.print.bundle.js
file