最近在忙一個私活,因為大屏展示頁面是用 React 寫的(使用 create-react-app),甲方想訪問後端控制頁面的網址就能直接訪問這個大屏展示,而不是前後端分離部署。
自己嘗試了一下,還是遇到了點問題。
create-react-app 會將打包的結果放在項目根目錄中的 build 文件夾,打包後的路徑結構:
- build
- static
- css
- style.[hash].css
- style.[hash].css.map
- js
- main.[hash].js
- main.[hash].js.map
- index.html
- [more meta files]
create-react-app 打包的靜態資源都放在了 static
路徑下。比如打包後 index.html
中的一個鏈接:
<link href="/static/css/2.0a6fdfd6.chunk.css" rel="stylesheet" />
瀏覽器解析後,會發出一個請求 GET /static/css/2.0a6fdfd6.chunk.css
。
如果簡單的添加一個路由返回 index.html
文件的話,就有以下的問題出現:
因為 Flask 本身就有 static_folder
的概念,所有請求 /static
路徑的請求都會從配置的 static_folder
中讀取文件並返回。
index.html 中請求的資源都會從 static_folder
中拉取,那你說把打包後的文件直接放在 static_folder
不就好了?
因為本身 static_folder
中就有一些後台頁面需要的靜態資源,也是按類型建立了文件夾:css/js 等,如果直接把 React 打包後的資源直接複製到 static_folder
中,那麼不同的 js 文件都混雜在一起了。React 每次重新打包生成的 js 文件都不一樣的話,每次更新起來還要把原來的刪除再複製過去。
解決方案#
搜了一下解決方案,發現了 Stackoverflow 的 討論,順利應用,記錄一下過程。
順便一提,一般前後端分離的部署是使用 Nginx 等 Server 來返回靜態資源,也方便做緩存之類的。
高票答案的解法是捕獲所有路由,根據請求的路徑來返回對應的文件。
他(以及群策眾力)的栗子:
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')
就是根據請求的 path
來從不同的文件夾返回內容。
測試路徑是否要請求一個文件 => 發送相應的文件 => 否則發送 index.html。
實戰#
根據思路,我們也要捕獲所有的路徑:
業務的關係,我這裡使用一個 front
藍圖來 serve 靜態網站,路徑在 app/front_api/__init__.py
。
把靜態資源放在 app/react_app
下:
然後設置 react_folder
這個變量為靜態資源的路徑就好。
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")
設置前端 homepage#
由於使用了藍圖的前綴功能,所以我們前端拉取靜態資源的時候也得有個前綴才能訪問到具體文件。
舉個例子,當你訪問 http://localhost:5000/front
的時候是獲取到 index.html
,然後 index.html
裡面的資源地址還是 /static/xxxx
,我們得讓瀏覽器往 /front/static/xxxx
訪問才能被我們的視圖函數捕捉到。
我們可以在 package.json
中設置 homepage
參數來指定基本路徑。
在 package.json
中設置即可:
"homepage": "."
這樣 index.html
裡面的資源地址就會被設置為 ./static/xxxx
,瀏覽器訪問時也就會訪問 /front
+ ./static/xxx
了。
這樣就會被我們的視圖函數捕捉到,然後從設置的文件夾中拉取文件了。
設置後端 API 地址#
還有一個小點要注意。
前端進行開發測試的時候也要請求後端,所以發請求的時候要注意根據不同開發環境來設置不同的 BASE_URL:
let BASE_URL;
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
BASE_URL = 'http://127.0.0.1:5000';
} else {
BASE_URL = '';
}
// 發請求時拼接鏈接: BASE_URL + "/front/alarm"
這樣整個功能就實現好了。
優化#
參考自:鏈接:https://www.jianshu.com/p/b348926fa628
作者:AricWu
來源:簡書
我們打包後的文件需要手動移動到後端的相應文件夾裡。手動移動比較麻煩的話,可以寫 npm scripts 的鉤子 來自動化這件事情。
npm 腳本有 pre 和 post 兩個鉤子,可以在這兩個鉤子完成一些準備工作和清理工作。
我們為 build 加入 prebuild 和 postbuild 兩個鉤子。
"prebuild": "rimraf ../backend/app/react_app/*",
"build": "cross-env GENERATE_SOURCEMAP=false craco build",
"postbuild": "cpx -C build/** ../backend/app/react_app/",
打包前刪除後端已有的文件,打包後把新文件複製過去。
使用了 rimraf 和 cpx 兩個跨平台的 cli 工具包。
這裡還有一些其他的跨平台的 cli 工具包:
awesome-nodejs-cross-platform-cli