从flaskbb,june到lolipop(一)

这是对刚上线的论坛程序lolipop的一些笔记。如果有兴趣学习flask,特别是打算写一个论坛的话,这里谈论的某些细节大概会对您有所裨益,同时(希望)能帮助您避开某些可能的坑。不过请注意,这篇文章不会指导您如何去做一个论坛,仅仅是希望您知道,您可能会用到这里的某些内容而已。lolipop的演示地址暂时是这个:浅间神社

这篇文章只谈技术,不谈风月。

先说说标题,flaskbb是一个很容易搜索到的基于flask的论坛项目,特点是代码理解难度很低,通过阅读这份代码并且跟写一遍的话,会对flask有一个相当不错的了解。但是必须指出的是,原作者的代码还很稚嫩,并不能完全体现出flask的全部魅力(当然我也不行,这只是和june作对比而已)。不过特别值得一提的是,这个项目还在稳步地开发之中,虽然我没读过最新的代码,但是我相信现在的代码质量会更高。而june,同样也是基于flask的论坛项目,代码很优雅,较为浅易,也是我主要模仿的项目。前端整个是移植她的(当然,移植并不完善),后端也借鉴了不少。这里对这两份优秀的项目致以深切的感谢。

顺便,上次说到了提笔忘言的问题,于是我这次使用了印象笔记,把遇到的值得记录的点做了一下笔记,刚才看了一下,一共是十九了(补记:写这篇文章的时候又增加了……),那么,这里主要讨论的就是这十九个点。

请使用Blueprint来归类您的代码

flask本身提供了Blueprint这个函数,老实说,很难从字面上去理解这玩意到底是干嘛的。举个栗子,您当然希望所有和话题相关的create,edit等操作,路由上都写成/topic/create以及/topic/edit这样的形式,但是,您并不想每次都笨拙地指定/topic/这样的重复部分,这时候您就需要用上Blueprint。另一方面,按照flask教程,注册路由都是像@app.route('xxxx')这样的形式对吧?但是这岂不是意味着,所有的路由处理函数都得写在一个文件么,相当不友好不是么。分拆路由到不同的文件,也是Blueprint的任务之一。

这样说似乎太过空泛了一些,有兴趣可以读一读Blueprint的定义:Blueprint。我这里的话,用lolipop里面的代码段进行说明。

回到最开始的那个和话题相关的问题上去。

我们并不希望@app.route(/topic/create) @app.route(/topic/edit)这样笨拙的路由声明方式,所以引入了Blueprint,然后这样使用:

我们把与路由相关的全放在views这个文件夹中,然后在里面的topic.py文件中这样写。

bp = Blueprint('topic',__name__)

@bp.route('/')
def topics():
    pass

@bp.route('/create')
def create():
    pass

注意到了么,我们这时候的路由不是@app.route了,而是@bp.route这样。然后只需要在app中注册路径就好。注册了之后/topic/create就会执行create函数。

from views import topic
app.register_blueprint(topic.bp,url_prefix='/topic')

向jinja2中注册过滤器和函数

在jinja模板中使用一些小巧的过滤器会很方便,比如时间处理函数xmldatetime之类的。

@app.template_filter('xmldatetime')
def xmldatetime(value):
    if not isinstance(value, datetime.datetime):
        return value
    return value.strftime('%Y-%m-%dT%H:%M:%SZ')

但是,我想讨论的不是这个,而是,我们可以写一个过滤器,对一些后台处理起来比较麻烦,耦合度也比较高的操作,进行一个统一的处理,并且可以降低耦合度。

我当时的问题是,前端的许多输入都是markdown,我需要转换。最开始的想法是写一个函数,对前端的输入进行转换后存入数据库—-这应该是一个自然的思路。但是很快,就出现了麻烦事。当处理edit这个功能的时候,很明显的,我需要把上一次的输入字符串完整地填写在输入框中,供修改。但是数据库中已经不是原字符串了,而是处理过的html字符串,这无疑相当不友好,也很傻。后来我考虑了一下是否应该针对edit重写那个转换函数,但是很快我就发现自己的错误—-其实渲染显示字符串基本上是前台的事,数据库存原字符串就很好了。在需要转换的地方放一个过滤器,简单轻快。

注册函数我最开始想当然地认为和注册过滤器没什么大的不同—-似乎至多就是调用的包装器不太一样,但是事实上,并不一样。为什么是这样不亲切(?)的设置我至今没想清。顺便,事实上这个过程不叫注册函数,而是叫注册上下文处理器(context processor)。似乎后者更加高大上?

from views.admin import load_sidebar_notice
@app.context_processor
def register_context():
    return dict(
            load_sidebar_notice=load_sidebar_notice,
        )

