Flask学习笔记

Flask

Web应用程序的本质

Web(World Wide Web)诞生最初的目的,是为了利用互联网交流工作文档。

Web框架:

协助开发者快速开发Web应用程序的一套功能代码

开发者只需要按照框架约定要求,在指定位置上写上自己的 业务逻辑代码

为什么要用Web框架

Web网站发展至今,特别低服务器端 ,涉及到的知识、内容,非常广泛。这对程序员的要求会 越来越高。如果采用成熟,稳健的框架,那么一些基础的工作 ,比如 ,安全性,数据流控制等都可以让框架来处理,那么程序开发人员可以把精力放在具体的业务逻辑上面 。

使用Web框架的优点:

稳定性强和可扩展性强

可以降低开发难度 ,提高开发效率

Flask

​ Flask本身相当于一个内核,其他几乎所有的 功能都要用到扩展(邮件扩展Flask-Mail,用户认证Flask-Login,数据库Flask-SQLAlchemy),都需要用第三方的扩展来实现。

​ 其 WSGI 工具箱采用 Werkzeug(路由模块),模板引擎则使用 Jinja2。这两个也是 Flask 框架的核心。

Flask有两大核心:Werkzeug和jinja2
	Werkzeug实现路由、调试和web服务器网关接口
	jinja2实现了模板

Werkzeng

Werkzeug是一个遵循WSGI协议的python函数库
	其内部实现了很多Web框架底层的东西,比如request和response对象
	与WSGI范围的兼容,支持Unicode
	支持基本的会话管理和签名Cookie
	集成URL请求路由等

Werkzeng库的routing模块负责实现URL解析,不同的URL对应不同的试图函数,routing模块会对请求信息的URL进行解析,匹配到URL对应的试图函数,执行该函数以生成一个响应信息.

routing模块内部有:
	Rule类: 用来构造不同的URL模式的对象,路由URL规则
	Map类:存储所有的URL规则和一些配置参数
	BaseConverter的子类:负责定义匹配规则
	MapAdapter类:负责协调Rule做具体的匹配的工作

Flask 常用扩展包

Flask-SQLalchemy: 操作数据库
Flask-script: 插入脚本
Flask-migrate: 管理迁移数据库
Flask-Session: Session存储方式指定
Flask-WTF: 表单
Flask-Mail: 邮件 
Flask-Bable: 提供国际化和本地化支持,翻译
Flask-Login: 认证用户状态
Flask-OpenID: 认证
Flask-RESTful:开发REST API的工具
Flask-Bootstrap:集成前端Twitter Bootstrap框架
Flask-moment:本地化日期和时间
Flask-Admin:简单而可扩展的管理接口的框架

相关参数配置

初始化参数
	app = Flask(__name__)
    	import_name
        Flask程序所在的包(模块),传 __name__ 就可以
        其可以决定 Flask 在访问静态文件时查找的路径
        static_path
        静态文件访问路径(不推荐使用,使用 static_url_path 代替)
        static_url_path
        静态文件访问路径,可以不传,默认为:/ + static_folder
        static_folder
        静态文件存储的文件夹,可以不传,默认为 static
        template_folder
        模板文件存储的文件夹,可以不传,默认为 templates

程序加载配置

在Flask程序运行的时候,可以给Flask设置相关配置,如:配置DEBUG模式,配置数据库连接地址等
设置Flask配置有三种方式:
	从配置对象中加载(常用): app.config.from_object()
    从配置文件中加载: app.config.from_pyfile()
    从环境变量中加载: app.config.from_envvar()

读取配置

app.config.get()
在试图中使用: current_app.config.get()

app.run的参数

可以指定运行的主机IP地址,端口,是否开启调试模式
app.run(host="0.0.0.0", port=5000, debug=True)

路由

指定路由地址
	# 指定访问路径为 demo1
    @app.route('/demo1')
    def demo1():
        return 'demo1'
给路由参数传参
	# 路由传递参数
    @app.route('/user/<user_id>')
    def user_info(user_id):
        return 'hello %s' % user_id
  路由传递的参数默认当做string处理,也可以指定参数类型
	# 路由传递参数
    @app.route('/user/<int:user_id>')
    def user_info(user_id):
        return 'hello %d' % user_id
    # 这里指定int,尖括号中的内容是动态的,在此暂时可以理解为接受 int 类型的值,实际上 int 代表使用 			  IntegerConverter 去处理 url 传入的参数
指定请求参数:在Flask中,定义一个路由,默认的请求方式为[GET, OPTIONS(自带), HEAD(自带)]
	@app.route('/demo2', methods=['GET', 'POST'])
    def demo2():
        # 直接从请求中取到请求方式并返回
        return request.method

视图常用逻辑

返回JSON:在使用Flask写一个借口时候需要给客户端返回JSON数据,在Flask中可以直接使用`jsonify`生成一个JSON的响应
    # 返回JSON
    @app.route('/demo4')
    def demo4():
        json_dict = {
            "user_id": 10,
            "user_name": "laowang"
        }
        return jsonify(json_dict)
    # 不推荐使用 json.dumps 转成 JSON 字符串直接返回,因为返回的数据要符合 HTTP 协议规范,如果是 JSON 需要指定 content-		type:application/json

重定向

重定向到网址:
	@app.route('/demo5')
    def demo5():
        return redirect('http://www.itheima.com')
