# Flask 網頁框架
<!-- :::success
Python除了速度非其優勢外、其容易上手的草稿語言特性,與多年發展的穩定程度,近年在大數據、爬蟲與人工智慧領域大放異彩,但要發展適合團體使用的應用系統,需要集中的資料庫進行資料儲存與分析,又因為行動化裝置大量普及,在節省成本與開發人力上建議採用 client-server 架構的 web 開發,但路由、資料存取都在後端的方法。綜上所述擬定後台使用 Python 的 Flask 網頁框架作為大家一起學習的技術。
::: -->

# 簡介
<!-- 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` 參數執行時,會跳出以下防火墻封鎖提示,若確定要開放其它電腦存取,請按【允許存取】

##### 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 # 承上,加上開啟除錯功能
```
:::
---
# 路由

路由是將網址(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

---
## 模版進階功能
以下介紹樣板繼承、過濾器與巨集。
### 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 檔案】

然後點選畫面正上方出現的 Python Debugger

然後點選 Flask 啟動 Flask Web....

然後挑選這個專案的主程式(這裡的範例是 app.py 但你的專案不一定是)

你就會看到 launch.json 建立成功了

:::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. 在專案主程式中設定中斷點
在某一行程式碼的行號前點擊滑鼠左鍵,會出現紅色的點,代表會在執行此行程式前暫停

點選左側第4個圖示【執行與偵錯】,然後按下上方的【綠色箭頭】Python 偵錯工具:Flask,或直接按下【F5】開始偵錯

執行程式到中斷點的程式碼時,整個程式會停下來,畫面會出現以下變化
- 左上角會出現中斷點暫停時,local 變數與 global 變數的值
- 畫面上方可按下繼續(F5)、逐步執行(F11)...停止(Shift+F5)等操作

### 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
```
---
# 序列化與反序列化

序列化是將資料物件轉換成可傳輸或儲存的格式(二進制的檔案或字串),例如 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

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. 可在右側看到執行結果

:::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 等。

請求(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 物件

在 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 物件

標準的 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 控制

除了使用 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

接下來有2種使用方式
1. 使用命令列
2. 安裝 pyngrok 套件
### 1. 使用命令列
- 下載免安裝執行檔:https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-windows-amd64.zip
- 解壓縮後,在與 ngrok 命令相同目錄下執行新增授權碼(authtoken)的指令如下

- 執行 flask 之後,再執行 ngrok,便可得到一組外網連結,如下圖
```bash=
flask --debug run
ngrok http http://127.0.0.1:5000
```

- 不要關閉視窗,點選或複製 Forwarding 右側的連結 https://2370-61-219-216-122.ngrok-free.app/
- 在瀏覽器貼上,便能訪問原先無法在外網使用的 flask 應用程式。

### 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)