所有例子代碼均來自于Flask的 7fca843b5f 版本
為了學(xué)習(xí)flask框架昆婿,我決定開始學(xué)習(xí)flask在GitHub上給出的官方example來熟悉flask的使用方法默色,在此版本中包含blueprintexample逊笆,flaskr泪勒,jqueryexample,minitwit這四個(gè)例子起暮,今天分析的是minitwit這個(gè)例子谒臼。
minitwit是什么?
顧名思義酬屉,minitwit就是tweet的一個(gè)mini示例版本半等。簡單地說,這是一個(gè)實(shí)現(xiàn)了注冊(cè)呐萨,登陸杀饵,發(fā)推,推文顯示谬擦,關(guān)注切距,取關(guān)等功能的flask示例。應(yīng)該說惨远,這是flask官方示例當(dāng)中python代碼量最多谜悟,最有實(shí)際意義的一個(gè)demo。
開始分析
話不多說北秽,首先來看一下文件目錄結(jié)構(gòu):
minitwit/
minitwit/
static/
style.css
templates/
layout.html
login.html
regester.html
timeline.html
__init__.py
minitwit.py
schema.sql
tests/
test_minitwit.py
.gitignore
MANIFEST.in
README
setup.cfg
setup.py
為了保持精力去關(guān)注核心部分葡幸,這里只去關(guān)注minitwit文件下的部分,而不去關(guān)注test部分(這并不意味著test本身沒有價(jià)值羡儿,恰恰相反礼患,TDD是一種非常有效,并且非常常用的開發(fā)組織流程,而本例使用的pytest也幾乎可以說是目前最好的python通用測(cè)試框架)缅叠,和setup安裝部分悄泥。因?yàn)樗鼈兒秃诵拇a部分的義務(wù)并沒有太大的關(guān)聯(lián)。
import部分
首先來看minitwit.py文件部分:
import time
from sqlite3 import dbapi2 as sqlite3
from hashlib import md5
from datetime import datetime
from flask import Flask, request, session, url_for, redirect, render_template, abort, g, flash, _app_ctx_stack
from werkzeug import check_password_hash, generate_password_hash
這是這個(gè)項(xiàng)目引入部分肤粱。
簡單看一下弹囚,有時(shí)間處理相關(guān)庫time和datetime,數(shù)據(jù)庫的處理庫sqlite3(常用的還有mysqlDB领曼,或者使用一種ORM來簡化相關(guān)的SQL操作)鸥鹉,hash處理庫hashlib(一般用來加密)還有flask庫里的一些api,最后庶骄,從werkzeug引入密碼的生成和檢查相關(guān)api毁渗。
配置和初始化
接下來看一下配置項(xiàng)的部分:
DATABASE = '/tmp/minitwit.db'
PER_PAGE = 30
DEBUG = True
SECRET_KEY = 'development key'
這里是配置部分,主要定義了數(shù)據(jù)庫的url連接位置单刁,推文分頁每頁的推文條數(shù)灸异,打開debug選項(xiàng)以輸出必要的調(diào)試信息(一般用于開發(fā)階段,實(shí)際生產(chǎn)環(huán)境中debug應(yīng)該處于關(guān)閉狀態(tài))羔飞,最后設(shè)置了一個(gè)秘鑰(在后面會(huì)用到)肺樟。
之后是初始化的部分:
app =Flask(__name__)
app.config.from_object(__name__)
app.config.from_envvar('MINITWIT_SETTINGS', silent=True)
這里顯示實(shí)例化一個(gè)Flask類,綁定到app上逻淌,之后從環(huán)境變量和name中引入相應(yīng)的配置項(xiàng)么伯。(這也是一般的做法:將配置信息與業(yè)務(wù)處理的代碼分開,這樣更有益于保護(hù)配置信息不被泄露卡儒,同時(shí)田柔,也更符合模塊化的需求)。
數(shù)據(jù)庫部分和一些輔助方法
接下來先看數(shù)據(jù)庫的部分:
def get_db():
top = _app_ctx_stack.top
if not hasattr(top, 'sqlite_db'):
top.sqlite_db = sqlite3.connect(app.config['DATABASE'])
top.sqlite_db.row_factory = sqlite3.Row
return top.sqlite_db
@app.teardown_appcontext
def close_database(exception):
top = _app_ctx_stack.top
if hasattr(top, 'sqlite_db'):
top.sqlite_db.close()
def init_db():
db = get_db()
with app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
@app.cli.command('initdb')
def initdb_command():
init_db()
print('Initialized the database.')
def query_db(query, args=(), one=False):
cur = get_db().execute(query, args)
rv = cur.fetchall()
return (rv[0] if rv else None) if one else rv
get_db和close_database分別定義了數(shù)據(jù)庫的鏈接操作和在app上下文結(jié)束的時(shí)候(即應(yīng)用關(guān)閉的時(shí)候)執(zhí)行數(shù)據(jù)庫的關(guān)閉操作骨望。
之后是init_db凯楔,引入schema.sql中的sql語句,進(jìn)行了初步的數(shù)據(jù)庫建表工作锦募,這里用到了user,follower,message三個(gè)表。隨后為了方便調(diào)用邻遏,將initdb引入命令行當(dāng)中糠亩。
最后,定義了數(shù)據(jù)庫查詢的操作query_db准验。
接下來赎线,看一下都有哪些輔助方法:
def get_user_id(username):
rv = query_db('select user_id from user where username = ?',[username], one=True)
return rv[0] if rv else None
def format_datetime(timestamp):
return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M')
def gravatar_url(email, size=80):
return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' %(md5(email.strip().lower().encode('utf-8')).hexdigest(), size)
@app.before_request
def before_request():
g.user = None
if 'user_id' in session:
g.user = query_db('select * from user where user_id = ?',[session['user_id']], one=True)
get_user_id定義了獲取用戶id的方法,根據(jù)用戶的username向數(shù)據(jù)庫里進(jìn)行查詢糊饱。format_datetime定義了方法垂寥,將時(shí)間戳轉(zhuǎn)化成可讀的時(shí)間表示格式。
gravatar_url是根據(jù)用戶的email,生成一個(gè)在Gravatar上對(duì)應(yīng)的頭像鏈接滞项,這里同時(shí)指定了頭像大小是80*80個(gè)像素狭归。
最后,before_request在request之前先清空全局變量g中的user文判,再向session中查詢是否有user_id过椎,如果有的話則返回相應(yīng)的用戶信息。這么做的原因是提供了cookie登陸的方法戏仓,不必要求每次都手動(dòng)輸入信息登陸疚宇。
正文部分
接下來看一下路由定義和處理的部分:
@app.route('/')
def timeline():
if not g.user:
return redirect(url_for('public_timeline'))
return render_template('timeline.html', messages=query_db(''' select message.*, user.* from message, user where message.author_id = user.user_id and (user.user_id = ? or user.user_id in (select whom_id from follower where who_id = ?)) order by message.pub_date desc limit ?''', [session['user_id'], session['user_id'], PER_PAGE]))
這里定義了 '/' 根路由的處理部分。如果用戶之前沒有進(jìn)行登錄操作赏殃,而且本地也沒有可用的cookie敷待,則將其重定向到公共的時(shí)間線頁面(可以按微博現(xiàn)在的邏輯理解:如果沒有登錄而去訪問微博首頁,顯示的是最新的熱門微博)仁热。如果用戶執(zhí)行了登錄操作或者本地有有效cookie榜揖,則渲染為用戶定制的時(shí)間線頁面(按時(shí)間逆序顯示用戶本人和用戶關(guān)注人所發(fā)送的推文,這點(diǎn)和微博邏輯也基本一樣:當(dāng)你登陸之后股耽,跳轉(zhuǎn)到的頁面只會(huì)顯示你關(guān)注人所發(fā)的推文根盒,默認(rèn)情況下,還會(huì)顯示本人發(fā)送的推文)物蝙。需要注意的是炎滞,這里使用了一個(gè)之前定義過的配置常量(PER_PAGE)。
之后來看一下公共的內(nèi)容定義:
@app.route('/public')
def public_timeline():
return render_template('timeline.html', messages=query_db('''select message.*, user.* from message, user where message.author_id = user.user_id order by message.pub_date desc limit ?''', [PER_PAGE]))
這里顯示的是為未登錄用戶(一般叫匿名用戶)定制的首頁诬乞,顯示最近來自所有用戶的推文册赛。
除了推文的顯示之外,應(yīng)該會(huì)需要一個(gè)用戶的個(gè)人頁面震嫉,或者說是個(gè)人資料頁面:
@app.route('/<username>')
def user_timeline(username):
profile_user = query_db('select * from user where username = ?', [username], one=True)
if profile_user is None:
abort(404)
followed = False
if g.user:
followed = query_db('''select 1 from follower where follower.who_id = ? and follower.whom_id = ?''', [session['user_id'], profile_user['user_id']], one=True) is not None
return render_template('timeline.html', messages=query_db('''select message.*, user.* from message, user where user.user_id = message.author_id and user.user_id = ? order by message.pub_date desc limit ?''', [profile_user['user_id'], PER_PAGE]), followed=followed, profile_user=profile_user)
這里用一個(gè)username部分作為動(dòng)態(tài)路由森瘪,這里的路由部分還可以這樣寫: /<string:username> 。
頁面部分的內(nèi)容用于顯示用戶關(guān)注者的推文票堵,如果這個(gè)username并不存在的話扼睬,則拋出404異常。
接下來是用戶的關(guān)注列表頁:
@app.route('/<username>/follow')
def follow_user(username):
if not g.user:
abort(401)
whom_id = get_user_id(username)
if whom_id is None:
abort(404)
db = get_db()
db.execute('insert into follower (who_id, whom_id) values (?, ?)', [session['user_id'], whom_id])
db.commit()
flash('You are now following "%s"' % username)
return redirect(url_for('user_timeline', username=username))
頁面主要是提供了用戶關(guān)注人的添加方法悴势,如果沒有用戶窗宇,或者用戶名不合法,則返回401和404異常特纤。
既然有了用戶關(guān)注人的添加方法军俊,肯定就會(huì)有用戶關(guān)注人的取消方法(即關(guān)注與取關(guān)的邏輯關(guān)系):
@app.route('/<username>/unfollow')
def unfollow_user(username):
if not g.user:
abort(401)
whom_id = get_user_id(username)
if whom_id is None:
abort(404)
db = get_db()
db.execute('delete from follower where who_id=? and whom_id=?', [session['user_id'], whom_id])
db.commit()
flash('You are no longer following "%s"' % username)
return redirect(url_for('user_timeline', username=username))
類似關(guān)注的邏輯處理方式,這里提供了取消關(guān)注的方法捧存,并對(duì)不合法的用戶返回401和404的錯(cuò)誤信息粪躬。
其實(shí)關(guān)注和取關(guān)的操作本質(zhì)上就是對(duì)于數(shù)據(jù)庫中follower表的插入和刪除操作担败。
到此,用戶之間的關(guān)系已經(jīng)大致清楚镰官,接下來需要處理推文的部分提前。
首先是發(fā)送推文:
@app.route('/add_message', methods=['POST'])
def add_message():
if 'user_id' not in session:
abort(401)
if request.form['text']:
db = get_db()
db.execute('''insert into message (author_id, text, pub_date) values (?, ?, ?)''', (session['user_id'], request.form['text'], int(time.time())))
db.commit()
flash('Your message was recorded')
return redirect(url_for('timeline'))
首先去校驗(yàn)用戶是否已經(jīng)合法登陸,只有登錄過之后的用戶才有發(fā)送推文的權(quán)限朋魔,否則拋出401異常岖研。接下來,從表單中獲取text內(nèi)容警检,將這部分內(nèi)容插入到message表中孙援,之后重定向到時(shí)間線頁面,來顯示添加過推文信息后的新頁面扇雕。
之后定義的是用戶登錄相關(guān)的一系列操作拓售。
首先是登錄:
@app.route('/login', methods=['GET', 'POST'])
def login():
if g.user:
return redirect(url_for('timeline'))
error = None
if request.method == 'POST':
user = query_db('''select * from user where username = ?''', [request.form['username']], one=True)
if user is None:
error = 'Invalid username'
elif not check_password_hash(user['pw_hash'], request.form['password']):
error = 'Invalid password'
else:
flash('You were logged in')
session['user_id'] = user['user_id']
return redirect(url_for('timeline'))
return render_template('login.html', error=error)
如果用戶已經(jīng)登陸過,則直接將頁面重定向到時(shí)間線頁面镶奉,如果頁面的請(qǐng)求是POST方法础淤,則向user表根據(jù)username和password查詢user的信息,如果username和password正確且對(duì)應(yīng)哨苛,則將用戶登入鸽凶,并重定向到時(shí)間線頁面;如果頁面的請(qǐng)求是GET方法建峭,則顯示登陸頁面和錯(cuò)誤信息玻侥。(GET和POST方法的區(qū)別)
之后是注冊(cè)頁面:
@app.route('/register', methods=['GET', 'POST'])
def register():
if g.user:
return redirect(url_for('timeline'))
error = None
if request.method == 'POST':
if not request.form['username']:
error = 'You have to enter a username'
elif not request.form['email'] or '@' not in request.form['email']:
error = 'You have to enter a valid email address'
elif not request.form['password']:
error = 'You have to enter a password'
elif request.form['password'] != request.form['password2']:
error = 'The two passwords do not match'
elif get_user_id(request.form['username']) is not None:
error = 'The username is already taken'
else:
db = get_db()
db.execute('''insert into user (username, email, pw_hash) values (?, ?, ?)''', [request.form['username'], request.form['email'], generate_password_hash(request.form['password'])])
db.commit()
flash('You were successfully registered and can login now')
return redirect(url_for('login'))
return render_template('register.html', error=error)
這里的注冊(cè)頁面類似于此前的登陸頁面,如果用戶已經(jīng)合法登陸亿蒸,則直接重定向到時(shí)間線頁面凑兰,否則根據(jù)用戶信息,進(jìn)行校驗(yàn)之后將新用戶的信息插入user表之中(POST方法)边锁。當(dāng)收到GET請(qǐng)求時(shí)姑食,將注冊(cè)頁面和錯(cuò)誤信息一同展示出來。
最后是登出部分:
@app.route('/logout')
def logout():
flash('You were logged out')
session.pop('user_id', None)
return redirect(url_for('public_timeline'))
登出部分十分簡單:從session查詢對(duì)應(yīng)的用戶信息茅坛,并將該用戶的相關(guān)信息從session中彈出音半,之后彈出提示信息,并重定向到公共時(shí)間線頁面贡蓖。(其實(shí)這里應(yīng)該提前校驗(yàn)全局變量g中是否有用戶信息祟剔,即用戶是否登錄過,如果用戶沒有登錄而直接進(jìn)行登出操作摩梧,有可能session部分的操作會(huì)拋出異常)
最后是一個(gè)模板的相關(guān)操作:
app.jinja_env.filters['datetimeformat'] = format_datetime
app.jinja_env.filters['gravatar'] = gravatar_url
總結(jié)
我認(rèn)為這個(gè)demo主要涉及到的要點(diǎn)和可以改進(jìn)的有以下幾點(diǎn):
- 用戶的登錄、注冊(cè)宣旱、登出操作
- 已登錄用戶的信息管理(session)
- 可以嘗試使用藍(lán)圖來更好的組織項(xiàng)目
- 數(shù)據(jù)庫之間的關(guān)聯(lián)操作仅父,和數(shù)據(jù)庫的組織方法
- 同個(gè)路由針對(duì)不同權(quán)限用戶(本例中是登錄用戶和匿名用戶)顯示不同的頁面內(nèi)容
- 同個(gè)路由針對(duì)不同的請(qǐng)求方法(本例中是GET和POST)進(jìn)行不同的處理方法
- 關(guān)于Flask的登錄部分,Flask Login是一個(gè)方便可靠的擴(kuò)展庫
- 關(guān)于數(shù)據(jù)庫的操作部分,一個(gè)好的ORM可以大大簡化SQL操作笙纤,F(xiàn)lask里常用到的ORM是SQLAlchemy耗溜。