第七章 留言板
使用click创建虚拟数据
1 |
|
Bootstrap-Flask 插件
注意 , 不是flask-Boostrap.
1 | pip install bootstrap-flask |
Bootstrap-Flask插件最牛逼的地方就是内置了很多常用的宏
宏 | 所在模板路径 | 说明 |
---|---|---|
render_field | bootstrap/form.html | 渲染单个 WTForms表单字段 |
render_form | bootstrap/form.html | 渲染整个 WTForms表单类 |
render_pager | bootstrap/pagination.html | 渲染一个基础分页导航,仅包含上一页、下一页按钮 |
render_pagination | bootstrap/pagination.html | 渲染一个标准分页导航部件 |
render_nav_item | bootstrap/nav.html | 渲染导航链接 |
render_breadcrumb_item | bootstrap/nav.html | 渲染面包屑链接 |
Flask-Moment 本地化日期和时间
- Moment.js是一个用于处理时间和日期的开源JavaScript库,它可以对时间和日期进行各种方式的处理。
- 它会根据用户电脑中的时区设置在客户端使用 Javascript来渲染时间和日期,
- 另外它还提供了丰富的时间渲染格式支持。
1 | pip install flask-moment |
使用datetime.utcnow这个纯正时间
替代 datetime.now这个细致时间
1 | class Message(db.Model): |
第八章 个人博客
为什么使用Endpoint?
我们在使用url_for()的时候 , 定位的都是Endpoint , 而不是使用视图函数 . 为啥?
因为使用端点可以实现蓝本的视图函数命名空间。即“蓝本名.视图函数名”
, 这样就支持视图函数重名了
邻接列表关系
- 楼中楼功能 : 给某条回复添加新的回复
- 方法:
- 方法一 : 创建Cormment和Replay表 , 然后使用一对多关系将评论和回复关联起来。
- 方法二 : 因为回复本身也是评论,如果可以在评论模型内建立层级关系,那么就可以在一个模型中表示评论和回复。
- 这种在同一个模型内的一对多关系在 SQLALchemy中被称为邻接列表关系
1 | class Post(db.Model): |
在这个关系函数中,通过将 remote side参数设为id字段,我们就把id字段定义为关系的远程侧(Remote side),而 replied id就相应地变为本地侧(Local side),这样反向关系就被定义为多对一,即多个回复对应一个父评论。
渲染导航链接
- 因为请求是先经过View函数 , 此时已经有endpoint
- 所以可以使用
request.endpoint== 'blog.index'
来确定激活的sidebar
1 | <li {% request.endpoint == 'blog.index' %} class="active" {% endif %}> |
切换博客主题(切换CSS)
切换博客主题 , 其实就是切换CSS文件 , 一般的操作思路如下
定义一个view专门来切换cookies . 前端按钮触发这个View
1
BLUELOG_THEMES = {'perfect_blue': 'Perfect Blue', 'black_swan': 'Black Swan'}
1
2
3
4
5
6
7
8
def change_theme(theme_name):
if theme_name not in current_app.config['BLUELOG_THEMES'].keys():
abort(404)
response = make_response(redirect(url_for('blog.index')
response.set_cookie('theme', theme_name, max_age=30 * 24 * 60 * 60)
return response修改template
1
<link rel="stylesheet" href="{{ url_for('static', filename='css/%s.min.css' % request.cookies.get('theme', 'perfect_blue')) }}" type="text/css">
主题列表 :
1 | <div class="dropdown-menu" aria-labelledby="dropdownMenuButton"> |
Flask-Login
login_user的背后逻辑
调用User模型的get_id()函数 (该函数返回User的唯一标识 , 如果User继承UserMixin , 那么函数get_id返回primaryKey)
将get_id()的结果存入session
每次执行被
@login_required
装饰的View的时候 , 都会从session中拿到这个唯一标识 . 接着把这个唯一标识传入load_user函数(该函数就是根据唯一标识找到对应的User实例)login user的remember参数 : 设为True时 Flask-Login会在用户浏览器中创建一个名为 remember token的 cookie,当通过 session设置的 user_id cookie因为用户关闭浏览器而失效时,它会重新恢复 user_id cookie的值。
currenr_user
currenr_user是一个和 current app类似的代理对象(Proxy),表示当前用户。调用时会返回与当前用户对应的用户模型类对象。
因为session中只会存储登录用户的id,所以为了让它返回对应的用户对象,我们还需要设置一个用户加载函数。这个函数需要使用 login manager.user_loader装饰器,它接收用户id作为参数,返回对应的用户对象,
1
2
3
def load_user(user_id):
return User.query.filter_by(id=user_id).first()
为一整个蓝图添加login_required
- 为蓝本注册一个 before request处理函数,然后为这个函数附加 login required装饰器。
- 因为使用 before request钩子注册的函数会在每一个请求前运行,所以这样就可以为该蓝本下所有的视图函数添加保护,函数内容可以为空
1 |
|
使用CSRFProtect拓展手动实现CSRF防御
CSRFProtect类内置于WTForm , 因此可以直接使用
1 | from flask_wtf import CSRFProtect |
创建一个hidden的input字段 ,name为csrf_token
, value为{{ csrf_token() }}
1 | <form class="inline" method="post" |
- CSRFProtect 会自动获取并验证csrf令牌
- 默认情况下,当令牌验证出错或过期时,程序会返回400错误,也可以单独创建一个错误处理函数捕捉令牌出错时抛出的 CSRFError异常,
- Werkzeug内置的HTTP异常类 , 都会将错误描述保存在异常对象的description属性中。
1 | from flask_wtf.csrf import CSRFError |
取消对某个蓝图的CSRF防御
1 | from flask_wtf import CSRFProtect |
第九章 图片社交网站
项目组织架构
在大型 Flask项目中,主要有三种常见的项目组织架构:
功能式架构
在功能式架构中,程序包由各个代表程序组件(功能)的子包组成,比如 blueprints(蓝本)forms(表单)、templates(模板)、models(模型)等,在这些子包中,按照程序的板块分模块来组织代码,比如 forms子包下包含 front.py、auth,py和 dashboard py
分区式架构
在分区式架构中,程序被按照自身的板块分成不同的子包。myapp使用分区式架构可以分别创建 front、auth和 dashboard三个子包,这些子包直接在程序包的根目录下创建,子包中使用模块组织不同的程序组件,比如 vIews.py、forms。py等。这种分类自然决定了每一个子包都对应着一个蓝本,这时蓝本在每个子包的构造文件中创建。
混合式架构
混合式架构,顾名思义,就是不按照常规分类来组织。比如,采用类似分区式架构的子包来组织程序,但各个蓝本共用程序包根目录下的模板文件夹和静态文件文件夹
在线的占位图片服务
1 | {# id这个query-args是为了破环存缓 #} |
- URL类似https://picsum.photos/600/800.如果想获取正方形的图片,那么只传递一个尺寸数字就可以了。
- 如果你想要每次请求都获得随机的图片,可以在URL后面附加?random。
- 不过这会有一点问题。因为当浏览器发现你有多个发往同一个URL的请求时,它会使用缓存的响应,这样你的图片就不再是随机的了。为了避免浏览器这个“好心”的缓存行为,我们可以在URL后附加一个无意义的查询字符串,使用数据库图片记录的id填充。这个查询字符串会被服务器忽略,但因为每个图片URL的参数都不同,浏览器会把它们都当作不同的请求来处理,
- 这种技术被称为 Cache Busting(缓存破坏)。
WTForm的EqualTo和Regexp校验器
1 | class RegisterForm(FlaskForm): |
生成JWS(json Web Signature)
- JWS由三部分组成,它们分别是存储签名所使用的算法、签名时间和过期时间的头部(Header)、存储数据的负载(Payload)和签名(Signature)。
- 序列化对象提供一个 dumps方法来写入数据,它接收包含数据的字典对象作为参数。它会根据过期时间创建头部(Header),然后将数据编码到JWS的负载(Payload)中,再使用密钥对令牌进行签名(Signature),最后将签名序列化后生成令牌值。
- 简单来说 , 只要为
TimedJSONWebSignatureSerializer
的dumps()函数传入query_args , 即可生成token - 后端接受到token后 , 调用
TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY']).loads(token)
, 如果没有报错 , 即可证明验证通过 - 之后就可以通过data.get(‘operation’)来获取query_args
1 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer |
验证token :
1 | def validate_token(user, token, operation, new_password=None): |
flask的装饰器必须使用wraps装饰器
- 使用 functors模块提供的 wraps装饰器可以避免被装饰函数的特殊属性被更改,比如函数名称
__name__
被更改。 - 如果不使用该模块,则会导致函数名称被替换,从而导致端点(端点的默认值即函数名)出错
flash函数中返回危险文本的操作
- flash函数发送的消息在模板中渲染,消息内容会被自动转义为普通文本。
- 可以在模板中使用safe过滤器来避免Jina2对变量转义,但对fash消息使用safe过滤器会造成安全隐患,因为攻击者可能会篡改消息内容。
- 更安全的做法是将传入flash函数的文本转换为 Markup对象。Flask提供的 Markup类可以将文本标记为安全文本,从而避免在渲染时对Jina2进行转义。
1 | message = Markup( |
基于用户角色的权限管理 RBAC (Role-Based Access Control)
角色和权限model设计
角色与权限模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23roles_permissions = db.Table('roles_permissions',
db.Column('role_id', db.Integer, db.ForeignKey('role.id')),
db.Column('permission_id', db.Integer, db.ForeignKey('permission.id'))
)
class Permission(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), unique=True)
roles = db.relationship('Role', secondary=roles_permissions, back_populates='permissions')
class Role(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), unique=True)
users = db.relationship('User', back_populates='role')
permissions = db.relationship('Permission', secondary=roles_permissions, back_populates='roles')
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, index=True)
password_hash = db.Column(db.String(128))
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
role = db.relationship('Role', back_populates='users')权限定义 示例
操作 权限名称 说明 关注用户 FOLLOW 关注其他用户 收藏图片 COLLECT 添加图片到自己的收藏 发表评论 COMMENT 在图片下添加评论 上传图片 UPLOAD 上传图片 协管员权限 MODERATE 管理资源权限,可以管理网站的用户、图片、评论、标签等资源 管理员权限 ADMINISTER 管理用户角色、编辑网站信息等 角色定义 示例
角色名称 拥有的权限 说明 访客(Guest) 仅可以浏览页面 未登录用户 被封禁用户(Blocked) 仅可以浏览页面 因违规行为被封禁账号,禁止登录的用户 被锁定用户(Locked) FOLLOW、COLLECT 因违规行为被锁定的用户 普通用户(User) FOLLOW、COLLECT、COMMENT、UPLOAD 注册后用户获得的默认角色 协管员(Moderator) 除了拥有普通用户具有的权限外,还拥有管理网站拥有 MODERATE权限 除了普通用户的权限外内容的权限,负责网站内容管理和维护 管理员(Administrator) 除了拥有普通用户和协管员的所有权限外,还拥有 ADMINISTER权限 拥有所有权限的网站管理员 1
2
3
4
5
6roles_permissions_map = {
'Locked': ['FOLLOW', 'COLLECT'],
'User': ['FOLLOW', 'COLLECT', 'COMMENT', 'UPLOAD'],
'Moderator': ['FOLLOW', 'COLLECT', 'COMMENT', 'UPLOAD', 'MODERATE'],
'Administrator': ['FOLLOW', 'COLLECT', 'COMMENT', 'UPLOAD', 'MODERATE', 'ADMINISTER']
}这里只有四种角色,没有访客和被封禁用户。
- 访客不需要写入数据库,因为访客的作用就是用来表示不在数据库中的用户。
- 而被封禁的用户不允许登录,虽然这类用户拥有账户,但是其权限状态和访客完全相同。
填充角色权限表数据 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class Role(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), unique=True)
users = db.relationship('User', back_populates='role')
permissions = db.relationship('Permission', secondary=roles_permissions, back_populates='roles')
def init_role():
roles_permissions_map = {
'Locked': ['FOLLOW', 'COLLECT'],
'User': ['FOLLOW', 'COLLECT', 'COMMENT', 'UPLOAD'],
'Moderator': ['FOLLOW', 'COLLECT', 'COMMENT', 'UPLOAD', 'MODERATE'],
'Administrator': ['FOLLOW', 'COLLECT', 'COMMENT', 'UPLOAD', 'MODERATE', 'ADMINISTER']
}
for role_name in roles_permissions_map:
role = Role.query.filter_by(name=role_name).first()
if role is None:
role = Role(name=role_name)
db.session.add(role)
role.permissions = []
for permission_name in roles_permissions_map[role_name]:
permission = Permission.query.filter_by(name=permission_name).first()
if permission is None:
permission = Permission(name=permission_name)
db.session.add(permission)
role.permissions.append(permission)
db.session.commit()
替换AnonymousUserMixin
1 | class User(db.Model, UserMixin): |
User模型如上 , 现在我们就可以使用
current_user
来代理User模型了 , 所以我们可以使用current_user.is_admin
来判断是不是管理员但是 , 如果current_user是AnonymousUserMixin的话就不行了 , 因为AnonymousUserMixin对象没有is_admin属性
此时的方法就是替换AnonymousUserMixin
1
2
3
4
5
6
7
8
9
10from flask_login import LoginManager, AnonymousUserMixin
login_manager = LoginManager()
class Guest(AnonymousUserMixin):
def is_admin(self):
return False
login_manager.anonymous_user = Guest一般这个操作放到extensions 模块中创建
权限装饰器 及 装饰器的继承(装饰器里调用装饰器)
1 | def permission_required(permission_name): |
Flask-Dropzone 优化文件上传
Dropzone. js是一款优秀的文件上传插件
详情参考 : http://greyli.com/flask-dropzone/ , https://www.dropzonejs.com/
1 | pip install flask-dropzone |
1 | from flask_dropzone import Dropzone |
相关宏
1 | {{ dropzone.load_css() }} |
View注意是request.files.get('file')
, 而不是request.files.getlist('file')
: 默认一个请求发送一个文件
1 |
|
- 当文件被拖拽到上传区域,或是点击上传区域选择上传文件后,这些文件会以AJAX请求的形式发送到你在dropzone.create()方法中使用action参数传入的URL。
- 因为Dropzone.js通过AJAX请求提交文件,所以你没法在保存文件后将页面重定向。对于这个问题,你可以使用配置变量DROPZONE_REDIRECT_VIEW设置上传完成后跳转到的目标端点,或是添加一个按钮让用户自己点击进行跳转。
- 尽管Dropzone.js可以在前端对用户提交的文件进行验证,但为了安全考虑,我们仍然需要在服务器端进行二次验证。在服务器端验证时,如果验证出错,我们不能像往常那样使用flash()函数“闪现”错误消息,因为AJAX请求接受到响应后并不会重载页面,所以不会显示通过flash()函数发送的消息。正确的做法是返回400错误响应,使用错误消息作为响应的主体。此时前端就会像flash一样显示错误提示
重要配置
设置文件类型过滤
Flask-Dropzone内置了一些文件类型(通过MIME定义),可选的值和对应的文件类型如下所示:
- default:默认值,运行所有类型
- image:图片
- audio:音频
- video:视频
- text:文本
- app:程序
1 | app.config['DROPZONE_ALLOWED_FILE_TYPE'] = 'image' |
如果你想要自己定义允许的文件类型列表,那么你需要将DROPZONE_ALLOWED_FILE_CUSTOM设置True,然后传入一个包含允许的文件后缀名列表组成的字符串给DROPZONE_ALLOWED_FILE_TYPE变量,使用逗号分隔多个后缀名,
1 | app.config['DROPZONE_ALLOWED_FILE_CUSTOM'] = True |
1 | # 上传字段的name值 , 默认file |
简单示例
1 |
|
1 | {% block styles %} |
Flask-Avatars 处理用户头像
1 | pip install flask-avatars |
相关宏
1 | {# 默认头像 #} |
1 |
|
文件删除
- 按照一般的做法,在数据库中删除某张图片前,首先要在文件系统中删除对应的图片文件,但是有更好的实现方法
- 为Photo模型创建一个数据库事件监听函数。这个监听函数的作用是,当 Photo记录被删除时,自动删除对应的文件。
1 | class Photo(db.Model): |
当页编辑的最佳实践
- 比如图片展示列表 , 点击时当页修改
图片描述
- 每个项目下面都渲染一个form , 但是form设置为
display:none
, 点击时show . - 注意这个form应该添加一个
取消button
, 点击这个button时 , 重新让这个formdisplay:none
1 | <div id="description-form"> |
注册全局Ajax错误
1 | $(document).ajaxError(function (event, request, settings) { |
flask-login的fresh_login_required : 重新登录
1 | from flask_login import fresh_login_required, login_fresh |
fresh_login _required装饰器 : 确保用户处于“活跃”的认证状态。
当用户登录账户时用户会话会被标记为“新鲜的(fresh)”,通过使用 session对象写人名为 fresh的 cookie实现。如果用户会话被销毁或过期了,而用户勾选了“记住我”选项。尽管这时用户仍然保持登录状态,但会话已经被标记为“不新鲜”。出于安全考虑,像修改密码这类敏感操作,应该在“新鲜”会话下进行。
编写一个重新验证视图函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def re_authenticate():
if login_fresh():
return redirect(url_for('main.index'))
form = LoginForm()
if form.validate_on_submit() and current_user.validate_password(form.password.data):
confirm_login()
return redirect_back()
return render_template('auth/login.html', form=form)
login_manager.refresh_view ='auth re_authenticate'
login_manager.needs_refresh_message_category='warning'
login_manager.needs_refresh_message='为了保护你的账户安全,请重新登录。'
subquery 与 in_ 与 join
以前的做法 :
1 | query = db.session.query(Follow.followed_id).filter(Follow.follower_id == current_user.id) |
直接使用subquery , 不必将其改成list
1 | followed_ids = db.session.query(Follow.followed_id).filter(Follow.follower_id == current_user.id).subquery() |
简单来说 , subquery的作用就是将query改成table
实际上 , in_ 操作的性能没有join好 , 最好使用join
1 | followed_photos = Photo.query.join(Follow, Follow.followed_id == Photo.author_id).filter(Follow.follower_id == self.id) |
sqlalchemy随机获取内容
- SQLALchemy通过 sqlalchemy.sql.expression模块的func属性提供了泛型函数(Generic Function)支持,使用这个属性可以调用对应的SQL函数。
func.random()
: 调用数据库引擎提供的随机函数
1 | from sqlalchemy.sql.expression import func |
Flask Whooshee 实现全文搜索
全文搜索的原理是索引程序通过扫描数据库中的每一个词,对每一个词建立一个索引指明该词在数据库中出现的次数和位置,当用户查询时,检索程序通过索引进行查找,并返回匹配的数据。
1 | pip instaoll flask-whooshee |
- Flask-Whooshes默认在项目的根目录下建立名为 whooshes的索引文件夹,你可以通过配置变量 WHOOSHEE_DIR自定义这个位置。
- 使用 Flask-Whooshee提供的
whooshes.register_model()
装饰器,即可对要索引的字段进行注册。 - 下面我们分别在这三个模型类前附加这个装饰器,传入要被索引的目标字段名称作为参数:
1 |
|
默认情况下,Flask-Whooshes在对这几个注册的字段执行写入操作后自动创建并更新索引。在下面这两种情况下,你需要重新生成索引:
- 索引数据丢失,比如误删除索引文件夹;
- 在新的程序中使用扩展,这时数据库已经包含许多数据。
最后执行reindex方法来重新创建索引:
1 | flask shell |
所以我们可以编写一个Celery程序来执行
whooshe.reindex()
示例
- 搜索请求通常会通过GET方法发出,这样可以支持用户对搜索URL进行收藏或分享,同时也可以避免浏览器对于POST请求重复提交问题。
- Fask-Whooshes覆盖了 SQALchemy的 query对象,为其添加了一个 whooshes_search()查询方法,这个方法接收搜索关键字作为参数,返回包含所有匹配记录的查询对象。
1 |
|
1 | <form class="form-inline my-2 my-lg-0" action="{{ url_for('main.search') }}"> |
第十章 代办事项程序
单页面应用布局
- 单页程序,并不意味着我们只能使用一个页面。程序中实际包含三个页面:介绍页、登录页和程序页,这三个页面分别在局部模板
_intro.html
、_login.html
和_app.html
中定义。 - 三个模板不会直接加载,而是通过AJAX请求获取并动态插到根页面中。
- 为了便于在AJAX中发送请求到对应的URL,我们在根页面中定义了多个 JavaScript变量,分别存储指向这三个页面的URL。另外,我们程序中的所有操作都通过 JavaScript发送AJAX请求实现,像登录、注册、注销等这类不包含URL变量的URL,也在这里定义:
1 | {# base.html #} |
在根页面内切换子页面
在单页面应用中 , Flask的视图函数的功能退化为提供数据的内部接口,而真正的视图处理则转移到了客户端JavaScript 中
在单页面中切换子页面时,我们希望这些子页面变化时也产生一个可以被保存为书签并且添加到浏览器历史的URL,而且使浏览器的前进、后退按钮发挥作用。为了让程序的状态在URL中表现出来,我们通过在URL后面添加hash(即URL中#后面的部分)来记录状态。程序中的三个主要页面使用对应的hash标签表示,比如导航栏上的登录按钮
1
<a class="waves-effect waves-light btn red" href="#login">{{ _('Login') }}</a>
单击这个按钮,会访问#login产生的URL类似http://example.com#login表示登录页面。
在URL中添加hash不会产生请求,而我们可以通过监听hash的变化来设置回调函数来更新页面。我们创建一个
hashchange事件的监听函数,用于在hash值改变时执行对应的函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28$(window).bind('hashchange', function () {
// 有些浏览器不返回#,这里统一去掉#
var hash = window.location.hash.replace('#', '');
var url = null;
// 根据hash值的不同,选择对应的页面URL
if (hash === 'login') {
url = login_page_url
} else if (hash === 'app') {
url = app_page_url
} else {
url = intro_page_url
}
// 向对应的页面URL发送GET请求,服务器端会返回对应的局部模板
$.ajax({
type: 'GET',
url: url,
success: function (data) {
$('#main').hide().html(data).fadeIn(800); // 插入子页面
activeM(); // 激活新插入的页面中的 Materia1ize组件
} // 错误回调已经统一设置,不需要定义 error回调
});
});
if (window.location.hash === '') {
window.location.hash = '#intro'; // home page, show the default view
} else {
$(window).trigger('hashchange'); // 触发 hashchange事件,重新加载页面
}所以 , 当我们执行其他操作后需要切换页面时,通过切换URL中的hash(通过 window,locationhash)即可切换页面。比如,当用户单击登录按钮时,如果验证通过,我们在发送AJAX请求的success回调函数中将hash设为对应程序页面的App,就可以切换到App页面:
1
2
3seccess: function (data) {
window.location.hash = "#app";
}后端代码
1
2
3
4
5
6
7
8
9
10
def new_item():
data = request.get_json()
if data is None or data['body'].strip() == '':
return jsonify(message=_('Invalid item body.')), 400
item = Item(body=data['body'], author=current_user._get_current_object())
db.session.add(item)
db.session.commit()
return jsonify(html=render_template('_item.html', item=item), message='+1')
template的True,False转为true,false
done属性后使用了 Flask内置的 tojson过滤器,因为 Python中的布尔值和 Javascript 中的布尔值(全小写形式的true和 false)不同,所以需要使用 tojson过滤器将变量值转换为JSON格式。
1 | <div class="row item card-panel hoverable" data-href="{{ url_for('.edit_item', item_id=item.id) }}" |
使用 Flask-Babel 支持国际化与本地化
- 国际化:国际化指设计和修改程序以便让程序支持多种语言或区域,而不是固定于某个语言或区域。国际化为本地化做了程序上的准备。
- 本地化:本地化指为程序添加某些资源(比如翻译文件)以便支持某个特定的语言或区域。本地化通常会进行多次,比如要支持10种语言,那么就要进行10次本地化处理。
1 | pip install flask-babel |
区域和语言
- 我们使用语言代码(language code)和区域代码(locale code)来区分不同的区域和语言。
- 语言代码 : 我们最常见到的语言代码有zh和en,分别表示中文和英文。为了覆盖这些语言大类下的各种分支,通过添加各种子标签,我们可以更具体地描述某种语言。比如,添加语言的脚本(script)标签,zh-Hans和zh-Hant分别表示简体中文和繁体中文;通过添加国家/地区标签,en-US,en-GB分别表示美国英语和英国英语;类似的,zh-CN、zh-TW和zh-HK则分别表示中国大陆简体中文,中国台湾繁体中文和中国香港繁体中文。
- 区域代码(locale code)则用来表示某一个区域的语法形式和语言代码类似。比如zh、en等。另外一种常见的形式是附加了国家/地区代码的形式,其使用下划线将语言代码与国家/区域连接起来,比如en_US,zh_CN、zh_TW,另外还有添加语言脚本的形式,比如 zh_hans_cn、zh_hant_TW等
语言代码中添加国家/区域代码时的连接符是连接线,而区域代码使用下划线。
文本的国际化
1 | flash(u"文章发表成功") |
对于英文等单复数包含单词变化的语言,使用ngettext
传入两个字符串,依次为单数形式和复数形式的字符串,其中包含用于区分单复数的变量num,第三个参数则传入代表num数值的变量。
1 | flash(ngettext(u"%(num)s Apple" , u"%(num)s Apples" , num=number_of_apples)) |
注意 :
_
只会在请求上下文中执行 , 如果想在请求上下文外执行 , 使用lazy_gettext
1
2
3
4 from flask_babel import Babel, lazy_gettext as _l
class LoginForm(FlaskForm):
username = StringField(_l("Username"))
在模板中使用
1 | <h1>Join now</h1> |
设置子域名
使用subdomain参数
1 | api_v1 = Blueprint('api_v1', __name__) |
用于注册路由的 routed装饰器也接收 subdomain参数,可以为某个视图定义子域。
flask-cors 支持跨域资源共享
同源策略(Same origin policy): 出于安全考虑,浏览器会限制从脚本内发起的跨域请求。这里的跨域包括不同域名、不同端口、不同HTTP模式(HTTP、Https等)。
在CORS流行之前,大多数AP都通过支持 JSONP(json with Padding)来支持跨域请求。和 JSONP相比,CORS更加方便灵活,支持更多的跨域请求方法,并且在2014年成为W3C的推荐标准,逐渐开始替代 JSONP。
CORS 原理简介: https://www.ruanyifeng.com/blog/2016/04/cors.html
- CORS需要浏览器和服务器同时支持。
- 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
- 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
1 | pip install flask-cors |
简单使用:
直接注册相关蓝图即可.
1 | from flask import Blueprint |
默认情况下,Flask-CORS会为蓝本下的所有路由添加跨域请求支持,并且允许来自任意源的跨域请求。
RESTfull API
- Resource Representational State Transfer。
- 这可以理解为“资源(Resource)在网络中以某种表现形式(Representation)进行状态转移(State Transfer)”。
资源端点与HTTP方法的操作含义
- api.example.com/users:所有用户。
- api.example.com/users/123/:id为123的用户。
- api.example.com/users//123/posts:id为33的用户的所有文章。
- api.example.com/posts:所有文章。
- api.example.com/posts/23:id为23的文章。
- api.example.com/posts/23/comments:id为23的文章的所有评论。
URL | GET | PUT | PATCH | POST | DELETE |
---|---|---|---|---|---|
资源集合 比如http://api.example.com/posts |
列出集合成员的所有信息 | 替换整个集合的资源 | 一般不使用 | 在集合中创建个新条目,新条目的URL自动生成并包含在响应中返回 | 删除整个资源 |
单个元素 比如http://api.example.com/posts/123 |
获取指定资源的详细信息,采用XML或JSON等表现形式 | 替换指定的集合成员,如果不存在则创建 | 更新集合成员,仅提供更新的内容 | 一般不使用 | 删除指定的集合成员 |
MethodView
类似于Django的Class View
1 | from flask.views import MethodView |
API的 OAuth 认证 及第三方认证登录
用户通过一次认证后,在服务器端为用户生成一个认证令牌,在之后的请求中,客户端可以通过认证令牌进行认证。出于安全的考虑,认证令牌还会设置过期时间。
1 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, BadSignature, SignatureExpired |
请求参数一般有4个:
- grant_ type
- username
- password
- scope : 允许的权限范围(可选)
返回的token_type : access令牌是客户端用于访问受登录保护时的认证凭证,令牌类型中的“Bearer”是RFC6750定义的令牌类型(常被翻译为不记名令牌)。
之后我们就可以使用View的装饰器了:
1 | def get_token(): |
发送请求时需要把认证令牌附加在请求首部的 Authorization字段中,并且在令牌前指定令牌类型(即Bearer)
Web API的序列化与反序列化
对于Web API来说,
- 序列化(Serialize)就是把数据库模型对象转换成JSON数据。
- 反序列化(Deserialize)就是把JSON数据转换成数据库模型对象。
JSON数据风格指南摘要
https://github.com/darcyliu/google-styleguide/blob/master/JSONStyleGuide.md
数组类型应该是复数属性名。其它属性名都应该是单数。
日期应该使用RFC3339建议的格式 , 时间间隔应该使用ISO 8601建议的格式
1
2
3
4{
"lastUpdate": "2007-11-06T16:34:41.000Z",
"duration": "P3Y6M4DT12H30M5S"
}
Marshmallow 与 Webargs
- Marshmallow的用法和 WTForms类似,通过创建模式类事先定义好字段类型以及验证函数。
- 对模式类对象调用dump()方法和 load方法分别执行序列化和反序列化操作,同时对传入的对象进行验证,并返回验证后的数据字典和相应的错误消息字典。
- Webargs可以解析请求中包含的表单、查询字符串、JSON、cookies、files、首部字段等系列数据,然后根据预定的样式进行验证,如果验证未通过会生成内置的错误消息。
webargs文档 : https://webargs.readthedocs.io/en/latest/
- 默认情况下,webargs将以JSON形式从请求正文中搜索参数。
- 你也可以在 use_args裝饰器中使用 location参数显式指定一个数据位置,可用的值为 :
- querystring(等同于query,表示查询字符串)、
- json(表示JSON)、
- form(表示表单数据)、
- headers(表示首部字段)、
- cookies(表示Cookie)
- files(表示文件)。
webargs的422错误
当验证出错时,Webargs会返回422响应(Unprocessable Entity,表示实体无法处理,即语义错误),我们可以注册一个对应的错误处理函数,如下所示:
1 |
|
webargs解析字段一览
1 | from webargs import fields, validate |
webargs配合marshmallow
1 | from marshmallow import Schema, fields |
webargs多字段位置
1 | # webargs 5.x and older |
第十一章 在线聊天室
使用 Flask-SocketIO 建立实时双向通信
- Socket.IO(http://socket.io)是一个基于 Web Socket实现实时通信的开源 JavaScript库,它可以简化实时web程序(real-time application,RTA)的开发过程。
- 它除了支持 Web Socket之外,还支持许多种轮询(Polling)机制以及其他模拟实时通信的方式。
- SocketIO会根据浏览器对通信机制的支持情况自动选择最佳的方式来实现实时通信,实现了对浏览器的“降级支持”。
- SocketIO为这些通信方式实现了统一的接口,因此使用起来非常简单。除此之外,SocketIO还提供了诸如广播、命名空间、房间、存储客户端数据、异步IO等非常方便的功能。
1 | pip install flask-socketio |
1 | from flask_socketio import SocketIO |
启动Socket.IO服务器
为了正确启动 Socket.IO服务器,Flask-Socket覆写了 Flask提供的 flask run命令,我们可以像往常一样使用 flask run命令启动 SocketIO服务器
另外,我们也可以直接调用 socketio对象提供的run方法来启动服务器,传入程序实例app作为参数:
1
2
3
4
5
6
7
8
9from flask import Flask
from flask_sockerio import SocketIO
app = Flask(__name__)
app.config['SECRET_KEY'] = 'serect string'
socketio = SocketIO(app)
if __name__ == '__main__':
socketio.run(app)在客户端,我们需要加载 Socket.IO资源并调用io()方法与我们在服务器端运行的 SocketIO服务器建立连接。首先我们需要加载对应的资源文件,你可以访问https://cdnjs.com/libraries/socket.io将资源文件下载到本地 , 之后就可以使用io()了
1
2
3
4
5
6<script type="text/javascript">
var socket = io();
// 如果不传入任何参数,会默认使用服务器的根URL,
// 等价于下面
// var socket = io('/');
</script>
SocketIO事件
消息流程
- 用户发送消息请求到服务器端。
- 服务器端接收消息请求并把消息广播给所有客户端。
- 所有客户端接收消息并显示。
- 在 SocketIO中,服务器端和客户端之间交流的数据被称为
SocketIO事件
(event),这里的事件就是包含特定信息的数据,类似我们常说的请求/响应 - 简单来说,在 Socket.IO中,双向通信是这样实现的:
- 客户端通过调用emit函数来将一个事件发送到服务器端,并传入数据作为参数,这会触发服务器端创建对应的事件处理函数。
- 而服务器端也可以通过调用 emit函数向客户端发送事件,并传人数据作为参数,类似地,这会触发客户端创建对应的事件处理函数。
简单示例
1 | <textarea rows="2" id="message-textarea" placeholder="Write your message here... Enter to send"></textarea> |
- Fask-SocketIo提供on()装饰器来注册用于接收客户端发来事件的事件处理函数。
- 创建
new message
事件处理函数,用来处理客户端发送的new message事件
1 | from catchat.extensions import socketio, db |
- 在emit()函数中,
- 第一个参数用来指定事件名称,当服务器端的 emit函数被调用时,会触发已连接客户端中对应的事件处理函数。
- 第二个参数是我们要发送的数据,要发送的数据根据客户端的需要而定,这里我们使用 render
template函数渲染存储单个消息HTML代码的message。html模板,在客户端使用 JavaScript可以直接将这个数据插入到消息列表的HTML元素中。事件中包含的数据类型可以为字符串、列表或字典。当数据的类型为列表或字典时,会被序列化为JSON格式。 - 因为我们要把消息发送给所有已经连接的客户端,所以需要将broadcast参数设为True,这样就会广播这个事件,即将事件发送给所有已连接的客户端。
储存global数据
如果不存入redis的话 , 使用global关键字
1 |
|
1 | socket.on('user count', function (data) { |
connect连接事件处理程序可以选择返回False
以拒绝连接。这样就可以在此时对客户端进行身份验证。
异常处理
1 | # Handles the default namespace |
gunicorn配置
通过gunicorn启动eventlet服务器的命令行
1 | gunicorn --worker-class eventlet -w 1 module:app |
使用gevent,
1 | gunicorn -k gevent -w 1 module:app |
Socket.io通信频道分离
命名空间
- 简单地说,不同的命名空间就是不同的URL路径,比如/foo和/bar就是两个不同的命名空间。
- 可以把 SocketIo中的命名空间比作 Flask中的蓝本
- 可以通过
request.namesapce
获取
1 |
|
1 | var socket = io.connect('/anoymous') |
房间
- 对于许多应用程序,有必要将用户分组为可以一起寻址的子集。
- 最好的例子是具有多个房间的聊天应用程序,其中用户从他们所在的房间或房间接收消息,而不是从其他用户所在的其他房间接收消息。
- SocketIO支持通过房间的概念
join_room()
和leave_room()
功能:这两个函数分别用来把当前用户(客户端)加入和退出房间。你还可以使用close room
函数来删除个房间,并清空其中的用户。另外,rooms函数可以返回某个房间内的客户端列表。
1 | from flask_socketio import join_room, leave_room |
基于房间你也可以实现私信私聊功能。只需要把room设为代表某个用户的唯一值,在发送事件时,就只有目标用户的客户端才能接收到事件。你可以把这种实现方法理解为“一个人的房间”。这个能代表用户的唯一值可以是主键值、username或是 Flask-SocketIo附加到 request对象上代表每个客户端id的 session id(request.sid)。
使用 Flask-OAuthlib 实现第三方登录
第三方登录的实质是借助第三方服务开放的 Web APl进行 OAuth授权。授权成功后,我们就可以通过第三方服务的 Web api获取到用户的资料以及其他资源。
1 | pip install flask-oauthlib |
在 OAuth认证中 :
- 需要获取和使用资源的一方,即我们的程序,通常被称为资源消费者(Resource Consumer)或
客户端
(Client)。 - 拥有用户资源的各种在线服务提供方则被称为
资源提供者
(Resource provider)。 - 用户被称为资源拥有者(Resource Owner)。
简单使用
流程:
- 用户点击登录 , 调用oauth_login视图 , 该视图重定向到第三方登录页面
- 用户键入 , 点击登录 , 第三方应用会将用户重定向到我们注册 OAuth程序时提供的回调URL(例如http://127.0.0.1/callback/github) , 并且返回access
- 视图接收到这个回调请求,获取code,发送一个POST请求到用于获取access 令牌的URL,进行验证 .
- 验证成功则说明登录成功
在 OAuth认证中,
- 我们开发的程序也被称为本地程序(local application),
- 我们要与之交互的第三方服务提供方则相应被称为远程程序(remote application)。
1 | <div class="ui message"> |
1 | from catchat.extensions import oauth |
我们首先需要创建用于第三方登录的视图,在视图函数中使用redirect重定向到服务提供方的授权URL,并附加相应的查询参数。但我们不必手动做这个工作,因为 Flask-OAuthlib为每一个注册后的远程程序对象提供了 authorized方法,这个方法用来构建授权URL并生成重定向响应。
github.authorize(url_for('.github_callback',_external=True))
response = provider.authorized_response()
封装了POST验证access的一系列操作
bleach+markdown库支持markdown
- markdown : 文本转换
- Bleach : HTML清理工具包 (防止恶意内容 , 比如 JavaScript脚本。)
1 | pip install markdown |
1 | >>> import markdown |
简单使用
Bleach的清理工作是基于白名单 (allowed_tags , allowed_attributes)进行的。
1 | from bleach import clean, linkify |
- 接收用户输入的包含 Markdown标记的源文本。
- 将 Markdown文本转换为HTML格式。
- 将转换好的HTML文本渲染到模板中。
代码语法高亮
Flask-CKEditor内置了代码语法高亮功能 , 这里我们使用pyments
1 | pip install pyments |
原理 : 对于HTML格式来说,通过解析代码片段的语法结构,Payments会使用标签分隔每一个语法单元,并添加对应的样式类,最后通过加载对应的CSS文件即可实现代码“上色”。
desktop notification(桌面通知)
1 | document.addEventListener('DOMContentLoaded', function () { |
Notification.permission
存储用户的许可状态值 : granted表示允许,denied表示拒绝,默认为 default(等同于 denied)。- 如果 Notification. permission的值不是 granted,那么就调用 Notification.requestPermission()方法请求授权,这会在用户浏览器中弹出一个授权请求窗口.
简单使用
- notification.onclick属性定义的函数会在提醒弹窗被单击时执行
- notification.close()方法关闭弹窗。
1 | const NotificationInstance = Notification || window.Notification; |