如同上面,注册函数,比较麻烦的是返回的是一个字典。

分页

sqlalchemy提供了一个叫paginate的方法,当然,返回的也是Paginate类。这个类很好玩,我如是觉得。网上有一份这玩意的简易版代码,基本把精髓是弄出来了。

在我不知道这玩意之前,说说我是怎么办的吧。我会找出实例列表的长度和每页应该容纳的元素个数,进行一个小计算。这个小计算也是有很多坑的,方法太丑无法直视,特别是,这代码没法复用。

paginate封装了一些很有用的方法,但是请注意,老实说,这不是让你在后台进行分页处理的。我现在才理解,分页这个操作应该在前台完成,才能最大程度地进行解耦,并且看上去会很优雅。

jinja是支持宏的,所以可以写一个分页宏,很容易就可以实现代码复用。这段分页宏是出自june的,话说我很怀疑这玩意有一些年头了……

{% macro pagination(paginator, url) %}
<div class="pagination pagination-centered">
  <ul>
    {% for page in paginator.iter_pages() %}
      {% if page %}
        {% if page == paginator.page %}
          <li class="active"><span>{{page}}</span></li>
        {% else %}
          <li><a href="{{url}}?page={{page}}">{{page}}</a></li>
        {% endif %}
      {% else %}
      <li class="disabled"><span class="ellipsis">…</span></li>
      {% endif %}
    {% endfor %}
  </ul>
</div>
{% endmacro %}

filter和filter_by

sqlalchemy中,元素过滤有两种方法,filter和filter_by。

相当容易混淆。

这里请先容我做一个定义,相较而言,filter_by适合单表查询,filter适合多表查询。

简单说一说区别。

filter_by进行过滤的时候,比如Topic.query.filter_by(user_id=uid),用个不太恰当的术语,我们如果称user_id为左值,uid为右值的话(相对左右而言,不是C语言中定义)。filter_by的左值是Topic对应那张表的字段名之一,或者,由于是ORM,也可以说,是Topic那个类中的一个属性。通常,右值是一个固定值。

filter相对filter_by其实会强大一些,Topic.query.filter(Topic.id = Profile.id),看到区别了吧?这里的左值,需要指名表(类)和字段(属性)。右值也通常是这样。

其实使用哪个基本是习惯的问题,个人倾向于前者。

user和profile的分离

lolipop中,我对user进行了拆分,只在user中保留了最主要的一些属性,剩下的东西统统塞入了profile中。没什么理由,当然有一个原因是我不太欣赏一张表中字段名太多,另外也是,user中方法比较多,字段多的话看着眼睛容易花。profile没什么特殊的操作,相当单纯。

当然,这样的拆分引入了一些新的问题,比如后面的Form填充问题。但是,使用python的一个好处就是,你不特意在意是否之前的数据库设计得合理,总有一些trick方法可以精巧地修正最初的小失误,并且代价很低。

trick方法我会在下一篇文章写到,基本上,就是热插拔的部分思路,会涉及到一些简单的函数式编程。

profile的id和user的id保持一致

由于user和profile本来就是从user中拆分出来的,所以似乎id保持一致很容易理解。

但是,为什么不在profile中另开一个user_id的字段呢。

两个方面:user_id本身是不必要的,确定无疑的说,当我没打算给每个用户增加多套profile的时候,user_id和id一定是一样的。这就形成了冗余字段(flask是单进程的,不太可能出现竞争的情况)。另一方面,引入了这个字段,势必会在user中再增加一个不必要依赖,白金不爽。

get_or_create

june中有这一小段代码,虽然不起眼但是很精巧。

由于user和profile进行了分离,所以,个人资料显示的时候,需要同时得到两个封装好的实例才对。但是,当user存在,profile没建立的时候,get操作会因为404而失败。所以,我们增加了这一个简单的方法,避免get不到的错误。

在app中注册一些属性

一些配置,我们希望全局访问—-比如项目的根路径—-每次输入同样的字符串,不仅乏味,而且很容易错。所以,用一个能全局访问的元素去储存是一个相当明智的选择。

app是可以全局访问的,你需要的只是from flask import current_app这样就可以访问当前的app了。

不仅仅是app能够全局访问,g对象也是可以的,但是这东西我没用过。

哈?怎么用app储存?

app[ROOTDIR] = '/path/to/your/project'不就好了?

PAUSE

看了看印象笔记里面的记录,这里大概写了一半的点……orz,真是一项艰苦的活计。

下一次讨论的会有几个小点,然后就是一个比较大的主题了……希望我能驾驭住,orz。

于是,看到这里的您辛苦了,晚安!

写博客写了两个小时,困到不行的AS……

以上!