本章新涉及的Python包:Flask-Dropzone, Pillow, Flask-Avatars, Whoosh, Flask-Whooshee。
1、功能式架构
在该架构中,程序包由各个代表程序组件(功能)的子包组成,如 blueprints(蓝本),froms(表单),templates(模板),models(模型)等。这也是前一章个人博客项目中,所采用的架构方式。
2、分区式架构
在分区式架构中,程序被按照自身的板块分成不同的子包,如 front(前台),auth(用户),dashboard(后台)。这种分类自然决定了每一个子包都对应着一个蓝本。
myapp/
dashboard/
- __init__.py
- views.py
- forms.py
templates/
static/
front/
...
auth/
...
__init__.py
也许上面两种架构的描述会让你有些混淆,你也可以这样理解:功能式架构将相似的代码组织在一起,而分区式架构将同一块业务组织在一起。
3、混合式架构
即不按照常规的分类来组织。比如在分区式架构的基础上,使各个蓝本公用程序包根目录下的模板文件夹和静态文件夹。
Flask并不限制你组织项目的方式,你可以根据自己程序的特点自由选择。
1、可能用到的资源
- 在线图片占位服务:如基于Unspalsh的Lorem Picsum。在程序开发的过程种,可以生成不同的用于占位的图片。
- 开源图标集:如Font Awesome,Material Design Icons,Octions等,还有与Bootstrap集成良好的Iconic。(p308)
2、缓存破坏
如果你总是向同一个url发起请求,而实际获取不同的返回结果。如
<img src="https://picsum.photos/800/?random">
但浏览器可能会使用缓存的响应。为了处理这个问题,你可以再加一个无意义但是会变化的查询字符串。
注册一个账号通常包含这样的步骤:填写注册信息、接收注册邮件、通过单击验证链接来确认账号。
1、验证邮箱地址
验证链接是一个类似/confirm/
的URL,后面是一个传递给视图函数的变量,即一个用来验证用户身份的令牌。用户点击链接使服务器收到请求后,会解析令牌以获得存储再其中的用户 id,然后执行确认的操作。
因为令牌经过签名,所以可以确保其不会被篡改。
- 问题:“签名”是原理是什么?
- 扩展Flask-Security:可以简化用户认证的实现过程。
在一些简单的程序中,通常只有两种用户角色:普通用户和管理员,所以在角色和权限上并不需要花费太多的功夫。但在一些复杂的程序中,你可能会有更丰富的角色,如被封禁的用户、被锁定的用户等等。每一种角色又有着不同的权限,角色和权限之间是多对多的关系。
roles_permissions_map = {
'Locked': ['Follow', 'Collect'],
'User': ['Follow', 'Collect', 'Comment'],
...
}
在程序开始时,你需要提供一个方法初始化角色和权限的关系,即将该关系写入数据库中。然后,你可以编写权限验证装饰器,附加到视图函数前,使具有响应权限的用户才能访问该视图函数。
使用普通表单进行文件上传时,如果同时上传多个文件,用户只能在一个毫无变化的页面面前苦苦等待。为了优化用户体验,我们可以显示一个包含上传文件的列表,展示文件上传的进度。Flask-Dropzone扩展可以帮助我们做到这件事情。
Flask-Dropzone提供了基本配置、内置的文件类型配置、以及错误消息的配置。(p326)
该扩展还内置了对于CSRFProtect扩展的支持,可以在上传文件表单中自动添加csrf令牌隐藏字段,并在处理文件上传请求时自动验证csrf令牌。
DROPZONE_ENABLE_CSRF = True
渲染上传区域需要加载对应的css和javascrip静态资源文件,可以在http://www.dropzonejs.com
下载。有可以分别使用dropzone.load_css()
和dropzone.load_js()
方法从CDN加载静态资源。
保存图片
Flask-dropzone扩展承担了渲染界面、接收文件和验证工作,而保存图片需要我们自己完成。图片的保存包含两个更具体的操作:
Flask-Dropzone还扩展内置了一个生成随机文件名的函数:
from flask_dropzone import random_filename
filename = random_filename(f.filename) # 从原文件名生成新的文件名
Dropzone.js(Flask-Dropzone的静态资源)通过AJAX请求发送文件,每个文件一个请求,因此在完成上传后并不会将网页的控制权交给Flask的视图函数。一个不错的做法是提供一个按钮,让用户自行控制。你也可以配置键DROPZONE-DEDIRECT_VIEW
在完成上传后自动跳转到指定页面。
图片裁剪
网页中大量体积较大的图片会延缓网页的加载速度。一种可行的做法是,为一张图片保存同时保存多个不同大小的副本,在不同的场景下,使用不同大小的图片。(p333)
在用户还没有自定义头像时,可以为用户提供一个默认头像。Flask-Avatars在模板中开放了avatars类:
<img src="{{ avatars.defualt() }}">
默认头像太过简陋,你可以在注册时为用户生成随机的头像(像素样式)。配置如下:
AVATARS_SAVE_PATH = os.path.join(ALBUMY_UPLOAD_PATH, 'avatars')
AVATARS_SIZE_TUPLE = (30, 100, 200) # 同时生成三种尺寸的头像
然后就可以在User模型中添加生成头像的代码。扩展会自动生成和保存图像到文件系统中,你只需要再将文件的信息保存到数据库中即可。
from flask_avatars import Identicon
avatar = Identicon()
filenames = avatar.generate(text=self.username) # 可以通过一些参数进行更加丰富的配置(p336)
你可以创建一个类似Flask内置的static视图的视图函数,以方便在模板中通过url_for()
函数访问图片文件。(p337)
在用户的主页,可以展示图片的列表。如果你的用户对象使用location字段存储城市信息,你还可以将这个字符渲染为链接,通过将城市值拼接到Google地图的查询链接后,点击可以跳转到Gootle地图显示相关结果。
<a href="https://www.google.com/maps?q={{ user.location }}" target="_blank">
{{ user.location | truncate(20) }}
a>
truncate
过滤器可以将字符截取到指定长度后显示。
在图片标签的外部添加一个标签,可以用于访问图片的详情页面(即实现通过点击图片进行页面跳转)。(p340)
删除确定模态框
删除图片时,相比浏览器默认的确认窗口,使用模态框可以使页面的样式显得更加一致。如果通过jinja2的for循环,对每个删除按钮都生成一个模态框,最终的页面中将包含大量的模态框代码,但这些代码大部分通常都不会被使用。
更合理的方式是使所有的删除按钮都指向同一个模态框,
{# 删除按钮 #}
<a class="btn" data-tggle="modal" data-target="#confirm-delete" data-href="...">Deletea>
{# 模态框 #}
<div class="modal fade" id="confirm-delete" ...>div>
然后通过JavaScript代码,监听模态框的打开事件。(p345)
$('#confirm-delete').on('show.bs.modal', function(e){
$('.delete-form').attr('action', $(e.relatedTarget).data('href'));
})
编辑图片描述
常用的一个做法是添加一个编辑按钮,在用户单击时跳转到对应的编辑页面,在完成编辑后再跳转回来。但,更好的方式是让用户直接在当前页面编辑。这个效果也会借助javascript实现,直接把编辑表单渲染在图片描述的下方,但是通过css属性display
设置为none将表单先隐藏起来,在用户单击时通过javascript显示表单。(p347)
用户资料弹窗
我们希望当鼠标悬停在某个用户的头像上时,会显示一个小弹窗来显示用户的详细信息。大致流程:鼠标悬停触发javascript函数,发送AJAX请求到某个URL,URL对应的视图函数返回数据,javascript获取到返回的数据后更新页面的内容。
具体实现较为复杂,详见(p353)。
动态显示提示消息
此外,简单的程序在出错时,可以使用alert()
调用浏览器内置的弹窗组件进行提示,但不够美观。我们希望自定义一个这样的弹窗:在页面的上层动态显示,并在一定时间内自动隐藏。一般称为Toast。(p357)
前面已经学习过如何使用关联表来表示多对多关系,缺点是只能表示关系,不能存储数据(如我还想记录下收藏图片的时间戳)。这种情况下,我们可以使用关联模型来表示多对多关系。
在关联模型中,我们将Photo模型与User模型的多对多关系,分离成了User模型和Collect模型的一对多关系,和Photo模型与Collect模型的一对多关系。
不过,在使用了关联模型后,Photo和User模型中的关系属性(relationship)返回的不再是关系另一侧的记录,而是关系的中间人——Collect记录。需要进一步调用collector和collected才会加载对应的用户和图片对象。
class Collect(db.Model):
...
collector_id = db.Column(db.Integer, db.ForeignKey(user.id), primary_key=True)
collected_id = db.Column(db.Integer, db.ForeighKey(photo.id), primary_key=True)
collector = db.relationship('User', back_populates='collections', lazy='joined')
collected = db.relationship('Photo', back_populates='collectors', lazy='joined')
class User(db.Model):
...
collections = db.relationship('Collect', back_populates='collectors', cascade='all')
class Photo(db.Model):
...
collectors = db.relationship('Collect', back_populates='collected', cascade='all')
和图片收藏一样,用户关注也是多对多的关系,我们同样希望可以记录关注行为的时间戳,因此仍然使用关系模型来表示多对多关系。但不同之处在于,关注关系的两侧都在同一个模型User中,这种关系被称为自引用关系。
我们不妨令关注关系为Follow,则在Follow模型上建立反向关系时,SQLAlchemy没法知道哪个外键对应哪个反向属性,需要在关系函数中使用foreign_keys参数来明确对应的字段。(p366)
感觉这一段很绕!
补充:如果对关系属性设置了dynamic类型的加载方式,调用关系属性会返回查询对象,此时可以进行进一步的过滤操作。
事件绑定
前面提到了用户资料弹窗,我们希望在用户资料弹窗中也有一个关注按钮,但jQuery中,on()
以及快捷方法click()
、hover()
等事件处理器只能绑定到已经存在的元素,所以不能通过弹窗元素的id作为选择器,如
$('.follow-btn').on(...)
有两种解决方式:
$(document).on('click', '.follow-btn', follow.bind(this));
这里on()
方法的三个参数分别是:事件,选择器,触发的回调函数。
AJAX请求下的错误消息
因为AJAX请求异步发送,不会重载页面,因此不能使用flash()发送消息。我们可以在视图中返回包含提示消息的json响应,并使用jquery提供的ajaxError()
方法设置一个统一的ajax错误回调函数。(p377)
我们希望未读消息数是实时更新显示的,而不是需要手动刷新一次页面才能看到新的数据。这个功能可以通过轮询实现,即每隔一段事件自动发出一个ajax请求。在JavaScript中,使用setInterval()
方法可以周期性地按照时间间隔不停地执行代码,见(p384)。
前面介绍了如何为用户生成随机的头像,但作为一个社交程序,我们希望可以使用自己喜欢的图片作为自己的头像。Flask-Avatars通过集成jQuery插件Jcrop(需要在模板中加载对应的资源文件)提供了裁剪头像的支持。(p387)
实现自定义头像的功能需要两个表单,一个用于单纯的上传图片,另一个用于保存裁剪图片的坐标。
修改密码(疑惑)
书中修改密码增加了“新鲜的”标记判断,据说通过“使用session对象写入名为_fresh
的cookie实现”。但登录状态和是否新鲜这两个字段不都是写在session对象中吗?怎么能够判断“虽然用户仍然保持登录状态,但会话已经被标记为不新鲜”?
我们希望在首页可以看到自己关注的用户最近发布的图片,这个查询包含以下步骤:(p399)
这里有两种实现方式:子查询和联结,通常来说联结的效率更高。
# 子查询
followed_ids = db.session.query(Follow.followed_id).
filter(Follow.follower_id == current_user.id).subquery()
# 主查询
followed_photos = Photo.query.filter(Photo.author_id.in_(followed_ids)).
order_by(Photo.timestamp.desc()).all()
# 联结查询
Photo.query.join(Follow, Follow.followed_id == Photo.author_id)
.filter(Follow.follower_id == current_user.id)
联结查询的代码也显得更加简洁。
我们生活中常见的各种网站,基本上都会提供一个搜索框,大多数数据库引擎本身也提供了全文搜索的功能。但如果你希望实现的是一个不局限于某个数据库引擎的搜索引擎,可以考虑使用集成了Whoosh的Flask-Whooshee扩展,它同时也与SQLAlchemy扩展进行了集成。
全文搜索的原理:索引程序通过扫描数据库中的每一个词,对每一个词建立一个索引,指明它出现的次数和位置。
from albumy.extensions import whooshee
@whooshee.register_model('username', 'name')
class User(db.Model):
...
索引默认会在写入相应的字段后,自动更新。你也使用reindex()
方法可以重建索引:
whooshee.reindex()
对于写入频繁的程序,最好关闭索引的自动更新,使用定时任务工具周期性手动地重建(更新)索引。关于定时任务工具,你可以考虑使用Celery,或者扩展Flask-APScheduler。
使用搜索:
User.query.whooshee_search(q).all() # q为想要搜索的字符串
对于一个真实的程序来说,后台管理可能会包含以下内容:
书中只实现了对于用户和资源(发布内容、评论等)的关系,实现比较简单,不再介绍。
这一章真的非常非常的长,在书中有100多页。虽然我现在已经将这一部分看完了,但是自己用来边做边学的玩具项目却没有跟上。不知道这些新鲜的知识会不会很快就从我的脑海里消逝。
啃砖头书确实也是一件比较有成就感的事情。身边有朋友说感觉看书很浪费时间,有很多东西根本没必要去看。但是,网站上的一些视频或文档教程常常让我感觉,一半好像知道了,一半却直入云里雾里。还是砖头的作者更有耐心。比如,即使我没有学习过javascript,书中的解释也可以使我基本明白代码的意思。(也仍是一件需要好好权衡的事情,毕竟看书会很花时间)。
为什么会学习flask呢?我并没有对依靠它寻找工作抱有希望,Web开发已经卷疯了。它相对简单易学,我可以使用它尝试做一些自己的小工具,希望会对我的学习生活起到一些帮助作用。
在写笔记的过程中,也在继续慢慢地学习如何写笔记。记下需要注意的、容易出错的地方?记下主要的流程和思路?总之,还有很长的路要走。