# Flask + React 前後端串接
## 前言
前陣子在工作上需要將後端Flask API Server與前端React進行串接
查了一下發現中文資料並不多,且範例程式碼有點舊或是不完善
所以打算寫一篇文章來做個紀錄
## 正文
這篇架設你對`Flask`與`React`有一定基礎
本篇會著重在前後端串接的實作
所以一些基礎教學就不會再說明
我們先模擬開發時的狀況
`Flask`會跑在預設的`5000`port上
而`React`則會跑在預設的`5173`port上
> 這邊前端打包工具使用Vite,如果你的port跟我不一樣,那也沒關係
> 只要前後端port不衝突就可以了👍
### 前端程式碼
首先實作幾個簡單的前端畫面
有首頁跟登入,只要點擊`login`,就可以看到後端傳來的資訊
為了保持簡潔,拆分成幾個檔案
分別是`main.jsx`, `layout.jsx`, `home.jsx`, `login.jsx`
> CSS的部分不展示在此
```jsx=
// main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Login from './login.jsx';
import Home from './home.jsx';
import Layout from './Layout.jsx';
const router = createBrowserRouter(
[
{
path: '/',
element: <Layout/>,
children: [
{
index: true,
element: <Home/>
},
{
path: 'login',
element: <Login/>
}
]
}
]
)
createRoot(document.getElementById('root')).render(
<StrictMode>
<RouterProvider router={router}/>
</StrictMode>
)
```
```jsx=
// layout.jsx
import { Link, Outlet } from "react-router-dom";
import './layout.css';
const Layout = () => {
// Layout
return (
<>
<div className="layout">
<Link to={"/"}>Home</Link>
<Link to={"/login"}>Login</Link>
</div>
<div id="detail">
<Outlet/>
</div>
</>
)
}
export default Layout;
```
```jsx=
// home.jsx
const Home = () => {
return (
<>
<p>Here is Home page :)</p>
</>
)
}
export default Home;
```
```jsx=
// login.jsx
import { useState } from "react";
import './login.css'
const Login = () => {
const [loginCode, setLoginCode] = useState(1);
const [loginMsg, setLoginMsg] = useState("");
const onLogin = async () => {
// 點擊觸發API
const url = "http://127.0.0.1:5000/api/login";
let response = await fetch(url)
.then((response) => response.json());
let { status, msg } = response;
setLoginCode(status);
setLoginMsg(msg);
}
return (
<>
<p>Here is Login page :)</p>
<button type={'button'} onClick={onLogin}>Login</button>
<p className="return_code">login return code: {loginCode}</p>
<p className="return_msg">login return msg: {loginMsg}</p>
</>
)
}
export default Login;
```
### 後端程式碼
下面則是後端程式碼
```python=
from flask import Flask, jsonify, render_template, send_from_directory
from pathlib import Path
template_folder = Path().resolve() / "react_app" / "dist"
static_folder = template_folder / "assets"
app = Flask(__name__, template_folder=template_folder, static_folder=static_folder)
@app.route('/')
def home():
template_path = "index.html"
return render_template(template_path)
@app.route('/api/login')
def login():
result = {
"status": 0,
"msg": "login successful."
}
return jsonify(result)
if __name__ == '__main__':
app.run(debug=True)
```
為了方便管理前端程式碼
我們重新指定了Flask的`template_folder`與`static_folder`
```python
template_folder = Path().resolve() / "react_app" / "dist"
static_folder = template_folder / "assets"
app = Flask(__name__, template_folder=template_folder, static_folder=static_folder)
```
並且讓後端的home controller渲染我們的react app
而其他的controller則用`json`的方式處理
當你分別啟動前後端程式碼後
應該會得到這樣子的畫面


然而此時點Login,並不會得到後端的資料
打開控制台(`F12`),會發現以下錯誤

