[Flask] 02 - Build a New Project.
一、路由配置
Ref: https://read.helloflask.com/c1-ready
Ref: https://read.helloflask.com/c2-hello
路由页面:
# -*- coding: utf-8 -*- from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return 'Welcome to My Watchlist!
运行时,记得指定入口:
$ export FLASK_APP=hello.py $ flask run Running on http://127.0.0.1:5000/
管理环境变量,goto : https://read.helloflask.com/c2-hello
使用模板写HTML页面内容。
在社交网站上,每个人都有一个主页,借助 Jinja2 就可以写出一个通用的模板:
Jinja2 的语法和 Python 大致相同,你在后面会陆续接触到一些常见的用法。在模板里,你需要添加特定的定界符将 Jinja2 语句和变量标记出来,下面是三种常用的定界符:
1. {{ ... }} 用来标记变量。
2. {% ... %} 用来标记语句,比如 if 语句,for 语句等。
3. {# ... #} 用来写注释。
模板中使用的变量需要在渲染的时候传递进去。
二、前后端分离
原生地调用前端的方法。但未来还是推荐vue.js的策略。
# -*- coding: utf-8 -*- from flask import Flask, render_template app = Flask(__name__) name = 'Grey Li' movies = [ {'title': 'My Neighbor Totoro', 'year': '1988'}, {'title': 'Dead Poets Society', 'year': '1989'}, {'title': 'A Perfect World', 'year': '1993'}, {'title': 'Leon', 'year': '1994'}, {'title': 'Mahjong', 'year': '1996'}, {'title': 'Swallowtail Butterfly', 'year': '1996'}, {'title': 'King of Comedy', 'year': '1999'}, {'title': 'Devils on the Doorstep', 'year': '1999'}, {'title': 'WALL-E', 'year': '2008'}, {'title': 'The Pork of Music', 'year': '2012'}, ] @app.route('/') def index(): return render_template('index.html', name=name, movies=movies)
三、连接数据库
选择了属于关系型数据库管理系统(RDBMS)的 SQLite,它基于文件,不需要单独启动数据库服务器,适合在开发时使用,或是在数据库操作简单、访问量低的程序中使用。
使用 SQLAlchemy——一个 Python 数据库工具(ORM,即对象关系映射)。借助 SQLAlchemy,你可以通过定义 Python 类来表示数据库里的一张表(类属性表示表中的字段 / 列),通过对这个类进行各种操作来代替写 SQL 语句。这个类我们称之为模型类,类中的属性我们将称之为字段。
数据表的创建,可以通过:
(1) flask shell
(env) $ flask shell >>> from app import db >>> db.create_all()
(2) 也可写在代码内,然后通过@click.option赋予flask shell直接运行函数的能力,如下所示。
执行 flask initdb
命令就可以创建数据库表
(env) $ flask initdb
(env) $ flask initdb --drop
-
代码:app.py
如果使用Click的command()
装饰器添加命令,执行时不会自动推入应用上下文。
# -*- coding: utf-8 -*- import os import sys import click from flask import Flask, render_template from flask_sqlalchemy import SQLAlchemy
# SQLite URI compatible WIN = sys.platform.startswith('win') if WIN: prefix = 'sqlite:///' else: prefix = 'sqlite:////' app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = prefix + os.path.join(app.root_path, 'data.db') # 告诉 SQLAlchemy 数据库连接地址 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 关闭对模型修改的监控 db = SQLAlchemy(app) # 初始化扩展,传入程序实例 app
#################################################
# 编写一个自定义命令来自动执行创建数据库表操作
#################################################
@app.cli.command() @click.option('--drop', is_flag=True, help='Create after drop.') def initdb(drop): """Initialize the database.""" if drop: db.drop_all() db.create_all() click.echo('Initialized database.')
# 因为有了数据库,我们可以编写一个命令函数把虚拟数据添加到数据库里。 @app.cli.command() def forge(): """Generate fake data.""" db.create_all() name = 'Grey Li' movies = [ {'title': 'My Neighbor Totoro', 'year': '1988'}, {'title': 'Dead Poets Society', 'year': '1989'}, {'title': 'A Perfect World', 'year': '1993'}, {'title': 'Leon', 'year': '1994'}, {'title': 'Mahjong', 'year': '1996'}, {'title': 'Swallowtail Butterfly', 'year': '1996'}, {'title': 'King of Comedy', 'year': '1999'}, {'title': 'Devils on the Doorstep', 'year': '1999'}, {'title': 'WALL-E', 'year': '2008'}, {'title': 'The Pork of Music', 'year': '2012'}, ] user = User(name=name) db.session.add(user) for m in movies: movie = Movie(title=m['title'], year=m['year']) db.session.add(movie) db.session.commit() click.echo('Done.')
####################################
# 有两类数据要保存:用户信息、电影条目信息 ####################################
class User(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(20)) class Movie(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(60)) year = db.Column(db.String(4))
@app.route('/') def index(): user = User.query.first() movies = Movie.query.all() return render_template('index.html', user=user, movies=movies)
-
添加记录
>>> from app import User, Movie # 导入模型类
>>> user = User(name='Grey Li') # 创建一个 User 记录 >>> m1 = Movie(title='Leon', year='1994') # 创建一个 Movie 记录 >>> m2 = Movie(title='Mahjong', year='1996') # 再创建一个 Movie 记录
>>> db.session.add(user) # 把新创建的记录添加到数据库会话 >>> db.session.add(m1) >>> db.session.add(m2)
>>> db.session.commit() # 提交数据库会话,只需要在最后调用一次即可
-
读取记录
<模型类>.query.<过滤方法(可选)>.<查询方法>
>>> from app import Movie # 导入模型类
>>> movie = Movie.query.first() # 获取 Movie 模型的第一个记录(返回模型类实例) >>> movie.title # 对返回的模型类实例调用属性即可获取记录的各字段数据 'Leon' >>> movie.year '1994' >>> Movie.query.all() # 获取 Movie 模型的所有记录,返回包含多个模型类实例的列表 [<Movie 1>, <Movie 2>] >>> Movie.query.count() # 获取 Movie 模型所有记录的数量 2 >>> Movie.query.get(1) # 获取主键值为 1 的记录 <Movie 1> >>> Movie.query.filter_by(title='Mahjong').first() # 获取 title 字段值为 Mahjong 的记录 <Movie 2> >>> Movie.query.filter(Movie.title=='Mahjong').first() # 等同于上面的查询,但使用不同的过滤方法 <Movie 2>
-
更新记录
>>> movie = Movie.query.get(2)
>>> movie.title = 'WALL-E' # 直接对实例属性赋予新的值即可 >>> movie.year = '2008'
>>> db.session.commit() # 注意仍然需要调用这一行来提交改动
-
删除记录
>>> movie = Movie.query.get(1)
>>> db.session.delete(movie) # 使用 db.session.delete() 方法删除记录,传入模型实例
>>> db.session.commit() # 提交改动
四、提交表单
<form method="post"> <!-- 指定提交方法为 POST --> <label for="name">名字</label> <input type="text" name="name" id="name"><br> <!-- 文本输入框 --> <label for="occupation">职业</label> <input type="text" name="occupation" id="occupation"><br> <!-- 文本输入框 --> <input type="submit" name="submit" value="登录"> <!-- 提交按钮 --> </form>
-
POST与GET
在 <form>
标签里使用 method
属性将提交表单数据的 HTTP 请求方法指定为 POST。如果不指定,则会默认使用 GET 方法,这会将表单数据通过 URL 提交,容易导致数据泄露,而且不适用于包含大量数据的情况。
Goto: 都 2019 年了,还问 GET 和 POST 的区别
(a) 如何获取提交的表单数据?
为了能够处理 POST 请求,我们需要修改一下视图函数,确保服务端能接收POST请求。
@app.route('/', methods=['GET', 'POST'])
(b) 两种方法的请求有不同的处理逻辑:
1) 对于 GET 请求,返回渲染后的页面;
2) 对于 POST 请求,则获取提交的表单数据并保存。
(c) 代码实践:为了在函数内加以区分,我们添加一个 if 判断。
from flask import request, url_for, redirect, flash # ... @app.route('/', methods=['GET', 'POST']) def index(): if request.method == 'POST': # 判断是否是 POST 请求 # 获取表单数据 title = request.form.get('title') # 传入表单对应输入字段的 name 值 year = request.form.get('year') # 验证数据 if not title or not year or len(year) > 4 or len(title) > 60: flash('Invalid input.') # 显示错误提示 return redirect(url_for('index')) # 重定向回主页
# 保存表单数据到数据库 movie = Movie(title=title, year=year) # 创建记录 db.session.add(movie) # 添加到数据库会话 db.session.commit() # 提交数据库会话
flash('Item created.') # 显示成功创建的提示 return redirect(url_for('index')) # 重定向回主页 movies = Movie.query.all() return render_template('index.html', movies=movies)
编辑:
@app.route('/movie/edit/<int:movie_id>', methods=['GET', 'POST']) def edit(movie_id): movie = Movie.query.get_or_404(movie_id) if request.method == 'POST': # 处理编辑表单的提交请求 title = request.form['title'] year = request.form['year'] if not title or not year or len(year) > 4 or len(title) > 60: flash('Invalid input.') return redirect(url_for('edit', movie_id=movie_id)) # 重定向回对应的编辑页面 movie.title = title # 更新标题 movie.year = year # 更新年份 db.session.commit() # 提交数据库会话
flash('Item updated.') return redirect(url_for('index')) # 重定向回主页 return render_template('edit.html', movie=movie) # 传入被编辑的电影记录
删除:
@app.route('/movie/delete/<int:movie_id>', methods=['POST']) # 限定只接受 POST 请求 def delete(movie_id): movie = Movie.query.get_or_404(movie_id) # 获取电影记录 db.session.delete(movie) # 删除对应的记录 db.session.commit() # 提交数据库会话 flash('Item deleted.') return redirect(url_for('index')) # 重定向回主页
五、用户认证
-
安全存储密码
继承这个类会让 User
类拥有几个用于 判断认证状态的属性和方法。
from werkzeug.security import generate_password_hash, check_password_hash class User(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(20)) username = db.Column(db.String(20)) # 用户名 password_hash = db.Column(db.String(128)) # 密码散列值 def set_password(self, password): # 用来设置密码的方法,接受密码作为参数 self.password_hash = generate_password_hash(password) # 将生成的密码保持到对应字段 def validate_password(self, password): # 用于验证密码的方法,接受密码作为参数 return check_password_hash(self.password_hash, password) # 返回布尔值
-
生成管理员账户
这里,直接编写一个命令来创建管理员账户。
import click @app.cli.command() @click.option('--username', prompt=True, help='The username used to login.') @click.option('--password', prompt=True, hide_input=True, confirmation_prompt=True, help='The password used to login.') def admin(username, password): """Create user.""" db.create_all() user = User.query.first() if user is not None: click.echo('Updating user...') user.username = username user.set_password(password) # 设置密码 else: click.echo('Creating user...') user = User(username=username, name='Admin') user.set_password(password) # 设置密码 db.session.add(user) db.session.commit() # 提交数据库会话 click.echo('Done.')
-
实现用户认证
扩展 Flask-Login 提供了实现用户认证需要的各类功能函数,需要独立安装:
(env) $ pip install flask-login
实现一个“用户加载回调函数”,初始化 Flask-Login。
from flask_login import LoginManager login_manager = LoginManager(app) # 实例化扩展类 @login_manager.user_loader def load_user(user_id): # 创建用户加载回调函数,接受用户 ID 作为参数 user = User.query.get(int(user_id)) # 用 ID 作为 User 模型的主键查询对应的用户 return user # 返回用户对象
其中最常用的是 is_authenticated
属性:如果当前用户已经登录,那么 current_user.is_authenticated
会返回 True
, 否则返回 False
。有了 current_user
变量和这几个验证方法和属性,我们可以很轻松的判断当前用户的认证状态。
@app.route('/', methods=['GET', 'POST']) def index(): if request.method == 'POST': if not current_user.is_authenticated: # <---- 判断 “当前用户已经登录?” return redirect(url_for('index')) title = request.form['title'] year = request.form['year'] if not title or not year or len(year) > 4 or len(title) > 60: flash('Invalid input.') return redirect(url_for('index')) movie = Movie(title=title, year=year) db.session.add(movie) db.session.commit() flash('Item created.') return redirect(url_for('index')) movies = Movie.query.all() return render_template('index.html', movies=movies)
-
用户登录、登出
from flask_login import login_user # ... @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] if not username or not password: flash('Invalid input.') return redirect(url_for('login')) user = User.query.first() # 验证用户名和密码是否一致 if username == user.username and user.validate_password(password): login_user(user) # 登入用户 flash('Login success.') return redirect(url_for('index')) # 登陆成功后,则重定向到主页 flash('Invalid username or password.') # 如果验证失败,显示错误消息 return redirect(url_for('login')) # 重定向回登录页面 return render_template('login.html')
from flask_login import login_required, logout_user # ... @app.route('/logout') @login_required # 用于视图保护,后面会详细介绍 def logout(): logout_user() # 登出用户 flash('Goodbye.') return redirect(url_for('index')) # 重定向回首页
六、认证保护
-
视图保护
未登录用户不能执行下面的操作:访问编辑页面、访问设置页面、执行注销操作、执行删除操作、执行添加新条目操作。
对于不允许未登录用户访问的视图,只需要为视图函数 附加一个 login_required
装饰器 就可以将未登录用户拒之门外。
添加了这个装饰器后,如果未登录的用户访问对应的 URL,Flask-Login 会把用户重定向到登录页面,并显示一个错误提示。
@app.route('/movie/delete/<int:movie_id>', methods=['POST']) @login_required # 登录保护 def delete(movie_id): movie = Movie.query.get_or_404(movie_id) db.session.delete(movie) db.session.commit() flash('Item deleted.')
return redirect(url_for('index'))
本篇章的大概内容,局限于了解。更多内容还需要系统学习。
剩余内容:
End.