flask的重复造轮子计划

是的,我又重复造了一个轮子—-而且她喵的还是一个没有完成的轮子。不过因为还算好玩,所以就放上来了。

这个项目的来源是暑假尾巴上,某个人组织的某个项目—-也就是一个动漫论坛,本来是打算用django实现,然后当时我也做出了一个再简陋不过的原型。所以您注意到了,前几篇博文中有一篇文章分析了那个原型的实现。

但是后来这个项目坑了。

具体的原因我不想细说,总之大概发生了一些对大家都不愿乐见的交流冲突,然后这个项目坑了。

某些时候,我算是一个相当念旧的人,况且我断断续续接触了一些写论坛的技术和经验,半途而废的事情我相当不喜欢,于是就重新捡起来重新做。正好,我原本就不算很喜欢django,作为对flask的练手,这个项目相当合适,于是我重写了它。作为对上个夭折的项目的纪念。由于上个项目叫做kutoto,所以这个项目姑且叫做kutoto_flask吧。

上面说过了,这个项目是对flask的练手,由于事先并没有这方面开发的经验,我参考了一个相当友好的开源项目flaskbb,虽然我对他过度模仿django的编程风格有一些意见,但是这依旧是一份完成度很高的实现方案,有兴趣的话,阅读其代码也许会让您对flask有一些新的理解。

这篇文章不会详细研究实现,如若希望了解整体的代码的话,请翻阅源代码。需要注意的是,作者目前对于flask理解并不深刻,也远远未到信手拈来的地步,所以理解并不一定正确。目前这个项目正在完成中,代码没有经过深思熟虑的重构过程,相当凌乱,不过理解起来应该问题不大才对。

首先说说flask。相较于django,flask要轻量很多,轻量的一个好处是,基本上,不用对着官方几百页的文档发呆,而且完成后,也有强烈的:这是我自己写的!这样独一无二的成就感。坏处是,对比起大又全的框架,一些功能就需要自己慢慢折腾了。幸运的是,flask有不少良好的插件,基本上可以最大限度地达成某个平衡。当然,某些方面而言,使用flask似乎会显得B格更高。另外,flask的语法相当优雅,官方的文档界面设计也比django高出一大截—-至少字大了不少,配色也深合我意。

然后是正题:如何用flask完成一个论坛。

我们依然把这个任务简化,分三步走:

第一步:实现一个能增删改查的界面;

第二步:实现登录的管理;

第三步:完整的权限设定(用户组);

当然,对有前端设计才能的现充们,还有前端的设计等着您。

于是首先是models

这一部分其实和上次django的那份实现道理上差距不大—-当然代码差距很大。

django有自己的ORM实现,但是flask没有。当然,flask推荐是使用sqlalchemy,当然,想要使用sqlalchemy的话,还必须install flask-sqlalchemy 这个请注意。

sqlalchemy的官方文档就我看来相当不友好—-主要是太丑了,字也小,看上去很吃力。当然,flask也提供了一份看上去更舒服的教程Flask-Sqlalchemy。这一份教程讨论了一些基本的东西,但是请注意,作为一份速成的教程,它讨论的东西终究是太少,很多参数都没有讨论。

这里随便对比一下django的ORM和Sqlalchemy的ORM。

就我自己的使用经验来说,django的ORM在语法上会更直观一些,特别是指定外键的时候,很方便很直观。反向查询使用起来比sqlalchemy好懂,多对多单对单这样的实现也比较无脑。sqlalchemy的话,反向查询写起来相当晦涩,但是参数真心很强大,相较django的使用也更加自由。当然我使用这两者的机会都不多,也许真实情况也不尽然是这样,但是目前的话,我还是觉得django的更简单……

sqlalchemy的models,每一个类的最开头都必须使用__tablename__="xxx",来显式指定表名,并不会自动生成。然后貌似id也不会自动添加。

Post里面使用了一个叫做use_alter的参数,这个参数的话,按照文档上的说法,只要两张表产生了相互引用,那么总应该指定这个参数。

举个栗子:

topics表中,出现了一个posts的对posts表的引用,但是问题是,posts表中有一个叫做topic_id的表项。这样就出现了相互循环引用。django的对象映射机制处理这个问题很笨拙,我记得必须自己处理。而sqlalchemy中只需要指定这个参数就好了。

然后Post中另外一个重要参数是 ondelete="CASCADE",这个参数的指定,表示,如果外键链接的那张表的相关表项被删除的话(parent row change),那么,这张表也会发生变化(the children row will change too)。

我重复一遍,sqlalchemy的反向查询个人觉得很不直观:posts = db.relationship("Post",backref="user",lazy="dynamic")这样的指定,可以通过执行Post.user来获取用户,但是总感觉这样的获取思路怪怪的。

