近期在搞一个简单的项目和库存管理系统,用Flask+Gunicorn边学边做,有一些心得如下,和大家分享。老司机们别笑;)
1. Gunicorn真好用
a) Flask自己的服务器性能很差,动不动抽风,只适合拿来开发和调试用。用Gunicorn按(1 + 2 * Ncpu)个worker启动之后,在只有10+用户量的系统里再没碰到性能问题,丝般顺滑。
b) Gunicorn的安装方式很简单,直接pip install gunicorn,启动告诉它Flask实例的变量名(比如`app`)就好了。然后Gunicorn每次接收到请求就通过app(env, response)的方式(Flask类重载了__call__)去调用一个worker里的实例。
c) 使用Gunicorn之后,更新程序也不用去kill process了。我现在的更新方法是自己电脑上的新版本代码git push到代码服务上,然后云服务器上git pull。记住Gunicorn的daemon进程的PID,通过发送HUP信号给Gunicorn的daemon进程,它就会自动kill并boot新的worker进程。命令如下:
$ kill –SIGHUP {PID}
d) 我的Gunicorn启动命令:
$ nohup gunicorn --workers=3 {package}:{variable} -b host:port &
2. ORM很方便
a) 尽管ORM存在性能问题,不过可以通过自己定制SQL语句优化。我在之前基于zabbix2.x版本数据库做后台开发的项目中尝试不用ORM,写了很多很多SQL代码。后来需求改了,重构的体验太差了,非常地不灵活。现在老老实实先用ORM做初步开发,非常的舒适。
b) 倒序查询示例:
db.session.query.order_by(db.desc(User.id)).all()
c) 参考<Flask Development>,目前我所有的db.relationship都用了lazy参数。遇到过一个小问题,比如我想知道User关联的`proeject`表中有几个项目,我直接len(u.projects)就出错了。因为u.projects其实还是个Query类。我暂时的fix方法是len(u.projects.all())。后续应该引入SQLAlchemy中的func类里的count作更加合适的改写。
d) merge好方便。前期开发偷懒用的SQLite,后来需要迁移数据到Mysql,需要SQLAlchemy做类似于“INSERT … ON DUPLICATE KEY UPDATE …”的功能(这里还有一个坑是把大文本写入Mysql时要把表的编码设置成utf8mb4,utf8会报”只支持3个字节”的异常)。
3. 修改flask-bootstrap默认采用的外国cdn源
默认flask会先找本地有没有jquery和bootstrap的资源,没有就加载cdn。我觉得cdn肯定比我的1M小水管快,那这cdn源的定制就是个问题了。
对一开始被Bootstrap(app)过的flask应用,通过python manage.py shell命令进入命令行,有:
>>> bootstrap = app.extensions['bootstrap']
>>> cdns = bootstrap[‘cdns’]
>>> cdns
{'local': <flask_bootstrap.StaticCDN object at 0x00000291D4273DA0>,
'static': <flask_bootstrap.StaticCDN object at 0x00000291D4273F28>,
'bootstrap': <flask_bootstrap.ConditionalCDN object at 0x00000291D427F438>,
'jquery': <flask_bootstrap.ConditionalCDN object at 0x00000291D427F9B0>,
'html5shiv': <flask_bootstrap.ConditionalCDN object at 0x00000291D427FE80>,
'respond.js': <flask_bootstrap.ConditionalCDN object at 0x00000291D427FEF0>}
>>> cdns['bootstrap']
<flask_bootstrap.ConditionalCDN object at 0x00000291D427F438>
>>> cdns['bootstrap'].primary
<flask_bootstrap.StaticCDN object at 0x00000291D4273DA0>
>>> cdns['bootstrap'].fallback
<flask_bootstrap.WebCDN object at 0x00000291D427F0F0>
>>> cdns['bootstrap'].fallback.baseurl
'//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/'
跟到这里,我们如果把这个baseurl修改了,且static目录下没有已经存放的bootstrap的css和js资源时,网页会加载我们修改后的cdn url。
当然我们可以直接修改flask-bootstrap的源码来更改cdn,不过这样我们要把代码部署到其他环境时就不能pip install -r requirements.txt就能运行了。
我的办法是自己建立WebCDN对象,替换app实例里ConditionalCDN.fallback的对象。
关键代码如下:
from flask_bootstrap import Bootstrap, WebCDN
# ...省略代码...
BOOTSTRAP_URL = 'http://apps.bdimg.com/libs/bootstrap/3.3.4/'
bootstrap_cdn = WebCDN(BOOTSTRAP_URL)
app.extensions['bootstrap']['cdns']['bootstrap'].fallback = bootstrap_cdn
# ...省略代码...
这样页面在加载时就会改用国内百度的bootstrap源了。
4. flask-wtf的一些问题
偷懒起见,我直接采用wtf.quick_form()宏来渲染页面表单。但是我有一个需求,类似于“修改资料”的页面,表单给用户时,输入项带上数据库中原先有的数据,以避免用户还需要再填写一遍。一开始我用的方法是在Form实例的Field具体属性上,我修改它的`default`属性,类似这样:
class Form(FlaskForm):
name = StringField('What\'s your name?')
submit = SubmitField('Submit')
def view_func():
if requet.method == 'Post':
pass
else: # request.method == 'GET'
form = Form()
name_old = get_name_from_db() # 从数据库拿到了名字
form.name.default = name_old
return render_template('form.html', form=form)
然后我发现渲染出来的页面并没有带上name_old。根据Stack Overflow上相关的问题,根据不同的wtform版本,这个方法有的管用有的不管用。
目前wtforms针对默认填入值的功能,正确的实现方法是在创建表单对象时传入一个data字典,把上面代码改成这样:
class Form(FlaskForm):
name = StringField('What\'s your name?')
submit = SubmitField('Submit')
def view_func():
if requet.method == 'Post':
pass
else: # request.method == 'GET'
name_old = get_name_from_db() # 从数据库拿到了名字
form = Form(data={'name': name_old})
return render_template('form.html', form=form)
这样可以成功在渲染页面时带上默认值,同时如果default和data的值不一样时,app会用data中的值覆盖。