User-131 2 years ago
Thank you for this great post. I have a python flask application with a simple html form to be rendered. I have a couple JS packages that I needed to use with my application. I wanted to bundle all JS files and provide a single source in my html file. I got the web service running but the html page does not render and I get a 404. Can you please help? What am I missing?
Introduction
I have probably spent a week or two looking for some "best practices" for this problem: How to set up a Webpack for multi-page apps or websites? I haven't found many resources on the internet, so I decided to dedicate a blog post to it. Hopefully, you will find this helpful. So let's start!
Project Setup
Go on and create flask-with-webpack
to try this out.
The project structure should be like this.
flask-with-webpack
|- app
| |- __init__.py
| |- asset.py
| |- templates/
| | |- index.html
| |- static/
|- assets/
assets
folder will be where our unbundled frontend code
leaves. app
folder will contain our python code.
This will be our boilerplate:
1 2 3 4 5 6 7 8 9 10 11 |
|
And our jinja2 template should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Let's do the frontend now: Go into the assets folder and run:
npm init -y
If npm and node are correctly installed, this should create
package.json
inside the assets
folder. While you are still
there, please run the following command to install our dev dependencies.
npm install -D webpack webpack-cli
Our webpack config file should be like this:
1 2 3 4 5 6 7 8 9 10 |
|
Pay attention to the 7th line; we directed the output to the static folder of our flask app, so our bundled assets go there.
Now let us update our package.json
:
{
"name": "assets",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack --mode=development",
"watch": "webpack --mode=development --watch",
"prod": "webpack --mode=production"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1"
}
}
We added dev, prod, and watch scripts. This is just a convenient way to run a webpack. We are going to use the watch script mostly during our development process.
Also, we need to create our entry point. Let it be something simple:
console.log("Hello World");
Let's run npm run dev
to test this out. If you did
everything well far, app/static/dist/main.js
should be created.
From this point, one could argue that we are almost there,
and now all we have to do is to call url_for("static", filename="dist/main.js")
inside our template, but that won't help us with problems on
the production environment where we still have to deal with the
cache-busting.
Cache busting
Cache busting is the process of uploading a new file to replace an existing file that is already cached. We should apply this in the production environment, where current users have old static files cached, and if we changed something in these files (images, CSS or JS) we expect that to reflect on their browsers.
app-name
|- app
|- __init__.py
|- templates/
| |- index.html
|- static/
|- dist/
|- main.js
|- assets
|- webpack.config.js
|- package.json
|- package-lock.json
|- node_modules/
|- src/
| |- index.js
Webpack manifest plugin will help us deal with this issue, alongside the flask extension we will create. Let's install the webpack plugin first in our assets folder.
npm install -D webpack-manifest-plugin
Then, update webpack.config.js
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
First, we import the plugin in add it to the plugins section.
Next, we update filename
and publicPath
inside webpack output.
Run npm run dev
again. In our app/static/dist
folder,
there should be a new file, manifest.json,
with content similar to this:
{
"main.js": "/static/dist/main.a515dae3a1276df56c7b.js"
}
Let's now create a simple extension that uses this file that helps us load the correct content.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
Code should be self-explanatory, tricky parts are these highlighted
lines. We want to make sure that if the app runs in development mode
before each request, we reload manifest.json cause content could
change in the meantime. We also want to make the extension available in
templates, so we use app.context_processor
.
Now we have to initialise this extension like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
And our last step is to use the asset extension
in our index.html
like this:
1 |
|
And that's it! If you get this far, you should have this code working. From this point, you can add new plugins to your webpack and tweak it to work in different ways. I would recommend adding CleanWebpackPlugin and MiniCSSExtractPlugin first.
Bonus - handling CSS
Let see how we can use Webpack to compile our styles. First, we are going to
create styles.scss
cause we want to extend Bulma, for example.
app-name
|- app
|- __init__.py
|- templates/
| |- index.html
|- static/
|- dist/
|- main.js
|- assets
|- webpack.config.js
|- package.json
|- package-lock.json
|- node_modules/
|- src/
| |- index.js
| |- styles.scss
We need to install some new packages:
npm install bulma
npm install -D css-loader sass-loader node-sass mini-css-extract-plugin
Let's copy-paste Bulma example for sake of this tutorial.
@charset "utf-8";
// Import a Google Font
@import url('https://fonts.googleapis.com/css?family=Nunito:400,700');
// Set your brand colors
$purple: #8A4D76;
$pink: #FA7C91;
$brown: #757763;
$beige-light: #D0D1CD;
$beige-lighter: #EFF0EB;
// Update Bulma's global variables
$family-sans-serif: "Nunito", sans-serif;
$grey-dark: $brown;
$grey-light: $beige-light;
$primary: $purple;
$link: $pink;
$widescreen-enabled: false;
$fullhd-enabled: false;
// Update some of Bulma's component variables
$body-background-color: $beige-lighter;
$control-border-width: 2px;
$input-border-color: transparent;
$input-shadow: none;
// Import only what you need from Bulma
@import "../node_modules/bulma/sass/utilities/_all.sass";
@import "../node_modules/bulma/sass/base/_all.sass";
@import "../node_modules/bulma/sass/elements/button.sass";
@import "../node_modules/bulma/sass/elements/container.sass";
@import "../node_modules/bulma/sass/elements/title.sass";
@import "../node_modules/bulma/sass/form/_all.sass";
@import "../node_modules/bulma/sass/components/navbar.sass";
@import "../node_modules/bulma/sass/layout/hero.sass";
@import "../node_modules/bulma/sass/layout/section.sass";
Finally, we need to update our webpack.config.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
So first, we import MiniCssExtractPlugin and add it to
the plugins section. Then, we add a new entry - styles
in the appropriate area.
I won't go into explaining the rules
section cause you
could check official webpack documentation about that.
Now, if we run npm run dev
new style file will be
generated and manifest.json
will look
something like this:
{
"main.js": "/static/dist/main.544743f724b236ee60fe.js",
"styles.css": "/static/dist/styles.3a4a4455de45c7b54bde.css",
"styles.js": "/static/dist/styles.31d6cfe0d16ae931b73c.js"
}
But wait, in this process, we also got styles.js
that
appears to be empty. This is a drawback of Webpack, but
fortunately, the webpack-remove-empty-scripts
plugin will eliminate empty
js
files. So let install it.
npm install -D webpack-remove-empty-scripts
Update your webpack.config.js
:
const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts')
module.exports = {
// ...
plugins: [
// ...
new RemoveEmptyScriptsPlugin(),
// ...
]
};
Now, if we run npm run dev
again, styles.js
will
disappear, and there won't be any junk!
We can include these styles in the same way we did with
the main.js
script.