# 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`的方式處理 當你分別啟動前後端程式碼後 應該會得到這樣子的畫面 ![圖片](https://hackmd.io/_uploads/SJETTlO6C.png) ![圖片](https://hackmd.io/_uploads/Syz0pxupC.png) 然而此時點Login,並不會得到後端的資料 打開控制台(`F12`),會發現以下錯誤 ![圖片](https://hackmd.io/_uploads/B1I_XT43A.png) 此時只需要在後端針對跨域問題進行設定就好 這邊是使用`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的終端機,也可以看到此訊息 ![圖片](https://hackmd.io/_uploads/ryUa4ZuTA.png) ## 如何解決? 為了解決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://hackmd.io/_uploads/B1MB5mu6R.png) 原始碼: 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