Webpack - Bundling Frontend & Fullstack React Applications.
What is Webpack?
Webpack is a module
bundler we can use to bundle our Node projects, such as any app you have been making in React. When you write applications using Node you are essentially writing modules
every time you create a new file. Webpack takes all of these modules
we write, and transforms it into code browsers can read and execute.
As you may have learned by now, trying to load Node applications by either double-clicking the index.html page, or trying to open it with a live-preview, will fail. This is because modules
need to be bundled back together into a cohesive set of javascript, css, and HTML to be read by browsers. In essence, webpack
's job is to take our project, (which may include hundreds of files), and compress it down into a index.html
and main.js
.
Webpack is HIGHLY configurable, so we will be sticking to a boiler plate setup that will allow you to make front-end/full-stack applications using all the skills and modules we teach at V School.
We will only be using webpack for our front end since our backend doesn't need to be bundled for a web browser to read. However, know it can be used on the backend as well to make syntax such as import/export
available rather than having to use the Node require
.
Dependencies
The following are the needed dependencies
used in this write-up. These are all the exact names you would use in an npm i --save-dev
. Note, all of these should be installed with the --save-dev
flag as they are dependencies only needed for development purposes:
- webpack: The webpack node module
- webpack-cli: The command line tool we use to interact with webpack.
- webpack-dev-server: Used to set up our live dev-server like the one in
create-react-app
. - babel-loader: Loader used to compile es6 down to javascript browsers can read. The next 3 dependencies are all used to configure babel-loader for compiling our javascript.
- @babel/core
- @babel/preset-env
- @babel/preset-react
- **html-webpack-plugin** and **html-loader**: Used together to allow webpack to generate HTML pages.
- **css-loader**: interprets @import and url() like import/require() and will resolve them.
- **style-loader**: Allows for `import ‘./style.css’` syntax.
- **file-loader**: Allows for imports and use of files such as .png and .jpg. (.json is already understood by webpack)
- **@babel/plugin-proposal-class-properties**: allows for Es6 `class` syntax and `=>` functions on class methods for auto-binding.
React Dependencies:
- react
- react-dom
- prop-types
Setting up a Node project
To begin, open the console and create a new directory called wpack-intro
. cd
into wpack-intro
and run an npm init -y
for a package.json
:
myconsole/mkdir wpack-intro
myconsole/cd wpack-intro
myconsole/wpack-intro/
myconsole/wpack-intro/npm init -y
Now let's create a src
folder and a .gitignore
inside of this directory.
myconsole/wpack-intro/mkdir src
myconsole/wpack-intro/cd src
Your directory should now look like this:
|- wpack-intro
|- src
|- package.json
|- .gitignore
In the .gitignore
, add node_modules
.
Now install webpack
and the webpack-cli
with the following command. These modules are for using and interacting with webpack
using our console:
npm i --save-dev webpack webpack-cli
Next, create the file needed to write and configure your webpack
settings. On the same level of your src
folder, create a webpack.config.js
file.
Your directory structure should now look like this:
|- wpack-intro
|- src
|- node_modules
|- .gitignore
|- package.json
|- package-lock.json
|- webpack.config.js
Entry and Output
The webpack.config.js
file is where our webpack configuration is written. It's here where we teach webpack where to get our code, how to handle different file types (css, js, html, jpg, etc.), and where to put the finalized bundle of code when webpack
is done doing it's job. We'll start with our entry
and output
// webpack.config.js //
module.exports = {
entry: '',
output: {}
}
Entry expects a path to where we want webpack
to enter our project for bundling. Convention for this is an index.js
, so create a index.js
inside of your src
folder, then put the string path ./src/index.js
as the entry
in the webpack.config.js
This is the same top level index.js
file that you have when you use create-react-app
.
Output is where we configure the location and name of the file that webpack will output when it's completed. The standard for this is a dist
directory and a main.js
file that will hold our bundled code. Update your webpack.config.js
to look like the following:
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
}
We have to require('path')
as it is not available to use by default. In the output
we are telling webpack to create a directory name called dist
, and to put the final main.js
file inside of it. Note, we do not have to create a dist
folder or main.js
, webpack will do this for us.
Note: These
entry
andoutput
settings are the new webpack default, so if you are going to want these settings as they are, you do not have to include them in yourwebpack.config.js
file as webpack will look in for a./src/index.js
and create the/dist/main.js
automatically.
package.json scripts
Before we continue with configuring our webpack, we need to add two scripts to our package.json
file. Change the scripts
section of your package.json
to reflect the following:
"scripts": {
"start": "webpack-dev-server --open --mode development",
"build": "webpack --mode production"
},
This allows us to now use npm start
to start up a development server( just like create-react-app
), and use npm run build
to have webpack bundle our code.
Module & Rules
The module
section of our webpack
is where the majority of our code will go. Inside of module
we will define a set of rules
that webpack
will use when bundling our code. The first rule we will define is our babel-loader
, which will teach webpack how to bundle our .js
files into javascript the browser can read.
Do a npm i babel-loader --save-dev
, and then update your webpack.config.js
file to look like the following:
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: { loader: "babel-loader" }
}
],
}
}
module
is an object that accepts a rules
key, and rules
is an array of objects that define different rules about our webpack
. Let's look at the different parts:
- test(required): This is a regular expression
/\.js$/
. The$
means this will look for any file that ends with.js
, and then usebabel-loader
when bundling it. The test part of loaders is required so that the loader knows what file-type it is for. - exclude(optional): This tells the rule to NOT to check any
.js
files inside of thenode_modules
folder. - use(required): Here we define which
loader
thisrule
should use.
The babel-loader
needs some configuration to work with all our js syntax and react
code. For this, we will install three more node modules which we can use to configure this loader to work as we need.
Now run npm i @babel/core @babel/preset-env @babel/preset-react --save-dev
These 3 packages are used by babel-loader
to read and compile our Javascript from Es6 and React, to a readable Javascript for browsers.
.babelrc
To implement this configuration for your babel-loader
, create a file on the same level as your webpack.config.js
and name it .babelrc
. In the .babelrc
file, add the following preset instructions:
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
The .babelrc
is the babel-loader
configuration file. Typically you do not need much in this file, but let's add one more part here that we'll need later.
Run an npm i @babel/plugin-proposal-class-properties --save-dev
, and then update the .babelrc
to include this as a plugin.
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}
This second package is used to configure babel so that we can write fat arrow functions for our methods inside of React classes. Without this package, fat arrow functions do not auto-bind this
in class
components, so binding in the constructor
would still be needed.
HTML, CSS, and Other File Types
Next we need to implement the ability to bundle CSS, produce HTML, and handle other file types such as images.
To begin, run the following npm i
's:
npm i html-webpack-plugin --save-dev
npm i html-loader --save-dev
npm i css-loader --save-dev
npm i style-loader --save-dev
npm i file-loader --save-dev
With these modules, we can now set up our rules for html
, css
, and other files such as .jpg
or .png
. Add a rule for /\.css$/
, /\.html$/
, and /\.(jpg|png|jpeg|gif)$/
files, making your webpack.config.js
look like the following.
const path = require('path') // needed if using path.resove() from node
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: { loader: "babel-loader" }
},
{
test: /\.html$/,
use: { loader: "html-loader" }
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'file-loader',
options: {
outputPath: './resources/imgs',
name: '[name].[ext]'
}
},
]
}
]
},
}
The css-loader
and html-loader
rules are pretty self-explanatory as they look almost exactly like our babel-loader
rule. However the file-loader
has a few more configurations we've added.
First, the regular expression /\.(png|jpg|gif|jpeg)$/
means to look for any file that begins with a .
, and ends in either a png
, jpg
, gif
, or jpeg
. The |
symbol is an or
statement. An added options
object is given to tell webpack to put the image files it finds into their own directory called resources/imgs
, and name the files by their name and extension. This means, if we have a test.png
file in our development directory, webpack will create a resources/imgs
directory structure inside of the dist
folder, and place the images in there naming it what they are. So test.png
would still be test.png
when bundling was done.
Plugins
Next we need to require html-webpack-plugin
and place it in our webpack config so that webpack can generate the needed index.html
when it does the bundle process. Add const HtmlWebPackPlugin = require("html-webpack-plugin")
to the top of your webpack.config.js
file. Then, we will add this as a plugin
in our webpack file. The plugins
section of our webpack will go after the module
section like this:
const HtmlWebPackPlugin = require("html-webpack-plugin")
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: { loader: "babel-loader" }
},
{
test: /\.html$/,
use: { loader: "html-loader" }
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'file-loader',
options: {
outputPath: './resources/imgs',
name: '[name].[ext]'
}
},
]
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./public/index.html",
filename: "./index.html"
})
],
}
This plugin shows webpack where to find a .html
template file for the html
file it will output at the end of its bundling process. Because this plugin requires a template, we now need to add this to our exisiting project directory.
Inside of your project directory, create a public
folder and put an index.html
inside of it with a standard html
boiler-plate. Your directory structure should now look like this:
|- wpack-intro
|- public
|- index.html
|- src
|- index.js
|- node_modules
|- .gitignore
|- package.json
|- package-lock.json
|- webpack.config.js
|- .babelrc
And the index.html
file in your public
folder should look like the one we use in create-react-app
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Webpack React</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
We will stick with the id="root"
so that our index.js
of our react app looks the same when we use the ReactDOM render method. etc:
ReactDOM.render(<App />, document.getElementById('root'))
Building & Running your application
Congrats! You now have a functioning webpack you can use to develop your React application, (or any other node program you write) without having to use create-react-app or other pre-built boilerplates.
Now whenever you want to make a new build
of your project, you can run npm run build
and see a dist
folder appear with your bundled app. You will also run npm start
just like you do with create-react-app to start up your live dev-server.
Go ahead a create a small react app with your index.js in the src
folder just as you would with create-react-app
. Once you have that built and can see your application using npm start
, try running npm run build
. webpack
will run for a few moments, and then a dist
folder should appear with an index.html
and main.js
. That dist
folder is your bundled app that is ready for deployment!
Using webpack for a full-stack application
While we won't need webpack for our backend, we will need to add an additional configuration to our webpack.config.js
if we are making a full stack app in order to proxy
to our own API. First expand your directory structure to be for full-stack. Everything we have made so far will need to go in a client
folder, and then we can make our server.js
, routes
, and models
directories along with creating a package.json
and installing needed backend dependencies. After moving your files around, this will be your full stack file structure:
|- wpack-intro
|-client
|- public
|- index.html
|- src
|- index.js
|- node_modules
|- .gitignore
|- package.json
|- package-lock.json
|- webpack.config.js
|- .babelrc
|- server.js
|- routes
|- models
|- package.json
|- package-lock.json
|- node_modules
|- .gitignore
|- .env
With this set-up, we only need to make one addition to our webpack.config.js
file, and that is to set up the proxy
so that we can make a http request to /data
on our front end, and hit the endpoint /data
on our backend. For this, we need to add another section called devServer
to our webpack config under our plugins, so the final webpack.config.js
would look like this:
const HtmlWebPackPlugin = require("html-webpack-plugin")
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'main.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: { loader: "babel-loader" }
},
{
test: /\.html$/,
use: { loader: "html-loader" }
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'file-loader',
options: {
outputPath: './resources/imgs',
name: '[name].[ext]'
}
},
]
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./public/index.html",
filename: "./index.html"
})
],
devServer: {
proxy: [{
context: ['/'],
target: 'http://localhost:7000',
}]
}
}
The devServer
section takes a proxy
array of objects. We define in the first object a context
and target
. context
is what end point request will trigger this proxy to function, and the target
is what webpack dev server should prefix on our request. With this setting, we make an axios request like this:
axios.get('/api/todos')
And webpack uses the proxy to make it request to:
axios.get('http://localhost:7000/api/todos')
The reason that context
is an array is so that you can add other request enpoints you would like webpack to proxy for you, such as /api
.