重定向到自己写的试图:
	可以直接填写自己url路径
	也可以使用url_for生成指定视图函数所对应的url
		@app.route('/demo1')
        def demo1():
            return 'demo1'

        # 重定向
        @app.route('/demo5')
        def demo5():
            return redirect(url_for('demo1'))
重定向到带有参数的试图函数中
	在url_for函数中传入参数
    	# 路由传递参数
        @app.route('/user/<int:user_id>')
        def user_info(user_id):
            return 'hello %d' % user_id

        # 重定向
        @app.route('/demo5')
        def demo5():
            # 使用 url_for 生成指定视图函数所对应的 url
            return redirect(url_for('user_info', user_id=100))		

自定义状态码

# 在Flask中,可以很方便的返回自定义状态码,以实现不符合http协议的状态码
    @app.route('/demo6')
    def demo6():
        return '状态码为 666', 666

正则匹配路由

在web开发中,可能会出现限制用户访问规则的场景,那么这个时候就需要用到正则匹配,根据自己的规则去限定请求参数在进行访问
具体的实现步骤为:
​ 导入转化器基类:在Flask中,所有的路由的匹配规则都是使用转化器对象进行记录
​ 自定义转换器:自定义类继承于转换器基类
​ 添加转换器到默认的转换器字典中
​ 使用自定义转换器实现自定义匹配规则

代码实现
# 导入转换器基类
from werkzeug.routing import BaseConverter
# 自定义转换器
class RegexConverter(BaseConverter):
    def __init__(self, url_map, *args):
        super(RegexConverter, self).__init__(url_map)
        # 将接受的第1个参数当作匹配规则进行保存
        self.regex = args[0]
# 添加转换器到默认的转换器字典中,并指定转换器使用时名字为:re
app = Flask(__name__)
app.url_map.converters['re'] = RegexConverter
# 使用转换器去实现自定义匹配规则(当前此处定义的规则为:3位数字)
@app.route('/user/<re("[0-9]{3}"):user_id>')
def user_info(user_id):
	return "user_id为%s" % user_id

系统自带转化器

# 系统自带的转换器具体使用方式在每种转换器的注释代码中有写,请留意每种转换器初始化的参数。
DEFAULT_CONVERTERS = {
    'default':          UnicodeConverter,
    'string':           UnicodeConverter,
    'any':              AnyConverter,
    'path':             PathConverter,
    'int':              IntegerConverter,
    'float':            FloatConverter,
    'uuid':             UUIDConverter,
}

异常捕获

HTTP异常主动抛出:
	abort方法:抛出一个给定的状态码的HTTPException或者指定响应,
    	参数:code-HTTP的错误状态码
        # 抛出状态码的话,只能抛出HTTP协议的错误状态码
        abort(404)
错误捕获:
	errorhandler装饰器:注册一个错误处理程序,当程序抛出指定错误状态码的时候,就会调用该装饰器所装饰的方法
    参数:code_or_exception-HTTP的错误状态码或指定异常
    
    例如:统一处理状态码为500的错误给用户有好的提示:
    	@app.errorhandler(500)
        def internal_server_error(e):
            return '服务器搬家了'
        
		捕获指定异常:
        @app.errorhandler(ZeroDivisionError)
        def zero_division_error(e):
            return '除数不能为0'

请求钩子

	在客户端和服务器交互的过程中,有些准备工作或扫尾工作需要处理,为了让每个试图函数避免编写重复功能的代码,Flask提供了通用设施的功能,即请求钩子
    请求钩子是同装饰器的形式实现,Flask中支持4中请求钩子:
    
    before_first_request:
    	在处理第一个请求前执行
    before—request:
    	在每次请求之前执行
        如果在某装饰器的函数中返回了一个响应,试图函数将不再被调用
    after_request:
    	如果没有抛出错误,再每次请求之后执行
        接受一个参数:试图函数做出响应
        在此函数中可以对响应值在返回之前做最后一步修改处理
        需要将参数中的响应在此函数中进行返回
    teardown_request:
    	在每次请求后执行
        接受一个参数:错误信息,如果有相关错误抛出(此错误不能是手动抛出的错误)

request

request 就是flask中代表当前请求的 request 对象,其中一个请求上下文变量(理解成全局变量,在视图函数中直接使用可以取到当前本次请求)
属性 说明 类型
data 记录请求的数据,并转换为字符串 *
form 记录请求中的表单数据 MultiDict
args 记录请求中的查询参数 MultiDict
cookies 记录请求中的cookie信息 Dict
header 记录请求中的报文头 EnvironHeaders
method 记录请求中使用的HTTP方法 GET/POST
url 记录请求中的URL地址 string
files 记录请求上传的文件 *

状态保存

1 因为http是一种无状态协议,浏览器请求服务器是无状态的
2 无状态:值一次用户请求时,浏览器、服务器无法知道之前这个用户做过什么,每次请求都是一次新的请求
3 无状态原因:浏览器与服务器是使用socket套接字进行通信的,服务器讲请求结果返回给浏览器之后,会关闭当前的socket连接,而且服务器也会在处理页面完毕之后销毁页面对象
4 有时需要保持下来用户浏览的状态,比如是否登陆过,浏览过那些商品
5 实现状态保持主要有两种方式:
	在客户端存储信息使用Cookie
	在服务端存储信息使用Session
