上回书说到,网站初具雏形,但经高人指点,还是有很多不足。
本文将大胆扩充网站结构,目标是将网站拓展成一个 CMS 。
所以,不要停下来啊!👆(指开发


0x00 蓝图与重构

与之前相比,网站将增加以下功能:

  • 图库:文件上传模块
  • 评论:楼中楼功能
  • 后台:权限模块,后台模块
  • 优化:更健壮的数据库接口,更细致的权限控制
  • plus功能:用 redis 实现热搜

在开发这些功能之前,首先重整项目结构。如:

  • 完全蓝图化。参考
    • 模板也放入独立子目录里,蓝图注册时使用template_folder参数,不过这样容易产生bug,flask 官方推荐使用硬编码,汗。
  • 清理依赖,不使用维护状态差的库。
    • 开发新功能的时候,去哪里找最佳实践,找好用的库呢?有一个 Github 搜索小技巧,名为 "awesome-xxx" 的仓库通常是某技术的优质资源列表。如:https://github.com/humiaozuzu/awesome-flask

0x01 文件系统

本部分参考了李辉大佬的系列文章,
https://zhuanlan.zhihu.com/p/23731819?refer=flask

头像,照片……文件上传是绕不开的话题。在上一篇参考的教程中,头像的实现是由托管网站生成随机的图片。遗憾的是并没有像 ORM 一样方便数据库处理的文件处理框架可供使用,还是自己把他啃下来吧。

注意踩坑!大部份资料推荐使用Flask_uploads插件,然而使用该插件时出现如下报错:

ImportError: cannot import name 'secure_filename' from 'werkzeug' 

查阅Stackoverflow得知是PYPI源上的Flask_uploads插件不再维护了,于是和 werkzeug 库的api不兼容,是插件内在的bug。网上的解决方法有二:

  • 一是修改库源码
  • 二是换另一个库,维护良好且可无缝迁移,名为Flask-Reuploaded

前者不利于后续部署,本人倾向于后者。然而,使用新库也遇到了诸多麻烦,使用UploadSet.url()方法时,报错如下:

werkzeug.routing.BuildError: Could not build url for endpoint '_uploads.uploaded_file' with values ['filename', 'setname']. 

UploadSet.url()方法返回对应文件的可访问url,返回的url默认带有_upload/前缀,这是 Flask-Uploads 自带的路由,也被称为 autoserve 。
然而官方文档里有这样一句话

autoserve of uploaded images now has been deactivated; this was a poorly documented “feature”, which even could have lead to unwanted data disclosure; if you want to activate the feature again, you need to set UPLOADS_AUTOSERVE=True

看来 Flask-Reuploaded 的作者似乎认为文件读取功能与我无瓜。好吧,这部分我们自己实现。

// 浪费了一晚上debug,结果只是因为文档没看明白,再次证明读文档的重要性。

Flask-Reuploaded: 文件上传

插件将上传的一类文件抽象成集合UploadSet。对每个 Set 有如下操作:

  • 配置文件类型:photos = UploadSet('photos', IMAGES) //类型包括:IMAGES、TEXT、AUDIO……
  • 配置存贮路径:app.config['UPLOADED_PHOTOS_DEST'] // Photos 为 Set 的变量名
  • 保存文件:filename = photos.save(request.files['photo'])
  • 返回链接:photos.url(filename)

最后,注册 Set 和插件注册类似:configure_uploads(app, [avatars, photos]) //可一次性全部注册

实现头像上传的步骤如下:

  • 模型层: User 添加 avatar 字段,储存头像的文件名。原avatar()方法作为默认头像。
  • 表单层: edit_profile 表单增加FileField字段
  • 视图层: 储存文件,向数据库提交文件名。
  • 模板层: 改用硬路由获取url。// 最终 .url() 还是有bug,再次说明不要乱用不知名的插件

图库模块

本模块包括如下路由:

  • /index:主页显示瀑布流
  • /upload:上传接口:参考头像上传
  • /detail:详情,显示评论
  • /delete:删除接口:同时删除文件

由于本项目前端框架是 Bootstrap ,👴不想写Jquery,所以直接刷新页面,也不弄无限滚动了,按钮了事。另外为了不同列长度尽量均匀,故采用取巧的方法,平均分配。根据大数定理,只要随机图片足够多肯定会差不多均匀。。。。

0x02 评论系统

数据库设计

评论包含了两个一对多关系,既是评论和文章的一对多关系,也是评论和用户的一对多。为此,只需要给User和Post添加关系即可。
然而,我们希望设计统一的Comment模型,评论的对象既可以是文章,也可以是图片,也可以是其他评论。为此,添加一个枚举类型的字段指示评论类型,从而采用不同的处理逻辑。

楼中楼

而主流网站不光支持对文章评论,还支持楼中楼。对楼中楼的实现有以下几种方案:

  • 按时间平铺:以原百度贴吧为例
    • 添加 reply_id 字段,指示要回复的人
  • 套娃式缩进:以某些老式bbs为例
    • 添加 parent_id 字段,指示父评论(顶层评论则为本身id),在实体类中保存子评论列表
  • 弹窗式查看:以知乎,b站为例
    • 在按时间平铺的基础上,若 reply_id存在添加“查看对话”按钮,递归的构建对话并弹窗。

其中,第一种实现简单,用户不友好;第二种实现复杂,对多层级对话无法胜任;第三种是最主流的实现方式。

通过以reply_id作为指针,所有评论连接成了一棵树,在任意一个节点进行“查看对话”操作,就是执行树的寻根。“查看对话”函数如下:

    @staticmethod
    def view_dialogue(c_id):
        dialogue = [c_id]
        while Comment.query.get(c_id).type == 'comment':
            c_id = Comment.query.get(c_id).reply_id
            if Comment.query.get(c_id) is None:
                break
            dialogue.append(c_id)
        return dialogue

0x03 网站后台

网站的后台通常给管理员提供统一监管数据库的界面。有以下插件帮助实现:

  • Flask-admin:一键生成后台页面,并可以自定义视图和模型。
  • Flask-Security: 比admin层次更高,封装了常用视图和模板。但是文档少,且很多功能我们已经实现了,再使用它就要推翻重做。遂弃用。

本教程中使用了 RBAC(Role-Based Access Control) 基于角色的访问控制,简单说就是设计一个角色表,用户表和角色表用关联表实现多对多关联。这样做的好处是,针对角色的权限分配,修改权限时无需修改每个用户。

0x04 热搜

本节再加入一个重量级内容,利用 redis 实现浏览量排行榜,也就是热搜。当然,真正的热搜榜单排名规则更加复杂,这里只通过简单的浏览量计数来练习 redis 的使用。

【👴有时间再做】

不要让开发停下来

可以加的功能还有很多:时间线,emoji支持,多媒体,前后端分离(Vue),,

除了功能,当面对更高量级的流量时,网站性能便更加重要,这时候消息队列,PRC,微服务/分布式,,,更让人头秃。

Web开发之路,道阻且长。但是,只要开发不停下来,道路就会不断延申。。。(希望之花.mp3)