前言
2023-8-11
以前對網站開發萌生了想法,又有些急于求成,在B站照著視頻敲了一個基于flask的博客系統。但對于程序的代碼難免有些囫圇吞棗,存在許多模糊或不太理解的地方,只會照葫蘆畫瓢。
而當自己想開發一個什么網站的時,就如同摸著石頭過河,常在許多小問題上卡住,不知怎么實現,也不知道需要去學習什么才能實現。例如,要做一個聊天室程序,我卻不知道在一方發出消息時,如何在另一方實時地顯示出來,思慮許久而終歸于放棄。
學習系統且詳細的知識有其好處,它可以沖退我那種徒手在黑暗中摸索的感覺。讀了“狼書”的兩個章節,自覺很有收獲,此前的許多疑惑也得到了解答。然凡事各有弊益,啃書不是易事,還需下苦功夫。
曾有人對我說,看視頻會更有效率。也許他是對的?但我好像更喜歡書籍給我的感覺。但有些諷刺的是,大學至今,我卻也沒看下來多少書,而時間在我休息的時候,它仍努力奔跑,兩載一晃而過。而不少人,也是這相似的困境吧?
回到本文,它是一篇讀書筆記,參雜少許個人想法但含量不高,所記零散,主要是作為個人提綱備忘,或許對諸位而言閱讀價值不高。若想學習 Flask 框架,我還是很推薦去讀“狼書”原著的。
文章目錄
- 前言
- 開始
- Git使用
- 1 初識Flask
- 1.1 搭建開發環境
- 1.2 簡單示例
- 2 Flask與HTTP
- 2.1 重定向回上一個頁面
- 2.2 使用AJAX技術發送異步請求
- 2.3 HTTP服務器推送
- 2.4 Web安全防范
開始
5個難度遞增的案例:留言板SayHello,個人博客Bluelog,圖片社交網站Albumy,待辦事項程序Todoism,聊天室CatChat。
前端學習:
Web很多程序離不開javascript,它可以方便、簡潔地實現很多頁面按邏輯和功能。
了解Git:https://try.github.io/
文本編輯器:
作者的博客:http://greyli.com
Git使用
克隆本書代碼倉庫:
git clone https://github.com/greyli/helloflask.git
查看當前項目倉庫中包含的所有標簽:
git tag -n
簽出對應標簽版本的代碼:
git checkout foo
簽出前對文件做了修改,需要撤銷:
git reset --hard
使用diff命令比較兩個標簽對應版本之間的變化:
git diff foo bar
使用git客戶端直觀查看版本變化:
gitk
定期使用git fetch命令來更新本地倉庫:
git fetch --all
git fetch --tags
git reset --hard origin/master
在本地復制新的派生倉庫,后可以在本地自由修改其中的代碼:
git clone https://github.com/你的用戶名/helloflask.git
1 初識Flask
Web框架可以讓我們不用關心底層的請求響應處理,更方便地編寫Web程序。
兩個主要依賴:(p3)
- WSGI(Web Server Gateway Interface,Web服務器網關接口)的工具集——Werkzeug(http://werkzeug.pocoo.org/)
- Jinja2模板引擎
1.1 搭建開發環境
Pipenv:pip的加強版,讓包安裝、包依賴管理、虛擬環境管理更加方便。
創建虛擬環境:在項目根目錄(即helloflask文件夾中),使用pipenv install
命令。Pipfile
文件列出的依賴包也會一并被安裝。
顯示激活虛擬環境:Pipenv會自動從項目目錄下的.env
文件中加載環境變量。
# 激活
pipenv shell
# 退出
exit
臨時使用虛擬環境中的python解釋器:(更推薦)
pipenv run python hello.py
查看當前環境下的依賴情況:
pipenv graph
關于Pipfile項目的更多情況,請訪問其主頁。
使用
pipenv install
命令安裝包時,都是安裝到虛擬環境中。相當于使用pip在激活虛擬環境的情況下安裝包。
更新flask版本:(書中為flask-1.0.2)
pipenv update flask
集成開發環境:pycharm專業版提供了更多針對Flask開發的功能,比如創建Flask項目模板,Jinja2語法高亮,與Flask命令行功能集成等。
設置Python解釋器(好像已經不需要了):因為PyCharm未集成支持Pipenv。(p10)
1.2 簡單示例
from flask import Flask
app = Flask(__name__)@app.route('/')
def index():return '<h1>Hello Flask!</h1>'
Flask類表示一個Flask程序,實例化這個類就得到了我們的程序實例app。__name__
對于文件app.py而言,值即為“app”,它幫助Flask在相應的文件夾里找到需要的資源,比如模板和靜態文件。(p12)
注冊路由:路由負責管理URL和函數之間的映射。
- 一個視圖函數可以綁定多個路由。
- 動態路由:可以傳遞參數,并可設置默認參數。
注:Flask內置一個開發服務器,但在實際生產環境中需要使用性能更好的生產服務器。
啟動開發服務器:
# 未啟動虛擬環境下使用
pipenv run flask run
# 在虛擬環境中
flask run
# 命令未找到
python -m flask run
app.run()
方法也可啟動開發服務器,已經不推薦使用。
flask尋找程序實例:
- 在當前目錄下,名為
app.py
或wsgi.py
的文件中尋找名為app
的程序實例。 - 根據環境變量FLASK_APP尋找。
# 在linux
$ export FLASK_APP=hello
# 在windows
> set FLASK_APP=hello
管理環境變量:使用python-dotenv
包,從.env
或.flaskenv
文件加載。其中.env
文件存放一些敏感數據。
pipenv install python-dotenv
可在.flaskenv
寫入:
# 默認為production(生產環境),開發模式將打開調試器和重載器。
FLASK_ENV=development
使用Pycharm的運行配置(而不使用命令行):在Run --> Edit Configurations(p19)
使服務器外部可見:讓局域網用戶可以通過你的內網IP進行訪問。想要公網訪問,可以考慮內網穿透工具、端口轉發工具等,如 ngrok、Localtunnel。
flask run --host=0.0.0.0
flask的環境變量:可通過FLASK_<COMMAND>_<OPTION>
設置各種選項。
重載器:安裝 Watchdog。Werkzeug內置有stat重載器,但耗電嚴重且準確性一般。
# dev: 開發依賴的包
pipenv install watchdog --dev
打開PythonShell:使用flask打開的shell自動包含程序上下文,并且已經導入了app實例。
flask shell
Flask擴展:使用Flask提供的接口編寫的Python庫。擴展可以加速開發,但也會降低靈活性,并可能存在bug。
Flask項目配置:可能用到Flask提供的配置、擴展提供的、程序特定的配置。它們用Flask對象的app.config屬性作為統一的接口。
- Flask配置章節:https://flask.pocoo.org/docs/latest/config/
app.config['ADMIN_NAME'] = 'Peter'
# 一次加載多個值
app.config.update()方法
URL:使用url_for()
,方便url規則的修改。
- 相對url與絕對url(p24)
自定義Flask命令:
- Click官方文檔(自定義命令):http://click.pocoo.org/6/
@app.cli.command()
def hello():click.echo('Hello, Human!')
> flask hello
Hello, Human!
視圖函數之名:可以溯源至MVC架構,即”模型 - 視圖 - 控制器”。但flask并不是MVC架構的框架,因為沒有內置數據模型的功能(需使用擴展),視圖函數成為控制器函數才更加合適。(p28)
2 Flask與HTTP
request對象常用的屬性和方法:(p43)
Response類常用屬性和方法:(p48)
查看路由列表:這個列表由app.url_map
解析得到。其中static為Flask添加的特殊路由,用來訪問靜態文件。
> flask routes
Flask內置的URL變量轉換器:(p37)
URL規則中的轉換器:<轉換器:變量名>
,
@app.route('goback/<int:year>')
def go_back(year):return '<p>Welcome to %d!</p>' % (2018 - year)
請求鉤子:也稱回調函數,可以用來注冊在請求處理的不同階段執行的處理函數,如預處理、后處理,它們使用裝飾器 實現。(p58)
響應:大多數情況下,我們只負責返回響應的主體內容(而不負責首部及各種字段)。Flask會調用make_response()
方法將視圖函數返回值轉換為響應對象。當然,響應也可以包含響應主體、狀態碼、首部字段 三個部分內容。
可使用redirect(<url字符串>)
方法重定向。
@app.route('/')
def hello_flask():return '', 302, {'Location':'https://www.baidu.com'}
注:狀態碼不可兒戲,如將上面的
302
改為202
,則重定向會失效。
錯誤響應:在視圖函數中使用abort(<狀態碼>)
,例如:
@app.route('/404')
def not_found():abort(404)
響應格式:在 HTTP 響應中,數據可以通過多種格式傳輸,默認為 HTML。可以設置不同的 MIME 類型來標識不同的數據格式,MIME 類型在 Content-Type 字段中定義。
# method 1 - 修改響應對象的屬性
# @plain 純文本
from flask import make_response
...
response = make_response("hello")
response.mimetype = 'text/plain'
# method 2 - 設置首部字段
response.headers['Content-Type'] = 'text/html; charset=utf-8'
-
XML:
application/xml
,一般作為 AJAX 請求的響應格式,或是 Web API 的響應格式。 -
JSON:
application/json
,指 JavaScript Object Notation(JavaScript對象表示法),更輕量、易解析。json模塊的dumps()方法,可以將python中的字典、列表、元組數據序列化為json字符串。
# 1 - python標準庫的json模塊
response = make_response(json.dumps(data))
response.mimetype = 'application/json'
return response
# 2 - 使用flask包裝的jsonify()函數
return jsonify(data)
Cookie:HTTP 是無狀態協議。Cookie是保存在瀏覽器上的小型文本數據,保存一定時間,在下一次向同一個服務器發送請求時附帶這些數據。但明文存儲存在安全隱患。
使用set_cookie()
方法設置(參數見p68),從cookies
屬性獲取。
Session:在Flask中,session對象用來存儲加密的cookie。
- 設置程序密鑰:通過
Flask.secret_key
屬性;或環境變量SECRET_KEY
(可保存在.env
文件),在腳本中通過getenv()
方法獲取。
import os
app.secret_key = os.getenv('SECRET_KEY', 'secret string')
疑問:寫進了環境變量還需再腳本中手動獲取?那我隨便用個環境變量名稱是不是也可以?
疑問:看不懂:使用session對象存儲的Cookie,用戶可以看到其加密后的值,但無法修改它。因為session中的內容使用密鑰進行簽名,一旦數據被修改,簽名的值也會變化。這樣再讀取時,就會驗證失敗,對應的session值也會失效。 (p51)
- session cookie的保存時間:
上下文:Flask中有兩種上下文:程序上下文 和請求上下文 。
兩種上下文在視圖函數中都會自動激活,這也意味折一些依賴于上下文的函數只能在視圖函數中使用,如url_for()
、jsonify()
等。
也可手動激活程序上下文:
>>> from app import app
>>> from flask import current_app# 方法1
>>> with app.app_context():... current_app.name# 方法2
>>> app_ctx = app.app_context()
>>> app_ctx.push()
>>> current_app.name
>>> app_ctx.pop()# 激活請求上下文類似
>>> from app import app
>>> from flask import request
>>> with app.test_request_context('/hello'):...
疑惑:g、request等對象如何區分不同的客戶端?
上下文鉤子:使用它注冊的回調函數會在程序上下文被銷毀時調用。
@app.teardown_appcontext
def teardown_db(exception):...db.close()
2.1 重定向回上一個頁面
利用referrer或URL的查詢參數。(p59)
referrer:即訪問來源。當用戶在某個站點單擊鏈接,瀏覽器向新鏈接所在的服務器發起請求,請求的數據中包含的HTTP_REFERER字段記錄了用戶所在的原站點URL。
疑惑:書中判斷url是否安全的代碼(如下)使我困惑了許久:既然
test_url
中也與request.host_url
做了拼接,那最后的netloc
不是必然相同嗎?后來我查找了
urljoin(base, url)
函數的處理機制:
- 如果
url
是一個相對URL,那么urljoin
會從url
中獲取路徑部分,并于base
中獲取的部分合并;- 如果
url
是一個絕對URL,則urljoin
會直接返回url
。那么在什么情況下,
is_safe_url
函數的返回值才為False
呢?
- 首先,
target
是一個絕對URL。- 同時,該絕對URL的協議或主機不是本機。
綜上,還是感覺該函數的邏輯寫得有些隱晦了,不便于理解(肯定不能是我太笨)。
def is_safe_url(target):ref_url = urlparse(request.host_url)test_url = urlparse(urljoin(request.host_url, target))return test_url.scheme in ('http', 'https') and \ref_url.netloc == test_url.netloc
2.2 使用AJAX技術發送異步請求
jQuery中和AJAX相關的方法和具體用法:http://api.jquery.com/category/ajax/
前言
在傳統的Web應用中,程序的操作都是基于請求響應循環來實現的。每當頁面狀態需要變動,或是需要更新數據時,都伴隨折一個發向服務器的請求。當服務器響應時,整個頁面會重載,并渲染新頁面。
頻繁更新頁面會犧牲性能,且影響用戶體驗。
AJAX是指異步Javascript和XML(Asynchronous JavaScript And XML),是一系列技術的組合體,如XMLHttpRequest、JavaScript、DOM。它讓Web程序更像是程序,而非一堆用鏈接和按鈕鏈接起來的網頁資源。
可以使用 jQuery 實現AJAX操作:函數ajax()
可以發送AJAX請求。
2.3 HTTP服務器推送
推送技術對比:https://stackoverflow.com/a/12855533/5511489
- 傳統輪詢
- 長輪詢
- SSE(Server-Sent Events)
- Websocket
2.4 Web安全防范
OWASP(Open Web Application Security Project,開放式Web程序安全項目):https://www.owasp.org 。(p66)
常見攻擊方式:
- 注入攻擊
- XSS攻擊(Cross-Site Scripting,跨站腳本):將代碼注入被攻擊者的網站
- CSRF攻擊:(Cross Site Request Forgery,跨站請求偽造):偽造用戶的登陸狀態。
提示:雖然在實際開發中,通過在”刪除“按鈕中加入鏈接來刪除資源非常方便,但安全問題應該作為編寫代碼時的第一考量,應該將這些按鈕內嵌在使用了POST方法的form元素中。攻擊者就無法通過GET請求來修改用戶的數據。
疑惑:未理解csrf攻擊的防御原理。