# 无状态协议:
	协议对于事物处理没记忆能力
	对同一个url请求没有上下文关系
	每次的请求都是独立的,它的执行情况和结果与前面的请求和之后的请求时无直接关系的,它不会受前面的请求应答情况直接影响,也不会直接影响后面的额请求应答情况。
	服务器中没有保存客户端的状态,客户端必须每次带上自己的状态去请求服务器
	人生若只如初见
Cookie: 指某些网站为了辨别用户的身份、进行会话跟踪而存储在用户本地的数据(通常经过加密)
1. 复数形式Cookies
2. Cookie是由服务端生成,发送给客户浏览器,浏览器会将Cookie的key/value保存,下次请求同一网站时就发送该Cookie给服务器(前提是浏览器设置为启用Cookie)
3. Cookie的kry/value可以有服务器端自己定义
应用: 
1. 最典型的应用是判定主从用户是否已经登录网站,用户可能会得到提示,是否在下一次进入此网站时保留用户信息 以便简化登录手续,这些都是Cookie的功用。
2. 网站的广告推送,经常遇到访问某个网站时,会弹出小窗口,展示我们曾经在购物网站上看过的商品信息
3. 购物车,用户可能会在一段时间内在同一家网站的不同页面中选择不同的商品,这些信息都会写入Cookie,以便在最后付款时提起信息。
# 提示:
1. Cookie是存储在浏览器中的一段纯文本信息,建议不要存储敏感信息如密码,因为电脑上的浏览器可能被其他人使用
2. Cookie基于域名安全,不同域名的Cookie是不能互相访问的(浏览器的同源策略)
3. 当浏览器请求某网站时,会将本网站下所有Cookie信息提交给服务器,所有在request中可以读取Cookie信息

# 设置Cookie
from flask imoprt Flask,make_response
@app.route('/cookie')
def set_cookie():
    resp = make_response('this is to set cookie')    # 返回的响应内容
    resp.set_cookie('username', 'itcast', max_age=3600)    # 设置Cookie    max_age过期时间
    return resp

# 获取Cookie
from flask import Flask,request
#获取cookie
@app.route('/request')
def resp_cookie():
    resp = request.cookies.get('username')  # 获取Cookie
    return resp

Session

对于敏感、重要的信息,建议要存储在服务器端,不能存储在浏览器中,如用户名、余额、等级、验证码等信息
在服务器端进行状态保持的方案就是Session
Session依赖于Cookie

# session数据的获取
session:请求上下文对象,用于处理http请求中的一些数据内容

    @app.route('/index1')
def index1():
    session['username'] = 'itcast'
    return redirect(url_for('index'))
@app.route('/')
def index():
    return session.get('username')

上下文

上下文:相当于一个容器,保存了 Flask 程序运行过程中的一些信息。

Flask中有两种上下文,请求上下文和应用上下文

  • 请求上下文:保存了客户端和服务器交互的数据
  • 应用上下文:flask 应用程序运行过程中,保存的一些配置信息,比如程序名、数据库连接、应用信息等
请求上下文(request context)

在 flask 中,可以直接在视图函数中使用 request 这个对象进行获取相关数据,而 request 就是请求上下文的对象,保存了当前本次请求的相关数据,请求上下文对象有:request、session

  • request
    • 封装了HTTP请求的内容,针对的是http请求。举例:user = request.args.get('user'),获取的是get请求的参数。
  • session
    • 用来记录请求会话中的信息,针对的是用户信息。举例:session['name'] = user.id,可以记录用户信息。还可以通过session.get('name')获取用户信息。
应用上下文(application context)

它的字面意思是 应用上下文,但它不是一直存在的,它只是request context 中的一个对 app 的代理(人),所谓local proxy。它的作用主要是帮助 request 获取当前的应用,它是伴 request 而生,随 request 而灭的。

应用上下文对象有:current_app,g

current_app:

​ 应用程序上下文,用于存储应用程序中的变量,可以通过current_app.name打印当前app的名称,也可以在current_app中存储一些变量

g变量:(不同的请求,会有不同的全局变量)

​ g 作为 flask 程序全局的一个临时变量,充当者中间媒介的作用,我们可以通过它传递一些数据,g 保存的是当前请求的全局变量,不同的请求会有不同的全局变量,通过不同的thread id区别

Flask-Script

通过使用Flask-Script扩展,我们可以在Flask服务器启动的时候,通过命令行的方式传入参数。而不仅仅通过app.run()方法中传参

python 模块名.py runserver -host ip地址

Flask-Script 还可以为当前应用程序添加脚本命令

安装: pip install flask-script
# 集成Flask-Script
from flask import Flask
from flask_script import Manager

app = Flask(__name__)
# 把 Manager 类和应用程序实例进行关联
manager = Manager(app)

@app.route('/')
def index():
    return '床前明月光'

if __name__ == "__main__":
    manager.run()

Jinja2模板引擎

两个概念:
  • jinja2:是python下一个被广泛应用的模板引擎,是由python实现的模板语言,它的设计思想来源于Django的模板引擎,并扩展了其语法和一系列强大的功能,其是Flask内置的模板语言。
  • 模板语言:是一种被设计用来 自动生成文档的简单文本格式,在模板语言中,一般都会把一些变量传给模板,替换模板的特定位置上预先定义好的占位变量名。

模板

试图函数作用:处理业务逻辑, 返回响应内容
模板(返回响应内容):
  • 模板其实是一个包含响应文本的文件,其中用占位符(变量)表示动态部分,告诉模板引擎具体的值需要从使用的数据中获取
  • 使用真实值替换变量,在返回最终得到的字符串,这个过程称为"渲染"
  • Flask是使用jinja2这个模板引擎来渲染模板
