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:
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

Compiled Angular resources with patched index.json

and index.json should have this content (with different hashes)

Content of index.json with Angular bundles links

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
@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!

Angular app running in Production mode within Flask context


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

Popular posts from this blog

HTTPS in local environment for Angular + Flask project.

Salesforce Authentication 2 (Token Validation and Refresh)

User authentification with Flask-Login