A tutorial on using Webpack 4 with standard conventions.
It is very important to understand the default behavior of webpack
using a minimal configuration,
or without any configuration at all.
Without understanding the default behavior, a developer cannot really understand any configuration file. Default conventions are a double-edged sword.
To assist webpack
users, this tutorial will demonstrate a no-configuration project,
and then incrementally scale a configuration to a more complex but very common scenario.
The only prerequisites are node
and npm
. To see if they are installed, type the following commands.
> node -v
v10.8.0
> npm -v
6.4.1
It is beyond the scope of this tutorial to describe installing and using Node and NPM.
Our goal it to create an HTML page that includes a single bundled script. The script will be the product of transpiling, minifying, and combining several Node modules, including our app along with the dependent third party libraries. Our Javascript will use features not available by default in web browsers, such as the es2016
module import/export statement
.
Our application will be a simple Vue
application that synchronizes the contents of two text fields.
Create an NPM project inside a folder.
mkdir WebpackTutorial
cd WebpackTutorial
npm init -y
The -y
argument will use defaults without prompting to create an npm
package.json
file.
The name of the folder will become the name of the project.
Install the dependencies needed to pack and transpile.
npm i -D webpack @babel/core @babel/cli @babel/preset-env
We use the -D
flag to save the modules in package.json
as a development-time dependency.
Our tutorial application uses Vue
at compile time and runtime, so we will install that, too.
npm i -S vue
We use the -S
flag to save the modules in package.json
as a runtime dependency.
Our HTML file will include our webpack
bundled script. The default output
for webpack
is dist/main.js
, so we will include the output script at the end of our body tag, and place our index.html file inside the dist
directory.
Contents of dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Webpack Tutorial</title>
</head>
<body>
<h1>Webpack Tutorial</h1>
<div id="app"></div>
<script src="main.js"></script>
</body>
</html>
If you try to view the HTML file, you will get an error in the console because we have not created our main.js
file yet.
Failed to load resource: the server responded with a status of 404 (Not Found)
We are going to use es2016
syntax and features not supported in browsers, then transpile them to a format that most browsers support.
The default entry
script name is src/index.js
. We do not have to configure webpack
with the entry
parameter if we use the convention.
src/index.js
import Vue from 'vue/dist/vue.esm.js'
new Vue({
el: '#app',
template: `
<div>
<input v-model="text"/>
<input v-model="text"/>
</div>
`,
data: {
text: 'webpack tutorial'
}
})
We are using a version of Vue that can compile on the fly at the expense of size and speed. We will optimize this later to precompile.
You do not need to understand Vue
here; Basically we two-way bind the text fields to a single text
member; Vue
takes care of the rest.
Build your bundle with webpack
webpack -d
Note how there is only one javascript file created: dist/main.js
; It contains a combination of our code along with the Vue
source code we imported as well.
Open your HTML file in a web browser. The two text fields should synchronize values; Changing one should change the other.
We use the -d
to build using development mode
. Development mode will turn off optimizations and enable debugging features.
We are now going to add a titlecase function inside a new Javascript file, also known as a module.
Create src/titlecase.js
export default function titlecase(str) {
return str.replace(
/\w*/g,
function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}
);
}
This file is a simple module that exports a single function for converting text to title-case.
Import the titlecase
module in your index.js
file, and add a watch function to your Vue
component.
import Vue from 'vue/dist/vue.esm.js'
import titlecase from './titlecase'
new Vue({
el: '#app',
template: `<div>
<input v-model="text"/> <input v-model="text"/></div>
`,
data: {
text: 'Webpack Tutorial'
},
watch: {
text(value) {
this.text = titlecase(value)
}
}
})
You do not need to understand Vue
here; Basically we watch for changes to the text
member, and set it to the title-case value when it changes.
Pack using webpack -d
, and then view index.html
in your browser to see the update. Note how there is still only a single JavaScript file that contains ALL of our code.
Build your bundle with Webpack's production mode
.
webpack -p
By default, production builds are minimized and tree-shaked; The default build mode is production, so we do not need to specify the mode.
We still use -p
however, because we will get a warning if relying on the default mode without defining it in a configuration file.
This will remove unused code (tree shake or dead-code-elimination) and minify the output. The resulting code will be smaller size, compile faster, and run faster. On my machine, the file size of main.js
was reduced from 813kb
to 90.7lkb
. That is a huge difference: Almost 90%!
For the remainder of this tutorial, you can use either webpack -p
to optimize, or webpack -d
to make debugging easier.
We have reached the point where the default configuration can no longer satisfy our needs.
The following examples all require a configuration file. If we put the file in the root of our project, and call it webpack.config.js
, we will not have to tell webpack where to find it.
webpack.config.js
module.exports = {
}
The configuration is simply a node module that exports an object.
Webpack will check the configurations validity when running and report any syntax errors.
It is very useful to remove our script
from index.html
and have webpack
add it automatically.
Let’s generate an index.html
file in the dist
directory, using our original index.html
file as a template.
The plugin will add our bundled JavaScript script
elements for us.
This is very helpful when including multiple entry points and/or code splitting into multiple bundles where each bundle name may include a hash,
or when having dynamic code that is loaded on-demand. Renaming the output bundle filename will also be handled by the HtmlWebpackPlugin
.
First, Move your dist/index.html
file to the root directory. DO NOT IGNORE THIS STEP, as the plugin will overwrite dist/index.html
.
Install the HtmlWebpackPlugin
plugin
npm i -D html-webpack-plugin
Update webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins:[
new HtmlWebpackPlugin({
template:'index.html',
inject:true
})
]
}
The template
parameter instructs the plugin to use index.html
instead of generating one on the fly, and the inject
parameter instructs the plugin to add the webpack output script
elements to the end of the body
tag.
There are many advantages to splitting your code into more than 1 bundle. By separating frequently changed code from unchanging library code, the client only needs to download the changed bundles.
Add this to your webpack configuration:
optimization: {
splitChunks: {
chunks: "all",
},
}
Now when you webpack, all code from outside the src
directory is put into vendors~main.js
, and your source is put into main.js
.
Once we build our project, our chunk sizes are:
Development Mode
Chunk | Size |
---|---|
main.js | 9.25kb |
vendors~main.js | 806kb |
Production Mode
Chunk | Size |
---|---|
main.js | 1.76kb |
vendors~main.js | 89.5kb |
This means when you change your sources, only a 1.76kb
file will need to be reloaded by the client.
If you installed and configured the HtmlWebpackPlugin
as suggested, both of these files will be injected into your dist/index.html
file, making maintenance and further code splitting a breeze.
An explanation of our configuration is available at https://webpack.js.org/guides/code-splitting/. Basically, we are telling webpack to split and refactor duplicate code as well.
We can also split code into modules that download only when needed.
This speeds up initial load times and lowers bandwidth.
Perfect candidates for lazy loading include dynamically loaded dropdowns, click-to-reveal content, multi-page applications, and security situations.
As an example, we will alter our project to only load the code for our lowercase
function when the user types.
index.js
new Vue({
el: '#app',
template: `<div>
<input v-model="text"/> <input v-model="text"/></div>
`,
data: {
text: 'Webpack Tutorial'
},
watch: {
async text(value) {
const titlecase = await import(/* webpackChunkName: "titlecase.lazy" */ './titlecase')
this.text = titlecase.default(value)
}
}
})
As you can see, instead of importing the module at the top, we call the function import()
instead.
It returns a promise
that resolves with the modules exported value.
We use async/await
to handle the asynchronous return.
If you open your console, the bundle will be downloaded the first time the text field's value is changed.
The comment /* webpackChunkName: "titlecase.lazy" */
is optional and provides a hint as to the bundle's name.
If omitted, a numeric name is used for the bundle (1.js, etc).
If we precompile our Vue templates, our code will be small and faster. We can also use an optimized module of the Vue
module.
Use the optimized (non compiler) version of Vue by changing import Vue from 'vue/dist/vue.esm.js
to import Vue from 'vue
in src/index.js
.
Convert the component definition from src/index.js
to a single file component.
Once your start using Vue
Single File Components
, you will never go back to string templates.
src/app.vue
<template>
<div>
<input v-model="text"/>
<input v-model="text"/>
</div>
</template>
<script>
import titlecase from "./titlecase";
export default {
data: function () {
return {
text: 'Webpack Tutorial'
}
},
watch: {
text(value) {
this.text = titlecase(value)
}
}
}
</script>
We now import the app module (our compiled Vue
template) and use render
instead of template
in our component definition.
src/index.js
import Vue from 'vue'
import app from './app.vue'
new Vue({
el: '#app',
render: h => h(app)
})
We cannot compile yet, because babel
does not know how to deal with .vue
files.
Install the Vue Loader
and Vue Template Compiler
npm i -D vue-loader vue-template-compiler
Update the webpack
configuration to use the vue-loader
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin')
```js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: 'index.html',
inject: true,
}),
new VueLoaderPlugin()
],
optimization: {
splitChunks: {
chunks: "all",
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
]
}
}
We import the loader, add an instance to the plugins, and add a rule to use vue-loader on all files that end with .vue
.
Once we build our project, our chunk sizes are: | Chunk | Size |
---|---|---|
main.js | 2.23kb | |
vendors~main.js | 65.2kb |
Our vendors bundle dropped in size from 89k
to 65k
.
For more information on the vue-loader-plugin, see https://vue-loader.vuejs.org/guide/#manual-configuration.
Until now, you have been viewing your static HTML page in a browser. Wouldn't it be nice if your changes were immediately updated in the browser without needing to reload?
Launch webpack-dev-server
webpack-dev-server -d
Open http://localhost:8080
in your browser.
In src/app.vue
, change the string 'Webpack Tutorial'
to 'My Webpack Tutorial'
and save.
Your page should update immediately.
Lets make our text fields yellow. Add this to the end of src/app.vue
<style>
input {
background: yellow;
}
</style>
Now we configure babel
to use the css-loader and the vue-style-loader
Install the css-loader into NPM
npm i -D css-loader
Add a .css
rule to webpack.config.js
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.css$/,
loader: "vue-style-loader!css-loader"
}
]
}
This will use the css-loader for css files, and then feed the output into the vue-style-loader.
To make things consistent, add build
and serve
commands to NPMs package.json
package.json
"scripts": {
"build": "webpack -p",
"serve": "webpack-dev-server -d"
},
And now run them.
npm run serve
npm run build
Using the default conventions makes your configuration file lean, and also makes your project easier to understand by others, and by you when you revisiting your own code. Even if following conventions, I suggest you always have a configuration file, even if it is empty. Even better, add the common HtmlWebpackPlugin
and optimization options but disable them until you need them.
https://webpack.js.org/api/cli/ https://www.valentinog.com/blog/webpack-tutorial/
This is the convention when we do not supply a configuration to webpack
.
webpack.config.js
module.exports = {
mode: 'production'
entry: 'src/index.js'
output:{
filename: 'dist/main.js`
}
}
Running webpack without arguments is the same as this:
webpack --config webpack.config.js