此時只需要在後端針對跨域問題進行設定就好
這邊是使用`Flask-Cors`這個套件處理
```shell
pip install Flask-Cors
```
安裝完成後,就可以加上這兩行程式碼
```python=
from flask import Flask, jsonify, render_template, send_from_directory
from flask_cors import CORS # 載入套件
from pathlib import Path
template_folder = Path().resolve() / "react_app" / "dist"
static_folder = template_folder / "assets"
app = Flask(__name__, template_folder=template_folder, static_folder=static_folder)
CORS(app) # 設定CORS, 處理跨域問題
@app.route('/')
def home():
template_path = "index.html"
return render_template(template_path)
@app.route('/api/login')
def login():
result = {
"status": 0,
"msg": "login successful."
}
return jsonify(result)
if __name__ == '__main__':
app.run(debug=True)
```
這個時候,再點擊一次login,就不會出現此錯誤了
> 若有需要,也可以限制特定網域、port等,才允許跨域請求
> 這方面就不多談,請自行參考官方文件
> 當然你想透過`Apache`或`Nginx`這類的web server進行設定也是可以👍
## 全都結束了嗎?
當你使用`npm run build`打包完前端程式碼後
並打開瀏覽器前往`http://127.0.0.1:5000/`
此時可以看到網頁順利運行
看起來沒什麼大問題,不少教學也都這樣就結束了
~~不旦賺你點閱,如果旁邊有廣告還賺廣告費~~
然而當你的畫面停在login頁面後,直接點瀏覽器上的重新整理
就會出現404 not found
## 為什麼會404?
我們在前端程式碼中,使用`<BrowserRouter>`處理前端路由
當直接點擊畫面的元素時,會透過JS來處理路由相關問題
而直接使用瀏覽器的重新整理,會跳過JS的處理,直接對後端發出request
然而我們後端路由並沒有對應的`http://127.0.0.1:5000/login`
這部分只有前端路由有
打開VScode的終端機,也可以看到此訊息

## 如何解決?
為了解決404的問題
我們需要在後端註冊這些路由
但當你的前端路由一多
要在後端逐一註冊這些路由顯然不太實際
為此,`Flask`提供一種方式來接收這些路由
具體的規則可以參考以下網址
https://flask.palletsprojects.com/en/3.0.x/api/#url-route-registrations
```python=
@app.route('/')
@app.route('/<path:path>') # 使用path來接收所有可能的路徑
def home(path=None):
template_path = "index.html"
return render_template(template_path)
```
我們在home controller中使用角括號來接收這些路由
一旦接收到,就會渲染react app
接著交給前端路由做後續處理
此時
我們在login畫面中重新整理
就不會再出現404了👍
最後附上資料夾結構與原始碼

原始碼: https://github.com/st10083010/Flask_React
## 後記
重新整理會引發404的問題當初其實花不少時間解決
研究了一下後發現,這個問題並非`Flask`獨有
其他語言或框架,只要是在前後端分離的狀況下都會遇到
只是實作的方式可能會些微不同,但核心概念都沒有差太多
除此之外,為了要讓整體資料夾結構看起來清晰
也希望不要讓編譯完後的React app拆來拆去
然後還要遷移`index.html`跟hash後的js與css到`Flask`中的`templates`跟`static`
在這方面研究、嘗試很久
也是因為這樣,本篇才決定自行指定了`template_folder`跟`static_folder`
## 參考資料
- https://blog.csdn.net/weixin_44491423/article/details/123402146
- https://medium.com/@kanaecoder/creating-a-full-stack-website-with-react-frontend-and-python-flask-backend-91ec2cbd81e0
- https://reactrouter.com/en/main
- https://medium.com/@charming_rust_oyster_221/flask-%E5%AF%A6%E7%8F%BE-cors-%E8%B7%A8%E5%9F%9F%E8%AB%8B%E6%B1%82%E7%9A%84%E6%96%B9%E6%B3%95-c51b6e49a8b5
- https://flask.palletsprojects.com/en/3.0.x/api/#url-route-registrations
- https://reactrouter.com/en/main/router-components/browser-router
- https://stackoverflow.com/questions/48060556/flask-serving-a-react-application-cannot-refresh-pages