# Flask 網頁框架 <!-- :::success Python除了速度非其優勢外、其容易上手的草稿語言特性,與多年發展的穩定程度,近年在大數據、爬蟲與人工智慧領域大放異彩,但要發展適合團體使用的應用系統,需要集中的資料庫進行資料儲存與分析,又因為行動化裝置大量普及,在節省成本與開發人力上建議採用 client-server 架構的 web 開發,但路由、資料存取都在後端的方法。綜上所述擬定後台使用 Python 的 Flask 網頁框架作為大家一起學習的技術。 ::: --> ![](https://i.imgur.com/pXZIeCm.png) # 簡介 <!-- Python 是一種 Scripting 程式語言,其支援完整的物件導向特性,適用於各種作業系統(Windows、Linux、Mac OS),還能夠撰寫網頁(搭配Flask、Django套件),進行科學計算等,據說 Google 公司在快速開發、驗證可行性時,都是使用 Python 語言。 Python 的設計哲學是「優雅」、「明確」、「易讀」。Python 的開發者的哲學是「用一種方法,最好是只有一種方法來做一件事」。在設計Python語言時,如果面臨多種選擇,Python開發者一般會拒絕花俏的語法,而選擇明確沒有或者很少有歧義的語法。這些準則被稱為「Python格言」。在Python解釋器內執行 import this可以獲得完整的說明。 撰寫 Python 程式的規範,官方稱之為 Style Guide for Python Code,因為放在第8章,所以又稱為 PEP 8,中文翻譯可參考PEP8 - Python 程式碼風格指引(中文)。希望大家所寫的程式都非常的 Pythonic (具有Python風格的)。 --> Flask 是一種使用 Python 語言開發的輕量級網頁應用框架,設計者是奧地利的 [Armin Ronacher](https://github.com/mitsuhiko),軟體採用了BSD授權,使用 Werkzeug<sup>1</sup> 作為 HTTP 工具箱和 Jinja2 作為樣板引擎。它遵循了 Python 的設計哲學,讓開發者可以快速地建構 Web 應用程式。 Flask 的設計易於使用與擴充,它的初衷是為了各種複雜的 Web 程序建構良好的基礎,因此被稱為「microframework」。它只提供了最基本的功能,比如路由、樣板、表單等等,但是同時也提供了豐富的延伸模組(extension),可以方便地擴充功能,比如資料庫連接、表單驗證、文件上傳等。 Flask 的文件非常清晰,提供了豐富的範例代碼和 API 參考,讓開發者可以輕鬆地學習和使用。使用 Flask 構建 Web 應用程序可以讓開發者更加自由地控制代碼結構和架構,並且可以快速地開發出簡單而靈活的 Web 應用程序。 ### 補充資訊:什麼是 Werkzeug :::spoiler Werkzeug 是一個 WSGI(Web Server Gateway Interface)工具箱,它可以讓 Python Web 應用程序與 Web 服務器進行通訊。WSGI 是一個 Python Web 應用程序和 Web 服務器之間的標準介面,它可以讓不同的 Web 應用程序和 Web 服務器之間進行互操作,實現了 Web 應用程序的可移植性。 Werkzeug 提供了一個 Request 和 Response 對象,可以讓開發者方便地處理 HTTP 請求和回應。它還提供了一個 URL 路由系統,可以讓開發者方便地定義 URL 路由規則,實現 Web 應用程序的路由功能。 除了這些基本功能外,Werkzeug 還提供了許多有用的工具和函數,比如 HTTP 請求和回應的解析、Cookie 和 Session 的處理、文件上傳、資料庫連接等等。這些工具和函數都可以讓開發者更加方便地構建 Web 應用程序。 Flask 使用 Werkzeug 作為其 HTTP 工具箱,通過 Werkzeug 提供的 Request 和 Response 物件,可以方便地處理 HTTP 請求和回應。Werkzeug 的 URL 路由系統也可以讓 Flask 更加靈活地處理 URL 路由。因此,Werkzeug 是 Flask 框架的重要元件,也是 Python Web 應用程序開發的重要工具之一。 ::: ### 補充資訊:與 Django 框架的比較 :::spoiler Flask 和 Django 是 Python 中兩個常用的 Web 框架。它們的使用時機可以從以下幾個方面比較: 1. 規模 Flask 是一個輕量級的 Web 框架,它的核心功能只包含基本的路由、樣板、表單等,但是它可以通過模組來擴充功能。相比之下,Django 是一個全功能的 Web 框架,包含了一系列的套件和工具,例如 ORM、管理後台、表單驗證等等。因此,如果你的應用比較簡單,或者你希望自己掌握更多的細節,可以選擇 Flask;如果你需要快速搭建一個完整的 Web 應用,或者你想要更高效地開發,可以選擇 Django。 2. 學習曲線 由於 Flask 是一個較簡單的框架,因此學習曲線較低。它的文件清晰,並且提供了豐富的範例,對於 Python 的初學者或者 Web 開發初學者來說比較友好。相比之下,Django 的學習曲線比較陡峭,因為它包含了很多的套件和工具,需要花一定時間來學習。但是,一旦你掌握了 Django,你可以更加高效地開發 Web 應用。 3. 自由度 Flask 的自由度比較高,因為它沒有強制規定結構和文件目錄。你可以自由地組織你的代碼,選擇你喜歡的套件和工具。相比之下,Django 的結構和文件目錄是固定的,你需要按照它的要求進行擺放。這樣做的好處是可以提高代碼的可讀性和可維護性,但是對於一些有特殊需求的應用來說可能會有局限性。 總體來說,如果你需要快速開發一個完整的 Web 應用,或者你需要一些高級的功能(例如管理後台、ORM等等),那麼可以選擇 Django;如果你需要自由度更高、學習曲線更低,或者你的應用比較簡單,那麼可以選擇 Flask。當然,最終的選擇還是取決於你的具體需求和喜好,可以根據自己的情況進行評估和選擇。 ::: # 開發建置 ## 安裝 flask 在 windows 作業系統下以系統管理員身分開啟命令提示字元`cmd` 之後,請依照下列指令,建立專案目錄並設定 env 虛擬環境,然後安裝 Flask 套件。 命令 python 可用 py 縮寫替代。 ```bash mkdir myweb # 建立專案目錄 cd myweb py -m venv env # 建立虛擬環境目錄 env env\Scripts\activate # 啟動虛擬環境 (env) pip install flask # 在虛擬環境中安裝 flask 套件 flask (env) pip show flask # 查看 flask 套件資訊 (env) pip list # 查看安裝了哪些套件 (env) pip freeze > requirements.txt # 將所有安裝套件資訊導向至指定的文字檔 deactivate # 離開虛擬環境 ``` :::info 若要升級 pip ``` py -m pip install -U pip ``` ::: 在初始的虛擬環境中,預設只安裝了 pip 與 setuptools 二個套件(`pip list`),安裝 flask 套件後,會因為安裝其相依套件的關係,增加了以下套件: | 套件 | 功能 | | ------------ | ---------------------------------- | | [*Flask](https://pypi.org/project/Flask/)| Flask 網頁框架主套件 | | [click](https://pypi.org/project/click/)| 提供製作命令列功能所需要的模組 | | [colorama](https://pypi.org/project/colorama/)| 提供跨平台終端機顯示顏色與字體粗細 | | [itsdangerous](https://pypi.org/project/itsdangerous/)| 設定秘鑰進行加解密資料 | | [Jinja2](https://pypi.org/project/Jinja2/)| 與 Python 語法類似的網頁模版板引擎 | | [MarkupSafe](https://pypi.org/project/MarkupSafe/) | 排除不可信任的輸入,讓文字物件可安全的使用 | | [Werkzeug](https://pypi.org/project/Werkzeug/)| 實作WSGI規範的函式庫 | 查看 flask 的版本 ```bash! flask --version Python 3.12.8 Flask 3.1.0 Werkzeug 3.1.3 ``` ## 撰寫第一隻 flask 程式 在虛擬環境下,呼叫 vscode 開啟目前目錄 ```bash (env) code . ``` 輸入以下程式碼,並以 app.py 或 wsgi.py 檔名存檔 ```python= from flask import Flask app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route("/") def hello(): """視圖函式 view function""" return "<p>Hello, World!</p>" ``` 繼續在命令提示字元下,輸入 `flask run` 指令 ```bash (env) flask run * Debug mode: off WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000 Press CTRL+C to quit ``` 此時可按住 Ctrl 鍵,配合滑鼠點擊 http://127.0.0.1:5000 連結,便會自動開啟瀏覽器瀏覽網頁。 透過上述執行畫面也揭露了以下訊息 - 內建有簡易 http server,方便開發時能快速透過瀏覽器檢視畫面。 - Debug 模式(除錯/調試)預設是關閉的 - 內建的 http server 不適合用在上線的正式環境,若要上線使用的話,必須用 WSGI server。 - 在命令提示字元下按 CTRL+C 則結束執行。 ## WSGI規範 WSGI(Python Web Server GateWay Interface)是一種用以規範 Web Server 和 Web Application 之間如何溝通的協定,僅用於 Python 語言,於 [PEP 333 – Python Web Server Gateway Interface v1.0](https://peps.python.org/pep-0333/) 被提出,而 Werkzeug 套件便是符合其規範的函式庫。 ``` mermaid graph LR Client -->|request| N[Web Server] -->|wsgi| W[WSGI Server] -->|wsgi| F[Web Application] W[WSGI Server] -->|wsgi| N[Web Server] F[Web Application] -->|wsgi| W[WSGI Server] ``` :::warning ``` mermaid graph LR Chrome -->|request| N[Nginx] -->|wsgi| W[waitress] -->|wsgi| F[flask] W[waitress] -->|wsgi| N[Nginx] F[flask] -->|wsgi| W[waitress] ``` ::: ## Flask 指令 若僅使用 `flask run` 執行網頁程式,只能在本機訪問(127.0.0.1),適合自行開發使用。 ### flask 命令語法 > flask [選項] 命令 [參數]... #### flask 指令的選項與參數 若要改變主網頁的檔案名稱、讓外部電腦可以訪問、除錯、改變 port...等需求,就必須搭配適當的選項或參數,以下是使用範例 ```bash= (env) flask --debug --app main run --host 0.0.0.0 --port 80 ``` ##### 選項 - `--version`:選項,顯示 flask 執行環境的版本 - `--debug`:選項,預設為關閉,啟用後可提供更詳細的錯誤訊息與存放堆疊要追蹤變數的值,且提供自動重新載入功能,當程式碼更改存檔後時,Flask 會自動重新啟動,但會影響效能,且不利資安。<span class="highlight">開發時建議開啟</span>,但部署到生產環境時則建議關閉。 ```bash= (env) flask --debug run ``` - `--app`:選項,指定主程式的名稱,預設名稱會使用【app.py 或 wsgi.py】 ```bash= (env) flask --app main run ``` ##### flask 的選項可參考環境變數 flask 的選項除了加在指令後方,還可使用作業系統的環境變數(flask_app, flask_debug) ```bash= (env) set flask_app=main (env) set flask_debug=1 (env) flask run ``` ##### run 命令配合的參數 - run:命令,啟動內建的 WSGI Server 功能(含簡易的 http server) - `--host, -h`: 參數,可指定允許訪問的主機IP,若為 0.0.0.0 代表所有主機皆可 - `--port, -p`: 參數,可自訂訪問的埠號 ```bash= (env) set flask_debug=1 (env) flask run -h 0.0.0.0 -p 80 ``` :::danger 若指定了 80 port 後,執行發生錯誤訊息,則應該是指定的 port 遭其它程式佔用的原因。此時可使用以下指令得知佔用的是哪一隻程式 ```bash= d:\pyap>netstat -ano | find "0.0:80" TCP 0.0.0.0:80 0.0.0.0:0 LISTENING 8396 d:\pyap>tasklist | findstr 8396 ``` ::: 第一次使用 `--host` 參數執行時,會跳出以下防火墻封鎖提示,若確定要開放其它電腦存取,請按【允許存取】 ![](https://i.imgur.com/v3Uc4rZ.png =450x) ##### routes 命令配合的參數 - routes:命令,檢視指定程式中所有的路由 ```bash= (env) flask routes ``` 執行結果範例如下: ``` [W 20240506 10:16:01 main:134] SNPD國際研討會簡報影片檢視網站啟動... [W 20240506 10:16:01 main:136] Load snpduser.csv as snpduserDict [W 20240506 10:16:01 main:139] Load d1.json, d2.json, d3.json Endpoint Methods Rule -------- --------- ----------------------- admin GET /admin demo GET, POST /demo index GET / log GET /log login GET, POST /login logout GET /logout reload GET /reload show GET /show static GET /static/<path:filename> ``` :::info 使用 uv 進行專案管理 ```bash! uv python list # 查看系統中可用的 Python 版本 uv python install 3.11 3.12 # 同時安裝 3.11 與 3.12 二個版本 md myweb # 建立專案目錄 cd myweb # 進入專案目錄 uv venv --python 3.12 # 建立虛擬環境時,指定要用 3.12 的版本 uv init # 初始化專案 uv add flask # 建立虛擬環境後再安裝套件 uv pip list # 查看虛擬環境中安裝了那些套件 uv run flask --version # 確認 flask 版本 uv run flask --app main run # 以 main.py 作為主程式執行 uv run flask --app main run -p 80 # 承上,加上使用 port 80 uv run flask --app main --debug run -p 80 # 承上,加上開啟除錯功能 ``` ::: --- # 路由 ![image](https://hackmd.io/_uploads/HJNUTvwfA.png) 路由是將網址(URL)對應到 Flask 應用中的處理函式(view function)的規則。每個路由定義了一個網址與其對應的處理邏輯。 ## 為什麼需要路由? 1. 隱藏副檔名與真實路徑:路由讓網址看起來簡潔,隱藏後端檔案結構,降低被駭客攻擊的風險。 2. 提升 SEO:有意義的網址命名,有利於搜尋引擎最佳化,例如: - https://24h.pchome.com.tw/prod/DSAA99-1900DVYGD - https://www.ncut.edu.tw/products/12 3. 靈活對應:透過路由,Flask 可以根據網址動態處理不同請求。 Flask 使用 @app.route 裝飾器來定義路由,後接的函式(view function)負責處理該網址的邏輯。 ## 靜態路由 為一個函數綁定一個或多個網址,這種路徑固定的方式稱之為靜態路由。 ```python= from flask import Flask app=Flask(__name__) # __name__ 代表目前執行的模組 # 綁定1個路由:http://127.0.0.1:5000/ @app.route("/") def index(): return ’Index Page’ # 綁定2個路由:http://127.0.0.1:5000/hi 與 http://127.0.0.1:5000/hello @app.route("/hi") @app.route("/hello") def hello(): return ’<h1>Hello!</h1>’ ``` 但這種一個路徑對應一種函式的路由做法,比較沒有彈性,例如:我們想針對不同姓名使用相同的函式處理,就延伸出以下二種做法 1. 使用 GET 方法傳遞變數 2. 使用動態路由 ### 1. 使用 GET 方法傳遞變數 使用 HTTP 的 GET 方法可在網址列後附加【?參數=值】傳遞資料,參數儲存在 request.args 物件中,它是一種不可變動的字典,就可以做到相同的路由便能處理不同的參數值。 ```python= from flask import Flask, request app = Flask(__name__) # __name__ 代表目前執行的模組 # URL:http://127.0.0.1:5000/getname?name=peter @app.route("/getname") def gname(): # print(type(request.args)) # <class 'werkzeug.datastructures.ImmutableMultiDict'> myname = request.args.get('name') return f'<p>i am {myname}</p>' ``` 但這種做法會有不利於搜尋引擎最佳化 (SEO)的缺點。 ### 2. 使用動態路由 要解決上述不利於搜尋引擎最佳化的缺點,就是將參數與值都變成路徑的一部分。 flask 可在路由規則上使用`<變數>`(將變數名稱放在角括弧中),可視為將路徑中的參數值傳遞給了函數,例如: > 路由規則:`@app.route("/name/<myname>")` > 網址:http://127.0.0.1:5000/name/Peter 意思是透過此變動的網址傳遞了 'Peter' 這個參數值給綁定的函式。 ```python!= from flask import Flask app=Flask(__name__) # __name__ 代表目前執行的模組 @app.route("/name/<myname>") def nm(myname): return f'<p>my name is {myname}</p>' ``` #### 動態路由中的型別轉換 動態路由中的`<變數>`預設為不包含斜線 `/` 的字串,若加上【轉換器(converter)】,則可將變數進行型別的轉換,語法為:`<轉換器:變數>` | 轉換器 converter | 轉換動作 | | -------- | -------- | | int | 正整數 | | float | 正浮點數 | | path | 包含斜線 `/` 的字串 | | uuid | UUID字串 | > 使用 path 轉換器時,建議搭配 escape 將使用者輸入進行轉義。 **範例:帶轉換器的動態路由** ```python!= from flask import Flask from markupsafe import escape # 將 html 轉義,防止 XSS 攻擊 app = Flask(__name__) # 年齡路由:http://127.0.0.1:5000/age/25 @app.route('/age/<int:age>') def show_age(age): return f'<p>I am {age} years old</p>' # 路徑路由:http://127.0.0.1:5000/path/main/sub # http://127.0.0.1:5000/path/<script>alert('test')</script> @app.route('/path/<path:subpath>') def show_subpath(subpath): return f'<p>Subpath: {escape(subpath)}</p>' # 轉義避免 XSS # 多參數路由:http://127.0.0.1:5000/users/Alice/posts/42 @app.route('/users/<username>/posts/<int:post_id>') def show_post(username, post_id): return f'<p>Post {post_id} by {username}</p>' ``` :::success :star: 練習題: 請為網址 http://127.0.0.1:5000/birth/2002 建立動態路由,使用 int 轉換器處理西元年份。函式應計算民國年份(西元年 - 1911)與年齡(2025 - 西元年),並顯示「您是民國 X 年出生,今年 Y 歲」。 ::: :::spoiler 解答: ```python= from flask import Flask app = Flask(__name__) @app.route('/birth/<int:year>') def show_birth(year): roc_year = year - 1911 # 民國年 = 西元年 - 1911 age = 2025 - year # 年齡 = 當前年份 - 出生年份 return f'<p>您是民國 {roc_year} 年出生,今年 {age} 歲</p>' ``` ::: --- # http 的請求方法與基本表單 Flask 允許透過 @app.route 的 methods 參數指定支援的 HTTP 方法(如 GET、POST、PUT、DELETE...)。這對於處理表單提交或 API 請求非常重要。 ## 最常用的 GET 與 POST 方法 - GET: - 用途:向伺服器「取得」資料(如載入網頁)。 - 參數:伴隨取得的【參數】會附加在 URL 之後。 - 特性:瀏覽器載入網頁時,預設都是 GET 請求,參數可見,適合公開、非敏感資料。 - 限制:URL 長度有限(約 2000 字元),不適合大量或敏感資料。 - POST: - 用途:向伺服器「提交」資料(如表單資料)。 - 參數:放在請求的 body 中,不會顯示在 URL 上。 - 特性:適合敏感資料(如密碼)或大量資料,較安全且無長度限制。 - 優勢:參數隱藏,不易被篡改或記錄在瀏覽器歷史。 預設情況下,路由僅支援 GET 和 HEAD 方法,否則須明確寫出要用到哪些方法,例如:`methods=['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS']`。 透過 request.method 屬性,可以得知目前請求的 HTTP 方法,從而根據不同的請求類型來做出不同的回應。 <!-- 如果客戶端使用了不允許的 HTTP 方法來訪問這些路由,Flask 會返回一個 405 的錯誤。 --> **範例1** 以下範例展示一個 BMI 計算器,支援 GET(顯示表單)和 POST(計算 BMI)。 ```python= from flask import Flask, request app = Flask(__name__) @app.route('/', methods=['GET', 'POST']) @app.route('/bmi', methods=['GET', 'POST']) def bmi(): if request.method == 'POST': if request.form.get('height') and request.form.get('weight'): try: height = float(request.form.get('height')) # 身高(公分) weight = float(request.form.get('weight')) # 體重(公斤) bmi = weight / (height / 100) ** 2 except Exception: return "<h1 style='color:red'>請輸入正確的身高和體重</h1>", 400 return f"<h1>您的BMI值為 {bmi:.2f}</h1>" else: return "<h1 style='color:red'>請輸入身高和體重</h1>" return """ <form action="" method="post"> 身高(cm):<input type="text" name="height"><br> 體重(kg):<input type="text" name="weight"><br> <input type="submit" value="計算 BMI"> </form> """ ``` **範例2** 若表單改用 GET 傳送 > request.form 用於抓取 POST,request.args 用於 抓取 GET ```python= from flask import Flask, request app = Flask(__name__) @app.route("/", methods=["GET", "POST"]) @app.route("/greet", methods=["GET", "POST"]) def greet(): if request.method == "POST": name = request.form.get("name") return f"<h1>POST 提交:你好,{name}!</h1>" if request.method == "GET" and request.args.get("name"): name = request.args.get("name") return f"<h1>GET 提交:你好,{name}!</h1>" return """ <h2>用 POST 提交</h2> <form action="/greet" method="post"> 姓名:<input type="text" name="name"><br> <input type="submit" value="送出"> </form> <h2>用 GET 提交</h2> <form action="/greet" method="get"> 姓名:<input type="text" name="name"><br> <input type="submit" value="送出"> </form> """ ``` --- # 表單的 action 屬性 表單部分除了前面探討過傳送方法有 GET 與 POST,但建議使用 POST 之外,表單還有一個 action 屬性表示送出給誰處理? 以下我們來探討設定與不設定 action 屬性的差別。 ## 1.指定 action 屬性 請依照下列專案階層架構建立對應的檔案: ``` project/ │ ├── app.py ├── templates/ │ ├── member.html │ ├── error.html │ └── name.html ``` **app.py** ```python= from flask import Flask, render_template, request app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/', methods=['GET']) def index(): return render_template('member.html') @app.route('/member', methods=['GET', 'POST']) def member(): if request.method == 'GET': return render_template('member.html') elif request.method == 'POST': username = request.form.get('acc') password = request.form.get('pwd') if username == 'tom' and password == '123456': return render_template('name.html', name=username) return render_template('error.html', message='登入失敗!帳號或密碼錯誤'), 404 ``` **member.html** form 的 action 屬性可指定表單資料傳送的 URL,若沒有指定 action,會傳送到當前頁面的 URL 以下範例代表使用者填完資料按下登入後,表單資料會發送到 /member 這個路由 ```htmlmixed= <!DOCTYPE html> <html lang="zh-Hans-TW"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>會員登入作業</title> </head> <body> <h1>會員登入作業</h1> <form method="post" action="/member"> <p>請輸入帳號: <input type="text" name="acc"></p> <p>請輸入密碼: <input type="password" name="pwd"></p> <p><input type="submit" value="登入"></p> </form> </body> </html> ``` **name.html** ```htmlmixed= <!DOCTYPE html> <html lang="zh-Hans-TW"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>會員登入成功</title> </head> <body> <h1>會員登入成功</h1> <p>Hi, 歡迎 {{ name }} 來學習 Flask</p> </body> </html> ``` **error.html** ```htmlmixed= <!DOCTYPE html> <html lang="zh-Hans-TW"> <head> <meta charset="UTF-8"> <title>錯誤</title> </head> <body> <h1>錯誤</h1> <p style="color:red">{{ message }}</p> </body> </html> ``` ## 2.不指定 action 屬性 請依照下列專案階層架構建立對應的檔案: ``` project/ │ ├── app.py ├── templates/ │ └── member.html ``` **app.py** ```python= from flask import Flask, render_template, request app = Flask(__name__) @app.route('/') def index(): return '<p><a href="/login">前往登入頁面 /login</a></p><p><a href="/register">前往註冊頁面 /register</a></p>' @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') # 處理登入邏輯... return f"從 /login 路由提交的表單資料: 用戶名為 {username}, 密碼為 {password}" return render_template('member.html') @app.route('/register', methods=['GET', 'POST']) def register(): if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') # 處理註冊邏輯... return f"從 /register 路由提交的表單資料: 用戶名為 {username}, 密碼為 {password}" return render_template('member.html') ``` **member.html** form 的 action 屬性可指定表單資料傳送的 URL,若沒有指定 action,會傳送到當前頁面的 URL。 若有二個路由都呼叫當前頁面,則預設會傳給呼叫的路由。 ```htmlmixed= <!DOCTYPE html> <html lang="zh-Hans-TW"> <head> <meta charset="UTF-8"> <title>會員登入/註冊</title> </head> <body> <h1>會員登入/註冊</h1> <form method="post"> <label for="username">用戶名:</label> <input type="text" id="username" name="username"><br> <label for="password">密碼:</label> <input type="password" id="password" name="password"><br> <input type="submit" value="提交"> </form> </body> </html> ``` --- # render_template 函式 從前面探討表單的範例來看,雖然簡單的網頁可以如同上例一般使用字串組合產生,但缺點是程式碼與頁面混在一起,且不適合複雜的頁面呈現,因此必須使用本章介紹的 render_template 函式搭配網頁樣板進行顯示。 `render_template` 函式的主要作用是將資料(例如使用者資訊或數據)和 HTML 樣板結合,並將渲染後的網頁傳回給瀏覽器顯示。這樣的樣板結合方式讓程式邏輯和 HTML 樣板分離,方便維護和重複使用。 就像是 Office 的合併列印功能一般,使用網頁作為模版套用。 網頁樣板可以將呈現的HTML內容與程式邏輯分離,提高了代碼的可重用性和維護性。Flask 使用 Jinja2 作為樣板引擎,通過特定的語法`{{ }}`或`{% %}`,可以在 HTML 中插入動態內容。 ## 基本模版功能 要讓 flask 套用現有的網頁進行顯示,必須搭配 render_template 這個方法。在主程式中引入 render_template,並指定【要套用的網頁模版】與【要傳遞的變數】。 請依照下列專案結構來建立資料夾與檔案: ``` project/ │ ├── app.py ├── templates/ │ └── user.html └── static/ ├── css/ │ └── style.css ├── js/ │ └── script.js └── images/ └── logo.png ``` ### 範例:基本樣板渲染 以下展示如何使用 render_template 渲染動態網頁。 **app.py** ```python= from flask import Flask, render_template app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/') def index(): return "網頁模版測試" @app.route('/user/<name>') def get_name(name): fruits = ['Apple', 'Banana', 'Kiwi'] user_dict = {'name': name, 'age': 20, 'fruits': fruits} return render_template('user.html', title='網頁樣板測試', user_info=user_dict, name=name, fruits=fruits) ``` **templates\user.html** 在專案目錄下建立一個名為 templates 的資料夾,然後在裡面建立一個名為 user.html 檔案。 - `{# 註解 #}`:註解的使用方式 - `{{ variable }}`:用於插入變數的值。 - `{% if condition %}...{% endif %}`:用於條件語句。 - `{% for item in iterable %}...{% endfor %}`:用於迴圈。 ```htmlmixed= <!DOCTYPE html> <html lang="zh-Hans-TW"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 引入 CSS 檔案 --> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <title>{{ title }}</title> </head> <body> <!-- 顯示圖片 --> <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" class="logo"> <h1>{{ title }}</h1> {% if name %} <p>Hi, 歡迎 {{ name }} 來學習 Flask</p> {% else %} <p>Hello, World!</p> {% endif %} <hr> <!-- 添加 JavaScript 動作 --> <button onclick="showMessage()">點我</button> <h2>Dict 的二種取值方式 </h2> <ul> <li>姓名:{{user_info.name}}</li> <li>年齡:{{user_info.age}}</li> <li>水果:{{user_info.fruits}}</li> </ul> <ol> {% for key in user_info %} <li>{{ key }}: {{ user_info[key] }}</li> {% endfor %} </ol> <h2>List 的二種取值方式 </h2> <ul> {# 註解:指定索引取出 #} <li>{{ fruits.0 }}</li> <li>{{ fruits.1 }}</li> <li>{{ fruits.2 }}</li> </ul> <ol> {# 註解:for 迴圈取出 #} {% for fruit in fruits %} <li>{{ fruit }}</li> {% endfor %} </ol> <!-- 引入 JavaScript 檔案 --> <script src="{{ url_for('static', filename='js/script.js') }}"></script> </body> </html> ``` :::info **url_for 函式** url_for('static', filename='path/to/file') 可根據 static 資料夾內檔案的相對路徑,產生檔案的 URL,確保靜態資源的引入無誤,即使 Flask 應用的路徑有變動也不會影響。 這樣設定後,網頁便能正確地使用 CSS、JavaScript、以及圖片等靜態檔案。 ::: **static\css\style.css** 在專案目錄下建立一個 static 資料夾,其下再建立 css 資料夾,然後在裡面建立一個 style.css 檔案。 ```css= /* 定義網頁標題樣式 */ h1 { border: 2px #eee solid; color: brown; text-align: center; padding: 10px; } /* 圖片樣式 */ .logo { display: block; margin: 0 auto; width: 150px; } ``` **static/js/script.js** 在專案目錄下建立一個 static 資料夾,其下再建立 js 資料夾,然後在裡面建立一個 script.js 檔案。 ```javascript= function showMessage() { alert("歡迎學習 Flask 和 Jinja2!"); } ``` 網址輸入:http://127.0.0.1:5000/user/John ![image](https://hackmd.io/_uploads/B1Ll2hemC.png) --- ## 模版進階功能 以下介紹樣板繼承、過濾器與巨集。 ### 1. 模版的繼承 樣板繼承允許定義共用結構(base template),子樣板透過繼承覆蓋特定區塊,減少重複代碼。 請依照下列專案階層架構建立對應的檔案: ``` project/ │ ├── app.py ├── templates/ │ ├── base.html │ ├── index.html │ └── header.html └── static/ └── css/ └── style.css ``` **1.1 app.py** ```python= from flask import Flask, render_template app = Flask(__name__) @app.route('/') def index(): return render_template('index.html', subtitle='Welcome', name='John', fruits=['Apple', 'Banana', 'Orange']) ``` **1.2 static\css\style.css** 在專案目錄下建立一個 static 資料夾,其下再建立 css 資料夾,然後在裡面建立一個 style.css 檔案。 ```css= h1 { border: 2px #eee solid; color: brown; text-align: center; padding: 10px; } ``` **1.3 templates\index.html** 樣板繼承可以減少重複的 HTML 代碼,提高頁面結構的一致性。 - index.html 通過 `{% extends "base.html" %}` 繼承了 base.html,並覆蓋了 `{% block title %}{% endblock %}` 和 `{% block content %}{% endblock %}` 區塊,以定義特定頁面的標題和內容。 - `{{ super() }}` 顯示父樣板中該區塊原本的內容。 - 使用了過濾器將變數進行轉換或格式化,例如: - `{{ subtitle|default('首頁') }}` 若沒有接收到 subtitle 變數,則預設值為'首頁' - `{{ fruits|length }}` 返回水果的數量 - `{{ fruits|first }}` 返回第一個水果 - `{{ fruits|last }}` 返回最後一個水果 ```htmlmixed= {% extends "base.html" %} {% block title %} {{ super() }} - {{ subtitle|default('首頁') }} {% endblock %} {% block content %} <h1>Hello, {{ name }}!</h1> <p>今天可享用的水果:</p> <ul> {% for fruit in fruits %} <li>{{ fruit }}</li> {% endfor %} </ul> <p>水果列表的總數:{{ fruits|length }}</p> <p>第1個水果:{{ fruits|first }}</p> <p>最後1個水果:{{ fruits|last }}</p> {% endblock %} ``` **1.4 templates\base.html** - base.html 定義了整個網站的基本結構,包括標題和內容區塊。 - 內容區塊用 `{% block content %}{% endblock %}` 定義,並在子模版中填充。 - 使用 `{% include "header.html" %}` 將另一個樣板文件的內容包含進來。常用於頁眉、頁尾等重複使用的區段。 ```htmlmixed= <!DOCTYPE html> <html lang="zh-Hans-TW"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="{{ url_for('static', filename= 'css/style.css') }}"> <title>{% block title %}Flask網頁設計{% endblock %}</title> </head> <body> {% include "header.html" %} <div class="content"> {% block content %}{% endblock %} </div> </body> </html> ``` **1.5 templates\header.html** header.html 包含了網站的頁首,我們通過 `{% include "header.html" %}` 在 base.html 中引入它。 ```htmlmixed= <header> <h1>Header</h1> </header> ``` ### 2. 過濾器 Jinja2 提供過濾器來轉換或格式化變數值,常見過濾器包括: 2.1. default 過濾器:當變數不存在或為空時,使用指定的預設值 ```{{ variable | default('空值') }}``` 2.2. lower / upper 過濾器:將字串轉換為小寫或大寫 ```{{ variable | lower }}```,```{{ variable | upper }}``` 2.3. truncate 過濾器:截取字串到指定的長度,並添加省略符號 ```{{ variable | truncate(50) }}``` 2.4. first / last 過濾器:返回列表中第一或最後一個元素 ```{{ variable | first }}```,```{{ variable | last }}``` 2.5. length 過濾器:計算列表或字典的長度 ```{{ variable | length }}``` 2.6. join 過濾器:將列表中的元素用指定的分隔符連接起來 ```{{ variable | join(', ')}}``` 2.7. sum 過濾器:計算列表中數值的總和 ```{{ variable | sum }}``` 2.8. safe 會將變數中的 HTML 標籤當做文字,去除標籤的作用 ```{{ variable | safe }}``` 2.9. striptags 會移除變數中所有的 HTML 標籤 ```{{ variable | striptags }}``` > 更多的過濾器請參考:https://docs.jinkan.org/docs/jinja2/templates.html#filters #### 自定義過濾器 若覺得上述的過濾器無法達到想要的功能,還可以透過 @app.template_filter() 自訂過濾器 ```python= # app.py from datetime import datetime @app.template_filter('format_date') def format_date(value, format='%Y-%m-%d'): if isinstance(value, datetime): return value.strftime(format) return value @app.template_filter('mask_phone') def mask_phone(phone): if len(phone) >= 10: return f"{phone[:3]}-****-{phone[-4:]}" return phone ``` ```html= # 網頁樣板 <p>日期:{{ order_date | format_date('%Y年%m月%d日') }}</p> <p>電話號碼:{{ user_phone | mask_phone }}</p> ``` ### 3. 巨集 在 Jinja2 中,「巨集」(Macro) 的功能類似於程式中的函式,允許我們將常用的樣板片段定義成可重複使用的代碼區塊。這在處理大量重複 HTML 結構(如表單欄位)時非常有用,也能提高代碼的可讀性和維護性。 #### 3.1 定義巨集 巨集可以在樣板中使用 {% macro macro_name(parameters) %} 開始定義,並使用 {% endmacro %} 結束。巨集的引數可以讓我們自定義不同的輸出內容。 以下範例展示了如何定義和使用巨集來產生表單輸入框: **templates/macros.html** ```html= {% macro input_field(label, name, value='', type='text') %} <label for="{{ name }}">{{ label }}</label> <input type="{{ type }}" id="{{ name }}" name="{{ name }}" value="{{ value }}"> {% endmacro %} ``` #### 3.2 使用巨集 將巨集定義在一個單獨的文件中(例如 macros.html),然後使用 {% import 'macros.html' as macros %} 引入該文件。引入後,可以透過 macros.macro_name() 語法來呼叫巨集。 以下範例在 HTML 表單中使用了 input_field 巨集,並傳遞了不同的引數,以產生名稱和年齡的輸入欄位: **templates/form.html** ```html= {% import 'macros.html' as macros %} <form action="/submit" method="post"> {{ macros.input_field('名稱', 'name') }} {{ macros.input_field('年齡', 'age', type='number') }} <button type="submit">提交</button> </form> ``` --- ## Jinja2 進階功能 ### 1. 內建變數 Jinja2 會自動把一些有用的資訊(稱為「內建變數」),像是 request、session 等,傳到你的 HTML 樣板裡,你可以直接拿來用,連一行 Python 都不用寫! ```html= <p>你用的請求方式是:{{ request.method }}</p> <p>使用者認證否: {{ '是' if session['user'] else '否' }}</p> ``` ### 2. 變數作用域 有時候,HTML 裡要重複用一個很長的變數(比如 user['profile']['name']),寫起來很麻煩,就像老是喊一個人的全名(「張小明同學」)一樣。Jinja2 的 with 語句就像幫你取個暱稱(「小明」),讓代碼更簡潔! #### 範例:簡化使用者名稱顯示 假設你有一個使用者資料 user,裡面有個 name 欄位,但要寫 user['name'] 很麻煩。我們用 with 給它取個簡單的名字。 **templates/show_user.html** ```html= <!DOCTYPE html> <html lang="zh-Hant"> <head> <meta charset="UTF-8"> <title>使用者資料</title> </head> <body> <h1>歡迎你!</h1> {% with username = user['name'] %} <p>你的名字是:{{ username }}</p> <p>再次歡迎,{{ username }}!</p> {% endwith %} <!-- 這裡不能用 username,因為它已經失效 --> <p>使用者全資料:{{ user }}</p> </body> </html> ``` **app.py** ```python= from flask import Flask, render_template app = Flask(__name__) @app.route('/user') def show_user(): user = {'name': '小明', 'age': 18} return render_template('show_user.html', user=user) ``` ### 3. 迴圈控制 前面已經學過 {% for %} 迴圈來顯示列表(比如列出所有水果)。現在,我們要讓迴圈更厲害,比如給第一個項目加個特別標記,或者顯示項目的編號。Jinja2 的 loop 變數就像迴圈的「小助手」,幫你控制細節。 #### 3.1 什麼是 loop 變數? 在 {% for %} 迴圈裡,loop 提供額外資訊: 1. loop.index:項目的編號(從 1 開始)。 2. loop.first:如果這是第一項,值是 True。 就像在點名時,loop.index 是學號,loop.first 告訴你誰是第一個報到的。 #### 3.2 範例:顯示有編號的購物清單 假設你有一個購物清單,想顯示所有項目,並把第一項用藍色高亮,還加上編號。 **templates/shopping_list.html** ```html= <!DOCTYPE html> <html lang="zh-Hant"> <head> <meta charset="UTF-8"> <title>我的購物清單</title> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> </head> <body> <h1>我的購物清單</h1> <ul> {% for item in items %} <li class="{% if loop.first %}first-item{% endif %}"> {{ loop.index }}. {{ item }} </li> {% endfor %} </ul> </body> </html> ``` **app.py** ```python= from flask import Flask, render_template app = Flask(__name__) @app.route('/shopping') def shopping_list(): items = ['蘋果', '香蕉', '牛奶'] return render_template('shopping_list.html', items=items) ``` **static/css/style.css** ```css= .first-item { color: blue; font-weight: bold; } ``` ### 4. 單行條件式 前面的範例中出現過的都是塊級條件語句:例如: {% if %}、{% elif %}、{% else %} 和 {% endif %},這是教案中較早介紹的功能(例如在 render_template 範例中用來判斷是否有 name 變數)。 這邊我們介紹在樣板中使用單行的條件語句: > {{ value if condition else other_value }} ```html=! <p> 名稱:{{ user.name if user.name else '無名氏' | truncate(10) }} </p> ``` --- # 設定 vscode 對 flask 的偵錯 vscode 偵錯可達成 1. 想在某行指令前暫停,觀察執行前的變數狀態 2. 想在 step by step 的 單步執行,觀察變數的變化 ## 前置需求 在開始之前,請確保以下條件已滿足: - Python - Flask (使用 pip 安裝) - Visual Studio Code (VSCode) - VSCode 的 Python 擴充套件 ## vscode 的設定 ### 1. 建立 .vscode/launch.json 在 app.py 開啟的情況下,按下左側第4個圖示【執行與偵錯】,然後點擊右邊出現的【建立 launch.json 檔案】 ![image](https://hackmd.io/_uploads/B12Ti8BEA.png) 然後點選畫面正上方出現的 Python Debugger ![image](https://hackmd.io/_uploads/S1wV5LS4R.png) 然後點選 Flask 啟動 Flask Web.... ![image](https://hackmd.io/_uploads/H1ujqLSVR.png) 然後挑選這個專案的主程式(這裡的範例是 app.py 但你的專案不一定是) ![image](https://hackmd.io/_uploads/rysXjIHVR.png) 你就會看到 launch.json 建立成功了 ![image](https://hackmd.io/_uploads/r1j4h8B4C.png) :::info :star: launch.json 的參數說明 - launch.json 會出現在專案目錄的 .vscode 子目錄下,此目錄為 vscode 編輯器專用,一般無須 git 控管。 - args 中的 --no-debugger:禁用 Flask 的內建偵錯工具,以避免與 VSCode 偵錯工具衝突。 - args 中的 --no-reload:禁用 Flask 自動重新載入,以避免與 VSCode 偵錯工具衝突。 - 上述 2 個設定只禁用 Flask 的內建偵錯與自動重新載入,避免與 VSCode 的偵錯衝突,但還是保留了 FLASK_DEBUG 的錯誤日誌與詳細錯誤訊息提供等功能,因此都需要設定。 - autoStartBrowser 為 false 時,需要手動在瀏覽器中打開應用的 URL,這樣可以避免在每次啟動偵錯時自動打開多個瀏覽器視窗。 ::: 若要服務於 80 port 的設定如下,但因為是開發機並不建議 ```json "args": [ "run", "--host", "0.0.0.0", "--port", "80", "--no-debugger", "--no-reload" ], ``` ### 2. 在專案主程式中設定中斷點 在某一行程式碼的行號前點擊滑鼠左鍵,會出現紅色的點,代表會在執行此行程式前暫停 ![image](https://hackmd.io/_uploads/BJQvTLH4C.png) 點選左側第4個圖示【執行與偵錯】,然後按下上方的【綠色箭頭】Python 偵錯工具:Flask,或直接按下【F5】開始偵錯 ![image](https://hackmd.io/_uploads/rkcbCLHV0.png) 執行程式到中斷點的程式碼時,整個程式會停下來,畫面會出現以下變化 - 左上角會出現中斷點暫停時,local 變數與 global 變數的值 - 畫面上方可按下繼續(F5)、逐步執行(F11)...停止(Shift+F5)等操作 ![image](https://hackmd.io/_uploads/Hkf5lwrER.png) ### 3. 若沒有設定中斷點 若沒有設定中斷點,則可視為透過 F5 按鍵執行程式,雖然免除了下達指令 `flask --debug run` 的麻煩,但也少了程式碼異動存檔後自動重載與例外處理的能力。 ### 4. 使用時機 建議只有在需要觀察變數的值或需要單步追蹤時才使用 VScode 的偵錯功能,畢竟只能使用一種偵錯模式,用了 VScode 的,則原先 Flask 的例外處理與自動重新載入功能就無法使用, --- # url_for 函式與 redirect 函式 url_for 函式的作用是根據 view function 的名稱和參數產生動態的 URL,與固定的網址相較,更容易維護與擴充。 redirect 函式的作用是重定向一個路由然後返回。常與 url_for 一起使用。 以下我們來看幾個使用 url_for 的範例,從範例來理解比較快。 ## 1. 在網頁樣板中使用 url_for() 取代固定網址 若需要在前往頁面前先進行相關處理時使用,例如需要在 view function 中進行 session 的處理、驗證或資料庫操作等。 **網頁樣板** ```htmlmixed= <!-- 不使用 <a href="home.html">Home Page</a> 而是使用 url_for('home') 強迫先經過路由 --> <a href="{{ url_for('home') }}">Home Page</a> ``` **app.py** ```python!= from flask import Flask, render_template, request app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/home') def home(): user_ip = request.remote_addr # 取得使用者的 IP return render_template('home.html', user_ip=user_ip) ``` ## 2. 在網頁樣板中使用 url_for() 配合動態路由產生對應的網址 url_for 還可以與動態路由參數配合使用,從而產生包含這些參數的 URL。 **網頁樣板** ```htmlmixed= <a href="{{ url_for('user_profile', username='john') }}">使用者資料</a> ``` **app.py** ```python!= @app.route('/user/<username>') def user_profile(username): return f"User: {username}" ``` ## 3. 防止路由發生變化時需要修改樣板 如果你修改了路由的定義,例如將 `/about` 改為 `/about-us`,或者將 `/user/<username>` 改為 `/user/<user_id>`,那麼使用對應的網址就需要在網頁模版中作出對應的修改。但是如果使用 url_for,就無需修改網頁模版中的 URL。 **app.py** ```python= # 舊路由 # @app.route('/about') # def about(): # ... # 新路由 @app.route('/about-us') def about(): return render_template('about.html') # 舊路由 # @app.route('/user/<username>') # def user_profile(username): # ... # 新路由 @app.route('/user/<user_id>') def user_profile(user_id): return render_template('profile.html', user_id=user_id) ``` 網頁樣板的網址無須改變,因為使用了 url_for **templates/about.html** ```htmlmixed= <!-- url_for 針對的是 about() 函數,無需修改即可自動匹配新路由 --> <a href="{{ url_for('about') }}">About Page</a> ``` **templates/user.html** ```htmlmixed= <!-- url_for 針對的是 user_profile() 函數,無需修改即可自動匹配新路由參數 --> <a href="{{ url_for('user_profile', user_id=123) }}">User Profile</a> ``` ## 4. 單一責任原則 每個路由應該負責處理一個特定的功能或頁面。如果兩個路由都需要呼叫相同的樣板網頁,這通常意味著可以將它們合併,或者讓一個路由重定向(redirect)到另一個路由。 使用 redirect 和 url_for 結合可以盡量讓一個樣板網頁只在一個路由中出現,且先執行必要的處理邏輯,例如使用者認證、資料驗證等。這種做法可以保證處理流程的完整性和一致性。 ```python= from flask import Flask, redirect, render_template, request, url_for app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/') def index(): return redirect(url_for('login')) @app.route('/login') def login(): user_ip = request.remote_addr # 取得使用者的 IP return render_template('login.html', user_ip=user_ip) ``` --- # jsonify 函式 jsonify 函式包裝了 json.dumps 用法,讓我們可以輕鬆地將 Python 物件 (例如:字典 dict 或列表 list) 轉換成 JSON 格式的字串,再將這個字串放到 Http response ,並在回應的標頭(header)中設置 Content-Type 為 application/json。 ```python= import json from flask import Flask, jsonify app = Flask(__name__) app.json.ensure_ascii = False # 避免中文亂碼 @app.route('/data') def data(): data = {'name': '朱孝國', 'age': 30} result = json.dumps(data, ensure_ascii=False, indent=2) print(result) response_json = jsonify(data) return response_json ``` --- # 序列化與反序列化 ![image](https://hackmd.io/_uploads/H1eNUL240.png) 序列化是將資料物件轉換成可傳輸或儲存的格式(二進制的檔案或字串),例如 JSON 或 XML。 反序列化則是將收到的二進制檔案或是字串轉換回原始的資料物件。 當我們需要進行不同的系統之間的資料交換或持久化儲存時,就需要使用到這個技術。 ## 序列化(Serialization) **使用 JSON 模組將 Python 字典序列化為 JSON 字串** ```python= import json data = { 'name': 'John', 'age': 30, 'email': 'john@example.com' } # 序列化為JSON字串 json_data = json.dumps(data) print(json_data) ``` **flask 使用 jsonify 序列化 Python 字典為前端網頁** ```python= from flask import Flask, jsonify app = Flask(__name__) @app.route('/api/data') def get_data(): data = { 'name': 'John', 'age': 30, 'email': 'john@example.com' } return jsonify(data) ``` ## 反序列化(Deserialization) **將 JSON 字串反序列化為 Python 字典** ```python= import json json_data = '{"name": "Alice", "age": 30, "email": "alice@example.com"}' # 反序列化JSON字串 data = json.loads(json_data) print(data["name"]) # 輸出 "Alice" print(data["age"]) # 輸出 30 print(data["email"]) # 輸出 "alice@example.com" ``` **Flask 從請求中獲取 JSON 資料,並反序列化為 Python 字典** ```python= from flask import Flask, request app = Flask(__name__) @app.route('/register', methods=['POST']) def register(): # 獲取請求中的 JSON,將之反序列化為 Python 字典 data = request.get_json() print(f'收到資料:"{data}"') # name = data['name'] # 取用個別資料方式 return "註冊成功" ``` --- # 使用 Flask 開發 RESTful API ![image](https://hackmd.io/_uploads/HkSRDsh4R.png) REST(Representational State Transfer) 是一種軟體架構風格,其核心理念是資源(Resources)可以使用統一的介面(HTTP 方法)來進行操作。每個資源都有一個唯一的地址(URI)來識別。遵循 REST 原則設計的 API 就被稱為 RESTful API。 > - REST 是由 Roy Fielding 博士在 2000 年的博士論文中所提出。他同時也是 HTTP 規範的主要作者之一。 > - 美麗 (Beauty) 的事物可以稱為 Beautiful;實作 REST 的方案就可以稱為 RESTful。 ## RESTful API 的核心概念 RESTful API 使用 HTTP 請求方法(GET、POST、PUT、DELETE)來對資源執行 CRUD(Create、Read、Update、Delete)的操作。 ## 簡單的 RESTful API 範例 以下是一個簡單的 RESTful API,它使用了 jsonify 將一個 List 序列化: ```python= from flask import Flask, abort, jsonify app = Flask(__name__) app.json.ensure_ascii = False # 避免中文亂碼 tasks = [ {'id': 1, 'title': u'買雜貨', 'description': u'牛奶, 起士, 披薩, 水果, 普拿疼', 'done': False}, {'id': 2, 'title': u'學習 Python', 'description': u'在網路上尋找好的Python教材', 'done': False}, ] @app.route('/api/v1.0/tasks', methods=['GET']) def get_tasks(): return jsonify({'tasks': tasks}) @app.route('/api/v1.0/tasks/<int:task_id>', methods=['GET']) def get_task(task_id): task = [task for task in tasks if task['id'] == task_id] if not task: return jsonify({"error": "找不到指定的task"}), 404 # abort(404) return jsonify({'task': task[0]}) # @app.errorhandler(404) # 此裝飾器可捕獲 404 錯誤 # def Not_Found(error): # 可進行更多的處理,例如儲存 log # return jsonify({"error": "找不到指定的task"}), 404 ``` **使用 cURL 測試 API** Windows 內建的 cURL 命令可扮演 client 端發送對應的 HTTP Request ```bash= curl -i http://127.0.0.1:5000/api/v1.0/tasks curl -i http://127.0.0.1:5000/api/v1.0/tasks/2 curl -i http://127.0.0.1:5000/api/v1.0/tasks/3 ``` ## 完整的 RESTful API 範例 以下是一個更完整的範例,包含建立、更新、刪除使用者的功能。 ### app.py ```python= import sqlite3 from flask import Flask, jsonify, request app = Flask(__name__) app.json.ensure_ascii = False # 避免中文亂碼 DATABASE = 'example.db' # 連線到 SQLite 資料庫 def connect_db(): conn = sqlite3.connect(DATABASE) conn.row_factory = sqlite3.Row # 可使用 row['key'] 來取得資料 return conn # 取得所有使用者 (GET 請求) @app.route('/api/users', methods=['GET']) def get_users(): conn = connect_db() # 連線資料庫 c = conn.cursor() # 建立一個 cursor 物件 c.execute('SELECT * FROM users') users = c.fetchall() # 獲取所有查詢結果 user_list = [{'id': user['id'], 'name': user['name'], 'email': user['email']} for user in users] # 將 SQLite 的 Row 物件轉換為 Python 字典 conn.close() # 關閉資料庫連線 return jsonify(user_list) # 將 user_list 轉換為 JSON 格式並回傳 # 建立新使用者 (POST 請求) @app.route('/api/users', methods=['POST']) def create_user(): data = request.get_json() name = data.get('name') email = data.get('email') if not name or not email: return jsonify({'訊息': '資料無效'}), 400 conn = connect_db() c = conn.cursor() c.execute('INSERT INTO users (name, email) VALUES (?, ?)', (name, email)) conn.commit() conn.close() return jsonify({'訊息': '使用者已成功建立'}), 201 # 取得特定使用者 (GET 請求) @app.route('/api/users/<int:id>', methods=['GET']) def get_user(id): conn = connect_db() c = conn.cursor() c.execute('SELECT * FROM users WHERE id=?', (id,)) user = c.fetchone() conn.close() # 如果找不到使用者,回傳 404 錯誤 if not user: return jsonify({'訊息': '找不到使用者'}), 404 # 否則回傳使用者資料 return jsonify({'id': user['id'], 'name': user['name'], 'email': user['email']}) # 更新特定使用者 (PUT 請求) @app.route('/api/users/<int:id>', methods=['PUT']) def update_user(id): data = request.get_json() name = data.get('name') email = data.get('email') if not name or not email: return jsonify({'訊息': '資料無效'}), 400 conn = connect_db() c = conn.cursor() c.execute('UPDATE users SET name=?, email=? WHERE id=?', (name, email, id)) conn.commit() conn.close() return jsonify({'訊息': '使用者已成功更新'}) # 刪除特定使用者 (DELETE 請求) @app.route('/api/users/<int:id>', methods=['DELETE']) def delete_user(id): conn = connect_db() c = conn.cursor() c.execute('DELETE FROM users WHERE id=?', (id,)) conn.commit() conn.close() return jsonify({'訊息': '使用者已成功刪除'}) ``` ### init_db.py ```python= import sqlite3 DATABASE = 'example.db' def connect_db(): conn = sqlite3.connect(DATABASE) conn.row_factory = sqlite3.Row return conn def init_db(): with connect_db() as conn: c = conn.cursor() c.execute( '''CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)''' ) conn.commit() # 插入初始資料 (僅在表格為空時插入) c.execute('SELECT COUNT(*) FROM users') if c.fetchone()[0] == 0: users = [ ('John', 'john@example.com'), ('Jane', 'jane@example.com'), ('Bob', 'bob@example.com'), ] c.executemany('INSERT INTO users (name, email) VALUES (?, ?)', users) conn.commit() init_db() ``` ### 使用 cURL 測試 API 使用 Windows 內建的 cURL 命令扮演 client 端發送對應的 HTTP Request ```bash= # 取得所有使用者 (GET 請求) curl -i http://127.0.0.1:5000/api/users # 建立新使用者 (POST 請求) curl -X POST http://127.0.0.1:5000/api/users -H "Content-Type: application/json" -d " {\"name\":\"Peter\",\"email\":\"peterju@ncut.edu.tw\"}" # 取得特定使用者 (GET 請求) curl http://127.0.0.1:5000/api/users/4 # 更新特定使用者 (PUT 請求) curl -X PUT http://127.0.0.1:5000/api/users/4 -H "Content-Type: application/json" -d "{\"name\":\"Peter\",\"email\":\"peterju.tw@gmail.com\"}" # 刪除特定使用者 (DELETE 請求) curl -X DELETE "http://127.0.0.1:5000/api/users/4" ``` 指令說明: - `-i,--include`:在 output 中顯示 response 的 header - `-X,--request`:使用指定的 http method 來發出 request - `-H, --header`:設定 request 裡要攜帶的 header - `-d, --data`:攜帶 HTTP Data - 命令提示字元下,雙引號內的雙引號必須使用 \ 作為前置 ### 使用 REST Client 測試 API REST Client 是一套VS Code 的 延伸模組,提供測試API相關功能,使用上十分簡單快速,可以取代 postman 或 cURL 等工具。使用方式如下: 1. 在 VScode 安裝 Rest Client 延伸模組 ```bash= code --install-extension humao.rest-client ``` 2. 建立檔案 `client.http` - 主檔名隨意,副檔名須為 http 或 rest - 不同的請求中間以 `###` 隔開 - 請求的格式符合 RFC2616 標準 ```jsonld= @host = http://127.0.0.1:5000 # 取得所有使用者 (GET 請求) GET {{host}}/api/users ### # 建立新使用者 (POST 請求) POST {{host}}/api/users Content-Type: application/json { "name": "Peter", "email": "peterju@ncut.edu.tw" } ### # 取得特定使用者 (GET 請求) GET {{host}}/api/users/1 ### # 更新特定使用者 (PUT 請求) PUT {{host}}/api/users/4 Content-Type: application/json { "name": "Peter", "email": "peterju.tw@gmail.com" } ### # 刪除特定使用者 (DELETE 請求) DELETE {{host}}/api/users/4 ``` 3. 點選需要發送請求上方的 Send Request 4. 可在右側看到執行結果 ![image](https://hackmd.io/_uploads/Skl-9Oa4R.png) :::info 在實際應用中,RESTful API 還需要考慮到安全性的問題,例如: - 驗證和授權:確保只有經過授權的用戶可以存取API資源。例如使用 Token 認證、OAuth 等機制。 - 輸入驗證:驗證並過濾來自用戶端的所有輸入數據,避免注入攻擊等安全漏洞。 - 傳輸層安全:使用 HTTPS 協定來保護傳輸過程中的數據安全。 - 資料加密:對敏感數據進行加密存儲和傳輸。 - 最小權限原則:只授予應用程式所需的最小權限,避免不必要的權限洩漏。 ::: > 參考資料:[使用 Python 和 Flask 設計 RESTful API](http://www.pythondoc.com/flask-restful/first.html) --- # request 物件 Request 物件代表每一個來自客戶端的 HTTP 請求。它包含了所有和該請求有關的資料,比如 URL、表單資料、請求方法(GET/POST)、Cookie 等。 ![image](https://hackmd.io/_uploads/ryMWaPLfC.png) 請求(HTTP Request)是前端向後端請求服務時所發送的相關資訊,我們可透過操作這個 request 物件取得相關請求資訊。以下列出常用的 Request 物件屬性。 - request.method:返回 HTTP 请求方法,例如 GET、POST、PUT、DELETE、HEAD、OPTIONS 等。 - request.scheme:URL的通訊協定。 - request.url:完整的 URL,包括主機名、路徑和查詢字串。 - request.path:URL 的路徑部分,但不包含查詢字串。 - request.full_path:URL 的路徑部分,包括查詢字串。 - request.host:請求的主機名。 - request.remote_addr:請求的客戶端的 IP 地址。 - request.user_agent:客戶端的 User-Agent(實際上是呼叫 request.headers.get("user-agent"))。 - request.accept_languages:客戶端的語言偏好(實際上是呼叫 request.headers.get("accept-languages"))。 - request.accept_languages:客戶端的引薦網址referrer(實際上是呼叫 request.headers.get("referrer"))。 - request.args:字典屬性,包含所有查詢參數的鍵值對。 - request.form:字典屬性,包含 POST 請求中提交的表單欄位鍵值對。 - request.files:字典屬性,包含所有上傳文件的鍵值對。 - request.cookies:字典屬性,包含所有 cookie 的鍵值對。 - request.headers:字典屬性,包含所有請求頭部的鍵值對。 - request.json:返回 JSON 格式的請求資料(實際上是呼叫 request.get_json())。 更多的 request 物件屬性請參考: https://flask.palletsprojects.com/en/3.0.x/api/#flask.Request ```python!= from flask import Flask, request app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route("/") #'/'叫正斜線反之'\'叫反斜線 def index():#用來回應路徑 / 的處理函式 print("請求方法:", request.method) # (物件.屬性) print("通訊協定 :", request.scheme) print("完整的網址 :", request.url) print("路徑 :", request.path) print("完整路徑 :", request.full_path) print("主機名稱 :", request.host) print("客戶端IP :", request.remote_addr) print("瀏覽器資訊 :", request.user_agent) print("語言偏好 :", request.accept_languages) print("引薦網址 :", request.referrer) print("json :", request.json) # print("取得 URL 參數 :",request.args.get('id')) # print("取得表單資料 :",request.form.get('name')) # print("取得檔案 :",request.files.get('file')) return "Hello Flask" # 回傳路徑 / 的內容 ``` ## 使用 request.method 判斷請求使用的方法 ```python= from flask import Flask, request, render_template app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route("/show", methods=['GET', 'POST']) def show(): if request.method == 'POST': return render_template('show.html') return render_template('index.html') ``` ## 使用 request.args 獲取 GET 請求參數 這是獲取 GET 請求參數的標準做法,此屬性是一個 ImmutableMultiDict 物件,用來存放所有參數的鍵值,可使用 get() 方法取得參數的值。 ```python= from flask import Flask, request app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/query', methods=['GET']) def query(): name = request.args.get('name') age = request.args.get('age', type=int) return f'Name: {name}, Age: {age}' ``` ## 使用 request.form 獲取 POST 請求的表單資料 request.form 這個屬性是一個字典,用來存放所有表單欄位的鍵值,可使用 get() 方法取得指定欄位名稱的值。 ```python= from flask import Flask, request app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/login', methods=['POST']) def login(): username = request.form.get('username') password = request.form.get('password') if username == 'admin' and password == 'pwd': return '登入成功!' else: return '登入失敗!' ``` ## 使用 request.get_json() 取得 JSON 資料 如果是通過 AJAX 或 API 呼叫發送的 JSON ,可以使用 request.get_json() 方法取得 JSON 資料。在開發 RESTful API 時非常有用。 ```python= from flask import Flask, request, jsonify app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/api/data', methods=['POST']) def receive_data(): data = request.get_json() # 處理 JSON 資料... return jsonify(success=True) ``` ## 使用 request.files 取得上傳的文件資料 ```python= from flask import Flask, request app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/upload', methods=['POST']) def upload_file(): file = request.files['file'] # 保存文件... return 'File uploaded!' ``` ## 結合使用 request.values 和 request.data 在某些情況下,可能需要同時處理查詢字串參數、表單資料和原始請求資料。此時可使用 request.values 和 request.data。 request.values 是一個合併了 args 和 form 的字典物件,而 request.data 則包含原始的請求資料。 ```python= from flask import Flask, request app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/mixed', methods=['GET', 'POST']) def mixed_data(): name = request.values.get('name') payload = request.data # 處理混合資料... return f'Name: {name}, Payload: {payload}' ``` ## 終止請求 如果需要終止請求並返回錯誤碼,可以使用 abort() ```python= from flask import Flask, abort, render_template app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/secret') def secret(): abort(401) # 拋出 401 Unauthorized HTTP錯誤碼 @app.errorhandler(401) # 此裝飾器可捕獲 401 錯誤 def page_unauthorized(error): # 也可以在錯誤處理函數中記錄錯誤、發送郵件等其他操作 return render_template('401.html', error=error), 401 ``` **401.html** ``` <!DOCTYPE html> <html> <head> <title>Unauthorized</title> </head> <body> <h1>401 Unauthorized</h1> <p>發生錯誤:{{error}}</p> </body> </html> ``` --- # Response 物件 ![image](https://hackmd.io/_uploads/BJUN0duGA.png) 在 Flask 中,當我們的視圖函式(view function)被呼叫時,它必須回傳一個 response 物件。這個 response 物件包含了將要被傳送到瀏覽器的資料和狀態碼等相關資訊。 Flask 提供了一個 make_response() 函數來輕鬆建立 response 物件。但通常情況下,我們不需要直接呼叫該函數,因為當視圖函式返回一個字串或 HTML 標記時,Flask 會自動將其轉換為 response 物件,並設置正確的 Content-Type 與標頭狀態碼。 Response 物件包含了以下主要屬性: - data: 要傳送到客戶端的資料 - status_code: HTTP 狀態碼 - headers: HTTP 標頭 ## 返回 HTML response 我們可以直接返回 HTML 標記,Flask 會正確地設置 Content-Type 標頭 200 (OK) ```python= @app.route('/') def index(): return '<h1>Hello, World!</h1>' ``` 這種方法常用於回傳簡單的靜態 HTML 頁面,複雜的頁面就要使用 render_template 呼叫網頁樣板了。 ## 自訂狀態碼 我們可以使用 make_response() 函數和 response 物件的 status 屬性來自訂狀態碼: ```python= from flask import Flask, make_response app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/error') def error(): resp = make_response('Something went wrong!', 400) return resp ``` 這種方法常用於回傳自訂的錯誤訊息及相應的狀態碼。 ## 使用 response 物件設置標頭 我們可以通過操作 response 物件的 headers 屬性來設置 response 標頭 ```python= from flask import Flask, make_response app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/') def index(): # 建立一個回應物件,並設置回傳的資料 resp = make_response('<h1>Hello, World!</h1>') # 設置 Cache-Control 標頭為 no-cache,表示每次都要從伺服器重新獲取資源,不使用緩存 resp.headers['Cache-Control'] = 'no-cache' # 回傳設置了 Cache-Control 標頭的回應物件 return resp ``` ## 設置 Cookie 我們可以通過操作 response 物件的 set_cookie 方法來設置 Cookie ```python= from flask import Flask, make_response app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/set_cookie') def set_cookie(): resp = make_response('Setting a cookie') resp.set_cookie('cookie_name', 'cookie_value') return resp ``` ## 自訂 mimetype 在某些特殊情況下,我們可能需要覆蓋 Flask 自動設置的 mimetype。例如:當我們要回傳一個 json 時,Flask 可能會將其錯誤地設置為 text/html。在這種情況下,我們需要手動將 mimetype 設置為正確的值,如 application/json。 ```python= from flask import Flask, make_response app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/api/data') def get_json_data(): # 假設這是我們要回傳的 JSON 資料 json_data = { "name": "John", "age": 30, "city": "New York" } # 將字典轉換為 JSON 格式的字串 json_str = json.dumps(json_data) # 建立一個回應物件,並設置回傳的資料為 JSON 格式 resp = make_response(json_str) # 設置 Content-Type 標頭為 application/json,表示回傳的資料是 JSON 格式 resp.headers['Content-Type'] = 'application/json' # 回傳設置了 Content-Type 標頭的回應物件 return resp ``` ## Response Stream (進階議題,初學者可無視) 當我們需要處理大型檔案或者持續產生資料的情況時,便需要將資料以串流的形式產生出來,而不是一次性將所有資料載入到記憶體。 我們可以使用 stream_with_context 函式,將負責產生資料的產生器函式作為參數傳入,並將其與 Response 物件一起使用。 要使用 Response Stream 的時機包括了: 1. 處理大型檔案下載 2. 實時串流資料,如日誌或即時更新的資料 3. 動態產生的資料,以免耗盡記憶體資源 ```python= from flask import Flask, stream_with_context, Response app = Flask(__name__) # __name__ 代表目前執行的模組 @app.route('/download_large_pdf') def download_large_pdf(): def generate_large_pdf(): with open('paper.pdf', 'rb') as file: chunk_size = 1024 # 設定每次讀取的 chunk 大小 while True: chunk = file.read(chunk_size) if not chunk: break yield chunk return Response(stream_with_context(generate_large_pdf()), mimetype='application/pdf') ``` 在這個範例中,我們建立了一個 generate_large_pdf() 產生器函式,它打開了一個名為 paper.pdf 的 PDF 檔案,並以二進位模式讀取。然後定義了一個 chunk_size 變數,這個變數表示每次從檔案中讀取的 chunk 大小。接著使用一個無限迴圈來持續從檔案中讀取資料,並將每次讀取的 chunk 作為產生器的輸出。當檔案讀取完畢時,迴圈會結束。 最後,將這個產生器傳遞給 Response() 物件,並將 mimetype 設定為 application/pdf,這表示將要傳送的資料是一個 PDF 檔案。 這個範例將以串流的方式來讀取大型 PDF 檔案,這樣可以減少伺服器的記憶體使用量,並且能夠更有效地傳送大型檔案給客戶端。 --- # session 物件 ![image](https://hackmd.io/_uploads/SJ8a5vUzC.png) 標準的 http 與 https 協定都是無狀態(stateless)的,並不適合發展網頁系統,因此網頁應用程式都會提供 session 控制,其原因有以下幾點: 1. 身份驗證:當使用者登入後,網頁程式會為其建立一個 session,用來記錄該使用者的身份。當使用者進行其他操作時,網頁程式可以通過 session 確認該使用者是否有權限進行該操作。 2. 狀態維護:網頁程式需要記錄使用者在網站上的狀態,例如購物車內容、表單填寫進度等等。這些資料可以通過 session 保存在服務器端,以便在下一次使用時恢復狀態。 總之,session 控制是網頁程式中保護使用者身份、維護狀態、防止攻擊的重要手段之一。 ## Flask 自帶的 session 模組 Flask 自帶了一個 session 模組,可以使用其提供的方法進行 session 控制。 使用時機:當需要簡單的 session 控制時,可以使用 Flask 自帶的 session 模組,但是其功能較為有限,不支持多種 session 儲存方式。 **app.py** ```python= from datetime import timedelta from flask import Flask, session, redirect, url_for, render_template, request app = Flask(__name__) app.secret_key = 'mysecretkey' # 設定密鑰 app.permanent_session_lifetime = timedelta(days=31) # 設定 session 31 天後過期 @app.route('/') def index(): if 'username' in session: username = session['username'] return render_template('index.html', username=username) return render_template('login.html') @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': # session['username'] = request.form['username'] session['username'] = request.form.get('username') return redirect(url_for('index')) return render_template('login.html') @app.route('/logout') def logout(): session.pop('username', None) return redirect(url_for('index')) ``` **index.html** ```htmlmixed= <!DOCTYPE html> <html> <head> <title>Index</title> </head> <body> {% if username %} <h1>Welcome, {{ username }}!</h1> <a href="{{ url_for('logout') }}">Logout</a> {% else %} <h1>Please login</h1> <a href="{{ url_for('index') }}">Home</a> {% endif %} </body> </html> ``` **login.html** ```htmlmixed= <!DOCTYPE html> <html> <head> <title>Login</title> </head> <body> <form method="post" action="{{ url_for('login') }}"> <label>Username:</label> <input type="text" name="username"><br><br> <input type="submit" value="Login"> </form> </body> </html> ``` ## cookie 控制 ![image](https://hackmd.io/_uploads/SkH3jwLMA.png) 除了使用 session 外,還可以使用 cookie 來控制用戶的狀態。通過在用戶的瀏覽器中設置一個 cookie,可以記錄用戶的狀態信息。 使用時機:當需要記錄用戶的狀態信息(如用戶名、語言偏好等)時,可以使用 cookie,但是其不支持對用戶身份的驗證,安全性較差。 **app.py** ```python= from flask import Flask, render_template, request, make_response, redirect, url_for app = Flask(__name__) @app.route('/') def index(): username = request.cookies.get('username') if username: return render_template('index.html', username=username) else: return render_template('login.html') @app.route('/login', methods=['POST']) def login(): username = request.form['username'] response = make_response(redirect(url_for('index'))) response.set_cookie('username', username) return response @app.route('/logout') def logout(): response = make_response(redirect(url_for('index'))) response.set_cookie('username', '', expires=0) return response ``` **index.html** ```htmlmixed= <!DOCTYPE html> <html> <head> <title>Index</title> </head> <body> {% if username %} <h1>Welcome, {{ username }}!</h1> <a href="{{ url_for('logout') }}">Logout</a> {% else %} <h1>Please login</h1> <a href="{{ url_for('index') }}">Home</a> {% endif %} </body> </html> ``` **login.html** ```htmlmixed= <!DOCTYPE html> <html> <head> <title>Login</title> </head> <body> <form method="post" action="{{ url_for('login') }}"> <label>Username:</label> <input type="text" name="username"><br><br> <input type="submit" value="Login"> </form> </body> </html> ``` :::info **cookie 與 session 的關係** - 當用戶首次訪問網頁應用程式時,Flask 會在伺服器端產生一個唯一的 session ID,然後將這個 session ID 儲存在用戶的瀏覽器中,通常是以 cookie 的形式存放。 - 之後當用戶發送請求時,瀏覽器會自動將這個 session ID 附加到請求中,這樣伺服器就可以根據這個 session ID 找到對應的 session 資料,從而實現用戶的會話管理。 ::: # 應用程式(app)的裝飾器 從以下最簡單的程式看來,app 物件是 Flask 類別的實例,也因此可以使用 @app.route 這個路由裝飾器來定義路徑與對應的處理函數。 ```python= from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "<p>Hello, World!</p>" ``` 這個應用程式(app) 除了 @app.route 裝飾器之外還有哪些常用的裝飾器呢? ## 異常處理裝飾器 @app.errorhandler 是用來處理特定 HTTP 錯誤代碼或異常類型的裝飾器。當請求處理過程中發生指定錯誤時,這個裝飾器定義的函數會自動被呼叫。你可以在應用程式中全域性地處理異常,如 404 找不到頁面,或處理特定的 Python 異常。 ```python= @app.errorhandler(401) # 此裝飾器可捕獲 401 錯誤 def page_unauthorized(error): return render_template('401.html', error=error), 401 @app.errorhandler(404) # 此裝飾器可捕獲 404 錯誤 def page_not_found(error): return jsonify({"error": "找不到指定的頁面"}), 404 @app.errorhandler(ZeroDivisionError) # 捕捉 Python 的除以零錯誤 def handle_zero_division_error(e): return jsonify({"error": "全域異常處理 - 除以零錯誤"}), 500 ``` ### 視圖函數內與全域異常處理的優先權 當你在視圖函數內自行處理異常(例如使用 try-except),這個處理會優先於 @app.errorhandler 的全域異常處理。這是因為視圖函數內的邏輯會在異常傳遞到全域處理器之前執行,而全域的 @app.errorhandler 則不會被觸發。 ```python= from flask import Flask, jsonify app = Flask(__name__) # 全域異常處理裝飾器 @app.errorhandler(ZeroDivisionError) def handle_zero_division_error(e): return jsonify({"error": "全域異常處理 - 除以零錯誤"}), 500 # 視圖函數中的異常處理 @app.route('/') def index(): try: 1 / 0 # 這裡會引發 ZeroDivisionError except ZeroDivisionError: return jsonify({"error": "視圖函數內的異常處理"}), 400 ``` ## 二個單次請求生命週期的裝飾器與 g 物件 (進階議題,初學者可無視) 在 Flask 中,每次的請求都可以分為請求前與請求後二個階段,而每個階段都可以使用裝飾器來設置和清理資源: - @app.before_request:此裝飾器用於在每次請求處理之前執行指定的函數。這個階段是設置每次請求所需的臨時資料的理想位置,例如初始化資料庫連接或設置用戶會話。 - @app.teardown_request:此裝飾器用於在每次請求結束後執行指定的函數。這個階段可以用來清理請求期間開啟的資源,例如關閉資料庫連接。 ### g 物件 g 物件是一個臨時的全域物件,用來在單次請求期間存取和共享資料。它會在每個請求開始時自動存在,但具體存放的內容由開發者決定。如果你沒有手動往 g 裡存放資料,它只是一個空的容器。請注意,g 物件的存在並不依賴於 @app.before_request,但通常會在這個階段初始化或設定其內容。 換句話說,g 物件是一個隨請求而生、隨請求而滅的物件,請求結束後 g 物件會被自動銷毀,不會保留跨請求的狀態。因此,它非常適合用來存放一些僅在單次請求內需要的資料,如當前用戶的資訊或資料庫連接。 雖然,g 物件的建立並不依賴於 @app.before_request,但通常會與它搭配使用,以便在每次請求之前初始化資料。釋放資源(例如:釋放資料庫連接)的工作則通常放在 @app.teardown_request 中,但與釋放 g 物件本身無關,因為 g 會在請求結束時自動銷毀。 ```python= import sqlite3 from flask import Flask, g, render_template app = Flask(__name__) # 在請求開始前連接資料庫 @app.before_request def before_request(): g.db = sqlite3.connect("database.db") # 初始化資料庫連接 # 在視圖函數中使用 g.db 操作資料庫 @app.route('/') def index(): cur = g.db.cursor() # 執行SQL查詢... data = cur.execute("SELECT * FROM table").fetchall() return render_template('index.html', data=data) # 在請求結束後自動關閉資料庫連接 @app.teardown_request def teardown(exception): conn = getattr(g, 'db', None) if db is not None: conn.close() # 關閉資料庫連接 ``` - @app.before_request:在每次請求處理之前執行,通常用來初始化請求期間所需的資料,如資料庫連接。 - @app.teardown_request:在請求結束後執行,用於清理請求期間開啟的資源,如關閉資料庫連接。 - g 物件:在每個請求期間自動存在,但不會自動包含資料。開發者可以選擇在請求期間的任何時候存入資料,g 物件會在請求結束後自動清除。 # flask 的環境設定 Flask 在運作之前可能需要定義一些環境參數,例如:session 金鑰、樣板網頁改變後自動重載等,可以通過設定 Flask 物件的 config 屬性來實現,正確的設定對於 Flask 應用程式的安全性和正常執行是非常重要的。 建議在生產環境中將配置存放於程式碼之外,從專用的設定檔案、環境變數等導入,以保護敏感資訊,也方便版本管控時將這些設定檔排除。 ## 1. 程式碼中直接設定配置 這種做法僅適合小型專案的快速開發階段,不適合正式的生產環境使用。 ```python= from flask import Flask app = Flask(__name__) # __name__ 代表目前執行的模組 # 方法1:使用字典操作 app.config['SECRET_KEY'] = 'Snpd-Ncut-2021/1124~1126' app.config['TEMPLATES_AUTO_RELOAD'] = True app.config['UPLOAD_FOLDER'] = '/path/to/uploads' app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB app.config['TESTING'] = True app.config['JSON_AS_ASCII'] = False # 方法2:使用物件屬性 app.secret_key= b'Snpd-Ncut-2021/1124~1126' app.templates_auto_reload = True app.upload_folder = '/path/to/uploads' app.max_content_length = 16 * 1024 * 1024 # 16MB app.testing = True app.json_as_ascii = False # 不建議在程式碼中設定 DEBUG=True,因為會帶來潛在的安全風險 ``` :::info `templates_auto_reload = True` 這個設定可以在網頁模版異動時,立刻進行重新載入,但必須搭配 `debug = True` 才能運作。 ::: 若要更新現有 config 的屬性,可利用 dict.update() 的方式進行 ```python= app.config.update( TEMPLATES_AUTO_RELOAD = True, TESTING=True, SECRET_KEY = 'Snpd-Ncut-2021/1124~1126' ) ``` 上述 config 配置完成後,使用以下指令執行 flask ```bash flask --debug run ``` :::danger Flask 官方文件建議不要直接在程式碼或設定檔中設定 DEBUG,而是建議在執行 Flask 應用程式前就使用 `flask --debug run`,避免一開始未啟用,但在程式執行後才啟用的前後不一致情況。 ::: :::info :star: Debug 模式與 Testing 模式的差別 - Debug 模式:主要用在開發階段,預設為關閉,啟用後可提供更詳細的錯誤訊息與存放堆疊要追蹤變數的值,且提供自動重新載入功能,當程式碼更改時,Flask 會自動重新啟動,但會影響效能,且不利資安,因此部署到生產環境時應該關閉。 - Testing 模式:主要用於測試階段,預設為關閉,啟用後例外不會由應用程序的錯誤處理程序捕獲和處理,而是傳播到呼叫堆疊的頂部,這樣測試框架才能捕獲並分析它們,只有在有測試需求時才打開。 ::: ## 2. 從 config.yaml 檔案導入設定 json 格式比較適合用在 API 資料交換使用,但設定配置之類的檔案建議使用 yaml 格式取代 json 格式,因為更簡單、可讀性較佳、支持註解等因素。 ### config.yaml ```yaml= SECRET_KEY: "your_secret_key_here" DATABASE_URI: "sqlite:///path/to/database.db" UPLOAD_FOLDER: "/path/to/upload/folder" MAX_CONTENT_LENGTH: 16 * 1024 * 1024 # 16 MB MAIL_SERVER: "smtp.example.com" MAIL_PORT: 587 MAIL_USE_TLS: true MAIL_USERNAME: "your_email@example.com" MAIL_PASSWORD: "your_email_password" ``` ### app.py > 使用前要先安裝套件:`pip install ruamel.yaml` ```python= from flask import Flask from ruamel.yaml import YAML app = Flask(__name__) def load_config(): yaml = YAML(typ='safe') with open("config.yaml", "r") as yamlfile: cfg = yaml.load(yamlfile) app.config.update(cfg) load_config() @app.route('/') def hello(): return 'Hello, World!' ``` ## 3. 從環境變數導入設定 ### 設定環境變數(Windows) ``` C:\> set FLASK_SECRET_KEY='YourProdSecretKey' ``` ### app.py ```python= import os from flask import Flask app = Flask(__name__) # 從環境變數中導入設定 app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET_KEY') @app.route('/') def hello(): return 'Hello, World!' ``` :::danger FLASK_ENV環境變數自 Flask 2.3 版本後已經被棄用,不能再使用。 ::: ## 4. 從 config.py 導入設定 這種方式的優勢是支持不同的運行環境,方便管理和維護配置,同時也增強了安全性,避免敏感資料意外暴露。 ### config.py 首先需要建立 config.py 檔案,並在其中定義不同環境的配置類別,例如 DevelopmentConfig、ProductionConfig和TestingConfig。每個配置類別對應不同的環境。 ```python= class Config(object): '''共用的設定,提供繼承之用,減少重複的配置''' TESTING = False class ProductionConfig(Config): DB_SERVER = '192.168.19.32' DATABASE_URI = 'mysql://user@localhost/foo' class DevelopmentConfig(Config): DB_SERVER = 'localhost' DATABASE_URI = "sqlite:////tmp/foo.db" class TestingConfig(): DB_SERVER = 'localhost' DATABASE_URI = 'sqlite:///:memory:' TESTING = True ``` ### app.py 在主程式中引用 config.py 中的設定,並根據需要選擇不同的配置。 ```python= from flask import Flask from config import DevelopmentConfig, ProductionConfig app = Flask(__name__) # 根據不同環境設定應用程序的配置 app.config.from_object(DevelopmentConfig) # 開發環境下使用 # app.config.from_object(ProductionConfig) # 生產環境下使用 @app.route('/') def hello(): return 'Hello, World!' ``` :::info 上述四種配置方式,各有優缺點: 1. 直接在程式碼中直接設定所需環境適用於小型專案的開發,但不利於版本管理和敏感資料的保護。 2. 從 yaml/json 等配置文件導入適用於較大型的專案,便於團隊協作和版本控制,缺點是增加了配置文件的維護成本。 3. 使用環境變數主要適用於雲環境部署,可以根據不同環境動態載入配置,缺點是環境變數的設置和獲取相對繁瑣。 4. 使用 config.py 文件導入適用於中大型專案的各種環境配置管理,提供了靈活性,但需要額外的模組化管理。 因此視實際需求,對於小型專案直接設定環境變數較為合適;對於中大型項目,建議使用配置文件或 config.py 模組的方式。 更多的 Flask 環境設定方式請參考:[Configuration Handling](https://flask.palletsprojects.com/en/3.0.x/config/) ::: # 使用 Blueprint 在較大型的應用中,我們可以使用 Blueprint 物件將相關的路由、視圖函數、靜態文件和樣板組織在一起,使程式結構更模組化和清晰。使用 Blueprint 的主要優點包括: 1. 更好的代碼組織和模組化 2. 更容易代碼重用 3. 更容易維護和擴充大型應用程序 ## 範例1:僅獨立路由 在這個範例中,我們將授權和部落格相關的路由獨立為 2 個 Blueprint 物件。 這種方式雖然將路由分離,但樣板和靜態文件仍然放在專案的根目錄下,不算拆分的很乾淨。 ``` project/ ├── app.py ├── auth.py ├── blog.py └── templates/ └── xxxxx.html ``` ### app.py 從 auth 和 blog 模組中導入 auth_bp 和 blog_bp,然後使用 app.register_blueprint 方法註冊它們,並在這裡設定 URL 前綴。 ```python= from flask import Flask from auth import auth_bp from blog import blog_bp app = Flask(__name__) app.register_blueprint(auth_bp, url_prefix='/auth') app.register_blueprint(blog_bp, url_prefix='/blog') @app.route('/') def index(): return 'Hello, World!' ``` ### auth.py 從 flask 中導入 Blueprint,然後建立 Blueprint 實例 auth_bp。Blueprint 的第一個參數是 Blueprint 的名稱,通常與模組名稱相同。 ```python= from flask import Blueprint auth_bp = Blueprint('auth', __name__) @auth_bp.route('/login') def login(): return "This is the login page." @auth_bp.route('/register') def register(): return "This is the register page." ``` ### blog.py 從 flask 中導入 Blueprint,然後建立 Blueprint 實例 blog_bp ```python= from flask import Blueprint blog_bp = Blueprint('blog', __name__) @blog_bp.route('/post/<int:post_id>') def show_post(post_id): return f"This is the post with id {post_id}" @blog_bp.route('/new') def new_post(): return "This is the page to create a new post." ``` ### 測試 - http://127.0.0.1:5000/ - 主頁面,應顯示 "Hello, World!" - http://127.0.0.1:5000/auth/login - 登入頁面,應顯示 "This is the login page." - http://127.0.0.1:5000/auth/register - 註冊頁面,應顯示 "This is the register page." - http://127.0.0.1:5000/blog/post/1 - 顯示文章頁面,應顯示 "This is the post with id 1" - http://127.0.0.1:5000/blog/new - 新文章頁面,應顯示 "This is the page to create a new post." ## 範例2:將每個 Blueprint 組織為獨立的套件 在這個範例中,我們將每個 Blueprint 及其相關文件組織為獨立的套件,以實現更徹底的模組化。 ``` project/ ├── app.py ├── auth/ │ ├── __init__.py │ ├── views.py │ ├── static/ │ │ └── style.css │ └── templates/ │ ├── login.html │ └── register.html └── blog/ ├── __init__.py ├── views.py ├── static/ │ └── style.css └── templates/ ├── post.html └── new.html ``` ### app.py 從 auth 和 blog 模組中導入對應的 bp (Blueprint 實例),然後使用 app.register_blueprint 方法註冊它們。 ```python= from flask import Flask from auth import bp as auth_bp from blog import bp as blog_bp app = Flask(__name__) app.register_blueprint(auth_bp) app.register_blueprint(blog_bp) @app.route('/') def index(): return 'Hello, World!' ``` ### auth/view.py 這個檔案包含了 auth 藍圖的所有路由和視圖函數。由於我們並未在主程式 app.py 設定 url_prefix,所以這裡必須加上前綴。 ```python= from flask import Blueprint, render_template bp = Blueprint('auth', __name__, url_prefix='/auth') @bp.route('/login') def login(): return render_template('auth/login.html') @bp.route('/register') def register(): return render_template('auth/register.html') ``` ### auth/templates/base.html 這是一個基礎樣板,其他頁面樣板可以繼承並覆蓋特定區塊。 ```htmlembedded= <!doctype html> <html> <head> {% block head %} <title>{% block title %}{% endblock %} - Auth Blueprint</title> <link rel="stylesheet" href="{{ url_for('auth.static', filename='style.css') }}"> {% endblock %} </head> <body> {% block body %} {% endblock %} </body> </html> ``` ### auth/templates/login.html 登入頁面樣板,繼承自 base.html ```htmlembedded= {% extends 'auth/base.html' %} {% block title %}Login{% endblock %} {% block body %} <h1>Login Page</h1> {% endblock %} ``` ### auth/templates/register.html 註冊頁面樣板,繼承自 base.html ```htmlembedded= {% extends 'auth/base.html' %} {% block title %}Register{% endblock %} {% block body %} <h1>Register Page</h1> {% endblock %} ``` ### blog/view.py 這個檔案包含了 blog 藍圖的所有路由和視圖函數。 ```python= from flask import Blueprint, render_template bp = Blueprint('auth', __name__, url_prefix='/auth') @bp.route('/post/<int:post_id>') def show_post(post_id): return render_template('blog/post.html', post_id=post_id) @bp.route('/new') def new_post(): return render_template('blog/new.html') ``` ### blog/templates/post.html 顯示文章網頁樣板 ```htmlembedded= <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Post</title> <link rel="stylesheet" href="{{ url_for('blog.static', filename='style.css') }}"> </head> <body> <h1>Post ID: {{ post_id }}</h1> </body> </html> ``` ### blog/templates/new.html 新增文章網頁樣板 ```htmlembedded= <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>New Post</title> <link rel="stylesheet" href="{{ url_for('blog.static', filename='style.css') }}"> </head> <body> <h1>Create a New Post</h1> </body> </html> ``` ### 測試 - http://127.0.0.1:5000/ - 主頁面,應顯示 "Hello, World!" - http://127.0.0.1:5000/auth/login - 顯示登入頁面 - http://127.0.0.1:5000/auth/register - 顯示註冊頁面 - http://127.0.0.1:5000/blog/post/1 - 顯示文章頁面 - http://127.0.0.1:5000/blog/new - 顯示新文章頁面 :::info :star:注意事項: url_prefix 可以在 app.py 中註冊 Blueprint 時設置,也可以在 Blueprint 的定義中設置。這兩種方式都可以使用,但通常只需在一個地方設置即可。這樣做可以避免重複設定並保持代碼簡潔。 ::: --- # Flask 擴充套件 1. [Flask-SQLAlchemy 套件](https://hackmd.io/@peterju/SkpFlj40s) 使用 ORM 方式存取資料庫 2. [Flask-WTF 套件](https://hackmd.io/@peterju/SyxnDUxZye) 表單產生器與欄位驗證器 3. [Flask-Session 套件](https://hackmd.io/@peterju/S1FoLvgbkg) 支援持久化儲存的 session --- # 使用 ngrok 工具 當我們需要測試由外網連結,特別是要有 tls 支援的情況,使用 ngrok 就非常方便。 ## 取得 ngrok 的 Authtoken 首先到 [ngrok 官網](https://ngrok.com/)註冊一個帳號,登入後取得自己的 Authtoken ![image](https://hackmd.io/_uploads/H1cB-GcNR.png) 接下來有2種使用方式 1. 使用命令列 2. 安裝 pyngrok 套件 ### 1. 使用命令列 - 下載免安裝執行檔:https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-windows-amd64.zip - 解壓縮後,在與 ngrok 命令相同目錄下執行新增授權碼(authtoken)的指令如下 ![image](https://hackmd.io/_uploads/r1W3vnqN0.png) - 執行 flask 之後,再執行 ngrok,便可得到一組外網連結,如下圖 ```bash= flask --debug run ngrok http http://127.0.0.1:5000 ``` ![image](https://hackmd.io/_uploads/ByT6tnq4R.png) - 不要關閉視窗,點選或複製 Forwarding 右側的連結 https://2370-61-219-216-122.ngrok-free.app/ - 在瀏覽器貼上,便能訪問原先無法在外網使用的 flask 應用程式。 ![image](https://hackmd.io/_uploads/Bk4cq2cN0.png) ### 2. 安裝 pyngrok ```bash= pip install pyngrok ``` ### app.py ```python= from flask import Flask from pyngrok import ngrok app = Flask(__name__) ngrok.set_auth_token("6piDJd2zwWTC6q9a1nyQh_7gwcGTDLbgoPEysJW5bjH") # conf.get_default().region = "jp" port = 5000 public_url = ngrok.connect(port, bind_tls=True).public_url print(f"使用 ngrok 產生的外網連結:{public_url}") @app.route('/') def index(): return "<h1 style='color:red'>測試 ngrok</h1>" ``` 然後以網頁打開使用 ngrok 產生的外網連結 # 範例參考 ## 1. 上傳文件範例 **app.py** ```python= import os from flask import Flask, render_template, request, redirect, url_for from werkzeug.utils import secure_filename ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'} MAX_FILE_SIZE = 2 * 1024 * 1024 # 2MB app = Flask(__name__) app.config['UPLOAD_FOLDER'] = 'uploads' def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app.route('/upload', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': file = request.files['file'] if file and allowed_file(file.filename): if file.content_length > MAX_FILE_SIZE: error = f'文件大小超過{MAX_FILE_SIZE / (1024 * 1024)}MB' return render_template('upload.html', error=error) filename = secure_filename(file.filename) file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) file.save(file_path) return redirect(url_for('upload_file', filename=filename)) else: error = '請選擇允許的文件格式: .png, .jpg, .jpeg, .gif, .pdf' return render_template('upload.html', error=error) return render_template('upload.html') ``` **upload.html** ```htmlmixed= <!DOCTYPE html> <html> <head> <title>文件上傳</title> </head> <body> {% if error %} <p style="color: red;">{{ error }}</p> {% endif %} <h1>上傳文件</h1> <form method="post" enctype="multipart/form-data"> <p> <input type="file" name="file" accept=".jpg,.png,.gif,.pdf"> </p> <p> <input type="submit" value="上傳"> </p> </form> </body> </html> ``` ## 2. 資料庫與樣板網頁繼承範例 ### 2.1 建立檔案 請使用 vscode 建立相關檔案,並留意 - html檔要放在 templates 目錄下 - css檔要放在 static\css 目錄下 ``` mermaid graph TD root[(根目錄)] --> run(run.cmd) & main(main.py) & lib(lib.py) & static目錄 & templates目錄 static目錄-->css目錄 & img目錄 css目錄-->樣式表(style.css) templates目錄-->base(base.html) & index(index.html) & login(login.html) & msg(msg.html) & show(show.html) ``` ### 2.2 run.cmd :::spoiler ```bash= @echo off rem 切換工作目錄至批次檔所在目錄 cd /d "%~dp0" if not defined _OLD_VIRTUAL_PROMPT ( call env\Scripts\activate ) flask --debug --app main run -h 0.0.0.0 -p 80 ``` ::: ### 2.3 main.py :::spoiler ```python= from flask import Flask from flask import request from flask import render_template from flask import url_for, redirect from lib import * app=Flask(__name__) # __name__ 代表目前執行的模組 db_init() # flask利用裝飾器@app.route來定義路由,其後的裝飾(Decorator)通常是一個函數,用來提供執行的動作 @app.route("/") def index(): """ 路徑為 / 時所執行的程式 """ return render_template('index.html') # 表單與傳送方法(若不指定 methods 時,預設為 GET 方法) @app.route("/login") def login(): """ 路徑為 /login 時所執行的程式 """ return render_template('login.html') @app.route("/show", methods=['GET', 'POST']) def show(): """ 路徑為 /show 時所執行的程式 """ if request.method == 'POST': acc = request.form['username'].strip().lower() pwd = request.form['password'].strip().lower() result = db_yn_query(acc, pwd) if result: return render_template('show.html', username=acc) return render_template('msg.html', msg='Account/Password not correct') return render_template('index.html') ``` ::: ### 2.4 lib.py :::spoiler ```python= import sqlite3 def db_init(): """ 建立資料庫與資料表 """ conn = sqlite3.connect('demo.db') conn.execute(''' create table if not exists member ( iid integer primary key autoincrement, mnm char(10) not null, mpwd char(32) not null ); ''') # 新增紀錄 conn.execute("INSERT INTO member (mnm, mpwd) VALUES (?, ?)", ('test', '123456')) conn.commit() # 將變動寫入檔案 conn.close() return def db_yn_query(id, pwd): conn = sqlite3.connect('demo.db') """ 查詢指定紀錄 """ cursor = conn.execute("SELECT * FROM member WHERE mnm=? AND mpwd=?", (id, pwd)) data = cursor.fetchall() conn.close() if len(data) > 0: return True return False ``` ::: ### 2.5 templates\base.html :::spoiler ```htmlmixed= <!DOCTYPE html> <html lang="zh-Hans-TW"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="{{ url_for('static', filename= 'css/style.css') }}"> <title>{% block title %} {% endblock %}</title> </head> <body> <nav> <div class="logo"> <img src="{{ url_for('static', filename='img/logo2007_01.jpg') }}" alt=""> <!-- <img src="http://w3.ncut.edu.tw/ncut/ncutlogo/logo2007_01.jpg" alt=""> --> <h1>勤益科大</h1> </div> <div class="links"> <ul> <li><a href="{{ url_for('index')}}">首頁</a></li> <li><a href="https://www.google.com.tw/">Google</a></li> <li><a href="https://flask.palletsprojects.com/">Flask</a></li> <li><a href="https://docs.google.com/document/d/1FZH_AaWkJonoA6BP8yZytZlAXUl-DiB2AbNJjIuc1QI">講義</a></li> </ul> </div> <div class="botoes"> <button class="btn1">測試1</button> <button class="btn2">測試2</button> </div> </nav> {% block content %} {% endblock %} </body> </html> ``` ::: ### 2.6 templates\index.html :::spoiler ```htmlmixed= {% extends 'base.html' %} {% block title %} 首頁 {% endblock %} {% block content %} <header> <span>Flask Demo</span> <h1>首頁</h1> <p>這裡是首頁,按下方可進行登入</p> <a href="{{ url_for('login')}}">登入</a> </header> {% endblock %} ``` ::: ### 2.7 templates\login.html :::spoiler ```htmlmixed= {% extends 'base.html' %} {% block title %} 登入 {% endblock %} {% block content %} <header> <span>Flask Demo</span> <h1>登入</h1> <form action="/show" method="post"> <label>姓名:</label> <input type="textbox" name="username"> <label>密碼:</label> <input type="textbox" name="password"> <input type="submit" value="送出"> </form> </header> {% endblock %} ``` ::: ### 2.8 templates\show.html :::spoiler ```htmlmixed= {% extends 'base.html' %} {% block title %} 主頁 {% endblock %} {% block content %} <header> <span>Flask Demo</span> <h1>主頁</h1> <p>歡迎您, {{ username }}</p> <p>您已完成登入主頁</p> <a href="{{ url_for('index')}}">首頁</a> </header> {% endblock %} ``` ::: ### 2.9 templates\msg.html :::spoiler ```htmlmixed= {% extends 'base.html' %} {% block title %} Message {% endblock %} {% block content %} <header> <span>Flask Demo</span> <h1>訊息</h1> <h2 class="emp">{{msg}}</h2> <br> <a href="{{ url_for('index')}}">首頁</a> </header> {% endblock %} ``` ::: ### 2.10 static\css\style.css :::spoiler ```htmlmixed= h1 { border: 2px #eee solid; color: brown; text-align: center; padding: 10px; } @import url(https://fonts.googleapis.com/earlyaccess/notosanstc.css); :root { --white: #fff; --red: #8e2424; --black: #101828; --gray: #667085; --footer: #98a2b3; --logo: #3a404e; --font: 'Noto Sans TC', '微軟正黑體', sans-serif; } * { margin: 0; padding: 0; box-sizing: border-box; } body { width: 100%; height: 100vh; font-family: var(--font); } /* NAVBAR */ nav { background: var(--white); width: 100%; height: 80px; } nav .logo img { position: absolute; left: 112px; top: 25px; width: 3% } nav .logo h1 { position: absolute; left: 170px; top: 26.5px; color: var(--logo); font-weight: 500; font-size: 18px; } nav .links { position: absolute; left: 379px; top: 24px; } nav .links ul { list-style: none; } nav .links ul > li { display: inline; font-size: 16px; font-weight: 500; padding-left: 32px; } nav .links li > a { text-decoration: none; color: var(--gray); } nav .botoes { display: flex; flex-direction: row; position: absolute; top: 18px; left: 1121px; } nav .botoes > .btn1, .btn2 { font-size: 16px; font-weight: 500; padding: 10px 18px; margin-right: 12px; cursor: pointer; } nav .botoes > .btn1 { color: var(--gray); background: var(--white); border: none; } nav .botoes > .btn2 { color: var(--white); background: var(--red); border: 1px solid var(--red); border-radius: 8px; box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); /* width: 112px; height: 44px; */ } /* HEADER */ header { text-align: center; background: var(--white); display: flex; flex-direction: column; align-items: center; padding: 96px 0px; width: 1440; height: 407; } header span { color: var(--red); font-size: 16px; font-weight: 600; } header h1 { color: var(--black); font-size: 48px; font-weight: 600; letter-spacing: -0.02em; margin: 40px 0; } header p { color: var(--gray); font-size: 20px; font-weight: 400; width: 791px; height: 60px; } .emp { color: gold; } ``` ::: # 參考網頁 * [Flask 入门教程](https://tutorial.helloflask.com/) * [2023 Flask 入門指南](https://www.maxlist.xyz/2020/05/01/flask-list/) * [什麼是 WSGI & ASGI ?](https://medium.com/@eric248655665/f0d5f3001652) * [Flask 快速指南](https://editor.leonh.space/2022/flask/) * [Flask快速上手](https://dormousehole.readthedocs.io/en/latest/quickstart.html) * [Flask 基础教程](https://www.cainiaojc.com/flask/flask-tutorial.html) * [Flask 教程](https://www.w3cschool.cn/flask/) * [Python Flask實作記錄](https://hackmd.io/@shaoeChen/HJiZtEngG/) * [2017年新版The Flask Mega-Tutorial教程](https://github.com/luhuisicnu/The-Flask-Mega-Tutorial-zh) * [Python 基礎 - pyc 是什麼](https://ithelp.ithome.com.tw/articles/10185442)