使用模板的好处:
  • 试图函数只负责业务逻辑和数据处理(业务逻辑方面)
  • 模板则取到属兔函数的数据结果进行展示(试图展示方面)
  • 代码结构清晰,耦合度低
渲染模板函数:
  • 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 indexs %}
    <li> {{ index }} </li>
    {% endfor %}
</ul>
  • 注释:使用{##}进行注释,注释的内容不会再html中渲染出来
{# {{ name }} #}
  • 创建试图函数,将该模板内容进行渲染返回
# 试图函数中的内容
@app.route('/')
def index():
    # 往模板中传入的数据
    my_str = 'Hello 黑马程序员'
    my_int = 10
    my_array = [3, 4, 2, 1, 7, 9]
    my_dict = {
        'name': 'xiaoming',
        'age': 18
    }
    return render_template('temp_demo1.html',
                           my_str=my_str,
                           my_int=my_int,
                           my_array=my_array,
                           my_dict=my_dict
                           )
{# 模板中的代码 #}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
我的模板html内容
<br/>{{ my_str }}
<br/>{{ my_int }}
<br/>{{ my_array }}
<br/>{{ my_dict }}

</body>
</html>
# 运行效果
<!DOCTYPE html>

我的模板html内容 
Hello 黑马程序员 
10 
[3, 4, 2, 1, 7, 9] 
{'name': 'xiaoming', 'age': 18}
  • 相关运算
<br/> my_int + 10 的和为:{{ my_int + 10 }}
<br/> my_int + my_array第0个值的和为:{{ my_int + my_array[0] }}
<br/> my_array 第0个值为:{{ my_array[0] }}
<br/> my_array 第1个值为:{{ my_array.1 }}
<br/> my_dict 中 name 的值为:{{ my_dict['name'] }}
<br/> my_dict 中 age 的值为:{{ my_dict.age }}
  • 结果
my_int + 10 的和为:20 
my_int + my_array第0个值的和为:13 
my_array 第0个值为:3 
my_array 第1个值为:4 
my_dict 中 name 的值为:xiaoming 
my_dict 中 age 的值为:18
  • *过滤器

    • 过滤器的本质就是函数。有时候我们不仅仅只是需要输出变量的值,我们还需要修改变量的额显示,甚至格式化、运算等等,而在模板中是不能直接调用python中的某些方法,那么久用到了过滤器。
  • 过滤器的使用方式:变量名 | 过滤器
{{variable | filter_name(*args)}}   # *args: 参数,如果没有可以省略
{# 链式调用: 在jinja2中,过滤器是可以支持链式调用的 #}
{{ "hello world" | reverse | upper }}
  • 常见內建过滤器;
# safe:禁用转义
	<p>{{ '<em>hello</em>' | safe }}</p>
# capitalize:把变量值的首字母转成大写,其余字母转小写
	<p>{{ 'hello' | capitalize }}</p>
# lower:把值转换为小写
	<p>{{ 'HELLO' | lower }}</p>
# upper:把值转换为大写
	<p>{{ 'hello' | upper }}</p>
# title:把值中的每个单词的首字母都转成大写
	<p>{{ 'hello' | title }}</p>
# reverse:字符串反转
	<p>{{ 'olleh' | reverse }}</p>
# format:格式化输出
	<p>{{ '%s is %d' | format('name',17) }}</p>
# striptags:渲染之前把值中所有的html标签都删掉
	<p>{{ '<em>hello</em>' | striptags }}</p>
# truncate:字符串截断
	<p>{{ 'hello every one' | truncate(9)}}</p>
  • 列表操作
# frist:取第一个元素
	<p>{{ [1,2,3,4,5,6] | first }}</p>
# last:取最后一个元素
	<p>{{ [1,2,3,4,5,6] | last }}</p>
# length:获取列表长度
	<p>{{ [1,2,3,4,5,6] | length }}</p>
# sum:列表求和
	<p>{{ [1,2,3,4,5,6] | sum }}</p>
# sort:列表排序
	<p>{{ [6,2,3,1,5,4] | sort }}</p>
  • 语句块过滤
{% filter upper %}
    #一大堆文字#
{% endfilter %}
  • *自定义过滤器(过滤器的本质是函数)

  • 重要:自定义的过滤器名称如果和内置的过滤器重名,会覆盖内置的过滤器。

    • 自定义过滤器的方式:
      • 通过Flask应用对象的add_template_filter 方法
      • 通过装饰器实现自定义过滤器
  1. 使用add_template_filter 方法实现自定义过滤器:该方法第一个参数是函数名,第二个参数是自定义的过滤器名称
# 列表反转过滤器
def do_listreverse(li):
    # 通过原列表创建一个新列表
    temp_li = list(li)
    # 将新列表进行返转
    temp_li.reverse()
    return temp_li

app.add_template_filter(do_listreverse,'lireverse')
  1. 使用装饰器实现自定义过滤器: 装饰器传入的参数是自定义的过滤器名称
@app.template_filter('lireverse')
def do_listreverse(li):
    # 通过原列表创建一个新列表
    temp_li = list(li)
    # 将新列表进行返转
    temp_li.reverse()
    return temp_li

在html中使用自定义过滤器

<br/> my_array 原内容:{{ my_array }}
<br/> my_array 反转:{{ my_array | lireverse }}

# 运行结果
my_array 原内容:[3, 4, 2, 1, 7, 9] 
my_array 反转:[9, 7, 1, 2, 4, 3]

控制代码块(if和for)

if语句:jinja2 语法中的if语句跟python中的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 post in posts %}
 <div>
     <h1>{{ post.title }}</h1>
     <p>{{ post.text | safe }}</p>
 </div>
{% endfor %}
循环语句和 if 语句可以组合使用
{% for post in posts if post.text %}
 <div>
     <h1>{{ post.title }}</h1>
     <p>{{ post.text | safe }}</p>
 </div>
{% endfor %}

在一个 for 循环中可以访问这些特殊的变量

在循环内部,可以使用一个叫loop 的特殊变量来获取关于for循环的一些信息

变量 描述
loop.index 当前循环迭代的次数(从1开始)
loop.index() 当前循环迭代的次数(从0开始)
loop.revindex 到循环结束需要迭代的次数(从1开始)
loop.revindex() 到循环结束需要迭代的次数(从0开始)
loop.first 如果是第一次迭代,为True
loop.last 如果最后一次迭代,为True
loop.length 序列中的项目数
loop.cycle 在一串序列间期取值的辅助函数

模板代码的复用

对宏的理解:
  • 把它看做jinja2中的UI个函数,它会返回一个模板或者HTML字符串
  • 为了避免反复地编写同样的模板代码,出现代码冗余,可以写成函数进行重用
  • 需要在多处重复使用的模板代码判断可以写入单独的文件,在包含在所有模板中,以避免重复
使用
# 定义宏
{% macro input(name,value='',type='text') %}
    <input type="{{type}}" name="{{name}}"
        value="{{value}}" class="form-control">
{% endmacro %}

# 调用宏
{{ input('name' value='zs')}}

# 输出结果
<input type="text" name="name"
    value="zs" class="form-control">

把宏单独抽取出来,封装成html文件,其他模板中导入使用,文件名可以自定义(这里用macro.html)

{% macro function(type='text', name='', value='') %}
<input type="{{type}}" name="{{name}}"
value="{{value}}" class="form-control">

{% endmacro %}

在其他模板文件中导入,先调用

{% import 'macro.html' as func %}
{% func.function() %}

模板继承

模板继承是为了重用模板中的公共内容,一般Web开发中,继承主要使用在网站的顶部菜单、底部。这些内容可以定义在父模板中,子模板直接继承,而不需要重复书写。

  • 标签定义的内容

    {% block top %} {% endblock %}
    
  • 相当于在父模板中挖个坑,当子模板继承父模板是,可以进行填充

  • 子模板使用extends指令声明这个模板继承自哪个模板

  • 父模板中定义的快在子模板中被重新定义,在子模板中调用父模板的内容可以使用super()

模板继承使用注意点:
  • 不支持多继承
  • 为了便于阅读,在子模板中使用extends时,尽量写在模板的第一行
  • 不能在一个模板文件中定义多个相同名字的block标签
  • 当前页面中使用多个block标签时,建议给结束标签起个名字,当多个block嵌套时,阅读性更好

包含

jinja2 模板中,除了宏和继承,还支持一种代码重用的功能,叫包含(include)。它的功能是将另一个模板整个加载到当前模板中,并直接渲染。

# include的使用
{% include 'hello.html' %}
包含在使用时,如果包含的模板文件不存在时,程序会抛出TemplateNotFound异常,可以加上ignore missing关键字。如果包含的模板文件不存在,会忽略这条include语句。
# include的使用加上关键字ignore missing
{% include 'hello.html' ignore missing %}
小结
  • 宏(Macro)、继承(Block)、包含(include)均能实现代码的复用。
  • 继承(Block)的本质是代码替换,一般用来实现多个页面中重复不变的区域。
  • 宏(Macro)的功能类似函数,可以传入参数,需要定义、调用。
  • 包含(include)是直接将目标模板文件整个渲染出来。

模板中特有的变量和函数

config:可以从模板中直接访问Flask当前的config对象
{{config.SQLALCHEMY_DATABASE_URI}}
sqlite:///database.db

request:flask中代表当前请求的request对象

{{request.url}}
http://127.0.0.1
session:Flask的session对象
{{session.new}}
True
g变量:在试图函数中设置g变量的name属性的值,然后在模板中直接可以取出
{{ g.name }}
url_for():url_for会根据传入的路由器函数名,返回该路由对应的URL,在模板中始终使用url_for()就可以安全的修改路由绑定的URL,则不必担心模板中渲染出错的链接:
{{url_for('home')}}
/
get_flashed_message():这个函数会返回之前在flask中通过flask()传入的消息的列表,flask函数的作用很简单,可以把由python字符串表示的消息加入一个消息队列中,再使用get_flashed_message()函数取出它们并消费掉:
{%for message in get_flashed_messages()%}
 {{message}}
{%endfor%}

Web表单

Web表单是Web应用程序的基本功能。它是html页面中负责数据采集的部件。

表单由三个部分组成:表单标签、表单域、表单按钮。

表单允许影虎输入数据,负责html页面的数据采集,通过表单将用户输入的数据提交给服务器。

在flask中,为了处理web表单,使用Flask-WTF扩展,它封装了WTForms,并且它有验证表单数据的功能

WTForms支持的html标准字段

字段对象 说明
StringField 文本字段
TextAreaField 多行文本字段
PasswordField 密码文本字段
HiddenField 隐藏文件字段
DateField 文本字段,值为 datetime.date 文本格式
DateTimeField 文本字段,值为 datetime.datetime 文本格式
IntegerField 文本字段,值为整数
DecimalField 文本字段,值为decimal.Decimal
FloatField 文本字段,值为浮点数
BooleanField 复选框,值为True 和 False
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激活的时候,该设置或根据设置的密钥生成加密令牌。

CSRF

  • CSRF全拼为Cross Site Request Forgery,译为跨站请求伪造。
  • csrf指攻击者盗用了你的身份,以你的名义发送恶意请求。
    • 包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账......
  • 造成的问题:个人隐私泄露以及财产安全。

防止CSRF攻击

  1. 在客户端向后端请求界面数据的时候,后端会往响应中的 cookie 中设置 csrf_token 的值
  2. 在 Form 表单中添加一个隐藏的的字段,值也是 csrf_token
  3. 在用户点击提交的时候,会带上这两个值向后台发起请求
  4. 后端接受到请求,以会以下几件事件:
    • 从 cookie中取出 csrf_token
    • 从 表单数据中取出来隐藏的 csrf_token 的值
    • 进行对比
  5. 如果比较之后两值一样,那么代表是正常的请求,如果没取到或者比较不一样,代表不是正常的请求,不执行下一步操作

ORM(对象—关系映射):主要实现模型对象到数据库数据的映射

优点:
  • 只需要面向对象编程,不需要面向数据库编写代码
    • 对数据库的操作都转化成对类属性和方法的操作
    • 不用编写各种数据库的sql语句
  • 实现了数据模型与数据库的解耦,屏蔽了不同数据库操作上的差异
    • 不在关注用的是mysql,oracle...等
    • 通过简单的配置就可以轻松更换数据库,而不需要修改代码
缺点:
  • 相比较直接使用SQL语句操作数据库,有性能损失
  • 根据对象的操作转换SQL语句,根据查询的结果转换成对象,在映射过程中有性能损失

Flask-SQLAlchemy安装及设置

  • SQLALchemy实际上是对数据库的抽象,让开发者不用直接和SQL语句大交道,而是通过python对象来操作数据库,在舍弃一些性能开销的同时,换来的是开发效率的较大提升
  • SQLALchemy是一个关系型数据库框架,它提供了高层地ORM和底层的原生数据库的操作。flask-sqlalchemy是一个简化了SQLALchemy操作的flask扩展

安装

# 安装
pip  install flask-sqlalchemy

# 如果连接的是mysql数据库,需要安装mysqldb
pip install flask-mysqldb
数据库连接设置
  • 在Flask-SQLAlchemy中,数据库使用URL指定,erq程序使用的数据库必须保存到Flask配置对象的SQLALCHEMY_DATABASE_URL键中
app.config['SQLALCHEMY_DATABASE_URL'] = 'mysql://账号:密码@ip:port/数据库名'
  • 其他设置
# 动态追踪修改设置,如未设置只会提示警告
app.config['SQLALCHENY_TRACK_MDDIFICATIONS'] = True
# 查询时会显示原始的SQL语句
app.config['SQLALCHEMY_ECHO'] = True
  • 配置完成需要去MySQL中创建项目所使用的数据库
mysql -uroot -pmysql
create database 数据库名 charset utf8;
  • 其他配置
名字 备注
SQLALCHEMY_DATABASE_URI 用于连接的数据库 URI 。例如:sqlite:////tmp/test.dbmysql://username:password@server/db
SQLALCHEMY_BINDS 一个映射 binds 到连接 URI 的字典。更多 binds 的信息见用 Binds 操作多个数据库
SQLALCHEMY_ECHO 如果设置为Ture, SQLAlchemy 会记录所有 发给 stderr 的语句,这对调试有用。(打印sql语句)
SQLALCHEMY_RECORD_QUERIES 可以用于显式地禁用或启用查询记录。查询记录 在调试或测试模式自动启用。更多信息见get_debug_queries()。
SQLALCHEMY_NATIVE_UNICODE 可以用于显式禁用原生 unicode 支持。当使用 不合适的指定无编码的数据库默认值时,这对于 一些数据库适配器是必须的(比如 Ubuntu 上 某些版本的 PostgreSQL )。
SQLALCHEMY_POOL_SIZE 数据库连接池的大小。默认是引擎默认值(通常 是 5 )
SQLALCHEMY_POOL_TIMEOUT 设定连接池的连接超时时间。默认是 10 。
SQLALCHEMY_POOL_RECYCLE 多少秒后自动回收连接。这对 MySQL 是必要的, 它默认移除闲置多于 8 小时的连接。注意如果 使用了 MySQL , Flask-SQLALchemy 自动设定 这个值为 2 小时。
连接其他数据库(常见的连接字符串)
  • Postgres:
postgresql://scott:tiger@localhost/mydatabase
  • MySQL:
mysql://scott:tiger@localhost/mydatabase
  • Oracle:
- oracle://scott:tiger@127.0.0.1:1521/sidname
  • SQLite (注意开头的四个斜线):
sqlite:////absolute/path/to/foo.db

常见SQLAlchemy字段类型

类型名 python中类型 说明
Integer int 普通整数,一般是32位
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.date 时间
Time datetime.datetime 日期和时间
LargeBinary str 二进制文件

常用的SQLAlchemy列选项

选项名 说明
primary_key 如果为True,代表表的主键
unique 如果为True,代表这列不允许出现重复的值
index 如果为True,为这列创建索引,提高查询效率
nullable 如果为True,允许有空值,如果为False,不允许有空值
default 为这列定义默认值

常用的SQLAlchemy关系选项

选项名 说明
backref 在关系的另一模型中添加反向引用
primary join 明确指定两个模型之间使用的联结条件
uselist 如果为False,不使用列表,而使用标量值
order_by 指定关系中记录的排序方式
secondary 指定多对多关系中关系表的名字
secondary join 在SQLAlchemy中无法自行决定时,指定多对多关系中的二级联结条件

数据库的基本操作

  • 在Flask-SQLALchemy中,插入、修改、删除操作,均由数据库会话管理
    • 会话用db.session表示。在准备把数据写入数据库前,要先将数据添加到会话中然后调用commit()方法提交会话
  • 在Flask-SQLALchemy中,查询操作是通过query对象操作数据
    • 最基本的查询是返回表中的所有数据,可以通过过滤器进行更精确的数据库查询

在试图函数中定义模型类

from flask import Flask
form flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)

# 设置连接数据库的url
app.config['SQLALCHEMY_DATABASE_URL'] = 'mysql://root:mysql@127.0.0.1:3306/数据库名'

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
# 查询时会显示原始的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(64), unique=True)
    us = db.relationship('User', backref='role')

    # repr()方法显示一个可读字符串
    def __repr__(self):
        return "Roles: %s" % self.name

if __name__ == '__main__':
    app.run(debug=True)

模型之前的关联

一对多
class Role(db.Model):
    # 关键代码
    us = db.relationship('User', backref='role', lazy='dynamic')

class User(db.Model):
    role_id = db.Column(dn.Integer, db.ForeignKey('roles.id'))
  • 其中realtionship描述了Role和User的关系。在此处,第一个参数为对应参照的类“User”
  • 第二个参数backref为类User申明新属性的方法
  • 第三个参数lazy决定了什么时候SQLALchemy从数据库中加载数据
    • 如果设置为子查询方式(subquery),则会在加载完Role对象后,就立即加载与其关联的对象,这样会让总查询数量减少,但如果返回条目数量很多,就会比较慢
      • 设置为subquery的话,role.users返回所有数据列表
    • 另外,也可以设置为动态方式(dynamic),这样关联对象会在被使用的时候在进行加载,并且在返回前进行过滤,如果返回的对象数很多,并没有做真正的查询,可以利用查询对象做其他逻辑,如:先排序咋返回
多对多
registrations = db.Table('registrations',  
    db.Column('student_id', db.Integer, db.ForeignKey('students.id')),  
    db.Column('course_id', db.Integer, db.ForeignKey('courses.id'))  
)  
class Course(db.Model):
    ...
class Student(db.Model):
    ...
    courses = db.relationship('Course',secondary=registrations,  
                                    backref='students',  
                                    lazy='dynamic')

常用SQLAlchemy查询过滤器

过滤器 说明
filter() 把过滤器添加到原查询上,返回一个新查询
filter_by() 把等值过滤器添加到原查询上,返回一个新查询
limit 使用指定的值限定原查询返回的结果
offset() 偏移原查询返回的结果,返回一个新查询
order_by() 根据指定条件对原查询结果进行排序,返回一个新查询
group_by() 根据指定条件对原查询结果进行分组,返回一个新查询
常用SQLAlchemy查询执行器
方法 说明
all() 以列表形式返回查询的所有结果
first() 返回查询的第一个结果,如果未查到,返回None
first_or_404() 返回查询的第一个结果,如果未查到,返回404
get() 返回指定主键对应的行,如不存在,返回None
get_or_404() 返回指定主键对应的行,如不存在,返回404
count() 返回查询结果的数量
paginate() 返回一个Paginate对象,它包含指定范围内的结果

创建表

db.create_all()

删除表

db.drop_all()

插入一条数据

ro1 = Role(name='admin')
db.session.add(ro1)
db.session.commit()

一次插入多条数据

us1 = User(name='wang',email='wang@163.com',password='123456',role_id=ro1.id)
us2 = User(name='zhang',email='zhang@189.com',password='201512',role_id=ro2.id)
...
db.session.add_all([us1,us2,...])
db.session.commit()

其他查询语句的使用方法

# 查询
# filter_by精准查询
User.query.filter_by(name='wang').all()

# first()返回查询到的第一个对象
User.query.first()

# all()返回查询到的所有对象
User.query.all()

# filter模糊查询,返回名字结尾字符为 g 的所有数据
User.query.filter(User.name.endwith('g')).all()

# get(): 参数为主键。如果主键不存在没有返回内容
User.query.get()

# 逻辑非,返回名字不等于wang的所有数据
User.query.filter(User.name != 'wang').all()

# not_ 相当于取反
form sqlalchemy import not_
User.query.filter(not_(User.name=='chen')).all()

# 逻辑与,需要导入 add,返回and()条件满足的所有内容
from sqlalchemy import and_
User.query.filter(and_(User.name != 'wang', User.email.endwith('163.com'))).all()

# 逻辑或, 需要导入 or_
from sqlalchemy import or_
User.query.filter(or_(User.name != 'wang', User.email.endwith('163.com'))).all()

# 查询数据后删除
user = User.query.first()
db.session.delete(user)
db.session.commit()
User.query.all()

# 更新数据
user = User.query.first()
user.name = 'dong'
db.session.commit()
User.query.first()

# 关联查询示例:角色和用户的关系是一对多的关系,一个角色可以有多个用户,一个用户只能属于一个角色
# 查询角色的所有用户
#查询roles表id为1的角色
ro1 = Role.query.get(1)
#查询该角色的所有用户
ro1.us.all()

# 查询用户所属角色
#查询users表id为3的用户
us1 = User.query.get(3)
#查询用户属于什么角色
us1.role

数据库迁移

  • 在开发的过程中,需要修改数据库模型,而且还要在修改之后更新数据库。最直接的方式是删除旧表,但这样会丢失数据
  • 更好的解决办法是使用数据库迁移框架,它可以追踪数据库模式的变化,然后把变动应用到数据库中
  • 在Flask中可以使用Flask-Migrete扩展,来实现数据迁移,并且集成到Flask-Script中,所有操作通过命令就能完成
  • 为了导出数据库迁移命令,Flask-Migrate提供了一个MigrateCommand类。可以附加到Flask-Script的manage对象上
# 安装
pip  install flask-migrate

# 代码文件内容:
from flask_migrate import Migrate, MigrateCommand

app = Flask(__name__)

# 第一个参数是Flask实例,第二个参数是sqlalchemy数据库实例
migrate = Migrate(app, db)

# manager是Flask-Script的实例,这条语句会在Flask-Script中添加一个db命令
manager.add_command('db', MigrateCommand)

创建迁移仓库:

# 这个命令会创建migrations文件夹,所有的迁移文件都放在里面
python 模型类所在的py文件名.py db init

创建迁移脚本:

  • 自动创建迁移脚本有两个函数
    • upgrade():函数把迁移中的改动应用到数据库中
    • downgrade():函数则将改动删除
  • 自动创建的迁移脚本会根据模型类的定义和数据库当前状态的差异,生成upgrade()downgrade()函数的内容
  • 对比不一定完全正确,有可能会遗漏一些细节,需要进行检查
python 模型类所在的py文件名.py migrate -m 'initial migration'
更新数据库
python 模型类所在的py文件名.py db upgrade
返回之前的版本
# 可以根据history命令找到版本号,然后传给downgrade命令
python app.py db history
输出格式:<base> ->  版本号 (head), initial migration

# 回滚到指定的版本
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) 版本号

蓝图(Blueprint)

模块化:随着flask程序越来越复杂,需要对程序进行模块化处理

Blueprint概念:Blueprint是一个存储操作方法的容器,这些操作在这个Blueprint被注册到一个应用之后就可以被调用,Flask可以通过Blueprint来组织url以及处理请求

Flask使用Blueprint让应用实现模块化,在Flask中,Blueprint具有以下属性:

  • 一个应用可以具有多个Blueprint
  • 可以将一个Blueprint注册代任何一个未使用的URL下
  • 在一个应用中,一个模块可以注册多次
  • Blueprint可以单独具有自己的模板、静态文件或者其他的通用操作方法,它并不是必须要实现应用的试图和函数的
  • 在一个应用初始化时,就应该要注册需要使用的Blueprint,但是一个Blueprint并不是一个完整的应用,它不能独立于应用运行,而是要注册到某一个应用中

使用蓝图:

  • 创建一个蓝图对象
admin = Blueprint('admin', __name__)
  • 在这个蓝图对象上进行操作,注册路由,指定静态文件夹,注册模板过滤器
@admin.route('/')
def admin_home():
	return 'admin_home'
  • 在应用对象上注册这个蓝图对象
app.register_blueprint(admin,__name__,url_prefix='/admin')
  • 当这个应用启动后,通过/admin/可以访问到蓝图中定义的试图函数

运行机制:

  • 蓝图是保存了一组将来可以在应用对象上执行的操作,注册路由就是一种操作
  • 当在应用对象上调用route装饰器注册路由时,这个操作将修改对象的url_map路由表
  • 然而,蓝图对象根本没有路由表,当我们在蓝图对象上调用route装饰器注册路由时,它只是在内部的一个延迟操作记录列表defered_functions中添加了一个项
  • 当执行应用对象的register_blueprint()方法时,应用对象将从蓝图对象的defered_functions列表中取出每一项,并以自身作为参数执行该匿名函数,即调用应用对象的add_url_rule()方法,这将真正的修改应用对象的路由表

蓝图的url前缀

  • 当在应用对象上注册一个蓝图时,可以指定一个url_prefix关键字参数(这个参数默认是/
  • 在应用最终的路由表url_map中,在蓝图上注册的路由URL自动加上了这个前缀,这个可以保证在多个蓝图中使用相同的URL规则而不会引起冲突,只要在注册蓝图时将不同的蓝图挂接到不同的自路径即可
  • url_for
url_for('admin.index')  # /admin/

注册静态路由

和应用对象不同,蓝图对象创建时不会默认注册静态目录的路由。需要在创建时指定static_folder参数

# 将蓝图所在目录下的 static_sdmin 目录设置为静态目录
admin = Blueprint("admin",__name__,static_folder='static_admin')
app.register_blueprint(admin,url_prefix='/admin')

之后就可以使用/admin/static_admin/访问static_admin,目录下的静态文件

定制静态目录URL 的规则:可以在创建蓝图对象时使用static_url_path来改变静态目录的路由

# 将static_admin文件夹的路由设置为 /lib
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中的文件
posted @ 2021-03-07 22:31  shangkh  阅读(177)  评论(0)    收藏  举报