Bootcamp
Search…
6.1.5: Webpack with Express

Introduction

Now that we can write frontend JS in ES6 with Webpack, let's incorporate Express to serve our frontend JS to browsers. This module describes how to implement Webpack with Express and how to deploy this setup to Heroku. We will start with the repo setup from the previous module.

Retain src and dist file structure for Webpack

Note that our file structure remains the same from previous Webpack modules. There is still a src folder for our JS and SASS code that we want Webpack to compile, and a dist folder that contains Webpack-compiled files and other files that we may want to expose to the client, such as main.html. Our Express index.mjs is configured to expose files in dist publicly.

Update Webpack config to hash compiled file names

Browsers typically cache client-side code, and if we update our client-side code without updating file names, browsers may believe they already have the latest code (based on the unchanged file name) and not retrieve updated code from the server.
Update our Webpack config to add hashes to the names of Webpack-compiled files to ensure browsers always retrieve the latest code from our servers. Henceforth, every time we run Webpack to compile files, if there has been a change to the content of the files, the compiled file names will be different. This will "break the cache" of the browser to make the browser retrieve fresh file contents on page load. Read more about browser file caching and caching headers here.
Continue with the repo from the previous day's pre-class. In webpack.config.js, change the filename from main.js to main-[contenthash].js
webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
​
module.exports = {
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
filename: 'main-[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [new MiniCssExtractPlugin()],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
],
},
};
You should see in the dist folder, a file created with a hashed string in its name.
We now want to dynamically generate the <script> tag in index.html such that it changes with every new .js file generated in the dist folder.
index.html
<!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" />
<link rel="stylesheet" href="./dist/main.css" />
<title>Document</title>
</head>
<body>
<input class="btn btn-primary" value="i am supposed to be blue" />
<input class="btn btn-secondary" value="i am supposed to be grey" />
​
<div class="outside">
<div class="inside">inside outside</div>
</div>
​
<div class="inside">outside outside</div>
<script src="./dist/main.js"></script>
</body>
</html>
To do this, we use a plugin called HtmlWebpackPlugin
npm install html-webpack-plugin
Next, create a file called template.html in the src folder, and copy over the code in index.html. Delete the script tag.
template.html
<!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>Document</title>
</head>
<body>
<input class="btn btn-primary" value="i am supposed to be blue" />
<input class="btn btn-secondary" value="i am supposed to be grey" />
​
<div class="outside">
<div class="inside">inside outside</div>
</div>
​
<div class="inside">outside outside</div>
</body>
</html>
Add the line that imports HtmlWebpackPlugin to webpack.config.js and add the line new HtmlWebpackPlugin({template: './src/template.html}), to the plugins section. webpack.config.js should look like the following.
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
​
module.exports = {
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
filename: 'main-[contenthash].js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new HtmlWebpackPlugin({ template: './src/template.html' }),
new MiniCssExtractPlugin(),
],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
],
},
};
Running npm run build will generate an index.html file in the dist folder that contains a dynamically-generated script tag. This script tag imports the content of src/index.js that has been transpiled (translated) by Webpack into browser-readable code.
dist/index.html
<!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" />
<link rel="stylesheet" href="./dist/main.css" />
<title>Document</title>
<script defer src="main-c25d5c12288a03e6bbd1.js"></script>
<link href="main.css" rel="stylesheet" />
</head>
<body>
<input class="btn btn-primary" value="i am supposed to be blue" />
<input class="btn btn-secondary" value="i am supposed to be grey" />
​
<div class="outside">
<div class="inside">inside outside</div>
</div>
​
<div class="inside">outside outside</div>
</body>
</html>

Refactor Webpack config files for separate development and production configs