上一次写kutoto的时候,对象的save操作我是放在form中的,也就是重写了form中的save函数,然后这个save函数被写得很复杂,当时感觉还是很不错的。flaskbb中,save函数是放在对应的类中的。然后form在提交的时候,当然也重写了save函数,但是那个save函数的执行,实际上是直接调用类的save方法。个人很喜欢这个设计,eat you own dog food。简明,直观,而且耦合更小。

顺便,我第一次知道了应该怎么正确处理密码的储存。django由于有了auth.User这个通用实现,诸如密码储存这样的细节实际上是隐匿的,个人并不喜欢这么多管闲事的做法……

以上是models的一些东西。

接下来是views

flask的路由由类似于@app.route('index')这样的包装器指定,相较于django的url.py文件,固然会看上去更加优雅,但是缺点也很明显,我这里压根没法对其解耦,所有的路由和处理函数全部耦合在一起,感觉很煞风景。flaskbb貌似做到了对其的解耦,但是我貌似沒弄懂他的实现……

好的来说,flask的路径处理很容易懂,写起来也很舒服。当需要提交表单的时候,只需要在包装器中指定methods=["GET","POST"]就好,也很好看。

然后是forms

django中有一套完整的form包,flask没有,但是有一个叫做Flask-WTF的插件漂亮地做了这部分。话说这名字我一直很想吐槽是怎么回事?

总的来说,差距并不大,同样都是需要继承Form的类,然后把需要生成的form项写进去就好,比如下面这段简明易懂的代码:

from flask_wtf import Form
from wtforms import TextField
from wtforms.validators import DataRequired

class MyForm(Form):
    name = TextField('name', validators=[DataRequired()])

当然,如果这个表单是需要储存某些东西的,那么您还需要实现save方法,正如前文所说,form只调用对应类的save方法是一种推荐的实现。

def save(self):
    user = User(username=self.username.data,
            email=self.email.data,
            password=self.password.data,
            date_joined = datetime.utcnow(),
            )
    return user.save()

诺,看上去不错吧?

form通常需要相匹配的html文件,而需要注意的是,form类总是会引入一个隐藏的,叫做form.csrf_token的玩意,必须把这东西放入form中的。当然,如果您引入的是form.hidden_tag()也是可以的。必须承认的是,我对跨域脚本攻击一点研究都没有。

基本的话,利用上面给出的部分,您大致可以做成一个能进行增删改查的"论坛",但是这并不够,我们还需要权限的控制—-至少,发帖,回复,编辑,删除这样的操作,只能限定为登录用户的特权。

登录这部分,实际上flask做得并不够,值得庆幸的是,flask提供了一个Flask-Login的有力工具。可以方便的进行权限管理。使用起来也很简单,必须登录才能进行的操作,对应的处理函数上,添加一个@login_required包装器就可以了。

当然也没这么简单。为了实现登录操作,我们至少需要三个处理函数register,login,logout,然后我们可以设定一个路由,比如@app.route('sanae'),给她设定@login_required,来测试登录是否成功。

register归根结底只是一个表单,按照一般表单的写法就好,比较不同的是,register需要检测,是否用户名和email地址已经被占用,然后调用password_hash去加密密码。

login更简单,如果用户账号密码输入无误,就把该用户记录到login_user()里面,login_user是Flask_Login提供的一个方法。大致应该是一个字典。

logout就是调用了logout_user()方法而已。

比较讨厌的是Reply的实现。因为个人的show_posts界面最下方就是一个表单,供快速回复,问题是,看帖子当然是不需要登录的,但是回复帖子就需要登录了,由于这两个处理(列出帖子和回复帖子)耦合在一起,所以没法直接应用@login_required包装器。个人的处理是,引入current_user这个参量。这个参量也是由Flask_Login所提供的。只需要在提交form的时候,验证current_user是否已经验证,如果验证,直接save帖子内容,否则,跳转回登录界面。

最后,我还使用了一个叫做Flask_Manage的玩意,用来管理manage命令,这个有兴趣可以看一下。

说一下,最近做东西进度很慢,原因是迷上了某个叫做ingress的现充游戏,虽然类似的大运动量游戏貌似不太符合我的风格,但是问题是最近真是沉迷到无法自拔,譬如说,这周有计划去大雁塔炸po什么的……

嘛,这些都是小事(乐观笑),总之项目慢慢做。

前几天读完了曼昆的《微观经济学》,然后又读完了《在世界中心呼唤爱》,也许有朝一日会写点读后感什么的,但是貌似又感觉很麻烦—-毕竟我真不是什么文学青年。最近感觉:每天空两个小时读读闲书真是挺棒的一个决定。

最近在看自然语言处理,为下一个项目做准备,话说吃着碗里看着锅里一向是我的好习惯?

总之其实我只是在逃避学习java……

以上。

逃避学习的AS。