大纲
- Flask基础
- 模板
- 数据库
- 其他
Flask基础
- web应用程序的核心:请求《-》响应
- 为什么要使用框架?
- web网站发展至今,特别是服务器端,涉及到的知识,内容,非常广泛。这对程序员的要求会越来越高。如果采用成熟,稳健的框架,那么一些基础的工作,比如,安全性,数据流控制,都可以让框架来处理,那么程序开发人员可以把精力放在具体的业务逻辑上面,使用框架的优点:
- 稳定性和可扩展性强
- 可以降低开发难度,提高开发效率
- 一句话:避免重复造轮子
- 以后的程序开发一定是趋于越来越简单的方向的。程序员要专注的是具体的逻辑和思路,而不是繁琐的代码编写
web框架主要帮我们搞定了路由分发
- web应用程序核心两件事
- 1.定义路由
- 2.定义视图函数
- web应用程序核心两件事
Flask诞生于2010年(django,2003),是用python基于Werkzeu工具箱编写的轻量级web开发框架
- Flask本身相当于一个内核,其他几乎所有的功能都需要用到扩展
- 核心是Werkzeug(路由模块)和Jinja2(模板引擎)
- Flask相比于Django,Tornado等框架,是最灵活的框架之一,这也是flask收到滚滚达开发者欢迎的原因之一
- Flask 常用拓展包
- FLask-SQLalchemy:操作数据库
- Flask-migrate:管理迁移数据库
- Flask-Mail:邮件
- Flask-WTF:表单
- Flask-Bable:提供国际化和本地化支持,翻译
- Flask-script:插入脚本
- Flask-Login:认证用户状态
- Flask-OpenID:认证
- Flask-RESTful:开发REST API的工具
- Flask-Bootstrap:集成前端Twitter Bootstrap框架
- Flask-Moment:本地化日期和时间
- Flask-Admin:简单而可扩展的管理接口的框架
Flask和Django的区别
- Django功能大而全,Flask只包含基本的配置
- Django是一站式解决的思路,能让开发者不用在开发之前就在选择应用的基础设置上花费大量时间,django有模板,表单,路由,认证,基本的数据库管理等内建功能,Flask只是一个内核,默认依赖两个外部库:jinja2模板引擎和WerkzuegWSGI工具集,其他很多功能都是以扩展的形式嵌入使用
- Flask比Django更灵活,用FLask来构建应用之前,选择组件的时候给开发者带来更多的灵活性,可能有的应用场景不适合使用一个标准的ORM,或者需要与不同的工作流和模板系统交互
- Flask2010年发布,Django是2003年发布(现阶段的拥有大量插件和拓展的版本发布于2005年)
hello world
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "hello world!"
if __name__ == '__main__':
app.run()
Django把一个项目分成各自独立的应用,而Flask认为一个项目应该是一个包含一些视图和模型的单个应用,当然我们也可以在Flask里做出像Django那样的结构
Flask程序运行过程
- 所有Flask程序必须有一个程序实例
- 当客户端想要获取资源时,一般会通过浏览器发起HTTP请求
- 此时,Web服务器使用WSGI协议,把来自客户端的所有请求都交给Flask程序实例,程序实例使用Werkzeug来做路由分发(URL请求和视图函数之间的对应关系)
- 根据每个URL请求,找到具体的视图函数并进行调用
- Flask中,路由的实现一般是通过程序实例的装饰器来实现
- Flask调用视图函数后,可以返回两种内容
- 字符串内容:将视图函数的返回值作为相应的内容,返回给客户端(浏览器)
- HTML模板内容:获取数据后,把数据传入HTML模板文件中,模板引擎负责渲染HTTP相应数据,然后返回响应的数据给客户端(浏览器)
实例
- 导入Flask类:
from flask import Flask
- Flask函数接收一个参数name,它会指向程序所在的模块
app = Flask(__name__)
- 装饰器的作用是讲路由映射待视图函数index
@app.route('/') def index(): return 'hello world'
- Flask应用程序实例的run方法启动web服务器
if __name __ == '__mian__': app.run()
初始化参数
- import_name:模块名
- static_url_path:静态文件访问前缀
- static_folder:默认‘static’
- template_folder:默认‘templates’
配置参数
- app.config.from_pyfile(“yourconfig.cfg”)
- app.config.from_object()
读取配置参数
- app.config.get()
- 在视图中:current_app.config.get()
app.run的参数
- app.run(host=”0.0.0.0”, port=5000, debug=True)
请求方式限定
- 使用method参数指定可接受的请求方式,可以是多种
@app.route('/', method=['GET','POST']) def hello(): return 'hello world'
路由的查找方式
- 同一路由指向两个不同函数,在匹配过程中,至上而下依次匹配
给路由传参
- 有时候我们需要将一类URL映射到同一个视图函数处理,比如,使用同一个函数来显示不同用户的订单信息,此时可以使用路由正则传参的形式
- 传参格式:<转换器:参数名> ,不加转换器,<参数名>,系统会将传递的参数默认当做string处理
@app.route('/orders/<order_id>') def hello_world(order_id): # 此处的逻辑:去查询数据库当前用户的订单信息,并返回 print type(order_id)# 类型为unicode return 'hello world %d'% order_id
- 这里指定转换器int,会调用系统的路由转换器进行匹配和转换
- 大致原理是将参数起那个强转为int,如果成功,则可以进行路由匹配
- 如果参数无法转换成功,就无法匹配该路由
@app.route('/orders/<int:order_id>') def hello_world(order_id): print type(order_id) # 类型为int return 'hello woeld %d' % order_id
重定向示例
- 方式1:使用redirect
from flask iport redirect @app.route('/redirect') def redirect_demo(): return redirect('http://www.baidu.com')
- 方式2:使用redirect和url_for
from flask import redirect, url_for @app.route('/redirect') def redirect_demo(): # url_for:根据传入的视图函数名,找到对应的路由地址 # 两者通常结合使用 print url_for('index') return redirect(url_for('index'))
返回JSON
- 使得返回的数据是一个json格式的数据,这是前后端通常使用的交互的数据格式
方式1:使用json.dumps()
- 仅仅是将字典转换为Json格式的字符串
- 返回的响应头为:Content-Type为test/html
from flask import Flask, json @app.route('/json') def do_json(): hello = {"name":"michale", "age":18} return json.dumps(hello)
方式2:使用jsonify
- 会将字典转换为JSON格式的字符串
- 同时会设置响应头Content-Type为application/json
- 建议使用jsonify, 前后端分离的项目中,前后端都通过JSON进行数据交流
from flask import Flask,jsonify @app.route('/jsonify') def do_jsonify(): hello= {"name":"michael", "age":18} return jsonify(hello)
返回状态码示例
- 前后端交互,后端需要在处理完逻辑后,返回状态码信息,告诉后端处理的结果
- 在Flask中返回状态码的方式有两种:
- 1.直接return
- 可以自定义返回状态码,可以实现不符合http协议的状态码
- 例如:error=666, errmsg=’查询数据库异常’, 作用为实现前后端数据交互的方便
- 2.abort方法
- 只会抛出符合http协议的异常状态码,用于手动抛出异常
@app.route('/') def hello_world(): return 'hello world', 666
正则路有示例
- 在web开发中,可能会出现限制用户访问规则的场景,那么这个时候就需要用到正则匹配,限制访问,优化访问,这一点Flask没有Django方便,通常需要自定义一个正则路由转换器
步骤
- 1.导入转换器包
from werkzeug.routing import BaseConverter
- 2.自定义转换器并实现
class Regex_url(BaseConverter): def __init__(self, uel_map, *args): super(Regex_url, self).__init__(uel_map) self.regex = args[0]
- 3.将自定义转换器类添加到转换器字典中
app.url_map.converters['re'] = regex_url
- 4.使用
@app.route('/user/<re("[a-z]{3}"):id>') def hello_world(id): return 'hello %s' %id
系统自带的几种转换器
- 系统提供了6个转换器可供开发者使用,当使用<>来接受参数的时候,就会调用系统默认的转换器进行匹配
- 默认的就是UnicodeConverter,不填写转换器名称,或者使用default,string都是一样的
- 默认将参数当做普通字符串处理
DEFAULT_CONVERTERS = { 'default': UnicodeConverter, 'string': UnicodeConverter, 'any': AnyConverter, 'path': PathConverter, 'int': IntegerConverter, 'float': FloatConverter, 'uuid': UUIDConverter, }
- 系统提供了6个转换器可供开发者使用,当使用<>来接受参数的时候,就会调用系统默认的转换器进行匹配
装饰器路由的实现
- Flask有两大核心:Werkzeug 和 Jinja2
- Werkzeug实现路由,调试,和Web服务器网关接口
- Jinja2实现了模板
- WerkZeug是一个遵循WSGI协议的python函数库
- 其内部实现了web框架底层的东西,如,request, response
- 与WSGI规范的兼容,支持Unicode
- 支持基本的会话管理和签名Cookie
- 集成URL请求路由等
- Werkzueg库的routing模块负责实现URL解析。不同的URL对应不同的视图函数,routing模块会对请求信息的URL进行解析,匹配到URL对应的视图函数,以此生成一个响应信息
- routing模块内部有:
- Rule类:用来构造不同的URL模式的对象
- Map类:存储所有的URL规则
- BaseConverter的子类:负责定义匹配规则
- MapAdapter:负责具体的URL匹配的工作
- Flask有两大核心:Werkzeug 和 Jinja2
获取请求中的数据
- flask中代表当前请求的是request对象
常用属性为:
- method:记录请求使用的HTTP方法
- headers:记录请求中的报文头
- url:记录请求中的url地址
- cookies:记录请求中的cookie信息
- args:记录请求中的查询参数
- form:记录请求中的表单数据
- data:记录请求中的数据,并转换为字符串
- files:记录请求上传的文件
# -*- coding:utf-8 -*- from flask import Flask, request @app.route('/', methods=['GET', 'POST']) def hello_world(): print 'method: %s' % request.method print 'headers: %s' % request.headers print 'url: %s' % request.url print 'cookies: %s' % request.cookies # args: name=XXX form:name=XXX data=XXX file=图片 print 'args: %s' % request.args.get('name') print 'form: %s' % request.form.get('name') print 'data: %s' % request.data print 'files: %s' % request.files # 保存图片到当前目录 # 需要设置当前项目的工作目录, 这样传入文件路径时, '.'就可以表示当前项目根目录 image_file = request.files.get('image') image_file.save('./image.jpg') return 'Hello World!'
异常捕获
- 有时候需要抛出异常状态码,就调用abort()函数
- 格式:abort(404)
- 只能抛出HTTP协议的状态码
errorhandler装饰器
- 注册一个错误处理程序,当程序抛出指定错误状态码的时候,就会调用该装饰器装饰的方法
- 例如统一处理状态码为500的错误给用户友好的提示:
app.errorhandle(500) def internal_server_error(e): return '服务器异常', 500
- 有时候需要抛出异常状态码,就调用abort()函数
开启调试模式
- 开发时启动调试模式,可以在浏览器中和编辑器控制台显示错误信息
if __name__=='__main__': app.run(debug=True)
请求钩子
- 在客户端和服务器交互的过程中,有些准备工作或者扫尾工作要处理,比如:在请求开始时,建立数据库连接;在请求结束时,指定数据的交互方式。为了让每个视图函数避免编写重复功能的代码,Flask提供了通用设置的功能,即请求钩子。
- 请求钩子是通过装饰器的形式实现,类似于Django的中间件,Flask支持如下四种请求钩子:
- before_first_request:在处理第一个请求前运行
- before_request:在每次请求前运行
- after_request:如果没有未处理的异常抛出,在每次请求后运行
- teardown_request:在每次请求后运行,即使有未处理的异常抛出
@app.route('/') def hello_world(): print '这里正在执行处理逻辑' return ‘Hello World!’ # 在处理第一个请求前运行,应用场景:比如链接数据库操作 @app.before_first_request def before_first_request(): print 'before_first_request' # 在每次请求前运行,比如:对数据的校验,如果数据有问题,可以直接返回,就不会再去执行对应的视图函数 @app.before_request def before_request(): print 'before_request' # return 'hehe' # 如果没有未处理的异常时抛出,在每次请求后运行。运用场景:比如拼接响应头信息,让所有json.dumps()的数据,统一增加Content-Type为application/json @app.after_request def after_request(response): print 'after_request' response.headers['Content-Type'] = 'application/json' return response # 在每次请求后运行,即使有未处理的异常抛出。可以捕获到异常信息 @app.teardown_request def teardown_request(e): print 'teardown_request %s' % e
状态保持
- 因为http是一种无状态的协议,不会保持某一次请求所产生的信息,如果想实现状态保持,在开发中解决的方式有
- cookie:数据存储在客户端,节省服务器空间,但是不安全
- session:会话,数据存储在服务器端
- 无状态协议:
- 协议对于事务处理没有记忆能力
- 对同一个url请求没有上下文关系
- 每次请求都是独立的,它的执行情况和结果与前面的请求和之后的请求时无直接关系的,它不会受前面的请求的应答情况的直接影响,也不会直接影响后面的应答情况
- 服务器中没有保存客户端的状态,客户端必须每次带上自己的状态去请求服务器
- 人生若只是如初见
- 设置cookie
from flask import Flask, make_response @app.route('/cookie') def set_cookie(): resp = make_response('this is to set cookie') resp.set_cookie('username', 'michael') return resp
- 获取cookie
from flask import Flask, request # 获取 cookie @app.route('/request') def res_cookie(): resp = resquest.cookies.get('username') return resp
- session的设置与获取
from flask import Flask, session, redirect, url_for @app.route('/set_session') def set_session(): session['username'] = 'michael' return redirect(url_for('get_session')) @app.route('/get_session') def get_session(): return session.get('username')
- Flask的session存放位置
- flask和之前用过的框架有一点不同的是,它的session默认是完全保留在客户端浏览器中的,也就是说往flask的session中写入数据,最终这些数据将会以json字符串的形式,经过base64编码写入到用户浏览器的cookie里,也就是说无需依赖第三数据库保存session数据,也无需依赖文件来保存
- 实际的项目中,还是需要将session数据转移到redis中 Flask-session扩展可以实现
- 因为http是一种无状态的协议,不会保持某一次请求所产生的信息,如果想实现状态保持,在开发中解决的方式有
上下文
- 相当于一个容器,保存了Flask程序运行过程中的一些信息
分类:请求上下文和应用上下文
请求上下文
- Flask从客户端收到请求时,要让视图函数能访问一些对象,这样才能处理请求。请求对象是一个很好的例子,它封装了客户端发送的HTTP请求
- 要想让视图函数能够访问请求对象,一个显而易见的方式是将其作为参数传入视图函数,不过这会导致程序中的每个视图都增加了一个参数,除了访问请求对象,如果视图函数在处理请求时还要访问其他对象,情况会变得更糟。为了避免大量可有可无的参数把视图函数弄得一团糟,Flask使用上下文临时把对象变为全局可访问
- request和session都是属于请求上下文对象
- request:封装了HTTP请求的内容,针对的是http请求。如,user= request.args.get(‘user’),获取的是get请求的额参数
- session:用来记录会话中的信息,针对的是用户信息。如,session[‘name’] = user.id,可以记录用户信息。还可以通过session.get(‘name’)获取用户信息。
- 当调用app=Flask(__name__)的时候,创建了程序应用对象app
- request在每次http请求时,WSGI server调用Flask.call(),然后在Flask内部创建request对象
- app的生命收起大于request,一个app存活期间,可能大声多次http请求,所以就会有多个request
- 最终传入视图函数,通过return,redirect或者render_template生成response对象,返回给客户端
应用上下文
- 它的字面意思是应用上下文,但它不是一直存在的,它只是request context中的一个对 app的代理,所谓local proxy。它的作用是帮助request获取当前的应用,它是伴request而生,随request而灭的
- 应用上下文对象有:current_app,g
- current_app:
- 应用程序上下文,用于存储应用程序中的变量,可以通过current_app.name打印当前app的名字,也可以在current_app中存储一些变量,例如
- 应用的启动脚本时哪个文件,启动时,指定了哪些参数
- 加载了哪些配置文件,导入了哪些配置
- 链接了哪个数据库
- 有哪些public的工具类, 常量
- 应用跑在拿个机器上,IP多少,内存多大
current_app.name
- 应用程序上下文,用于存储应用程序中的变量,可以通过current_app.name打印当前app的名字,也可以在current_app中存储一些变量,例如
- g变量
- g作为flask程序全局的一个临时变量,充当着中间媒介的作用,我们可以通过它传递一些数据,g保存的是当前请求的全局变量,不同的请求会有不同的的全局变量,通过不同的thread id区别
g.name='abc'
两者区别
- 请求上下文:保存了客户端和服务器交互的数据
- 应用上下文:flask应用程序运行过程中,保存的一些配置信息,比如程序名,数据库链接,应用信息等
Flask-Scrip
- 命令行启动服务器
- 通过使用Flask-Scaript扩展,我们可以在命令行启动Flask服务器
from flask import Flask from flask_scaript import Manager app = Flask(__name__) # 把Manager类和应用程序示例进行关联 manager = Manager(app) @app.route('/') def index(): return ‘hello’ if __name__ == "__main__": anger.run()
- 在命令行输入:
python hello.py runserver -host ip地址
- 可以通过
python hello.py runserver --help
来查看参数
- 可以通过
模板
在前面的示例中,视图函数的作用是生成请求的响应,这是最简单的请求。 实际上,视图函数有两个作用:处理业务逻辑和返回响应内容。在大型应用中,把业务逻辑和表现内容放在一起,会增加代码的复杂度和维护成本。模板的作用,是承担视图函数的其中一个作用:返回响应内容。
- 模板其实是一个包含响应文本的文本,其中用占位符(变量)表示动态的部分,告诉模板引擎其具体的值需要从使用的数据中获取
- 使用真实值替换变量,再返回最终得到的字符串,这个过程成为“渲染”
- Flask使用jinja2这个模板引擎来渲染模板
使用模板的好处
- 视图函数只负责业务逻辑和数据处理(业务逻辑方面)
- 而模板则取到视图函数的数据结果进行展示(视图展示方面)
- 代码结构清晰,耦合度低
关于jinja2
- 是python中一个被广泛应用的模板引擎,是有python实现的模板语言,他的设计思想来源于Django的模板引擎,并扩展了其语法和一系列强大的功能,其实flask内置的模板语言
- 模板语言:是一种被设计来自动生成文档的简单文本格式,在模板语言中,一般都会把一些变量传给模板,替换模板的特定位置上预先定义好的占位变量名
渲染模板函数
- flask提供render_template函数封装了该模板引擎
render_template函数的第一个参数是模板的文件名,后面的参数都是键值对,表示模板中变量对应的真实值
使用
变量代码块:
<h1>{{post.title}}</h1>
jinja2模板中的变量代码块可以是任意python类型或者对象,只要它能被python的str()方法转换为一个字符串就可以,比如,可以通过下面的方式,显示一个字典或者列表中的某个元素
{{your_dict['key']}} {{your_list[0]}}
控制代码块:
{% %}
- 可以实现一些语言层次的功能,比如循环或者if语句
{% if user %} {{user}} {% else %} hello! <ul> {% for index in index %} <li>{{index}}</li> {% endfor %} </ul>
- 注释:{# #}
- 注释的内容不会在html中被渲染出来
- {# {{ name }} #}
过滤器
- 过滤器的本质就是函数。有时候我们不仅仅是需要输出变量的值,我们还需要修改变量的显示,甚至格式化,运算等等,而在模板中是不能直接调用python 中的某些方法,那么就用到了过滤器。
- 使用方法
- 变量名|过滤器:
{{var|filter_name(*args)}}
- 没有参数:
{{ var|filter_name }}
- 过滤器可以链式调用:
{{ var| reverse | upper }}
- 变量名|过滤器:
常见过滤器
- safe:禁用转义
- capitalize:把变量的值的首字母转成大写,其余字母转小写
- lower:把值转成小写
- upper:把值转成大写
- title:把值中的每个单词的首字母都转成大写
- reverse:字符串反转
- format:格式化输出:
<p>{{ '%s is %d' | format('name',17) }}</p>
- striptags:渲染之前把值中的所有的HTML标签都删掉
- truncate:字符串截断
- 列表操作:
- first:取出第一个元素
- last:取出最后一个元素
- lengh:获取列表长度
- sum:列表求和
- sort:列表排序
- 语句块过滤
{% filter upper %} 一大堆文字 {% endfilter %}
自定义过滤器
- 过滤器的本质是函数。当模板内置的过滤器不能满足需求,可以自定义过滤器,方法有二
- 一:通过Flask应用对象add_template_filter方法
- 参数1:函数名;参数2:自定义的过滤器的名称
def do_list_reverse(list): list.reverse() return list app.add_template_filter(do_list_reverse, 'lsreverse')
- 二:通过装饰器来实现自定义过滤器
@app.template_filter('lsreverse') def do_list_reverse(list): list.reverse() return list
- 自定义过滤器如果和内置的过滤器重名,会覆盖内置的过滤器
控制代码块
- if语句
{%if user.is_logged_in() %} <a href='/logout'>Logout</a> {% else %} <a href='/login'>Login</a> {% endif %}
- 过滤器可以被用在if语句中
{% if comments | length > 0 %} There are {{ comments | length }} comments {% else %} There are no comments {% endif %}
循环
可以子啊jinja2中使用循环迭代任何列表或者生成器对象
{% for num in my_list %} {% if num > 3 %} {{ num }} {% endif %} <br> {% endfor %}
循环语句和if可以组合使用
{% for num in my_list if num > 3 %} {{ num }} <br> {% endfor %}
循环内部的特殊变量
- loop.index:当前循环迭代的次数(从1开始)
- loop.index0:当前循环迭代的次数(从0开始)
- loop.revindex:到循环结束需要迭代的次数(从1开始)
- loop.revindex0:到循环结束需要迭代的次数(从0开始)
- loop.first:如果是第一次迭代,为True
- loop.last:如果是最后一次迭代,为Truw
- loop.length :序列中的项目数
- loop.cycle:在一串序列间循环取值的辅助函数
# my_list = [1,2,3,4] {% for num in my_list %} {{ loop.cycle('a','b') }}- {{ num }} <br> {% endfor %} 结果: a-1 b-2 a-3 b-4
模板代码的复用
- 为了提高模板代码的重用度,精简代码,应当考虑模板代码的复用
模板复用的方式:
- 继承
- 包含
- 宏
继承
- 为了重用模板中的公共部分,继承主要使用在网站的顶部菜单,底部菜单。这些内容可以定义在父模板中,字模板直接继承,而不需要重复书写
- 格式
父模板
base.html {% block top %} <h1>这是头部内容</h1> {% endblock %} {% block center %} 这是父类中间的内容 {% endblock %} {% block bottom %} <h1>这是底部内容</h1> {% endblock %}
子模板
{% extends 'base.html' %} {% block center %} {{ super() }}<br> 需要填充的内容<br> {% endblock %}
注意点
- 不支持多继承
- 为了便于阅读,在子模板中使用extends时,尽量写在模板的第一行
- 不能在一个模板文件中定义多个相同名字的block标签
- 当在页面中使用多个block标签时,建议给结束标签起个名字,当多个block标签嵌套时,阅读性更好
包含
- jinja2模板中,包含(include)的功能是讲另一个模板整个加载到当前模板中,并直接渲染
- 使用:
{% include 'hello.html' %}
- 包含在使用中,如果包含的模板文件不存在时,程序会抛出TemplateNotFound异常,可以加上ignoremissong关键字。如果包含的模板文件不存在,会忽略这条include语句
{% include 'hello.html' ignore missing %}
宏
- 把它看做jinja2中的一个函数,它会返回一个模板或者HTML字符串
- 为了避免反复的编写同样的模板代码,出现代码冗余,可以把他们写成函数以进行重用
- 需要在多处重复使用的模板代码片段可以写入单独的文件,再包含在所有的模板中以避免重复
- 示例:构造一个表单
<form> <label>用户名:</label><input type="text" name="username"><br> <label>昵称:</label><input type="text" name="nickname"><br> <label>身份证:</label><input type="text" name="idcard"><br> <label>密码:</label><input type="password" name="password"><br> <label>确认密码:</label><input type="password" name="password2"><br> <input type="submit" value="提交"> </form>
定义宏
- 类似python函数的定义,提取公用的参数,如果某个参数的值反复被调用,可以设置成默认值
{% macro input(label="", type="text", name="", value="") %} <label>{{ label }}</label><input type="{{ type }} name="{{ name }}" value="{{ value }}" "><br> {% endmacro %}
调用宏
{{ input('用户名', name="username") }}
把宏单独抽取出来,封装成html文件,其他模板中导入使用,文件名可以定义macro.html
- 在其他模板文件中先导入,再调用
{% import 'macro.html' as func %} <form> {{ func.input("用户名:", name="username") }} {{ func.input("昵称::", name="nickname") }} {{ func.input("身份证:", name="idcard") }} {{ func.input("密码:", type="password", name="password") }} {{ func.input("确认密码:", type="password", name="password2") }} {{ func.input(type="submit", value="提交") }} </form>
模板代码复用方式小结
- 继承(Block),包含(Include),宏(Macro)均能实现代码的复用
- 继承(Block)的本质是代码替换,一般用来实现多个页面中重复不变的区域
- 包含(Include)是直接将目标模板文件整个渲染出来
- 宏(Macro)的功能类似于函数,可以传入参数,需要定义,调用
Flask特有的变量和函数
- 可以在自己的模板中访问一些Flask默认内置的函数和对象
config
- 直接访问当前的config对象
{{ config.root_apth }} 结果: /User/...
request
- 就是flask中代表当前请求的request对象
{{request.url}} 结果: http://127.0.0.1:5000/
url_for()
- url_for 会根据传入的路由器函数名,返回该路由对应的URL,在模板中使用url_for()就可以安全的修改路由绑定的URL,则不用担心模板中渲染出出错的链接
url_for('hello_world') 结果: /
session
- Flask中的session对象
{{ session.get('name') }}
- g
- 应用上下文对象,可以在一次请求中方便的进行属性值的传递
g.age
get_flashed_messages()
- 这个函数会返回之前在flask中通过flask()传入的消息列表,flash函数的作用很简单,可以把由python字符串表示的消息加入一个消息队列中,再使用get_flashed_message()函数取出它们并消费掉
{% for message in get_flashed_messages() %} {{ message }} {% endfor %}
Web表单
- web表单是Web应用程序的基本功能
- 它是HTML页面中负责数据采集的部件。表单有三部分组成:表单标签,表单域,表单按钮。表单允许用户输入数据,负责HTML页面数据采集,通过表单将用户输入的数据提交给服务器。
- 在Flask中,为了处理web表单,我们一般使用Flask-WTF扩展,它封装了WTFForms,并且它有验证表单数据的功能
- WTFForms支持的HTML标准字段
- stringField:文本字段
- TextAreaFiels:多行文本字段
- PassWordField:密码文本字段
- HiddenField:隐藏文件字段
- DateField:文本字段,值为datetime.date文本格式
- DateTimeField:文本字段,值为datetime.datetime文本格式
- IntegerField:文本字段,值为整数
- DecimalField:文本字段,值为decimal.Decimal
- FloatField:文本字段,值为浮点数
- BooleanField:复选框,值为True和Flase
- RadioField:一组单选框
- SelectField:下拉列表
- SelectMutipleField:下拉列表,可选多个值
- FileField:文件上传字段
- SubmitField:表单提交按钮
- FormField:把表单作为字段嵌入另一个表单
- FieldList :一组指定类型的字段
- WTForms常用验证函数
- DataRequired:确保字段中有数据
- EqualTo:比较两个字段的值,常用于比较两次密码输入
- Length:验证输入的字符串长度
- NumberRange:验证输入的值在数字范围内
- URL:验证URL
- AnyOf:验证输入值在可选列表中
- NoneOf:验证输入值不在可选列表中
- 使用Flask-WTF需要配置参数SECRET_KEY
- CSRF_ENABLED是为了CSRF(跨站点请求伪造)保护。SECRET_KEY用来生成加密令牌,当CSRF激活的时候,该设置会根据设置的秘钥生成加密令牌。在HTML页面中直接写form表单
使用步骤
- 1.定义表单类
- 2.实例化表单类
- 3.调用验证方法:
validate_on_submit()
,完成所有验证函数的逻辑 - 4.获取表单数据,处理逻辑
#coding=utf-8 from flask import Flask, render_template, request, flash #导入wtf扩展的表单类 from flask_wtf import FlaskForm #导入自定义表单需要的字段 from wtforms import SubmitField,StringField,PasswordField #导入wtf扩展提供的表单验证器 from wtforms.validators import DataRequired,EqualTo # 解决编码问题 import sys reload(sys) sys.setdefaultencoding("utf-8") app = Flask(__name__) app.config['SECRET_KEY']='heima' #自定义表单类,文本字段、密码字段、提交按钮 # 需要自定义一个表单类 class RegisterForm(FlaskForm): username = StringField('用户名:', validators=[DataRequired()], render_kw={'placeholder': '请输入用户名'}) password = PasswordField('密码:', validators=[DataRequired()]) password2 = PasswordField('确认密码:', validators=[DataRequired(), EqualTo('password', '密码输入不一致')]) input = SubmitField('提交') #定义根路由视图函数,生成表单对象,获取表单数据,进行表单数据验证 @app.route('/form', methods=['GET', 'POST']) def form(): register_form = RegisterForm() if request.method == 'POST': # 调用validate_on_submit方法, 可以一次性执行完所有的验证函数的逻辑 if register_form.validate_on_submit(): # 进入这里就表示所有的逻辑都验证成功 username = request.form.get('username') password = request.form.get('password') password2 = request.form.get('password2') print username return 'success' else: #message = register_form.errors.get('password2')[0] #flash(message) flash('参数有误') return render_template('wtf.html', form=register_form)
数据库
Flask使用的数据路
- SQL(结构化查询语言):mysql
- 表形式存储
- 列数固定,行数可变
- 定义数据,主键,外键,引用同表或者不同表的主键,这种联系称为关系
- NoSQL(非结构化查询语言):redis,mongoDB
- 不遵循关系型数据库的数据库统称为NoSQL数据库
- 集合:关系型数据库的表
- 文档:关系型数据库的行
- 不遵循关系型数据库的数据库统称为NoSQL数据库
- Web应用中普遍使用的是关系型数据库
- Flask本身不限定数据库的选择,你可以选择SQL或则NoSQL的任何一种
- 也可以选择使用更方便的SQLAlchemy,类似于Django中的ORM
- SQL(结构化查询语言):mysql
Flask-SQLAlchemy扩展
- SQLAlchemy 实际上是对数据库的抽象,让开发者不用直接跟SQL语句打交道,而是通过python对象来操作数据库,在舍弃了一些性能开销的同时,换来的是开发效率的较大提升
- SQLAlchemy是一个关系型数据库框架,它提供了高层的ORM和底层的原生数据库操作,flask-sqlalchemy是一个简化了SQLAlchemy操作的的flask扩展
数据库设置
1.安装
pip install flask-sqlalchemy pip install flask-mysqldb
2.配置
app.config['SQLALCHEMY_DATABASE_URL']='mysql://root:mysql@127.0.0.1:3306/test'
- 其他设置
# 动态追踪修改设置,如未设置只会提示警告, 不建议开启 app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 查询时会显示原始SQL语句 app.config['SQLALCHEMY_ECHO'] = True
常用的SQLAlchemy字段类型
- Interger :int(相当于python中的数据类型),普通整数
- SmallInteger:int,取值范围小的整数,一般是16位
- BigInteger:int/long,不限制精度的整数
- Float:float,浮点数
- Numeric:decimal.Decimal,普通整数,一般是32位
- String:str,变长字符串
- Text:str,变长字符串,对较长或者不限长的字符串做了优化
- Unicode:unicode,变长Unicode字符串
- UnicodeText:unicode,变长Unicode字符串,对较长或者不限长的字符串做了优化
- Boolean:bool,布尔值
- Date:datetime,时间
- Time:datetime.datetime,时间和日期
- LargeBinary:str,二进制文件
常用的SQLAlchemy列选项
- primary_key:如果为True,代表表的主键
- unique:如果为True,代表不允许出现重复的值
- index:如果为True,为这一列创建索引,提高查询效率
- nullable:如果为True,允许为空值
- default:为这列设置默认值
常用的SQLAlchemy关系选项
- backref:在关系的另一模型中添加反向引用
- primary join:明确指定两个模型之间使用的链接条件
- uselist:如果为False,不使用列表,而使用标量值
- order_by:指定关系中记录的排序方式
- secondary:指定多对多中记录的排序方式
- secondary join:在SQLAlchemy中无法自行决定时,指定多对多关系中的二级链接条件
数据库基本操作
- 在Flask-SQLAlchemy中,插入,修改,删除等操作均有数据库会话管理
- 会话用db.session表示。在准备把数据写入到数据库前,要先将数据添加到会话中,然后调用commit()方法提交会话
- 在Flask-SQLAlchemy中查询操作是通过query对象操作数据
- 最基本的查询是返回表中的所有数据,可以通过过滤器进行更精确的数据库查询
db.session.add(role):将role添加到数据库中 db.session.add_all([user1,user2]):添加多个数据到数据库中 db.session.delete(user):删除数据库 db.session.commit():提交数据库的修改(增删改) db.session.rollback():数据库的回滚操作
- 在Flask-SQLAlchemy中,插入,修改,删除等操作均有数据库会话管理
示例
- 在视图中定义模型类
# -*- coding:utf-8 -*- from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) # 设置连接数据库的URL app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/Flask_test' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 查询时会显示原始SQL语句 app.config['SQLALCHEMY_ECHO'] = True db = SQLAlchemy(app) class Role(db.Model): # 定义表名 __tablename__ = 'roles' # 定义列对象 id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(32), unique=True) users = db.relationship('User', backref='role') # 当被关联表查询时,返回的字符串 def __repr__(self): return '<Role: %s %s>' % (self.name, self.id) class User(db.Model): __tablename__='users' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(32), unique=True, index=True) email = db.Column(db.String(32), unique=True) password = db.Column(db.String(32)) role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) def __repr__(self): return '<User: %S %s %s %s>' % (self.name, self.id, self.email, self.password) if __name__ == '__main__': # 创建表 db.create_all() # 删除表 db.drop_all() app.run(debug=True)
- 数据的增删改
终端界面测试: from test_sqlalchemy import * # 添加一条Role数据 role = Role(name = 'admin') db.session.add(role) db.session.commit() # 添加一条User数据,数据有误可以使用回滚,将add的对象从session中删除 user = User(name='zhangsan') db.session.add(user) db.session.rollback() user.role_id = 1 db.session.add(user) db.session.commit() # 修改数据 user.name = 'lisi' db.session.commit() # 删除数据 db.session.selete(user) db.session.commit()
模型之间的关联
- 一对多
class Role(db.Model): ... 关键代码 user = db.relationship('user', backref='role') ... class User(db.model): ... role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
查询操作
常用过滤器
- filter():把过滤器添加到原查询上,返回一个新查询
- filter_by():把等值的过滤器添加到原查询上,返回一个新查询
- limit():使用指定的值限定原查询返回的结果
- offset():偏移原查询返回的结果,返回一个新的查询
- order_by():根据指定条件对原查询结果进行排序,返回一个新查询
- group_by():根据指定条件对原查询结果进行分组,返回一个新查询
常用查询执行器
- all():以列表的形式返回查询的所有结果
- first():返回查询的第一个结果,如果未查询到,返回None
- first_or_404():返回查询的第一个结果,如果未查到,返回404
- get():返回指定主键对应的行,入不存在,返回None
- get_or_404:返回指定主键对应的行,如不存在,返回404
- count():返回查询结果的数量
- paginate():返回一个paginate对象,它包含指定范围内的结果
示例
1.查询所有用户数据 User.query.all() 2.查询有多少个用户 User.query.count() 3.查询第一个用户 User.query.first() 4.查询id=4的用户 User.query.get(4) User.query.filter_by(id=4).first() User.query.filter(User.id==4).first() 5.查询名字结尾字符为g的所有数据 User.query.filter(User.name.endwith('g')).all() 6.查询名字不等于wang的所有数据 User.query.filter(User.name!='wang').all() from sqlalchemy import not_ User.query.filter(not_(User.name=='wang')).all() 7.查询名字和邮箱都以li开头的所有数据 User.query.filter(User.name.startwith('li'), User.email.startwith('li')).all() from sqlalchemy import and_ User.query.filter(and_(User.name.startwith('li'), User.email.startwith('li'))) 8.查询password是‘123456’ 或者 ‘email’以‘qq.com’结尾的所有数据 User.query.filter(or_(User.password='123456', User.emailendwith('qq.com'))).all() 9.查询id为【1,3,5,7,9】的用户列表 User.query.filter(User.id.in_([1,3,5,7,9])).all() 10.查询name为liu的角色数据 User.query.filter(User.name=='liu').first().role 11.查询所有用户数据,并以邮箱排序 User.query.order_by(User.email).all() 12.查询第2页的数据,每页只是显示3条数据 # paginate三个参数:参1:查询的页码;参2:分页的个数;参3:查询出错是否需要返回错误 result = User.query.paginate(2,3,False) # 获取查询的结果 result.items # 获取总页数 result.pages # 获取当前页面 result.page
数据库的迁移
- 在开发过程中,需要修改数据库模型,而且还要在修改之后更新数据库,最直接的方式就是删除旧表, 但这样会丢失数据
- 更好的办法是使用数据库迁移框架,它可以追踪数据库模式的变化,然后把变动应用到数据库中。
- 在Flask中,使用Flask-Migatate扩展来实现数据迁移。并且集成到Flask-Script中,所有操作通过命令行就能完成
为了导出数据库迁移命令,Flask-Migarate提供了一个MigarateCommand类,可以附加到flask-script的manager对象上
代码
#coding=utf-8 from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate,MigrateCommand from flask_script import Shell,Manager app = Flask(__name__) manager = Manager(app) app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/Flask_test' db = SQLAlchemy(app) #第一个参数是Flask的实例,第二个参数是Sqlalchemy数据库实例 migrate = Migrate(app,db) #manager是Flask-Script的实例,这条语句在flask-Script中添加一个db命令 manager.add_command('db',MigrateCommand) #定义模型Role class Role(db.Model): # 定义表名 __tablename__ = 'roles' # 定义列对象 id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True) user = db.relationship('User', backref='role') #repr()方法显示一个可读字符串, def __repr__(self): return 'Role:'.format(self.name) #定义用户 class User(db.Model): __talbe__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, index=True) #设置外键 role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) def __repr__(self): return 'User:'.format(self.username) if __name__ == '__main__': manager.run()
创建迁移仓库:
- 创建migarations文件夹,所有的迁移文件都会放在里面
python database.py db init
创建迁移脚本
- 自动创建迁移脚本有两个函数
- upgrate():函数把迁移中的改动应用到数据库中
- downgrade():函数将改动删除
python database.py db migrate -m 'initial migration'
- 自动创建迁移脚本有两个函数
- 更新数据库
python database.py db upgrate
返回以前的版本
- 找到版本号:
python app.py db history
- 回滚到指定版本:
python app.py db downgrade 版本号
- 找到版本号:
实际操作的顺序
- 1.python 文件 db init
- 2.python 文件 db migrate -m ‘注释’
- 3.python 文件 db upgrade 然后观察表结构
- 4.根据需求更改模型
- 5.python 文件 db migrate -m ‘注释’
- 6.python 文件 db upgrade 然后观察表结构
- 7.若返回版本,则利用python 文件 db history 查看版本号
- 8.python 文件 db downgrade(upgrade) 版本号
其他
邮件拓展
- 有发送邮件的需求的时候
- 模块:
Flask-Mail
- 步骤
- 1.设置邮箱授权码
- 2.配置邮件
- 3.mail.send()
#coding:utf-8 from flask import Flask from flask_mail import Mail, Message app = Flask(__name__) # 配置邮件:服务器/端口/安全套接字层/邮箱名/授权码 app.config['MAIL_SERVER'] = "smtp.126.com" app.config['MAIL_PORT'] = 465 app.config['MAIL_USE_SSL'] = True app.config['MAIL_USERNAME'] = "huidongpeng@126.com" app.config['MAIL_PASSWORD'] = "heima666" app.config['MAIL_DEFAULT_SENDER'] = 'FlaskAdmin<huidongpeng@126.com>' mail = Mail(app) @app.route('/') def hello_world(): return '<a href="/send_mail">发送邮件</a>' @app.route('/send_mail') def send_mail(): msg = Message('这是邮件的主题', recipients=['huidongpeng@126.com'],body='This is flask mail') mail.send(msg) return '已发送邮件' if __name__ == '__main__': app.run(debug=True)
蓝图(BluePrint)
- 对程序进行模块化处理
- 简单来说,Blueprint是一个存储操作方法的容器,这些操作在这个Blueprint被注册到一个应用之后就可以被调用,Flask可以通过BluePrint来组织URL以及处理请求
Flask使用Blueprint让应用实现模块化,在Flask中,Blueprint具有如下属性:
- 一个应用可以具有多个Blueprint
- 可以将一个blueprint注册到任何一个未使用的URL下,比如‘/’,‘sample’或者子域名
- 在一个应用中,一个模块可以注册多次
- Blueprint可以单独具有自己的模板,静态文件或者其他的通用操作方法,它并不是必须要实现应用的视图和函数的
- 在一个应用初始化时,应应该要注册需要使用的blueprint,但是一个blueprint并不是一个完整的应用,它不能独立于应用运行,而必须要注册到某一个应用中
使用步骤
- 1.创建一个蓝图对象:
admin = Bluprint('admin',__name__)
- 2.在这个蓝图对象上进行操作,注册路由,指定静态文件夹,注册模板过滤器
@admin.route('/') def admin_home(): return 'admin_home'
- 3.在应用上注册这个蓝图
app.regidter_bluprint(admin, url\prefix='admin')
- 当这个应用启动后,通过/admin/可以访问到蓝图中定义的视图函数
- 1.创建一个蓝图对象:
运行机制
- 蓝图是保存了一组将来可以在应用对象上执行的操作,注册路由就是一种操作
- 当在应用对象上调用route装饰器注册路由的时候,这个操作将修改对象的url_map路由表
- 然而没蓝图对象根本没有路由表,当我们在蓝图对象上调用route装饰器注册路由的时候,它只是在内部的一个延迟操作记录列表defered_functions中添加了一个项
- 当执行应用对象的regidter_blueprint()方法时,应用对象将从蓝图对象的defered_functions列表中取出每一项,并以自身作为参数执行该匿名函数,即调用应用对象的add_url_rule()方法,这将真正修改应用对象的路由表
URL前缀
- 当我们在应用对象上注册一个蓝图时,可以指定一个url_prefix关键字参数(默认是/)
- 在应用最终的路由表url_map中,在蓝图上注册的路由URL自动加上了这个前缀,这个可以保证在多个蓝图中使用相同的URL规则而不会最终引起冲突,只要在注册蓝图时将不同的蓝图挂接到不同的自路径即可
注册静态路由
- 和应用对象不同,蓝图对象创建时不会默认注册静态目录的路由,需要我们在创建时指定static_folder参数
admin = Blueprint('admin',__name__, static_folder='static_admin') app.register_blueprint(admin, url_prefix='/admin')
- 则现在就可以使用/admin/static_admin/访问dtatic_admin目录下的静态文件了,定制静态目录URL规则:可以在创建蓝图对象时使用static_url_path来改变静态目录的路由
admin = blueprint('admin', __name__,static_folder='static_admin', static_url_path='/lib') app.register_blueprint(admin, url_prefix='/admin')
设置模板目录
- 蓝图对象默认的模板目录为系统的模板目录,可以在创建蓝图对象时使用template_folder关键字参数设置模板目录
admin = Blueprint('admin', __name__, template_folder=‘my_templates’)
- 如果在templates中存在和my_templates同名文件,则系统会优先使用templates中的文件,需要通过路径区分
- 如,my_templates存在两个,若使用admin目录下的my_temolates目录,则需要使用如下方式注册:
admin = Blueprint(‘admin’, __name__, template_folder='admin/my_templates')
单元测试
Web程序开发过程一般包括以下几个阶段:需求分析,设计阶段,实现阶段,测试阶段,其中测试阶段是通过人工或者自动来运行测试某个系统的功能。目的是检验是否满足要求,并得到特定的结果,以达到弄清楚预期结果和实际结果之间的差别
测试的分类
- 单元测试:对单独的代码块(例如函数)分别进行测试,以确保他们的正确性
- 集成测试:对大量的程序单元的协同工作情况做测试
- 系统测试:对整个系统的正确性进行检查,而不是针对独立的片段
在众多的测试中,与程序开发人员最密切的就是单元测试,因为单元测试是由开发人员进行的,而其他测试都是由专业的测试人员来完成。
程序开发过程中,写代码是为了实现需求。当我们的代码通过了编译,只是说说明了它的语法正确,功能是否实现则不能保证。因此,当我们的某些功能代码完成后,为了检验其是否满足程序要求,可以通过编写测试代码,模拟程序运行的过程,检验功能代码是否符合预期。
- 单元测试就是开发者编写一小段代码,检验目标代码的功能是否符合预期。用代码验证代码
- 举个例子:一部手机有许多零部件,在正式组装一部手机前,手机内部的个个零部件,CPU,内存,电池,摄像头,都需要进行功能性测试,这就是单元测试
在web开发中,单元测试实际上就是一些‘断言’(assert)代码
- 断言就是判断一个函数或者对象的一个方法所产生的结果是否符合你期望的那样,python中的断言是声明布尔值为真的判断,如果表达式为假会发生异常。单元测试中,一般使用assert来断言结果
断言方法的使用
def num(num1, num2): # num1,num2必须为整数 # num2不能为0 assert isinstance(num1, int), 'num1 must be int' assert isinstance(num2, int), 'num2 must be int' assert num2 != 0, 'num2 can not be 0' print num1 / num2 if __name__ == '__main__': num(1, 0)
常用的断言方法
- assertEqual:如果两个值相等,则pass
- assertNotEqual:如果两个值不相等,则pass
- assertTrue:判断bool值为True,则pass
- assertFalse:判断bool值为False,则pass
- assertIsNone:不存在,则pass
- assertIsNotNone:存在,则pass
单元测试的基本写法
import unittest class TestClass(unittest.TestCase): # 该方法会首先执行,相当于做测试前的准备工作,为固定写法 def setup(self): pass # 该方法会在测试代码执行完成后执行,相当于做测试后的扫尾工作,为固定写法 def teardown(self): pass # 测试代码 def test_app_exists(self): pass
- 示例,数据库测试
#coding=utf-8 import unittest from author_book import * #自定义测试类,setUp方法和tearDown方法会分别在测试前后执行。以test_开头的函数就是具体的测试代码。 class DatabaseTestCase(unittest.TestCase): def setUp(self): app.config['TESTING'] = True app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@localhost/test0' self.app = app db.create_all() def tearDown(self): db.session.remove() db.drop_all() #测试代码 def test_append_data(self): au = Author(name='itcast') bk = Book(info='python') db.session.add_all([au,bk]) db.session.commit() author = Author.query.filter_by(name='itcast').first() book = Book.query.filter_by(info='python').first() #断言数据存在 self.assertIsNotNone(author) self.assertIsNotNone(book)
使用uWSGI和Nginx进行服务器部署
概念区分
- WSGI
- 全程Web Server Gateway Interface(web服务器网关接口)
- 是一种规范,是wen服务器和web应用程序之间的接口
- 作用就像桥梁,链接在web服务器和web应用框架之间
- 没有官方的实现,更像一个协议,只要遵照这个协议WSGI应用就可以在任何服务器上运行
- uwsgi:是一种传输协议,用于定义传输信息的类型,常用于在uWSGI服务器与其他网络服务器的数据通讯
- uWSGI:是实现了uwsgi协议的WSGI的web服务器
- WSGI
helloworld程序部署
- 购买云服务器
- 软件安装:uwsgi, nginx,mysql, resdis,虚拟环境
- 使用pycharm创建python项目
- 创建config.ini文件作为uwsgi的初始化配置文件
- 利用scp命令将整个项目上传到远程服务器上
- 进入虚拟环境并通过指令运行uwsgi.ini服务器
- 配置nginx服务器并启动