Build Angular frontend to static resource in Flask (with few hacks)
Last time (https://angular-python-salesforce.blogspot.com/2020/03/join-flask-and-angular-into-one-project.html) we join backend and frontend in Development Mode. But in Production Mode Flask still returns us blank page. Let's fix that. In Production Mode we don't have angular dev server in localhost:4200. Instead we should use compiled frontend bundle from Flask Static Resources. We need to build our frontend but not with a standard way.
First of all update "build" in package.json with this value:
"build": "cd ./frontend && ng build --prod --base-href / --deploy-url /static/app/ --output-path ../backend/static/app && node ../deploy/generate_index_json.js"
all looks simple according to standard Angular Docs except last part. Here generate_index_json.js is our custom script that patches build results to make it friendly for latter use from Flask.
generate_index_json.js:
This script gets generated index.html and extract required information with links to generated resource. Next it saves this information to index.json file and removes the old index.html. Your ./backend/static/app folder should looks this way
and index.json should have this content (with different hashes)
Why we did it? Because of filenames with hashes. I don't know yet the better way how to extract bundles filenames with hashes, Angular doesn't offer this possibility. Yes, we can disable hashes in angular compiler at all and hardcode filenames in Home template (like we did it in Development mode), but it will cause a lot of issues with caching latter. As an option we can simulate hashes in Flask itself or enforce cache disabling for static resources, but it is just another options. For me index.html that Angular prepare for us is source of trust and I prefer to re-use it as mush as possible.
Next step is to add support for new index.json to Flask Home template.
- home template controller
- home.html
I left <h2>PRODUCTION MODE</h2> to be clear that now our angular app is running from compiled bundles in static resource and works inside Flask context. Profit!
IMPORTANT: Add ./backend/static/app folter to .gitignore. We don't need compiled bundles in our repository. At this moment my .gitignore looks this way:
First of all update "build" in package.json with this value:
"build": "cd ./frontend && ng build --prod --base-href / --deploy-url /static/app/ --output-path ../backend/static/app && node ../deploy/generate_index_json.js"
all looks simple according to standard Angular Docs except last part. Here generate_index_json.js is our custom script that patches build results to make it friendly for latter use from Flask.
generate_index_json.js:
const fs = require('fs');
console.log('POST BUILD SCRIPT STARTED');
const indexHtml = fs.readFileSync('../backend/static/app/index.html', 'utf8')
let result = indexHtml.match(/<head>((.|\s)*?)<\/head>/);
let headHtml = result[1].trim();;
console.log('---- HEAD ----');
console.log(headHtml);
result = indexHtml.match(/<body>((.|\s)*?)<\/body>/);
let scriptsHtml = result[1].trim();
console.log('---- SCRIPTS ----');
console.log(scriptsHtml);
let data = {
head: headHtml,
scripts: scriptsHtml,
}
fs.writeFileSync('../backend/static/app/index.json', JSON.stringify(data));
fs.unlinkSync("../backend/static/app/index.html");
console.log('POST BUILD SCRIPT FINISHED');
This script gets generated index.html and extract required information with links to generated resource. Next it saves this information to index.json file and removes the old index.html. Your ./backend/static/app folder should looks this way
and index.json should have this content (with different hashes)
Next step is to add support for new index.json to Flask Home template.
- home template controller
@app.route('/')
def home():
is_development_mode = os.environ.get('FLASK_ENV', 'production') == 'development'
if is_development_mode:
return render_template('home.html', is_development_mode=is_development_mode)
else:
with open('./backend/static/app/index.json') as json_file:
resources = json.load(json_file)
return render_template('home.html', is_development_mode=is_development_mode, resources=resources)
- home.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Frontend from Flask</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
{% if is_development_mode %}
<base href="/">
{% else %}
{{ resources.head | safe }}
{% endif %}
</head>
<body>
{% if is_development_mode %}
<div>
<script>
console.log('************************************');
console.log('************************************');
console.log(' DEVELOPMENT MODE');
console.log('************************************');
console.log('************************************');
</script>
<div style="position: fixed; top: 0; right: 0; background-color: #ef4646; color: #fff; padding: 0 7px; z-index: 99999;">DEVELOPMENT MODE</div>
</div>
<app-root></app-root>
<script src="https://localhost:4200/runtime.js" type="module"></script>
<script src="https://localhost:4200/polyfills.js" type="module"></script>
<script src="https://localhost:4200/styles.js" type="module"></script>
<script src="https://localhost:4200/vendor.js" type="module"></script>
<script src="https://localhost:4200/main.js" type="module"></script>
{% else %}
<h2>PRODUCTION MODE</h2>
<app-root></app-root>
{{ resources.scripts | safe }}
{% endif %}
</body>
</html>
I left <h2>PRODUCTION MODE</h2> to be clear that now our angular app is running from compiled bundles in static resource and works inside Flask context. Profit!
IMPORTANT: Add ./backend/static/app folter to .gitignore. We don't need compiled bundles in our repository. At this moment my .gitignore looks this way:
node_modules
.vscode
backend/static/app
.env
key
Comments
Post a Comment