Recently, I have been busy with a freelance project. The display page for the large screen is written in React (using create-react-app). The client wants to access the backend control page directly by visiting the URL of the display page, instead of deploying the frontend and backend separately.
I tried it myself, but encountered some problems.
create-react-app will place the packaged result in the build folder in the project root directory. The directory structure after packaging is as follows:
- build
- static
- css
- style.[hash].css
- style.[hash].css.map
- js
- main.[hash].js
- main.[hash].js.map
- index.html
- [more meta files]
The static resources packaged by create-react-app are all placed under the static
path. For example, a link in the index.html
file after packaging:
<link href="/static/css/2.0a6fdfd6.chunk.css" rel="stylesheet" />
After the browser parses it, it will send a request GET /static/css/2.0a6fdfd6.chunk.css
.
If I simply add a route to return the index.html
file, the following problems will occur:
Because Flask itself has the concept of static_folder
, all requests to the /static
path will read files from the configured static_folder
and return them.
The resources requested in index.html
will be fetched from the static_folder
. So you might suggest putting the packaged files directly into the static_folder
, right?
Because there are already some static resources required for backend pages in the static_folder
, they are also organized into folders by type: css/js, etc. If we directly copy the resources packaged by React into the static_folder
, different js files will be mixed together. If the js files generated by React each time it is repackaged are different, we would have to delete the original files and copy them again every time we update.
Solution#
I searched for a solution and found a discussion on Stackoverflow here. I successfully applied it and recorded the process.
By the way, in general, frontend and backend separation is deployed using servers like Nginx, which makes it easier to handle static resources and caching.
The highly voted answer suggests capturing all routes and returning the corresponding files based on the requested path.
Here is an example:
import os
from flask import Flask, send_from_directory
app = Flask(__name__, static_folder='react_app/build')
# Serve React App
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def serve(path):
if path != "" and os.path.exists(app.static_folder + '/' + path):
return send_from_directory(app.static_folder, path)
else:
return send_from_directory(app.static_folder, 'index.html')
It returns the content from different folders based on the requested path
.
Test whether the path needs to request a file => send the corresponding file => otherwise send index.html
.
Implementation#
Based on this idea, we also need to capture all paths:
Due to the business relationship, I use a front
blueprint to serve the static website, and the path is in app/front_api/__init__.py
.
Place the static resources under app/react_app
:
Then, set the react_folder
variable to the path of the static resources.
from pathlib import Path
from flask import Blueprint, send_from_directory
front = Blueprint("front", __name__, url_prefix="/front")
react_folder = (
Path(__file__).parent.parent / "react_app"
).absolute()
# Serve React App
@front.route("/", defaults={"path": ""})
@front.route("/<path:path>")
def serve(path):
if path != "" and (react_folder / path).exists():
return send_from_directory(str(react_folder), path)
else:
return send_from_directory(str(react_folder), "index.html")
Set Frontend Homepage#
Since we are using the prefix feature of the blueprint, we need to have a prefix when fetching static resources in the frontend to access specific files.
For example, when you visit http://localhost:5000/front
, you will get the index.html
file, and the resource URLs in index.html
are still /static/xxxx
. We need to make the browser access /front/static/xxxx
in order to be captured by our view function.
We can set the homepage
parameter in package.json
to specify the base path.
Simply set it in package.json
:
"homepage": "."
This will set the resource URLs in index.html
to ./static/xxxx
, and the browser will access /front
+ ./static/xxx
when visiting.
This way, it will be captured by our view function and fetch files from the specified folder.
Set Backend API Address#
There is one more thing to note.
When developing and testing the frontend, we also need to make requests to the backend. So when sending requests, we need to set different BASE_URL
based on different development environments:
let BASE_URL;
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
BASE_URL = 'http://127.0.0.1:5000';
} else {
BASE_URL = '';
}
// Concatenate the link when sending requests: BASE_URL + "/front/alarm"
This completes the implementation of the entire feature.
Optimization#
Reference: Link: https://www.jianshu.com/p/b348926fa628
Author: AricWu
Source: Jian Shu
We need to manually move the packaged files to the corresponding folder in the backend. If manual movement is too cumbersome, we can write npm scripts hooks to automate this process.
Npm scripts have pre and post hooks, which can be used to perform preparation and cleanup work.
We add prebuild and postbuild hooks to the build script.
"prebuild": "rimraf ../backend/app/react_app/*",
"build": "cross-env GENERATE_SOURCEMAP=false craco build",
"postbuild": "cpx -C build/** ../backend/app/react_app/",
Before packaging, delete the existing files in the backend, and after packaging, copy the new files.
We use two cross-platform CLI tools, rimraf and cpx.
Here are some other cross-platform CLI tools:
awesome-nodejs-cross-platform-cli