Flask是一个使用Python编写的轻量级web框架,相比其他框架如Django更加简单易学。本文将实现一个简单示例,完成以下功能:从数据库中读取用户信息,在浏览器中分页展示,添加分页导航栏,并且实现根据用户名搜索。用到的主要知识点:数据库操作、表单、Jinja2模板。
学习环境搭建
操作系统
CentOS Linux release 7.4.1708 (Core)
执行yum update -y然后重启系统,确保软件包处于最新状态。
Python版本 2.7.5
安装Python虚拟环境
利用virtualenv可以实现一个隔离的Python环境,在这个虚拟环境中安装、删除Python的pip包而不会影响环境外的其他进程。虚拟环境还会让系统移植更容易,只要把虚拟环境的文件拷到另一个系统中然后恢复虚拟环境,就可以保证跟之前的Python包环境一模一样。
安装virtualenv:
[[email protected] ~]# pip install virtualenv
Collecting virtualenv
Downloading virtualenv-15.1.0-py2.py3-none-any.whl (1.8MB)
100% |████████████████████████████████| 1.8MB 78kB/s
Installing collected packages: virtualenv
Successfully installed virtualenv-15.1.0
创建一个名为env的虚拟环境:
[[email protected] ~]# virtualenv env
New python executable in /root/env/bin/python2
Also creating executable in /root/env/bin/python
Installing setuptools, pip, wheel...done.
进入虚拟环境env:
[[email protected] demo]# source env/bin/activate
(env) [[email protected] demo]#
前面的(env)标识了当前shell所在的虚拟环境,把env目录拷到其他系统就可以进行环境移植。
安装数据库
安装数据和相关开发包:
[[email protected] demo]# yum install mariadb mariadb-server python-devel mariadb-devel -y
安装完后,执行mysql_secure_installation,设置数据库root密码,因为是测试环境,我们为了方便设置为“test”,生产环境的话一定要设置复杂密码。
简单示例实现
项目结构
我们创建一个demo目录存放项目文件,用tree命令查看这个目录的结构如下:
(env) [[email protected] ~]# tree -I 'env' demo
demo
├── app #程序包目录
│ ├── __init__.py
│ ├── main
│ │ ├── forms.py #定义表单类
│ │ ├── __init__.py
│ │ ├── views.py #定义视图函数,实际处理http请求
│ ├── models.py #定义数据模型
│ ├── static #存放静态文件
│ └── templates #存放模板
│ ├── _macro.html
│ ├── userlist.html
├── config.py #项目配置文件
├── manage.py #项目启动脚本
└── requirements.txt #项目依赖的所有pip包
各文件的主要作用已经在上面注释。Flask并没有限定项目的结构必须如此,但是通常这是比较合理的结构,也可以根据自己的项目特点进行微调。下面分文件详细介绍如何实现本文开头提到的各个功能。
requirements.txt
这个文件保存了项目依赖的所有包及其版本,方便其他人移植你的Flask项目。文件是由命令pip freeze > requirements.txt生成的,在要运行该项目的系统中执行pip install -r requirements.txt就可以安装完所有依赖包,很方便。这个示例的requirements.txt内容如下:
alembic==0.9.6
click==6.7
Flask==0.12.2
Flask-Migrate==2.1.1
Flask-Script==2.0.6
Flask-SQLAlchemy==2.3.2
Flask-WTF==0.14.2
ForgeryPy==0.1
itsdangerous==0.24
Jinja2==2.10
Mako==1.0.7
MarkupSafe==1.0
MySQL-python==1.2.5
python-dateutil==2.6.1
python-editor==1.0.3
six==1.11.0
SQLAlchemy==1.2.0
Werkzeug==0.14.1
WTForms==2.1
config.py
项目的配置文件,集中存放配置参数,内容如下:
class Config:
SQLALCHEMY_DATABASE_URI = "mysql://root:[email protected]/demo"
SQLALCHEMY_COMMIT_ON_TEARDOWN = True
SECRET_KEY = 'hard to guess string'
@staticmethod
def init_app(app):
pass
文件中配置了数据库的访问方式,还有SECRET_KEY,这是一个密钥,用于保护表单免受跨站请求伪造(CSRF)的攻击。
models.py
编写业务逻辑前要先设计好数据模型,我们的示例的数据很简单,一张表——用户信息,包括两个字段:用户名和用户邮箱地址。代码如下:
from . import db
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
email = db.Column(db.String(64), unique=True, index=True)
def __repr__(self):
return '<User %r>' % self.username
@staticmethod
def generate_fake(count=100):
from sqlalchemy.exc import IntegrityError
from random import seed
import forgery_py
seed()
for i in range(count):
u = User(username=forgery_py.internet.user_name(True),
email=forgery_py.internet.email_address())
db.session.add(u)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
Flask-SQLalchemy是Flask的数据库扩展,通过对象关系映射ORM,把对数据库的操作转换为对Python对象的操作。Class User在数据库中对应的就是一张表,表名在__tablename__中设置,两个属性username和email对应表字段。代码中还用到了一个很有意思的模块forgery_py,它用于随机生成符合要求的测试数据,函数generate_fake()随机生成数量等于count条的数据并写入数据库。生成数据的具体操作方法会在最后执行程序的时候讲。
app/__init__.py
程序包的构建文件,在这文件里定义创建程序实例的函数create_app,也叫工厂函数。工厂函数完成3个主要工作:
1. 从config.py中导入程序配置;
2. 大多数Flask的扩展需要在这个函数里初始化,例如我们的示例用到的Flask数据库扩展Flask-Sqlalchemy;
3. 还有一个工作是给程序实例注册蓝本(blueprint),蓝本用于定义URL到视图函数的路由。
代码如下:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from config import Config
db = SQLAlchemy()
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(Config)
Config.init_app(app)
db.init_app(app)
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
manage.py
manage.py是整个程序的启动脚本,从上面的app/__init__.py导入工厂函数create_app(),然后创建程序实例,初始化Flask的扩展Flask-Script和Flask-Migrate,代码如下:
#!/usr/bin/env python
#conding: utf-8
import os
from flask.ext.script import Manager, Shell
from flask.ext.migrate import Migrate, MigrateCommand
from app import create_app, db
from app.models import *
app = create_app('default')
manager = Manager(app)
migrate = Migrate(app, db)
def make_shell_context():
return dict(app=app, db=db, User=User)
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
forms.py
表单的功能是接收用户提供的数据。用户将数据填到表单的各字段中,通过Post方法提交给服务端。在我们的示例中,表单用于接收用户输入的用户名,用于后续的搜索功能。代码如下:
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class Form(FlaskForm):
name = StringField('Username', validators=[DataRequired()])
submit = SubmitField('Search')
flask.wtf是Flask框架处理表单的扩展,我们自己定义的表单Form继承该模块中的FlaskForm类。Form很简单,一共2个字段,StringField提供给用户填写要搜索的用户名,检测函数DataRequired()确保数据不为空;SubmitField是一个提交按钮,用户点击按钮后,就会把表单数据提交给服务端。
views.py
该文件中定义路由和视图函数,视图函数是实际处理http请求的函数。我们的示例只有一个视图函数,代码如下:
from flask import render_template, url_for, request, session
from . import main
from .. import db
from ..models import User
from .forms import Form
@main.route('/',methods=['GET','POST'])
def index():
page = request.args.get('page',1,type=int)
pagination = User.query.order_by(User.id).paginate(
page,per_page=10,error_out=False)
form = Form()
users = pagination.items
if form.validate_on_submit():
session['username'] = form.name.data
users = User.query.filter_by(username=session.get('username')).all()
return render_template('userlist.html',users=users,pagination=pagination,form=form)
main是一个蓝本,修饰器@main.route定义的路由的含义是,访问“/”的的GET或POST方法都由index()函数处理。开头提到了我们要实现用户信息的分页展示,所以index()先从全局变量request中获取了页数,默认显示第1页。
然后分页查询数据库,每页10个数据;
然后初始化搜索表单,If判断把视图函数的执行结果分成了表单有数据和没数据两种情况:如果表单有数据,则把表单数据存到会话session中,并且查询数据库,把符合条件的用户传递给模板;如果表单没有数据,就把所有用户传递给模板。模板根据收到的用户信息生成html页面。
视图函数最核心的任务就是返回一个html页面给客户端,render_template()通过渲染模板生成一个html页面,所谓渲染就是根据传入的参数去修改模板中对应的可变部分。模板将在下一部分详细介绍。
templates
Flask集成了Jinja2模板引擎,templates这个目录存放所有jinja2模板。模板中有一些可以被替换的部分,渲染流程也支持通过条件判断或循环控制。
_macro.html中定义了一个宏pagination_widget,宏类似于Python中的函数,这个宏用于生成分页导航栏,它接收的参数是数据库分页查找返回的对象pagination和视图函数名endpoint。pagination对象支持一系列方法和属性:
l 方法has_prev()和has_next()分别检测当前页之前和之后还有没有页,返回布尔值;
l 方法prev()和next()分别返回上一页和下一页的分页对象
l 属性page返回当前页码;
l 方法iter_pages是一个迭代器,根据参数返回一组页码;
_macro.html代码如下:
{% macro pagination_widget(pagination, endpoint) %}
<style>
li{ list-style:none;}
</style>
<ul class="pagination">
<tr>
<td{% if not pagination.has_prev %} class="disabled"{% endif %}>
<a href="{% if pagination.has_prev %}{{ url_for(endpoint, page=pagination.prev_num, **kwargs) }}{% else %}#{% endif %}">
«
</a>
</td>
{% for p in pagination.iter_pages(right_current=2) %}
{% if p %}
{% if p == pagination.page %}
<td class="active">
<a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
</td>
{% else %}
<td>
<a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
</td>
{% endif %}
{% else %}
<td class="disabled"><a href="#">…</a></td>
{% endif %}
{% endfor %}
<td{% if not pagination.has_next %} class="disabled"{% endif %}>
<a href="{% if pagination.has_next %}{{ url_for(endpoint, page=pagination.next_num, **kwargs) }}{% else %}#{% endif %}">
»
</a>
</td>
</tr>
</ul>
{% endmacro %}
其核心内容是中间的for循环,根据iter_pages()方法返回的页码,生成指向该页码的URL的链接。因此分页导航栏本质是由若干链接组成的,只不过这些特殊链接都指向与自己名字相关的URL。执行效果会在本文最后一部分看到。
userlist.html是视图函数index()真正渲染的文件,它通过一个列表来展示视图函数传给它的参数users中包含的用户信息。然后它引用了上面定义的宏,生成了分页导航栏。在最后是搜索表单。
{% import "_macro.html" as macro %}
<html>
<style>
li{ list-style:none;}
</style>
<body>
<h4>用户列表:</h4>
<ul>
{% for user in users%}
<li>{{ user.username }}: {{ user.email }}</li>
{% endfor %}
</ul>
<div>
{{ macro.pagination_widget(pagination, '.index')}}
</div>
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name() }}
{{ form.submit() }}
</form>
</body>
</html>
执行效果
生成测试数据
(env) [root@localhost demo]# python manage.py shell
manage.py:6: ExtDeprecationWarning: Importing flask.ext.script is deprecated, use flask_script instead.
from flask.ext.script import Manager, Shell
manage.py:7: ExtDeprecationWarning: Importing flask.ext.migrate is deprecated, use flask_migrate instead.
from flask.ext.migrate import Migrate, MigrateCommand
/root/demo/env/lib/python2.7/site-packages/flask_sqlalchemy/__init__.py:794: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future. Set it to True or False to suppress this warning.
'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
>>> db.create_all()
>>> User.generate_fake(100)
db.create_all()创建所有表,User.generate_fake(100)生成表中的数据。
启动程序
Flask框架内置了一个用于测试的web服务器,启动服务:
(env) [root@localhost demo]# python manage.py runserver -h 0.0.0.0 -p 5000
manage.py:6: ExtDeprecationWarning: Importing flask.ext.script is deprecated, use flask_script instead.
from flask.ext.script import Manager, Shell
manage.py:7: ExtDeprecationWarning: Importing flask.ext.migrate is deprecated, use flask_migrate instead.
from flask.ext.migrate import Migrate, MigrateCommand
/root/demo/env/lib/python2.7/site-packages/flask_sqlalchemy/__init__.py:794: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future. Set it to True or False to suppress this warning.
'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and '
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
访问页面
打开一个浏览器,输入URL http://x.x.x.x:5000/,x.x.x.x替换为你的测试机的ip,可以看到以下页面:
数据库中一共存了100条用户记录,分为了10页展示,点击下面的分页导航可以跳转页面,下面是第9页的内容:
然后测试搜索功能,在表单中输入“amy63”,点击按钮,结果如下:
原文地址:http://blog.51cto.com/3646344/2120170