If you ever find a developer hiding in a closet head in hands and tears running down his or her face, you can be sure that webpack did it to them. Web pack is this omnipresent monster that most front-end developers don't know is lurking in the shadows, so close that it can touch you, so near that it can feel your heartbeat. well in this post we are going to pull out that flashlight and shine a light under the bed to expose webpack for the pussy cat it is rather than the hound of hell it's been made out to be.
let's start by creating a directory for our project, i went with pav.wpbase because i go by pav and wpbase stands for "winnie the pooh bear abolishes sour elephants"... no wait it stands for webpack base
mkdir pav.wpbase
any way next enter your directory and initialize a npm project
cd pav.wpbase
npm init
once you initialize your npm project you'll be asked a bunch of config questions, your answers wont really matter but they will be used to initialize your package.json file, the only thing of consequence that i changes was the entry point to the application, however you can always do that after in the actual package.json file.
now with that done if you do a dir of the directory you should have a package.json file
if you open that file it should look very familiar to you
{
"name": "pav.wpbase",
"version": "1.0.0",
"description": "base webpack project",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "pav",
"license": "ISC"
}
exactly what you entered above during your npm interrogation.
anyway next we obviously need to bring in webpack and a whole bunch of other packages so run the following command
npm i -D webpack webpack-cli webpack-dev-server html-webpack-plugin copy-webpack-plugin babel-loader style-loader sass-loader sass @babel/core @babel/preset-env webpack
now to break that down
npm i -D install dev dependencies
webpack: bundles all of your assets into
webpack-cli: lets you use the webpack command line interface ie npx commands
webpack-dev-server: lets you run a localhost server
html-webpack-plugin: lets you copy you html file to your dist folder
copy-webpack-plugin: lets you copy files from your working directory to your dist folder
@babel/core
@babel/preset-env webpack
babel-loader: lets us write backwards/cross browser compatible code
css-loader: translates css to common js
style-loader: Creates style nodes from JS strings
sass-loader: Complies your scss to css
sass: lets you write sass instead of basic css
now that those are referenced make sure to run the npm install command this will actually pull down the referenced packages to be used in your project.
now let's take a look at our package.json file one more time
{
"name": "pav.wpbase",
"version": "1.0.0",
"description": "base webpack project",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "pav",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/preset-env": "^7.15.6",
"babel-loader": "^8.2.2",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "^6.3.0",
"html-webpack-plugin": "^5.3.2",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"style-loader": "^3.3.0",
"webpack": "^5.56.1",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.3.1"
}
}
and as you can see we reference all of the packages that we are going to need for our "simple" webpack project.
next lets create our src folder structure, this is where we are going to code up our website, now this is mostly a matter of preference; there are some conventions that are followed but not religiously, so figure out what works for you are just copy somebody else's and remember you can always refactor.
Now that's the simple structure that I go with.
Next let's create a webpack.config.jsfile at the root of the project (the same level as your src folder), this is where the terror I mean magic happens. so let's start with a very simple implementation, just enough to build a dist folder and create a bundle.js with a index.html page
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/scripts/index.js',
output: {
path: __dirname + '/dist',
filename: "bundle.js"
},
plugins:[
new HtmlWebpackPlugin({
template: "./src/index.html"
})
]
}
Now let's go back into our command line and run the command
npx webpack build
this command will use our webpack config file to create a dist folder; after running the above command take a look at our project
pretty cool, we now have an index.html based on what we did in our src folder, and a bundle.js that is based on our index.js file and anything that it references.
now let's start with something simple open up your html in your src folder and add some content.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>wpbase</title>
</head>
<body>
<header>
<h1>Hello world</h1>
</header>
<section>section 1</section>
<section>section 2</section>
<section>section 3</section>
<section>section 4</section>
<section>section 5</section>
<section>section 6</section>
<footer>Goodbye world</footer>
</body>
</html>
now let's run our build command again
npx webpack build and if we open the index.html file in our dist folder we'll see the following
the exact changes we made and if you notice the URL it is in fact being served from our dist folder, now before we continue let's set up a script in our package.json file so that we don't have to use npx
{
"name": "pav.wpbase",
"version": "1.0.0",
"description": "base webpack project",
"main": "src/scripts/index.js",
"scripts": {
"serve": "webpack serve",
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack build"
},
"author": "pav",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/preset-env": "^7.15.6",
"babel-loader": "^8.2.2",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "^6.3.0",
"html-webpack-plugin": "^5.3.2",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"style-loader": "^3.3.0",
"webpack": "^5.56.1",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.3.1"
}
}
notice the scripts section, we added two more script run and build, these are our scripts that we execute on an npm run <<script name>> so in our cases
npm run build
npm run serve
both commands will work however the latter will not have live update, that is once the server starts it will not update with any changes we make, but let's fix that; back to the webpack.config.js file
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/scripts/index.js',
output: {
path: __dirname + '/dist',
filename: "bundle.js"
},
plugins:[
new HtmlWebpackPlugin({
template: "./src/index.html"
})
],
devtool: 'source-map',
devServer: {
static: {
directory: path.join(__dirname, './'),
watch: true
}
}
}
notice the last two properties in our config, devTool: this will allow for friendlier debugging, so when you you console logs the debugger will let you know the originating js file rather than just the bundled one. as for the second property devServer, this loads your src file into memory and lets your local host serve it via the browser, also the watch property tells our server to update any time our source code changes.
this is huge, this will drastically speed up our workflow.
next let's configure our scss, because who writes basic css anymore. so let's again open up our webpack.config.js file and add a rule
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/scripts/index.js',
output: {
path: __dirname + '/dist',
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
// 3 Creates `style` nodes from JS strings
"style-loader",
// 2 Translates CSS into CommonJS
"css-loader",
// 1 Compiles Sass to CSS
"sass-loader",
]
}
]
},
plugins:[
new HtmlWebpackPlugin({
template: "./src/index.html"
})
],
devtool: 'source-map',
devServer: {
static: {
directory: path.join(__dirname, './'),
watch: true
}
}
}
notice the new module section with an array of rules. first we test for files that end in a sass extension if a file ends in a sass extension we then run it through our pipeline, where we start by converting sass to scss then css into common js then finally we move that js into our js bundle
to test this lets create two scss files base.scss and header.scss
in our base.scss
next in our header.scss
@import 'base';
header{
h1 {
color:$primaryColor
}
}
and finally in our index.js
import "../styles/header.scss";
now for our scss files to be bundled they have to be referenced in our js file.
make sure to hit ctrl+c to stop your web server and npm run serve to restart it so that your new webpack.config file takes effect.
and voilia we have the following
we're successfully compiling our styles into our bundle.js file, if you build your project using npm run build you'll find something like the following in bundle.js
// Module
___CSS_LOADER_EXPORT___.push([module.id, "h1 {\n color: #F0f;\n}",
"",{"version":3,"sources":["webpack://./src/styles/header.scss",
"webpack://./src/styles/base.scss"],"names":[],"mappings":
"AAEA;EACE,WCHa;ADEf","sourcesContent":["@import 'base';\r\n\r\nh1
{\r\n color:$primaryColor\r\n}\r\n","$primaryColor: #F0f;\r\n"],
"sourceRoot":""}]);
which is a js compiled version of our scss
now for the final step let's include our babel to make our code backwards compatible, again you guessed it back to the webpack.config.js file
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/scripts/index.js',
output: {
path: __dirname + '/dist',
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.s[ac]ss$/i,
use: [
// 3 Creates `style` nodes from JS strings
"style-loader",
// 2 Translates CSS into CommonJS
"css-loader",
// 1 Compiles Sass to CSS
"sass-loader",
]
}
]
},
plugins:[
new HtmlWebpackPlugin({
template: "./src/index.html"
})
],
devtool: 'source-map',
devServer: {
static: {
directory: path.join(__dirname, './'),
watch: true
}
}
}
and again we added a rule this time to use the babel-loader. next we have to create a .babelrc file at our project root.
{
"presets": ["@babel/preset-env"]
}
simple enough, 99 times out of 100 this will suffice
and that's it, now our JavaScript is cross browser and backwards compatible to the best of babel's ability.