Development and production runtime environments have different requirements. Dev environments typically require quick refresh on file changes for faster development cycles, necessitating extra dependencies. Prod environments typically want minimal code dependencies to reduce the size of files for the client to download.
We will split our original webpack.config.js into 3 files: common, dev, and prod. We will put configuration that is shared between dev and prod environments in webpack.common.js, and dev and prod-specific configuration in webpack.dev.js and webpack.prod.js respectively.
Create 3 files called webpack.common.js, webpack.dev.js, webpack.prod.js in the project directory.
Install webpack-merge.
npm install webpack-merge
webpack.common.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
​
module.exports = {
entry: './src/index.js',
output: {
filename: '[name]-[contenthash].bundle.js',
path: path.resolve(__dirname, '../dist'),
},
plugins: [
new HtmlWebpackPlugin({ template: './src/template.html' }),
new MiniCssExtractPlugin(),
],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
},
},
{
test: /\.scss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
],
},
};
webpack-merge is used to integrate the 3 config files
webpack.dev.js
const common = require('./webpack.common');
const { merge } = require('webpack-merge');
​
module.exports = merge(common, {
mode: 'development',
});
webpack.prod.js
const common = require('./webpack.common');
const { merge } = require('webpack-merge');
​
module.exports = merge(common, {
mode: 'production',
});

Add Node scripts to run Webpack

Previously we ran Webpack directly from the command line. Now we will encapsulate Webpack commands in Node scripts defined in package.json for convenience and so that Heroku can run Webpack. By default, Heroku looks for Node scripts called build and start to execute Node applications deployed to Heroku. Define the following scripts in package.json.
"scripts": {
"build": "webpack --config webpack.prod.js"
"start": "node index.mjs",
"watch": "webpack --watch --config webpack.dev.js",
},
  1. 1.
    build compiles our application with Heroku into a "build" for production environments.
  2. 2.
    start starts our server.
  3. 3.
    watch compiles our application for development environments, and auto-recompiles the application according to the specification in webpack.dev.js. In our example, webpack.dev.js tells Webpack to recompile the application when we have changed files in src.

Configure Express to serve base HTML file

In previous modules we created a dist/main.html file to load our JS to our browsers. Let's connect this file to Express so that Express can serve this file. Notice we are making less use of EJS and more use of DOM code that is injected via a single HTML file. This is a pattern that we will see more of as we move into React in Module 7.
Instruct Express to serve main.html for the root (/) route. The following code is on GitHub here.
routes.mjs
import { resolve } from 'path';
import db from './models/index.mjs';
import initItemsController from './controllers/items.mjs';
​
export default function bindRoutes(app) {
const itemsController = initItemsController(db);
​
app.get('/items', itemsController.index);
​
// special JS page. Include the webpack main.html file
app.get('/', (request, response) => {
response.sendFile(resolve('dist', 'main.html'));
});
}

Developing Express apps with Webpack locally

To develop Express apps with Webpack, we want our Express apps to auto-reload when we change our code so that we can quickly test our changes. Previously we had been using just nodemon, but now there is an extra Webpack compilation step that we will automate with Webpack's "watch" feature, in the Node watch script we wrote above in package.json.
While developing Express apps with Webpack, have 2 terminals open, 1 running nodemon index.mjs and another running npm run watch. The former will reload our Express apps on changes, and the latter will re-run Webpack on changes in src.

Exercises

Clone the below Webpack MVC Base repo and create a demo Heroku app based on the below instructions. The repo contains the above changes and a few additions in webpack.common.js for more complex projects.

Run app locally

Clone the repo.
git clone https://github.com/rocketacademy/webpack-mvc-base-bootcamp.git
cd into the repo folder.
cd webpack-mvc-base-bootcamp
Checkout the full-example branch, which contains default models, migrations and seeders for this example. The main branch is the same code without the default data.
git checkout full-example
Install dependencies.
npm install
Update the config/config.js database config file with your Unix username.
Create the DB locally.
npx sequelize db:create
Run migrations.
npx sequelize db:migrate
Run seeders.
npx sequelize db:seed:all
Start the server.
nodemon index.mjs
Have Webpack watch and re-compile files that need to be re-compiled when changed.
npm run watch
Verify the application works locally by visiting http://localhost:3004.

Deploy app on Heroku

Create a Heroku app.
heroku create
Provision a Postgres database.
heroku addons:create heroku-postgresql:hobby-dev
Push our code to Heroku. We need to specify the full-example branch to push code from that branch to Heroku. See git push heroku syntax here.
git push heroku full-example:main
Run migrations on Heroku.
heroku run npx sequelize-cli db:migrate
Run seeders on Heroku.
heroku run npx sequelize-cli db:seed:all
Visit the app that is hosted on Heroku.
heroku open /