Flask 函式庫及後端基礎知識
Full Stack
Discord Bot
Machine Learning
Unity
Competitive Programming
Web Crawler
Server Deployment
Minecraft Datapack
Scratch
Flask 簡介
建立 Flask 應用程式
結合資料庫
前端基礎互動
API 設計
即時通訊技術
雜項
Flask 簡介
建立 Flask 應用程式
結合資料庫
前端基礎互動
本簡報用於建北電資2025寒訓
及IZCC四校聯合2025寒訓
目的以輔助講義為主
上一篇我們提到了前端會向後端發送數據
而後端用以接收數據的接口則稱為 API (Application Programming Interface / 應用程式交互介面)
API 在應用程式要跟伺服器請求資訊或是回傳資訊時使用到,詳細的實作介紹將會在後面章節提及
後端架構最常見的應用莫過於網站,我們在網頁中的所見以及能互動的這些組件大多都是從連上特定的 URL(Uniform Resource Locator / 統一資源定位符) 開始,經過後端處理應該傳給你的資料,再由瀏覽器解析這些資料成為網頁
前端的資料通常由三樣東西組成:
(HyperText Markup Language / 超文本標記語言)
負責控制整個介面的排版、各個標籤的名稱,以及網頁的基礎設定(標題、預覽畫面、加載的 CSS、JS 等)
(Cascading Style Sheets / 階層式樣式表)
負責對每個標記進行樣式上的設定,像是文字顏色位置字體、點擊時的顏色變化、甚至是一些簡單的動畫呈現等等
三件套中唯一的程式語言,各種邏輯處理、API交互、進階的動畫或介面呈現都是藉由 JS 完成,用於網頁的 JS 也衍伸出各種框架來幫助前端設計,甚至包含到後端架構,像是 Vue.js、React.js、Next.js。
Flask 是 Python 語言所建立的一個 Web 應用框架,相對於其他於其他框架較為架構單純
輕量化也使得他入門難度較低,相對的在進行較大架構的編寫時較需要去設計檔案架構,以及對於各種功能的補充與函式庫支援更需要加以維護
以 Ruby on Rails 為例,其架構相對固定,而 Flask 的檔案架構則相當自由,基本上除了 Blueprint 有點架構性之外,其餘部分都需要各位自行進行編制
另外常見的 Python 框架還有 Django、FastAPI 等等
前者的內建功能豐富,像是帳號系統與資料庫管理等
後者語法上與 Flask 相似,但在運行速度上做了不少優化,若對更廣的應用有興趣者可以深入研究
現今最為泛用框架還有眾多基於 JS 語言的框架,好處是可以同時兼顧前端的 JS 邏輯,
使兩者之間的交互編程更加絲滑,以及 npm(Node Package Manager / node套件管理器) 的優秀套件管理能對網站設計帶來相當大的幫助
進入 Python 官網,之後根據你的作業系統下載指定版本,之後點開下載下來的安裝程式
勾選完畢後點擊 Install Now ,便完成安裝
若是沒有新增 Python 到 %PATH% 環境變數,之後 IDE 與終端可能會抓不到你的 Python 編譯器
勾選完畢後點擊 Install Now ,便完成安裝
若是沒有新增 Python 到 %PATH% 環境變數,之後 IDE 與終端可能會抓不到你的 Python 編譯器
在終端機中輸入以下內容以安裝 Flask
pip install flask
可以在終端輸入以下內容以確認是否成功安裝以及寫入環境變數
flask --version
看到正常顯示版本代表安裝成功
打開你習慣的程式編輯軟體(此處以 Visual Studio Code 為例),建立一個 Python 檔案,並寫入以下內容
from flask import Flask
app = Flask(__name__)
if __name__ == '__main__':
app.run()
app 為我們的 Flask 應用程式核心,對網站後端的各種操作都是藉由它去進行的,這邊我們建立了一個應用,並且在以此檔案執行時調用 app.run() 來啟動應用
from flask import Flask
app = Flask(__name__)
if __name__ == '__main__':
app.run()
成功後會見到以下內容
可以看到預設是跑在 127.0.0.1:5000 上面的,這代表的是我們電腦本身的位置,當然你也可以透過 app.run() 的參數去調整 host / port
from flask import Flask
app = Flask(__name__)
if __name__ == '__main__':
app.run(host="localhost", port=8080)
可以看到它現在跑在 localhost:8080 了
另外,Flask 提供了 debug 模式可以去做使用,一樣在 app.run() 的參數去做設定:
在 debug 模式時,每次對程式的更動都會自動重啟 app
並且在發生錯誤時會在網頁中顯示錯誤內容
也可以直接在前端進入終端機進行調整
在開發時啟用是個不錯的選擇
app.run(debug=True)
在編程應用時,我們通常習慣以一個檔案甚至是一個 package 去處理各種設定檔
在程式啟動時便能透過引用該設定檔的內容去快速的取得各方面的設定資訊
其中有個常用的設定儲存手段為環境變數,在一個應用中可能有許多資訊是不適合公開的,如資料庫密碼、API 金鑰、TOKEN 等等
這種時候會將這些資料存取在 .env 檔案中,此檔案會透過 .gitignore 避免跟著上傳,而設定檔程式內使用 Python 的 dotenv 函式庫將 .env 的內容存進環境變數,在透過內建函式庫調用
# config.py
import os
from dotenv import load_dotenv
BASEDIR = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(BASEDIR, ".env"), override=True) # 將.env寫入環境變數
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI") # 資料庫連結
API_KEY = os.getenv("API_KEY") # API 金鑰
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == '__main__':
app.run(host="localhost", port=8080)
在第一章的時候提到使用者電腦連上特定的 URL
之後會經過後端的處理來發送指定的資料內容給客戶端
我們可以使用 Flask 簡單的實現
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
if __name__ == '__main__':
app.run(host="localhost", port=8080)
在程式中設定了當連結上 / 路徑時會回傳一個 "Hello World!",而 / 也就是根路徑,以我們設定的 host 為例,它會相當於 localhost:8080/,也就是直接連上時第一個映入眼簾的內容
@app.route("/home/<int:number>")
def home(number):
return f"Hello World! {number}"
也可以透過 <變數型別:變數名稱> 來讓執行函式接收使用者在網址的內容
上圖設定下進入 localhost:8080/home/100
會看到 "Hello World! 100"
而進入 localhost:8080/home/一百
則會因為型別不符合而無法找到頁面(404 Error)
回傳的資料當然也可以是 html,可透過 Flask 的函式進行回傳,而預設抓取 html 檔案的路徑會在 templates 資料夾底下,我們建立此資料夾並建立新檔案命名為 index.html,並且寫入以下內容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>我是標題</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
之後再將原 Python 程式改為以下內容
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def home():
return render_template("index.html")
if __name__ == '__main__':
app.run(host="localhost", port=8080)
會看到我們連上根路徑後的標題如同 html 所設定,並且按下 F12 開啟開發者工具可以看見我們剛剛寫的 html
另外可以透過後端將使用者重新導向到別的 URL
像是這樣
from flask import redirect
# 略
@app.route("/very_good_link")
def very_good_link():
return redirect("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
不知道那個網址是什麼的點了就知道了:D
Flask 使用 Jinja2 模板,可以在前端的 html 中寫入一些特殊的語法來達到後端回傳時在裡面加入指定資料。
透過 {{ 變數名 }} 可以得到後端傳入的變數,範例如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>我是標題</title>
</head>
<body>
<h1>your number is {{ number }}!</h1>
</body>
@app.route("/<int:number>")
def home(number):
return render_template("index.html", number=number)
另外,Jinja2 能夠使用的遠遠不止於此,我們甚至可以對它傳入字典、陣列等結構,之後在 html 進行解析、條件判斷或是對它進行迭代,讓我們看看下面範例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>我是標題</title>
</head>
<body>
{% for book in books %}
{% if book.title != "這是禁書" %}
<h1>{{ book.title }}</h1>
<p>{{ book.author }}</p>
<p>{{ book.price }}</p>
{% endif %}
{% endfor %}
</body>
@app.route("/")
def home():
books = [
{
"title": "這是一本書",
"author": "這是一名作者",
"price": 100
},
{
"title": "這是第二本書",
"author": "這是第二名作者",
"price": 200
},
{
"title": "這是第三本書",
"author": "這是第三名作者",
"price": 300
},
{
"title": "這是禁書",
"author": "你猜",
"price": 114514
}
]
return render_template("index.html", books=books)
成功的話你會在根路徑看到以下畫面
可以看到禁書被隱藏了,且它確實按照我們希望的邏輯去對資料做遍歷了
另外,在網頁中許多重複性極高的標籤
像是一開始的宣告、導覽列等等
可以藉由模板來避免重複寫
也可以在需要修改的時候方便許多
{% extends "base.html" %}
{% block content %}
{% for book in books %}
{% if book.title != "這是禁書" %}
<h1>{{ book.title }}</h1>
<p>{{ book.author }}</p>
<p>{{ book.price }}</p>
{% endif %}
{% endfor %}
{% endblock content %}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>我是標題</title>
</head>
<body>{% block content %}{% endblock content %}</body>
base.html
※ Python 檔案無須修改
不知你是否有遇到過 404 Page Not Found 等錯誤
這些錯誤代碼是在 http 連線中有固定規範的
在不同情況的狀態代碼會有所區別
其中簡易規則如下
狀態代碼 | 1XX | 2XX | 3XX | 4XX | 5XX |
表示意義 | 訊息 | 成功 | 重新導向 | 客戶端錯誤 | 伺服器錯誤 |
其中我們需要捕捉的通常是 404(用戶亂戳不存在的 URL)、500(伺服器運行時發生錯誤) 等等
另外可以視情況主動引發 403(權限不足) 等錯誤
狀態代碼 | 1XX | 2XX | 3XX | 4XX | 5XX |
表示意義 | 訊息 | 成功 | 重新導向 | 客戶端錯誤 | 伺服器錯誤 |
預設發生錯誤的介面長這樣
預設發生錯誤的介面長這樣
嗯,很醜~ 因此我們可透過 Flask 內建函式去設定在錯誤時使用自訂的 html 呈現的介面
@app.errorhandler(404)
def page_not_found(e):
return render_template("404.html"), 404
如此一來就可以讓介面變成自己喜歡的樣子
※ 上圖範例的 html 的部分過於冗長,故此處不提供
一般而言可配合網頁風格進行設計
剛剛提到的手動引發則是透過 abort() 函式,提供狀態碼後將會執行設定好的錯誤處理函式或是預設的醜醜介面
from flask import abort
# 略
@app.route("/admin")
def admin():
abort(403)
※ abort() 不需要放在 return 後
(Relational Database Management System
/ 關聯式資料庫管理系統)
(Structured Query Language / 結構化查詢語言)
將每個需要儲存的東西以物件的形式儲存
跟物件導向的概念相似
先創建好對應物件的表格
並設定好其屬性值
而後便可透過給予對應的值產生新的物件存放在資料庫內
優點 | 缺點 |
快速、泛用、一致性高 | 在實現橫向擴展上困難 |
其範例如圖
(Non-SQL / 非結構化查詢語言)
有別於關聯式資料庫之儲存方式的查詢語言皆可稱為 NoSQL
其型式各自有別,但主流型式為 JSON 物件格式,以字串及數字的字典、陣列物件構造出的結構
優點 | 缺點 |
資料結構自由、儲存邏輯與程式相同 | 各查詢語言之間格式不通 |
由於 NoSQL 的形式各自有別
且 SQL 的泛用性及複雜程度都勝過 NoSQL
因此本章將會著重在 SQL 的使用教學
在教學開始前,請先安裝 flask_sqlalchemy
pip install flask_sqlalchemy
前面提到 SQL 的語言格式是固定的
也因此我們通常會使用 ORM(Object Relational Mapping / 物件關聯對映) 相關的函式庫
來讓我們的程式能夠以相同程式碼相容各種 SQL 資料庫
並且在物件的操作過程中提高了極大的安全性
因為通常函式庫內都會事先做好 SQL Injection (SQL 注入) 的防護
前面提到 SQL 的語言格式是固定的
也因此我們通常會使用 ORM(Object Relational Mapping / 物件關聯對映) 相關的函式庫
來讓我們的程式能夠以相同程式碼相容各種 SQL 資料庫
並且在物件的操作過程中提高了極大的安全性
因為通常函式庫內都會事先做好 SQL Injection (SQL 注入) 的防護
讓我們看看相同功能實現上的差別
(未使用 ORM 函式庫)
import sqlite3
dbfile = "sqlite3.db"
conn = sqlite3.connect(dbfile)
cursor = conn.cursor()
def init():
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT)")
cursor.execute("INSERT INTO users (username, password) VALUES ('admin', 'admin')")
cursor.execute("INSERT INTO users (username, password) VALUES ('user', 'user')")
conn.commit()
def query(username, password):
cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password))
return cursor.fetchone()
(使用 ORM 函式庫)
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, Integer, String
db = SQLAlchemy()
class User(db.Model):
id = Column(Integer, primary_key=True)
username = Column(String(80), unique=True, nullable=False)
password = Column(String(80), nullable=False)
def __init__(self, username: str, password: str):
self.username = username
self.password = password
def init():
db.create_all()
db.session.add(User(username="admin", password="admin"))
db.session.add(User(username="user", password="user"))
db.session.commit()
def query(username, password):
return User.query.filter_by(username=username, password=password).first()
可以看到後者有別於前者
能夠對應到各種資料庫而非 sqlite
並且在查詢的地方也不怕被輸入 ";"
等方式來越過密碼取得用戶資料
如同前一章看到的
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT)")
class User(db.Model):
id = Column(Integer, primary_key=True)
username = Column(String(80), unique=True, nullable=False)
password = Column(String(80), nullable=False)
如同前一章看到的
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT)")
class User(db.Model):
id = Column(Integer, primary_key=True)
username = Column(String(80), unique=True, nullable=False)
password = Column(String(80), nullable=False)
在 ORM 的架構中會使用物件導向的邏輯去設定資料庫內存取的物件結構
透過給予物件需要的 Column 的屬性
可以在可讀性極高的情況下設定好資料庫 Table 的欄位格式
另外
通常我們會在物件中加上一些函式
來便於物件的一些使用
class User(db.Model):
id = Column(Integer, primary_key=True)
username = Column(String(80), unique=True, nullable=False)
password = Column(String(80), nullable=False)
def __init__(self, username: str, password: str):
self.username = username
self.password = password
def __repr__(self):
return f"<User '{self.username}'>"
設定初始化函式以及被調用時的回傳訊息
class User(db.Model):
id = Column(Integer, primary_key=True)
username = Column(String(80), unique=True, nullable=False)
password = Column(String(80), nullable=False)
def __init__(self, username: str, password: str):
self.username = username
self.password = password
def __repr__(self):
return f"<User '{self.username}'>"
設定初始化函式以及被調用時的回傳訊息
class User(db.Model):
id = Column(Integer, primary_key=True)
username = Column(String(80), unique=True, nullable=False)
password = Column(String(80), nullable=False)
def __init__(self, username: str, password: str):
self.username = username
self.password = password
def __repr__(self):
return f"<User '{self.username}'>"
user = User(username="name", password="qwertyuiop")
print(user) # <User 'name'>
資料庫的創建需要先建立資料庫(db)的物件
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
資料庫的創建需要先建立資料庫(db)的物件
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
而這個 db 要跟我們的應用綁定可以有兩種方式
在建立時賦予 app 物件作為參數
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app)
使用 init()
函式給予 app 參數
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy()
db.init_app(app)
其兩者看似無區別
實際應用上多以後者來使 db 的初始化檔案能夠被母資料夾內的 main.py
引用
之後在初始化函式內再一併初始化 app
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy()
db.init_app(app)
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app)
前面提到的物件結構化也是在這邊先建立好 Class
# models.py
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, Integer, String
db = SQLAlchemy()
class User(db.Model):
id = Column(Integer, primary_key=True)
username = Column(String(80), unique=True, nullable=False)
password = Column(String(80), nullable=False)
def __init__(self, username: str, password: str):
self.username = username
self.password = password
def __repr__(self):
return f"<User '{self.username}'>"
光是做到這邊在執行時會發現發生錯誤
那是因為我們沒有給予他資料庫的連結
這個連結會包含你的資料庫帳密讓他能夠直接連接上
通常會在 config 檔案內進行設定
光是做到這邊在執行時會發現發生錯誤
那是因為我們沒有給予他資料庫的連結
這個連結會包含你的資料庫帳密讓他能夠直接連接上
通常會在 config 檔案內進行設定
# config.py
import os
from dotenv import load_dotenv
BASEDIR = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(BASEDIR, ".env"), override=True) # 將.env寫入環境變數
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI") # 資料庫連結
API_KEY = os.getenv("API_KEY") # API 金鑰
class Config(object):
JSON_AS_ASCII = False # 不要讓他 JSON 預設使用 ASCII,如此一來可以預防中文編碼爛掉的問題
SQLALCHEMY_DATABASE_URI = SQLALCHEMY_DATABASE_URI
還記得這個嗎,前面章節的 config 檔案,此時便揭曉 SQLALCHEMY_DATABASE_URI
之用途了!
# config.py
import os
from dotenv import load_dotenv
BASEDIR = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(BASEDIR, ".env"), override=True) # 將.env寫入環境變數
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI") # 資料庫連結
API_KEY = os.getenv("API_KEY") # API 金鑰
class Config(object):
JSON_AS_ASCII = False # 不要讓他 JSON 預設使用 ASCII,如此一來可以預防中文編碼爛掉的問題
SQLALCHEMY_DATABASE_URI = SQLALCHEMY_DATABASE_URI
# main.py
from flask import Flask
from config import Config
from models import db
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
@app.before_request
def db_init():
db.create_all() # 執行此函式以創建設定好的 Table,因此讓他在所有 Request 之前調用
@app.route("/")
def hello():
return "Hello World!"
if __name__ == '__main__':
app.run(host="localhost", port=8080)
對資料庫的操作最主要聚焦於創建、查詢與更新資料庫
我們先來介紹如何創建新的物件
對資料庫的操作最主要聚焦於創建、查詢與更新資料庫
我們先來介紹如何創建新的物件
from flask import Flask
from models import db, User
app = Flask(__name__)
# 略
with app.app_context():
user = User(username="name", password="test")
db.session.add(user)
db.session.commit()
先是建立 user
作為新的用戶資料
之後將它新增進資料庫的 Session 中
每次對資料庫進行更新時都要加上 db.session.commit()
(查詢因為沒有更新到資料庫所以不用)
from flask import Flask
from models import db, User
app = Flask(__name__)
# 略
with app.app_context():
user = User(username="name", password="test")
db.session.add(user)
db.session.commit()
接下來示範查詢的例子
在查詢時會使用到我們建立好的物件 Class
User.query.all() # 查詢所有 User (回傳一個 List)
User.query.filter_by(username="name").all() # 查詢所有 username 為 "name" 的 User (回傳一個 List)
User.query.filter_by(username="name").first() # 查詢第一個 username 為 "name" 的 User
User.query.filter_by(username="name").first_or_404() # 查詢第一個 username 為 "name" 的 User,查詢不到則引發404錯誤
User.query.count() # 查詢 User 數量
上面的 filter_by()
函式回傳的物件與 User.query
相同,因此對查詢加上過濾前後可以適用的回傳選項( first()
、first_or_404()
、all()
、count()
…)相同
繼續看下去更新的部分
User.query.filter_by(username="name").update({"password": "new_password"}) # 更新所有 username 為 "name" 的 User 的 password 為 "new_password"
User.query.filter_by(username="name").delete() # 刪除所有 username 為 "name" 的 User
db.session.commit()
可以對查詢到的物件(可複數物件)進行刪除或是更新
更新時透過給予一個字典包含更新的欄位與值進行
最後不要忘記更新或刪除後要進行 commit,不然就白弄了
帳號系統是大多網站中不可或缺的要素之一
而此時便是使用後端結合資料庫進行操作的時候了
其中可以簡易分為使用第三方平台的帳號服務登入(像是 Google、Discord 帳號登入等)
亦或是建立一個自己的帳號系統
我們先來介紹前者
OAuth2 是一種廣泛被使用的授權方式
簡易流程如下:
以 Discord 身分驗證服務為例
客戶端會見到如右圖介面
在實作上我們要先獲取對應的 OAuth2 URL
該部分視使用的服務而有所差異
本處以 Discord 為例
# config.py
# 略
SQLALCHEMY_DATABASE_URI = os.getenv("SQLALCHEMY_DATABASE_URI") # 資料庫連結
API_KEY = os.getenv("API_KEY") # API 金鑰
TOKEN = os.getenv("TOKEN")
CLIENT_ID = os.getenv("CLIENT_ID")
CLIENT_SECRET = os.getenv("CLIENT_SECRET")
REDIRECT_URI = os.getenv("REDIRECT_URI")
OAUTH_URL = "https://discord.com/oauth2/authorize?client_id=" + CLIENT_ID + "&redirect_uri=" + REDIRECT_URI + "&response_type=code&scope=identify+email"
class Config(object):
JSON_AS_ASCII = False # 不要讓他 JSON 預設使用 ASCII,如此一來可以預防中文編碼爛掉的問題
SQLALCHEMY_DATABASE_URI = SQLALCHEMY_DATABASE_URI
SECRET_KEY = os.urandom(12).hex() # 產生一個隨機的 12 位元的 HEX 字串
# main.py
from flask import Flask, jsonify, redirect, request, session
from zenora import APIClient
from config import Config, TOKEN, CLIENT_SECRET, OAUTH_URL, REDIRECT_URI
from models import db, User
app = Flask(__name__)
app.config.from_object(Config)
client = APIClient(TOKEN, client_secret=CLIENT_SECRET, validate_token=False)
db.init_app(app)
@app.before_request
def db_init():
db.create_all()
@app.route("/login")
def login():
return redirect(OAUTH_URL)
@app.route("/oauth/callback")
def callback():
if "code" in request.args:
code = request.args["code"]
access_token = client.oauth.get_access_token(code, REDIRECT_URI).access_token
session["token"] = access_token
session.permanent = True
return redirect("/")
@app.route("/logout")
def logout():
session.clear()
return redirect("/")
@app.route("/")
def home():
if "token" not in session:
return redirect("/login")
bearer_client = APIClient(session.get("token"), bearer=True)
current_user = bearer_client.users.get_current_user()
return jsonify([
current_user.id,
current_user.username,
current_user.email
])
if __name__ == '__main__':
app.run(host="localhost", port=8080, debug=True)
成功的話可以透過進入 /login
、/logout
控制登入登出
並且在根路徑能夠看到自己的使用者資訊
注意使用 Discord 帳號系統時
必須要在 Discord 應用程式管理介面將自己網站的路徑輸入進 Redirects 中
範例中路徑為
http://localhost:8080/oauth/callback
注意使用 Discord 帳號系統時
必須要在 Discord 應用程式管理介面將自己網站的路徑輸入進 Redirects 中
範例中路徑為
http://localhost:8080/oauth/callback
在 Discord 的設定細項由於較便離主題故不多做著墨
如需詳細介紹可參考我之前的簡報
若是希望能在帳號內做更多自己的設定
那麼可以自己創建一個帳號系統
Flask 的輕量化框架本身沒有內建帳號系統
因此我們要來自己設計
在設計帳號系統時為了要加密
因此我們需要安裝 flask-bcrypt
pip install flask-bcrypt
然後在設定帳號系統之前,我們需要在 config 中設定 secret_key
參數,來讓我們的在 session 加密時能透過這個金鑰產生每個人不一樣的數值
# config.py
# 略
class Config(object):
JSON_AS_ASCII = False # 不要讓他 JSON 預設使用 ASCII,如此一來可以預防中文編碼爛掉的問題
SQLALCHEMY_DATABASE_URI = SQLALCHEMY_DATABASE_URI
SECRET_KEY = os.urandom(12).hex() # 產生一個隨機的 12 位元的 HEX 字串
到了這邊可能有人好奇什麼是 session
在網站中有兩個短期儲存使用者資料的媒介
分別是存在伺服器端的 session 與存在客戶端的 cookie
而我們的帳號驗證當然不能讓使用者能從客戶端自行修改
因此選擇存在 session
接著就可以來設定我們的帳號系統了
先設定好裝飾器
在函式執行之前先判斷 session 內的使用者名稱是否有值
若是的話正常執行,否則重新導向至首頁
def login_required(func):
def wrapper(*args, **kwargs):
if not session.get("username"):
return redirect("/")
return func(*args, **kwargs)
return wrapper
當使用者要註冊時透過前端的 JS 傳送請求到 /register
後端會從請求中獲得使用者名稱與密碼
經過幾個檢查過後創建先前在 models.py
建立的 User
物件
之後新增進資料庫就註冊完畢了
@app.route("/register", methods=["POST"])
def register():
username = request.json.get("username")
password = request.json.get("password")
# 若是沒給滿資料直接 return
if not username or not password:
return jsonify({"message": "Missing username or password"}), 400
# 已經註冊過也是 return
if User.query.filter_by(username=username).first():
return jsonify({"message": "User already exists"}), 400
user = User(username, Bcrypt().generate_password_hash(password).decode("utf-8"))
db.session.add(user)
db.session.commit()
return jsonify({"message": "User created"}), 201
另外這邊將密碼透過 Bcrypt 加密過後在儲存
因為密碼不會從資料庫提出,所以可以對他進行 hash
並且這邊通常會在客戶端傳輸時就進行加密避免明碼直接被傳輸
此處僅示範簡易的系統因此沒有特別在前端對資料做處理
@app.route("/register", methods=["POST"])
def register():
username = request.json.get("username")
password = request.json.get("password")
# 若是沒給滿資料直接 return
if not username or not password:
return jsonify({"message": "Missing username or password"}), 400
# 已經註冊過也是 return
if User.query.filter_by(username=username).first():
return jsonify({"message": "User already exists"}), 400
user = User(username, Bcrypt().generate_password_hash(password).decode("utf-8"))
db.session.add(user)
db.session.commit()
return jsonify({"message": "User created"}), 201
登入部分與註冊相似
從資料庫拿到用戶後檢查密碼是否相同
若是相同往 session 放入使用者名稱即可
@app.route("/login", methods=["POST"])
def login():
username = request.json.get("username")
password = request.json.get("password")
if not username or not password:
return jsonify({"message": "Missing username or password"}), 400
user = User.query.filter_by(username=username).first()
if not user or not Bcrypt().check_password_hash(user.password, password):
return jsonify({"message": "Invalid username or password"}), 401
session["username"] = username
return jsonify({"message": "Logged in"}), 200
登出只需要將 session 清空即可
@app.route("/logout", methods=["GET", "POST"])
def logout():
session.clear()
return redirect("/")
首頁會先從資料庫內拿到當前使用者
前端可以讓 Jinja2 判斷 current_user
是否存在
進而改變顯示的內容
@app.route("/")
def home():
current_user = User.query.filter_by(username=session.get("username")).first()
return render_template("index.html", current_user=current_user)
最後測試一個需要登入才能瀏覽的介面
在未登入時會因為 @login_required
重新導向至首頁
只有在登入狀態下才能看到 user page 的字樣
@app.route("/user")
@login_required
def user():
return jsonify({"message": "user page"})
完整程式碼
# main.py
from flask import Flask, jsonify, render_template, redirect, request, session
from flask_bcrypt import Bcrypt
from config import Config
from models import db, User
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
@app.before_request
def db_init():
db.create_all()
def login_required(func):
def wrapper(*args, **kwargs):
if not session.get("username"):
return redirect("/")
return func(*args, **kwargs)
return wrapper
@app.route("/register", methods=["POST"])
def register():
username = request.json.get("username")
password = request.json.get("password")
if not username or not password:
return jsonify({"message": "Missing username or password"}), 400
if User.query.filter_by(username=username).first():
return jsonify({"message": "User already exists"}), 400
user = User(username, Bcrypt().generate_password_hash(password).decode("utf-8"))
db.session.add(user)
db.session.commit()
return jsonify({"message": "User created"}), 201
@app.route("/login", methods=["POST"])
def login():
username = request.json.get("username")
password = request.json.get("password")
if not username or not password:
return jsonify({"message": "Missing username or password"}), 400
user = User.query.filter_by(username=username).first()
if not user or not Bcrypt().check_password_hash(user.password, password):
return jsonify({"message": "Invalid username or password"}), 401
session["username"] = username
return jsonify({"message": "Logged in"}), 200
@app.route("/logout", methods=["GET", "POST"])
def logout():
session.clear()
return redirect("/")
@app.route("/")
def home():
current_user = User.query.filter_by(username=session.get("username")).first()
return render_template("index.html", current_user=current_user)
@app.route("/user")
@login_required
def user():
return jsonify({"message": "user page"})
if __name__ == '__main__':
app.run(host="localhost", port=8080, debug=True)
而前端只需要在使用者觸發登入登出事件時
向對應的 URL 發送請求即可
以下為一個簡單的範例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login System</title>
</head>
<body>
<h1>Login System</h1>
{% if current_user %}
<p>Welcome, {{ current_user.username }}!</p>
<form action="/logout" method="post">
<button type="submit">Logout</button>
</form>
<a href="/user">User</a>
{% else %}
<h2>Register</h2>
<form id="registerForm">
<label for="registerUsername">Username:</label>
<input type="text" id="registerUsername" name="username" required>
<label for="registerPassword">Password:</label>
<input type="password" id="registerPassword" name="password" required>
<button type="submit">Register</button>
</form>
<h2>Login</h2>
<form id="loginForm">
<label for="loginUsername">Username:</label>
<input type="text" id="loginUsername" name="username" required>
<label for="loginPassword">Password:</label>
<input type="password" id="loginPassword" name="password" required>
<button type="submit">Login</button>
</form>
{% endif %}
<script>
document.getElementById('registerForm').addEventListener('submit', async (event) => {
event.preventDefault();
const username = document.getElementById('registerUsername').value;
const password = document.getElementById('registerPassword').value;
const response = await fetch('/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const result = await response.json();
alert(result.message);
});
document.getElementById('loginForm').addEventListener('submit', async (event) => {
event.preventDefault();
const username = document.getElementById('loginUsername').value;
const password = document.getElementById('loginPassword').value;
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const result = await response.json();
alert(result.message);
if (response.ok) {
window.location.reload();
}
});
</script>
</body>
</html>
在前面的章節中
我們獲取前端訊息都是依靠 render_template()
時跟著傳入
但經常會有訊息隨時間更新的情況
前面的狀態下無法在前端需要獲得新訊息的時候順利得到
這種時候我們會使用一種方式成為 Ajax
我們會先設定一個 URL 做為 API 的連結
在前端對此發出訪問時回傳 JSON 資料
from datetime import datetime
# 略
timedata = []
@app.route("/api/data")
def get_data():
timedata.append(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
return jsonify(timedata)
以此函式為例
他會在每次被訪問時往列表新增一個當前時間
from datetime import datetime
# 略
timedata = []
@app.route("/api/data")
def get_data():
timedata.append(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
return jsonify(timedata)
接著我們要來寫前端
這邊使用 jQuery 內建的 ajax 功能來處理
在 Flask 架構寫 CSS、JS 時
我們通常會創建一個資料夾成為 static
做為靜態資源的儲存
在裡面再創建一個 index.js
,此時資料夾結構如下
之後在 HTML 檔案中加入以下內容
<script src="{{ url_for('static', filename='index.js') }}"></script>
之後在 HTML 檔案中加入以下內容
<script src="{{ url_for('static', filename='index.js') }}"></script>
然後因為我們要使用 jQuery,因此要再加上一條
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
(版本可以自行更改)
之後就可以開始寫 JS 了
$(function() {
$.ajax({
url: '/api/data',
success: function(data) {
console.log('data:', data);
$('#content').text(data);
}
})
})
之後就可以開始寫 JS 了
$(function() {
$.ajax({
url: '/api/data',
success: function(data) {
console.log('data:', data);
$('#content').text(data);
}
})
})
如此一來便會在頁面加載完成時對 API 發出請求獲得 data
之後將 HTML 內 id 為 content 的標籤內容設定為資料內容
完整 HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Test</title>
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script src="{{ url_for('static', filename='index.js') }}"></script>
</head>
<body>
<div id="content"></div>
</body>
</html>
完整 HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Test</title>
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
<script src="{{ url_for('static', filename='index.js') }}"></script>
</head>
<body>
<div id="content"></div>
</body>
</html>
成功的話會發現每次進入到有回傳這個 HTML 的路徑時
裡面紀錄的時間戳記就會增加
上一章在使用 jQuery 時是在 HTML 檔案中透過連結直接載入
而需要載入的模組變多或是需要使用前端框架時
我們通常會使用 npm 來進行管理
Flask 框架本身是沒有內建 npm 相關的串接功能
但是兩者在使用上沒有衝突
因此可以將兩者整合在我們的專案中
首先我們要先安裝 node.js 以獲取 npm ,進入 node.js 官網安裝,或是 linux 系統且有 apt 的話可以使用以下指令安裝
apt-get install nodejs
安裝完畢後可以透過檢視版本確定是否安裝成功
node -v
npm -v
安裝完畢後我們就可以開始使用了
首先讓終端機進入專案的資料夾路徑
之後輸入以下指令初始化我們的環境
npm init
成功的話會生成一個檔案名為 package.json
裡面可以設定你的專案資訊
我們將裡面的資訊設定好後就可以開始裝需要用到的套件了
npm i -D react bootstrap jquery typescript webpack webpack-cli
npm i -D react bootstrap jquery typescript webpack webpack-cli
完成後會看到他幫你生成了一個 package-lock.json
這個文件是後面 build 的時候讓他知道要載那些東西的
我們不用動他
"scripts": {
"build": "webpack",
"watch": "webpack --watch"
},
之後我們要讓每次 build 時執行 Webpack
因此往 package.json
輸入以下內容
(注意不要覆蓋掉原本的其他預設內容)
const path = require('path');
module.exports = {
entry: './static/index.js',
output: {
filename: 'index.bundle.js',
path: path.resolve(__dirname, './static/dist')
}
};
這個時候我們建立一個檔案命名為 webpack.config.js
這是 Webpack 在每次 build 時轉換出 JS 的設定檔
我們寫入以下內容
<script src="{{ url_for('static', filename='dist/index.bundle.js') }}"></script>
如此一來
每次 build 的時候就會把 index.js
轉換成 index.bundle.js
並存放在 static/dist
中
HTML 的部分只需要在模板寫入
# main.py
# 略
if __name__ == "__main__":
os.system(f"cd {os.path.abspath(os.path.dirname(__file__))} && npm run build")
app.run(host="localhost", port=8080, debug=True)
接著往 main.py
寫入以下內容
import $ from 'jquery';
import react from 'react';
...
然後我們在寫 JS 時就可以直接匯入想要使用的插件了
require('./main');
require('./bg-anime');
require('./word_test');
require('./sentence_test');
require('./library');
最後在 index.js
入口檔案中將使用到的 JS 檔案全部寫入
他可能會長成這個樣子
實務上根據你創建的檔案而有所不同