flask框架基础

简介

  • 之前学习了Django框架,确实很沉,但也对web开发有了更进一步的认识,这里对比学习flask框架,大部分是理论,后面会总结一个自己跟过的项目
  • 客户端不一定是浏览器,也可以是PC软件、手机APP、爬虫程序
  • Web框架的核心是:实现路由和视图;即根据客户端的不同请求执行不同的逻辑形成要返回的数据
  • 重量级的框架:为方便业务程序的开发,提供了丰富的工具、组件(例如操作数据库),如Django
    • 耦合关系严密,不适合改动扩展,但是开发快速
  • 轻量级的框架:只提供Web框架的核心功能,自由、灵活、高度定制,如Flask、Tornado
  • Flask是用Python语言基于Werkzeug工具箱编写的轻量级Web开发框架。它主要面向需求简单的小应用
  • Flask本身相当于一个内核,其他几乎所有的功能都要用到扩展
    • 比如可以用Flask-extension加入ORM、窗体验证工具,文件上传、身份验证等
    • Flask没有默认使用的数据库,你可以选择MySQL,也可以用NoSQL
    • 其 WSGI 工具箱采用 Werkzeug(路由模块),模板引擎则使用 Jinja2
  • Flask可手动创建工程各种目录(灵活到有无限可能,以至于你不会用…)
  • 简而言之:路由有方法,视图自己写,其他装扩展!

对比

  • django提供了:

    django-admin快速创建项目工程目录

    manage.py 管理项目工程

    orm模型(数据库抽象层)

    admin后台管理站点

    缓存机制

    文件存储系统

    用户认证系统

  • flask提供了:

    啥也没提供…

扩展包

  • 蛋,flask有丰富的扩展包可以满足各种需求

    Flask-SQLalchemy:操作数据库;

    Flask-migrate:管理迁移数据库;

    Flask-Mail:邮件;

    Flask-WTF:表单;

    Flask-script:插入脚本;

    Flask-Login:认证用户状态;

    Flask-RESTful:开发REST API的工具;

    Flask-Bootstrap:集成前端Twitter Bootstrap框架;

    Flask-Moment:本地化日期和时间;

  • 中文文档:http://docs.jinkan.org/docs/flask/

  • 英文文档:https://flask.palletsprojects.com/en/0.12.x/,已更新到1.1

  • 目前很多扩展包使用Python2,因此创建基于Python2的虚拟环境

安装

  • 同样的,一个环境一堆事,建立新的虚拟环境是非常必要的

    mkvirtualenv flask_py	# 默认创建py3的
    deactivate
    rmvirtualenv flask_py	# 删除
    which python			# python执行目录
    mkvirtualenv -p /usr/bin/python2.7 flask_py2	# 不能sudo
    cd .virtualenv/flask_py2/
    # 在bin下拷贝py2的解释器
    # 在lib下用python2.7的包,达到隔离的目的
    
  • 配置虚拟环境

    # 可以使用 pip list 查看已安装包
    # 也可将现有的环境复制一份清单
    pip freeze > packages.txt
    pip install -r packages.txt
    
  • 安装

    pip install Flask	# 可以参考官方文档  1.1.2
    
  • 对比Django学习

    • 所以有些地方不会从头说起,如果你是小白建议点个赞就行了别看了…

基操

  • Hello World:hello.py

    # coding:utf-8
    
    # 导入Flask类,尽量不要使用 * 导入,有歧义
    from flask import Flask
    
    # Flask类接收一个参数__name__
    # __name__代表当前模块名称,这里就以hello.py文件所在目录为总目录
    # 从而定位同级static和templates文件夹
    app = Flask(__name__)	# 可以传任意字符串,默认也是将当前文件作为启动文件
    
    # 装饰器的作用是将路由映射到视图函数index
    # 视图也不需要request,动态路由直接传re匹配的参数名即可
    # 路径和参数都交给route处理=Django的urls=装饰器+application
    @app.route('/')
    def index():
        return 'Hello World'
    
    # Flask应用程序实例的run方法启动自带WEB服务器
    # 项目上线再替换
    if __name__ == '__main__':
        app.run()	# 没有manage.py管理脚本
    
    • 默认访问127.0.0.1:5000
      • 注:abc 是python内置模块哦
  • 关于__name__

    • 要看是否以当前文件作为启动文件
    # test.py
    # 以此文件直接运行
    print(__name__)		# 输出 __main__
    
    # 若导入到其他模块
    # 输出  test
    

配置参数

  • 参数配置

    from flask import Flask
    
    app = Flask(__name__,
                # 默认值是/static
               static_url_path = "/python",	# 访问的是静态文件,但使用/python/...
               static_folder = "static",	# 默认
               template_folder = "templates",	# 默认
               )
    
    # 配置参数并使用,三种方式
    app.config.from_pyfile('config.cfg')	# 从文件拿,还是以当前文件所在目录查找
    app.config.from_object('app.Config')	# 从对象拿,自定义class
    app.config['DEBUG'] = True	# 直接操作字典对象(内置配置)	Debuger is active
    
    class Config(object):
        DEBUG = True	# 开启调试模式
    
    # 视图函数拿取配置参数
    @app.route('/')
    def index():
        pring(app.config.get('DEBUG'))	# 字典的get()方法
        return 'Hello World'
    
    # 方式二
    from flask import current_app
    @app.route('/')
    def index():
        pring(current_app.config.get('DEBUG'))	# 字典的get方法
        return 'Hello World'	# 可以直接返回响应体
    
  • 运行服务器:需要配上host,不然防火墙过不去

    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=5000)	# 服务器绑定IP,任何都可访问
    

路由参数

  • 按照路由——视图——模板——模型的顺序学习

  • 查看当前路由配置

    print(app.url_map)
    
  • 限定路由方法

    @app.route('/post_get', methods=['POST','GET'])	# 方法不对报错 405
    def post_get():
        return 'both post and get method are allowed '
    
    # 和Django相同,如果路由相同(方法也同),先定义的会覆盖后定义的,按顺序访问
    # 如果多路由一视图:
    @app.route('/h1')
    @app.route('/h2')
    def hello():
        return "hello world"
    
  • 重定向,配合反向解析 url_for

    from flask import Flask, redirect
    @app.route('/login')
    def login():
        # 类似于reverse(),通过name属性找到路由名称,即是路由名称改变也不会影响
        url = url_for("index")	# 传递视图函数名称
        return redirect(url)	# 跳转报 302
    
  • 动态路由(获取参数)

    # 类似Django的转换器
    @app.route('/user/<int:id>')	# 还支持 float、path(接受 /)
    def hello_itcast(id):
        return 'hello Roy %d' %id
    
    # 默认使用字符串规则
    @app.route('/user/<id>')	# 后面的参数会以字符串形式被接收,名为id,不接受 / 
    
  • 自定义转化器

    # 自定义转换器获取路由参数
    from werkzeug.routing import BaseConverter
    
    class RegexConverter(BaseConverter):
    	# 外部传入regex即可
        def __init__(self, url_map, regex):
            # 父类初始化,交给flask操作
            super(RegexConverter, self).__init__(url_map)   
            # 将正则表达式的参数保存到对象属性中,flask就会使用这个pattern进行正则匹配
            self.regex = regex
    
    # 自定义转换器添加到flask应用中;url_map有点像request,对象
    app.url_map.converters['re'] = RegexConverter   # 例如还有 IntConverter...
    
    @app.route("/mobile/<re(r'1[34578]\d{9}'):numbers>")
    def mobile(numbers):
        return numbers
    
    • 上面定义的转化器类是万能版,在route中传入正则表达式,相当于Django的re_path
    • 可以直接如下定义,将 regex写死:
    class Mobile(BaseConverter):
        def __init__(self, url_map, regex):
            # 父类初始化,交给flask操作
            super(RegexConverter, self).__init__(url_map)   
            # 将正则表达式的参数保存到对象属性中,flask就会使用这个pattern进行正则匹配
            self.regex = r'1[34578]\d{9}'
    
    • 即从converters字典中获取类对象,绕了一圈得到匹配规则,提取参数
    • 这样看来,经过类岂不是很麻烦?其实关键在BaseConverter中的两个方法
    # 这是基类定义
    class BaseConverter(object):
        """Base class for all converters."""
    
        regex = "[^/]+"
        weight = 100
    
        def __init__(self, map):	# 需要传递map
            self.map = map
    
        def to_python(self, value):
            return value
    
        def to_url(self, value):
            if isinstance(value, (bytes, bytearray)):
                return _fast_url_quote(value)
            return _fast_url_quote(text_type(value).encode(self.map.charset))
    
    • to_python方法中,直接返回了匹配得到的值,我们在这里可以做点别的!
    class Mobile(BaseConverter):
        def __init__(self, url_map, regex):
            super(RegexConverter, self).__init__(url_map)   
            self.regex = r'1[34578]\d{9}'
            
        def to_python(self, value):
            # return value
            return "abcd"	# 这只是举例说明,最后匹配的结果要经过这里返回
    
    • to_url方法呢?之前使用url_for反向解析,目的是得到路由进行重定向,传入的是视图函数名称
    # 如何把参数也传过去呢?对应mobile视图
    @app.route("/mobile/<re(r'1[34578]\d{9}'):numbers>")
    def mobile(numbers):
        return numbers
    
    @app.route("/login")
    def login():
        # def url_for(endpoint, **values):
        url = url_for("mobile", numbers=13219510963)
        # 这里url_for先去找视图函数,拿着url会调用to_url方法,返回最后的url
        return redirect(url)
        
    class Mobile(BaseConverter):
        def __init__(self, url_map, regex):
            super(RegexConverter, self).__init__(url_map)   
            self.regex = r'1[34578]\d{9}'
            
        def to_url(self, value):
            # return value	# 原本返回:/mobile/13219510963
            return "18813008122"	# 就不会返回传入的numbers参数了
    
  • 以上便是完整的路由匹配的内容

    • route()相当于Django中的urls
    • 匹配参数有内置转换器,但有时候需要自定义
    • 自定义转换器类实现参数正则匹配,最后的结果需要经过两个关键方法返回
  • 路由参数是框架学习的重难点,后面会通过项目具体总结一下,各种参数的获取和传递

视图

  • 接下来看业务逻辑部分的重点

获取参数

  • 动态路由获取参数后,如何传递给视图函数呢?

    • 注:参数可以包含在查询字符串中,或者在请求体
  • 在flask中,使用全局request传递参数,也是对象

    from flask import Flask, request
    
  • 参数都可通过此对象的属性获取,区别就在于参数的传递方法和类型了:
    f1

    • 表单数据:表单可以上传参数,以key=value&的形式,也可以包含文件,flask会将其处理成类似字典的形式MultiDict,类似Django中的TypeDict
    • data属性不能拿出表单数据,可以拿其他请求体中的数据,都搞成字符串
    • args专门获取查询字符串参数,即url中用?key=value&的参数
      • 注:QueryString不局限于GET方法
  • 前端先定义模板携带参数,然后后端提供数据

    app = Flask(__name__)
    
    @app.route('/index', methods=['GET','POST'])
    def index():
        name = request.form.get('name')	# 尽量不要直接用['']获取字典值
        age = request.form.get('age')
        names = request.form.getlist('name')	# 重复键名
        city = request.args.get('city')
        
        print("data:%s"%request.data)
        return "name=%s, age=%s, namelist=%s,city=%s"%(name.age,names,city)
    
  • 为了避免修改代码测试,使用postman工具模拟请求

    • 属于Chrome的扩展程序,解压后在浏览器开发者模式添加即可
    • 下载链接

保存文件

  • 还是使用request对象的属性

    @app.route('/upload', methods=['GET', 'POST'])
    def upload_file():
        if request.method == 'POST':		# 不用方法对应不同逻辑,也是常见策略
            f = request.files['the_file']	# 表单的name属性值
            f.save('/var/www/uploads/uploaded_file.txt')
            
    # 上面这么写有点low了
    @app.route('/upload', methods=['POST'])
    def upload_imgs():
        file = request.files.get('img')
        if file is None:
            return "未上传..."		# 健壮了是不是
        
        # 打开文件
        f = open('./demo.png', 'wb')	# 打开写入空间
        data = file.read()
        f.write(data)
        f.close()
        # 直接使用文件对象保存
        # file.save('./demo.png')
        return "上传成功"
    
  • 深入解析with函数

    # 使用with可以自动捕获异常,上面就可以写成
    @app.route('/upload', methods=['GET', 'POST'])
    def upload_file():
        file = request.files.get('img')
        if file is None:
            return "未上传..."
        
        with open("./demo.png", "wb") as f:	# 一般是在读取时使用,更有可能异常
            data = file.read()
            f.write(data)
    
    # with为什么能捕获异常?实则是open类的功劳
    class OpenFiles(object):
        def __enter__(self):	# 刚进入with语句时
            print("enter...")
            
    	def __exit__(self, exc_type, exc_val, exc_tb):
            # 离开with语句时调用
            print("异常类型:%s" % exc_type)
            print("异常提示:%s" % exc_val)
            print("追踪信息:%s" % exc_tb)
            self.close()	# with自动关闭打开的文件
            
    with OpenFiles("file.png", "wb") as of:
        print("自定义with使用的对象...")
        a = 1/0
        print("异常结束...")
    
  • abort()函数,异常处理

    from flask import Flask, abort, Response
    # 终止当前视图函数的运行, 并传递信息
    @app.route("/login")
    def login():
        if name != 'roy' or password != '123456':
            # 返回给前端信息
            abort(400)	# 必须是标准状态码
            res = Response("login failed")
            abort(res)	# 返回响应体信息,不能直接传字符串
    
    • 自定义错误处理方法:
    @app.errorhandler(404)
    def error(err):	# 必须接收一个参数,方便如下自定义吧,不然还不如直接在外面print了
        return '您请求的页面不存在,请确认后再次访问!%s'%err
    

返回信息

  • 返回自定义响应信息,之前数据流是从前向后,现在从后向前

  • 可以直接return响应体、状态码、响应头等信息,也可以使用make_response对象

    # 使用元祖,返回自定义响应信息
    @app.route('/index')
    def index():
        # 元祖的两个元素组成键值对
        # 响应体  状态码  响应头
        return "index~", 400, [('name', 'roy'), ('prov', 'NingXia')]
    	# {'name':'roy', 'prov':'NingXia'}  用字典也可以
        # 自定义状态码并附加说明信息:  "888 special"
    
    from flask import Flask, make_response
    # 构造响应头信息
    @app.route('/index')
    def index():
        resp = make_response("success")	# 响应体
        resp.headers["sample"] = "value"
        resp.status = "404 not found"
        return resp
    
    • 注:直接return,不论是体、头还是状态,都包含在元祖,只不过可以不写括号
  • python字典与json字符串互化:

    • 后端处理得到的数据类型可能是各种,返回给前端需要的格式
    • 使用 u 在字符串前面是转成Unicode编码(py2遗留问题)
      f2
      import json
      # 直接返回json格式数据,响应头中的content-type还是text/html
      def index():
          # request拿到数据,处理...
          ......
          data = {
              "name":"roy",
              "age":18
          }
          json_str = json.dumps(data)
          return json_str, 200, {"Content-Type":"application/json"}
      
      # 这么多事让我做?类似于JsonResponse
      from flask import jsonify
      def index():
          # request拿到数据,处理...
          ......
          data = {
              "name":"roy",
              "age":17
          }
          return jsonify(data)	# 以json返回
      

cookie

  • 设置cookie,通过make_response 添加响应头,而非request对象
    from flask import Flask, make_response, request
    
    @app.route('/set_cookie')
    def set_cookie():
        resp = make_response("success")	# 通过返回设置,添加响应头:Set-Cookie
        resp.set_cookie("name","roy")	# 关闭浏览器失效
        resp.set_cookie("age", "18", max_age=3600)	# 而不是request对象了
        return resp
    
    @app.route('/get_cookie')
    def get_cookie():
        cookie = request.cookies.get("name")	# 获取是request
        return cookie
    
    @app.route('/del_cookie')
    def del_cookie():	# 无法真正删除,只能设置过期
        resp = make_response("del success")
        resp.delete_cookie("age")
        return resp
    

session

  • 设置session不是request也不是response,而是通过专门的session模块
    from flask import Flask, session
    
    app.config['SECRET_KEY'] = 'fdnavnrNOVFNONOnnce147r1qNFDAIN'	# 随机设置密钥
    
    @app.route('/login')
    def login():
        session['name'] = 'roy'
        session['age'] = 18
        return "login...	"
    
    @app.route('/get_session')
    def get_session():
        name = session.get('name')
        return name
    
    if __name__ = '__main__':
        app.run(debug=True)
    
  • Django会在cookie中添加session_id,session信息保存在后端数据库,但是flask不一样
  • flask会使用密钥对session加密直接保存在cookie中
    • session可以保存在哪里呢?MySQL数据库、Redis数据库、文件、内存中(字典变量)
  • 如果用户禁止了cookie呢?
    • 放在URL的查询字符串中,但这样也不能设置过期时间咯
  • 上下文
    • 上下文是指当前用户所处的环境,例如request是全局对象,当同一时刻有多个用户请求时存在竞争,此时的request代表哪个用户呢?
      666

      • 这就需要request反映出来的内容和具体的用户请求有关,也叫作请求上下文
        request.png
    • 怎么结合具体的用户请求呢?线程编号(一个线程对应一个键值)

    • 每个用户对应一个线程编号,可以理解成request是线程内全局变量,线程间的局部变量

    • 请求上下文(request context) :request和session都属于请求上下文对象

      • 请求当下文,即前端请求代表当前用户所处的环境
    • 应用上下文(application context):current_app和g都属于应用上下文对象

      • 即当前请求的视图所在的整个应用,app = Flask(__name__),关系到后端环境
      • g 类似一个空对象,可以自己设置属性名称存点东西;哪里不能存,非朝这里存?
      from flask import g
      @app.route('/login')
      def login():
          g.username = 'roy'
          index()
          return "OK..."
      
      def index():
          name = g.username	# 在一次请求的多个视图函数之间传递变量
          print("fucking...")
      
    • 特点是每次请求之前都会清空保存的属性

请求钩子

  • hook

  • 请求钩子类似于css中的伪类选择器before和after,或者说中间件

  • 请求钩子是通过装饰器的形式实现,Flask支持如下四种:

    # before_first_request:在处理第一个请求前运行。
    @app.before_first_request
    
    # before_request:在每次请求前运行
    @app.before_request
    
    # after_request(response):如果没有未处理的异常抛出,在每次请求后运行
    @app.after_request
    
    # teardown_request(response):在每次请求后运行,即使有未处理的异常抛出
    @app.teardown_request
    
  • request.path获取请求路径:例如127.0.0.1:5000/index得到 /index

    @app.teardown_request
    def handle_teardown_request(response):
        path = request.path
        if path in [url_for("index"), url_for("login")]:
            print("钩子中判断视图逻辑:index")
        else:
            print("未包含相关请求路径")
    	return response
    

扩展命令行

  • 需要安装扩展包 pip install Flask-Script

    # manager_test.py
    from flask import Flask
    from flask_script import Manager
    
    app = Flask(__name__)
    
    manager = Manager(app)	# 管理当前应用
    
    @app.route('/')
    def index():
        return '床前明月光'
    
    if __name__ == "__main__":
        manager.run()
    
  • 此时需要使用命令行启动此文件,类似Django中的manage.py

    python manager_test.py runserver	# 还支持shell,但不需要再像ipython导入了
    

模板

  • 模板文件放在templates文件夹下,之前在实例化应用时配置过

变量

  • 和Django中类似,在模板中新建index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <p>{{name}}</p>
        <p>{{age}}</p>
        <p>{{dicts.city}}</p>
        <p>{{dicts["county"]}}</p>
        <p>{{lists[0]}}</p>
    </body>
    </html>
    
  • 使用render_template渲染

    from flask import Flask, render_template
    
    app = Flask(__name__)
    
    @app.route('/index')
    def index():
        data = {
            "name":"flask",
            "function":"zhuangB",
            "dicts":{"city":"BJ", "county":"HD"},
            "lists":[1,2,3,4,5]
        }
        return render_template("index.html", **data)	# 可以直接平铺在这
    	# return render_template("index.html", name="flask",function="zhuangB")
    

过滤器

  • 类似的,提供以下转义

    // 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>
    
    // trim:把值的首尾空格去掉;
      <p>{{ ' hello world ' | trim }}</p>
    
    // reverse:字符串反转;
      <p>{{ 'olleh' | reverse }}</p>
    
    // format:格式化输出;
      <p>{{ '%s is %d' | format('name',17) }}</p>
    
    // striptags:渲染之前把值中所有的HTML标签都删掉;
      <p>{{ '<em>hello</em>' | striptags }}</p>
    
  • 支持链式使用过滤器

    <p>{{ “ hello world  “ | trim | upper }}</p>
    
  • 列表过滤器

    // first:取第一个元素
      <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>
    

xss

  • xss指注入恶意指令代码到网页

  • 前端传递过来的可执行代码:
    f3

  • 如果此时提交内容如下:

    <script>
    	alert("Hello Attack")
    </script>
    
  • 注:Chrome是自动防范xss攻击的,可在火狐测试

自定义过滤器

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

  • 有两种自定义方式

    • 通过当前应用的add_template_filter 函数注册
    def filter_double_sort(ls):	# 必须传参啊
        return ls[::2]	# [0:end:2]
    
    # 参数:过滤器函数, 模板中使用的过滤器名称
    app.add_template_filter(filter_double_sort,'step_2')
    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        <p>{{lists | step_2}}</p>
    </body>
    </html>
    
    • 通过template_filter 装饰器
    @app.template_filter('ver_3')	# 传递过滤器名称
    def filter_double_sort(ls):
        return ls[::-3]
    

表单

  • 前端会对提供的数据进行校验,但是数据可以伪造,因此无论前端是否校验,后端都要校验数据

  • 对表单数据的校验有特定的方式,因此可以抽象化

  • 使用Flask-WTF表单扩展,可以帮助进行CSRF验证,帮助我们快速定义表单模板,而且可以在视图中验证表单的数据,

  • 后端定义前端模板,前端提交后反过来进行验证,并进行相应处理(后-前-后)

  • 安装环境pip install Flask-WTF

    <form method="post">
        <!--设置csrf_token-->
        {{ form.csrf_token }}
        {{ form.us.label }}
        <p>{{ form.us }}</p>
        
        {{ form.ps.label }}
        <p>{{ form.ps }}</p>
        <!--打印出自定义error信息-->
        {% for msg in form.ps.errors %}
        <p>{{ msg }}</p>
        {% endfor %}
        
        {{ form.ps2.label }}
        <p>{{ form.ps2 }}</p>
        {% for msg in form.ps2.errors %}
        <p>{{ msg }}</p>
        {% endfor %}
        
        <p>{{ form.submit }}</p>
        
        {% for x in get_flashed_messages() %}
        {{ x }}
        {% endfor %}
     </form>
    
  • 定义表单的模型类,一般是数据库ORM,这里是表单模型,后台定义表单渲到前端

    • 按用户流程理解,肯定是先return最后那个,然后提交走if
    from flask import Flask,render_template, redirect,url_for,session,request,flash
    
    # 导入wtf扩展的表单类
    from flask_wtf import FlaskForm
    # 导入自定义表单需要的字段,有支持的标准HTML字段
    from wtforms import SubmitField,StringField,PasswordField
    # 导入wtf扩展提供的表单验证器,有提供常用验证器
    from wtforms.validators import DataRequired,EqualTo
    
    app = Flask(__name__)
    app.config['SECRET_KEY']='anfojac13rCAWac'
    
    #自定义表单模型类
    class Login(Flask Form):	# 以后中文属性前面都要加u
        us = StringField(label=u'用户:',validators=[DataRequired(u"用户名不能为空")])
        ps = PasswordField(label=u'密码',validators=[DataRequired(u"密码不能为空")])
        ps2 = PasswordField(label=u'确认密码',validators=[DataRequired(),EqualTo('ps',u'两次密码不一致')])
        submit = SubmitField(u'提交')		# 提交后还是到这里来
    
    # 定义根路由视图函数,生成表单对象,获取表单数据,进行表单数据验证
    @app.route('/register',methods=['GET','POST'])
    def register():
        form = Login()	# 实例化,从视图函数把表单模板渲染出来
        # 表单提交,如果验证通过:
        if form.validate_on_submit():
            name = form.us.data
            pswd = form.ps.data
            pswd2 = form.ps2.data
            print name,pswd,pswd2	# 后端打印
            session['username'] = name
            return redirect(url_for('index'))
        else:
            if request.method=='POST':	# 通过请求体提交的数据才刷新(删除已输入)
                flash(u'信息有误,请重新输入!')
    	return render_template('index.html',form=form)
    @app.route('/index')
    def index():
        username = session.get('username')
        return "Hello, %s"%username
    
    if __name__ == '__main__':
        app.run(debug=True)
    
  • 浏览器渲染结果:
    f4

  • WTForms支持的HTML标准字段(帮忙建表和基础校验)

    字段对象说明
    StringField文本字段
    TextAreaField多行文本字段
    PasswordField密码文本字段
    HiddenField隐藏文本字段
    DateField文本字段,值为datetime.date格式
    DateTimeField文本字段,值为datetime.datetime格式
    IntegerField文本字段,值为整数
    DecimalField文本字段,值为decimal.Decimal
    FloatField文本字段,值为浮点数
    BooleanField复选框,值为True和False
    RadioField一组单选框
    SelectField下拉列表
    SelectMultipleField下拉列表,可选择多个值
    FileField文本上传字段
    SubmitField表单提交按钮
    FormField把表单作为字段嵌入另一个表单
    FieldList一组指定类型的字段
  • WTForms常用验证器:

    验证函数说明
    DataRequired确保字段中有数据
    EqualTo比较两个字段的值,常用于比较两次密码输入
    Length验证输入的字符串长度
    NumberRange验证输入的值在数字范围内
    URL验证URL
    AnyOf验证输入值在可选列表中
    NoneOf验证输入值不在可选列表中

  • 类似于python中的函数,宏的作用就是在模板中重复利用代码,避免代码冗余
  • 在模板中定义如下:
    {% macro input() %}
      <input type="text"
             name="username"
             value=""
             size="30"/>
    {% endmacro %}
    <!--使用-->
    {{ input() }}
    
    <!--带参数的宏-->
    {% macro input(name='define',value='',type='text',size=20) %}
        <input type="{{ type }}"
               name="{{ name }}"
               value="{{ value }}"
               size="{{ size }}"/>
    {% endmacro %}
    <!--调用-->
    {{ input(value='name',type='password',size=40)}}
    
  • 将宏单独封装在html文件中:macro.html
    {% macro input() %}
        <input type="text" name="username" placeholde="Username">
        <input type="password" name="password" placeholde="Password">
        <input type="submit">
    {% endmacro %}
    
  • 在其他模板文件中导入
    {% import 'macro.html' as func %}
    {% func.input() %}
    

模型

  • flask的数据库还是使用扩展
  • 到目前,我们使用了命令行扩展、表单扩展、数据库扩展

数据库

  • Web应用中普遍使用的是关系数据库,把所有的数据都存储在表中,使用结构化的查询语言。关系型数据库的列定义了表中表示的实体的数据属性

  • Flask本身不限定数据库的选择,你可以选择SQL或NOSQL的任何一种

  • 也可以选择更方便的SQLALchemy,类似于Django的ORM

  • SQLAlchemy实际上是对数据库的抽象,让开发者不用直接和SQL语句打交道,而是通过Python对象来操作数据库,在舍弃一些性能开销的同时,换来的是开发效率的较大提升

  • 安装扩展:

    pip install flask-sqlalchemy	# 这只是一个将python模型类转化成sql语句的工具
    pip install flask-mysqldb		# 仍然需要拿着sql操作数据库
    
    • flask-mysqldb相当于对Python2中使用的MySQL-Python进行封装,都是数据驱动
  • 数据库设置

    app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/flask_test'
    
    # 自动跟踪数据库
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
    
  • 使用,这里用类对象加载配置(一般用config文件)
    f5

模型类

  • SQLAlchemy支持的字段类型,对应MySQL的字段要求

    类型名python中类型说明(mysql)
    Integerint普通整数,一般是32位
    SmallIntegerint取值范围小的整数,一般是16位
    BigIntegerint或long不限制精度的整数
    Floatfloat浮点数
    Numericdecimal.Decimal普通整数,一般是32位
    Stringstr变长字符串
    Textstr变长字符串,对较长或不限长度的字符串做了优化
    Unicodeunicode变长Unicode字符串
    UnicodeTextunicode变长Unicode字符串,对较长或不限长度的字符串做了优化
    Booleanbool布尔值
    Datedatetime.date时间
    Timedatetime.datetime日期和时间
    LargeBinarystr二进制文件
  • 同样,完整性约束不能少:

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

    • 自定义表名
    • 多端外键
    • 建立relationship进行一查多
    • 实例化类新增数据
    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'] = True
    
    #设置每次请求结束后会自动提交数据库中的改动
    # app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
    
    #查询时会显示原始SQL语句
    app.config['SQLALCHEMY_ECHO'] = True
    
    db = SQLAlchemy(app)	# 得到数据库操作对象
    
    class Role(db.Model):
        # 自定义表名,一般对表名都是有制定规范的
        __tablename__ = 'roles'
        # 定义列对象
        id = db.Column(db.Integer, primary_key=True)	# Column也代表真实数据
        name = db.Column(db.String(64), unique=True)	
        users = db.relationship('User', backref='role')	# 方便一查多
        # flask没有Django的 user_set语法
        # backref='role' 相当于给User添加了一个属性,让User查Role的时候不再是得到id,而是直接得到对应的行值;(非必要)
    
        #repr()方法显示一个可读字符串
        def __repr__(self):
            return 'Role:%s'% self.name
    
    # 注意定义格式,该背的,就背一背
    class User(db.Model):
        __tablename__ = 'users'
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(64), unique=True, index=True)	# 给name字段加索引
        email = db.Column(db.String(64),unique=True)	# 可以在唯一字段加索引
        pswd = db.Column(db.String(64))
        role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))	# 多端(一个角色多个User),多查一
    
        def __repr__(self):
            # query.get()时显示的信息格式
            # 类似__str__()
            return 'User:%s'%self.name
        
    if __name__ == '__main__':
        db.drop_all()	# 第一次建库才做,清除所有数据
        # 建表
        db.create_all()
        # 实例化对象插入新数据
        ro1 = Role(name='admin')
        ro2 = Role(name='user')
        # db.session记录对象任务(类似于git中的add,到暂存区)
        db.session.add_all([ro1,ro2])
        # 提交任务执行
        db.session.commit()	# 由于建立了跟踪,ro1/ro2的id被同步
        
        us1 = User(name='wang',email='wang@163.com',pswd='123456',role_id=ro1.id)
        us2 = User(name='zhang',email='zhang@189.com',pswd='201512',role_id=ro2.id)
        us3 = User(name='chen',email='chen@126.com',pswd='987654',role_id=ro2.id)
        us4 = User(name='zhou',email='zhou@163.com',pswd='456789',role_id=ro1.id)
        db.session.add_all([us1,us2,us3,us4])
        db.session.commit()
        
        app.run(debug=True)
    
  • SQLAlchemy可能因为MySQL的版本问题出现隔离级别错误,需要修改其源代码:base.py
    f7

    • mysql的隔离级别有四种,是为了防止事务执行过程中的问题(脏读、不可重复读、幻读),也和InnoDB引擎默认行级锁相关联(隔离就是加锁)

查询

  • 常用的SQLAlchemy查询执行器

    • 任何查询都要使用查询执行器才能执行
    方法说明
    all()以列表形式返回查询的所有结果
    first()返回查询的第一个结果,如果未查到,返回None
    first_or_404()返回查询的第一个结果,如果未查到,返回404
    get()返回指定主键对应的行,如不存在,返回None
    get_or_404()返回指定主键对应的行,如不存在,返回404
    count()返回查询结果的数量
    paginate()返回一个Paginate对象,它包含指定范围内的结果
    • count()first()非常常用
  • 常用的SQLAlchemy查询过滤器

    过滤器说明
    filter()把过滤器添加到原查询上,返回一个新查询
    filter_by()把等值过滤器添加到原查询上,返回一个新查询
    limit使用指定的值限定原查询返回的结果
    offset()偏移原查询返回的结果,返回一个新查询
    order_by()根据指定条件对原查询结果进行排序,返回一个新查询
    group_by()根据指定条件对原查询结果进行分组,返回一个新查询
  • 查询,方法有两类:flask-mysql和sqlalchemy

    # 类.query查询,flask-mysqldb的方法
    us = User.query.all()
    ro1 = us[0]
    ro1.name
    User.query.get(1)	# 拿到id=1的数据,对应的显示信息在类中的__repr__魔术方法指定
    User.query.filter_by(name='wang').all()
    
    # db.session查询,SQLAlchemy的方法
    db.session.query(Role).all()
    
    # 或查询
    User.query.filter(or_(User.name=='wang', User.email.endswith('163.com'))).all
    # 同样的还有and_ not_
    
    # 跳过两条,从记录第一条开始跳
    User.query.offset(2).all()
    # 如果这样写呢?
    User.query.all().offset(2)
    
    User.query.order_by(User.id.asc()).all()	# desc()
    
    # 分组查询
    db.session.query(User.role_id, func.count(User.role.id)).group_by(User.role_id).all()
    
    # 关联查询
    # 一查多,建立了relationship之后
    Role.users[0].name
    # 多查一
    u = User.query.get(1)
    # 没有反向引用
    Role.query.get(u.role_id)
    # 若backref,直接查询这个角色下的所有用户
    u.role
    
    • 更常用的是db.session.query,接上过滤器,真香!
    • db.session.commit()是提交了数据到数据库(面向整个服务会话),但是没有刷新模型映射中的数据,也就是说,model.query()中的数据没刷新,有可能查不到!
    • 总之,你就当model.query()是个插曲!
    • label('xxx')给查询出来的值添加常量名,可以用对象. 的方式调用
  • 查询更新

    # 链式操作
    User.query.filter_by(User.name='zhou').update({'name':'yang', 'email':'szsplyr@163.com'})
    
  • 查询删除

    u = User.query.get(3)
    db.session.delete(u)
    
  • 一般在执行更新和删除之前,都要先进行查询验证,看条件是否正确,防止误操作

案例

  • 图书添加删除

  • 分别建立作者和书名类:

    #coding=utf-8
    from flask import Flask,render_template,redirect,url_for
    from flask_sqlalchemy import SQLAlchemy
    
    app = Flask(__name__)
    
    #设置连接数据
    app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/test1'
    
    #设置每次请求结束后会自动提交数据库中的改动
    app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
    #设置成 True,SQLAlchemy 将会追踪对象的修改并且发送信号。这需要额外的内存, 如果不必要的可以禁用它。
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
    
    #实例化SQLAlchemy对象
    db = SQLAlchemy(app)
    
    #定义模型类-作者
    class Author(db.Model):
        __tablename__ = 'author'
        id = db.Column(db.Integer,primary_key=True)
        name = db.Column(db.String(32),unique=True)
        email = db.Column(db.String(64))
        au_book = db.relationship('Book',backref='author')
        def __str__(self):
            return 'Author:%s' %self.name
    
    #定义模型类-书名
    class Book(db.Model):
        __tablename__ = 'books'
        id = db.Column(db.Integer,primary_key=True)
        info = db.Column(db.String(32),unique=True)
        leader = db.Column(db.String(32))
        au_book = db.Column(db.Integer,db.ForeignKey('author.id'))
        def __str__(self):
            return 'Book:%s,%s'%(self.info,self.lead)
    
  • 表单界面:使用WTF,从后端到前端

    from flask import Flask,render_template,url_for,redirect,request
    from flask_sqlalchemy import SQLAlchemy
    from flask_wtf import FlaskForm
    from wtforms.validators import DataRequired
    from wtforms import StringField,SubmitField
    
    app = Flask(__name__)
    
    app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@localhost/test1'
    # app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
    app.config['SECRET_KEY']='s'
    
    db = SQLAlchemy(app)
    
    # 创建表单类,用来添加信息
    class Append(Form):
        au_info = StringField(validators=[DataRequired()])
        bk_info = StringField(validators=[DataRequired()])
        submit = SubmitField(u'添加')
    
    
    @app.route('/',methods=['GET','POST'])
    def index():
        # 创建表单对象
        form = Append()
        # 查询所有作者和书名信息
        author = Author.query.all()
        book = Book.query.all()
        # -----------------------------------------
        if form.validate_on_submit():	# 验证通过,返回数据插入数据库
            # 获取表单输入数据
            wtf_au = form.au_info.data
            wtf_bk = form.bk_info.data
            # 把表单数据存入模型类
            db_au = Author(name=wtf_au)
            db_bk = Book(info=wtf_bk)
            # 提交会话
            db.session.add_all([db_au,db_bk])
            db.session.commit()
            #添加数据后,再次查询所有作者和书名信息
            author = Author.query.all()
            book = Book.query.all()
            return render_template('index.html',author=author,book=book,form=form)
        else:
            if request.method=='GET':
                render_template('index.html', author=author, book=book,form=form)
    	# ----------------------------------------
        return render_template('index.html',author=author,book=book,form=form)
    
    # 使用ajax异步请求
    # 删除作者
    @app.route('/delete_author<int:aid>')
    def delete_author(aid):
        #精确查询需要删除的作者id
        au = Author.query.filter_by(id=aid).first()
        db.session.delete(au)
        #直接重定向到index视图函数
        return redirect(url_for('index'))
    
    # 删除书名
    @app.route('/delete_book<int:bid>')	# /delete_book{{book_id}}
    # 前端还可以使用 /delete_book/{{book_id}}	
    # 也可以 /delete_book?book_id={{book_id}},要指明methods=['GET'],后端使用request.args.get('book_id')
    def delete_book(bid):
        #精确查询需要删除的书名id
        bk = Book.query.filter_by(id=bid).first()
        db.session.delete(bk)
        #直接重定向到index视图函数
        return redirect(url_for('index'))
    
    
    if __name__ == '__main__':
        db.drop_all()
        db.create_all()
        #生成数据
        au_xi = Author(name='我吃西红柿',email='xihongshi@163.com')
        au_qian = Author(name='萧潜',email='xiaoqian@126.com')
        au_san = Author(name='唐家三少',email='sanshao@163.com')
        bk_xi = Book(info='吞噬星空',lead='罗峰')
        bk_xi2 = Book(info='寸芒',lead='李杨')
        bk_qian = Book(info='飘渺之旅',lead='李强')
        bk_san = Book(info='冰火魔厨',lead='融念冰')
        #把数据提交给用户会话
        db.session.add_all([au_xi,au_qian,au_san,bk_xi,bk_xi2,bk_qian,bk_san])
        #提交会话
        db.session.commit()
        app.run(debug=True)
    
  • 展示界面

    <h1>玄幻系列</h1>
    <form method="post">
        {{ form.csrf_token }}
        <p>作者:{{ form.au_info }}</p>
        <p>书名:{{ form.bk_info }}</p>
        <p>{{ form.submit }}</p>
    </form>
    
    <ul>
        <li>{% for x in author %}</li>
        <li>{{ x }}</li><a href='/delete_author{{ x.id }}'>删除</a>
        <li>{% endfor %}</li>
    </ul>
    <hr>
    <ul>
        <li>{% for x in book %}</li>
        <li>{{ x }}</li><a href='/delete_book{{ x.id }}'>删除</a>
        <li>{% endfor %}</li>
    </ul>
    
  • 除了上面的方式,页面时上面添加下面展示,可以使用ajax异步请求

    <a href="javascript:;" book_id="{{book.id}}"></a>
    
    <script type="text/javascript" src="js/jquery-1.12.4.min.js"></script>
    <script type="text/javascript">
        $("a").click(function(){
            var data = {
                book_id : $(this).attr('book_id')
            };
            var del_id = JSON.stringify(data)	// 转化为json数据格式
            $.ajax({
                url : '/delete_book',
                type : 'post',
                data : del_id,
                contentType : 'application/json',
                dataType : 'json',
                success : function(data){
                    if(data.code == 0){	// 返回code
                        alert('OK');
                        location.href = '/'
                    }
                } 
            })
        })
    </script>
    
  • 相应的视图函数要改变:使用request.get_json()获取参数

    @app.route('/delete_book', methods=['POST'])	# ajax的POST请求
    def delete_book():
        data = request.get_json()	# 获取前端请求参数
        bk_id = data.get('book_id')
        # 精确查询需要删除的书名id
        bk = Book.query.filter_by(id=bk_id).first()
        db.session.delete(bk)
        #直接重定向到index视图函数
        return redirect(url_for('index'))
    

数据库迁移

  • 在开发过程中,需要修改数据库模型,而且还要在修改之后更新数据库

  • 最直接的方式就是删除旧表,但这样会丢失数据

  • 更好的解决办法是使用数据库迁移框架,类似Django中的migration,它可以追踪数据库模式的变化,然后把变动应用到数据库中

  • 在Flask中可以使用Flask-Migrate扩展,来实现数据迁移(来了来了扩展它又来了)

  • Flask-Migrate提供了一个MigrateCommand类,可以附加到flask-script的manager对象上管理

    # 安装
    pip install flask-migrate
    
  • 模型类:database.py

    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'
    app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
    db = SQLAlchemy(app)
    
    # 第一个参数是Flask的实例,第二个参数是Sqlalchemy数据库实例
    migrate = Migrate(app,db) 	# 会自动将Migrate对象塞到app中维护,不接收也可以
    
    # 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)
        def __repr__(self):
            return 'Role:'.format(self.name)
    
    #定义用户
    class User(db.Model):
        __tablename__ = 'users'
        id = db.Column(db.Integer, primary_key=True)
        username = db.Column(db.String(64), unique=True, index=True)
        def __repr__(self):
            return 'User:'.format(self.username)
        
    if __name__ == '__main__':
        manager.run()
    
  • 创建迁移仓库,下面这个命令会创建migrations文件夹,所有迁移文件都放在里面

    python database.py db init		# db 是自定义命令
    
  • 创建自动迁移脚本,类似Django中的makemigrations,更新数据库

    python database.py db migrate -m 'initial migration'	# -m是说明信息
    python database.py db upgrade
    
  • 查询日志,回退版本

    python database.py db history	# 可以查出版本号
    python database.py db downgrade 版本号
    

发送邮件

  • 扩展又来了!
  • Flask的扩展包 Flask-Mail 通过包装了Python内置的smtplib包,可以用在Flask程序中发送邮件
  • Flask-Mail连接到简单邮件协议(Simple Mail Transfer Protocol,SMTP)服务器
  • 开启QQ邮箱的SMTP服务:登录到自己的qq邮箱设置即可,以后你就是发送人
    from flask import Flask
    from flask_mail import Mail, Message
    
    app = Flask(__name__)
    #配置邮件:服务器/端口/传输层安全协议/邮箱名/密码
    app.config.update(
        DEBUG = True,
        MAIL_SERVER='smtp.qq.com',
        MAIL_PROT=465,
        MAIL_USE_TLS = True,
        MAIL_USERNAME = '371673381@qq.com',
        MAIL_PASSWORD = 'xxxxxxxxx',	# QQ密码
    )
    
    mail = Mail(app)
    
    @app.route('/')
    def index():
     # sender 发送方,recipients 接收方列表
        msg = Message("This is a test message ",sender='371673381@qq.com', recipients=['3248828058@qq.com', 'szsplyr@163.com'])
        #邮件内容
        msg.body = "Flask test mail"
        #发送邮件
        mail.send(msg)
        print "Mail sent"
        return "Sent Succeed"
    
    if __name__ == "__main__":
        app.run()
    

蓝图

  • 学习Flask框架,是从写单个文件,执行hello world开始的。我们在这单个文件中可以定义路由、视图函数、定义模型等等
  • 但随着业务代码的增加,将所有代码都放在单个程序文件中,是非常不合适的。这不仅会让代码阅读变得困难,而且会给后期维护带来麻烦
  • 如果自己进行模块划分,将部分视图抽出来,以模块的方式导入
    f8
    • 解决上述问题可以通过延迟一方导入,例如将main中的导入放到index()函数内执行
    • 或者通过装饰器的函数调用方式,还记得三层/两层装饰器吗?分别接收装饰器参数(最外层)和函数参数(最内层),这里用三层装饰器
      f9
  • 蓝图:用于实现单个应用(app)的视图、模板、静态文件的集合;简而言之,蓝图是一个小模块,将路由、视图封装起来,在项目中注册使用

使用

  • 创建蓝图对象

    # Blueprint必须指定两个参数,admin表示蓝图的名称,__name__表示蓝图所在模块
    from flask import Blueprint
    admin = Blueprint('admin', __name__)	# __name__方便蓝图定位文件
    
  • 可以定义具体的视图函数了

    @admin.route('/')
    def index():
        return 'admin_index'
    
  • Flask的实例化应用中(app)注册该蓝图,使用其视图函数

    app.register_blueprint(admin, url_prefix='/admin')	# 一般在另一个文件,先import admin
    # 添加了前缀 ,那么路径就变成:/admin/index,而不是/index
    # 懂了没有?
    
  • 效果:

    • 可以将视图函数和路由模块化(按功能或者习惯分组),类似Django中的一个应用(urls.py+views.py)
    • 将视图函数和路由在需要的时候注册进app即可使用;可以理解成flask就一个项目文件,这里面分多个组,组=Django应用
  • 案例

    • 下面是登录模块和用户模块(组):login.py、user.py
    from flask import Blueprint,render_template
    #创建蓝图
    logins = Blueprint('login',__name__)
    
    @logins.route('/')
    def login():
        return render_template('login.html')
    
    from flask import Blueprint,render_template
    #创建蓝图,第一个参数指定了蓝图的名字。
    users = Blueprint('user',__name__)
    
    @users.route('/')
    def user():
        return render_template('user.html')
    
    • 来,要使用了,注册 进来啊:
    from flask import Flask
    # 导入蓝图对象
    from login import logins
    from user import users
    
    app = Flask(__name__)
    
    app.register_blueprint(logins,url_prefix='/login')
    app.register_blueprint(users,url_prefix='/user')
    
    @app.route('/')
    def hello_world():
        return 'Hello World!'
    
    if __name__ == '__main__':
        print(app.url_map)	# 打印出全局url信息
        app.run(debug=True)
    

蓝图包

  • 之前说过蓝图的效果类似于Django中的应用,我们把每个蓝图组单独建立一个文件夹,搞成包

  • 搞成包需要添加__init__.py文件,这个文件会让包在被导入时执行,就在这里定义蓝图吧

    from flask import Blueprint
    
    app_cart = Blueprint("app_cart", __name__)
    
    from .views import get_cart		# 让蓝图知道有这个视图,不然主目录无法使用
    
  • 在蓝图组的包中建立views.py文件定义视图

    from . import app_cart	# . 代表本文件所在包
    
    @app_cart.route('/get_cart')
    def get_cart():
        return "cart"
    
  • 然后在主项目文件中引入蓝图包:
    f12

单元测试

  • Web程序开发过程一般包括以下几个阶段:[需求分析,设计阶段,实现阶段,测试阶段]
  • 其中测试阶段通过人工或自动运行测试某个系统的功能,目的是检验其是否满足需求,并得出特定的结果,以达到弄清楚预期结果和实际结果之间的差别的最终目的
  • 测试从软件开发过程可以分为:单元测试、集成测试、系统测试等
  • 在众多的测试中,与程序开发人员最密切的就是单元测试,因为单元测试是由开发人员进行的,而其他测试都由专业的测试人员来完成。所以我们主要学习单元测试
  • 什么是单元测试
    • 单元测试就是开发者编写一小段代码,检验目标代码的功能是否符合预期。通常情况下,单元测试主要面向一些功能单一的模块进行。
    • 在Web开发过程中,单元测试主要是一些“断言”(assert)代码

断言

  • 使用方式

    def fibo(x):
        if x == 0:
            resp = 0
        elif x == 1:
            resp = 1
        else:
            return fibo(x-1) + fibo(x-2)
        return resp
    assert fibo(5) == 5
    
  • 但不能到处断言测试一个个函数吧,可以启动服务器,用postman模拟请求,也可以使用爬虫模块发出请求(万能方式)

  • 最常用的,是python中的单元测试类,封装了类似postman的功能,有测试客户端

unittest

  • 想怎么测试,就在类里面定义函数,函数名必须以 test_为前缀

    import unittest
    class TestClass(unittest.TestCase):	# 类名随便起
    
        # 该方法会首先执行,方法名为固定写法
        def setUp(self):
            # 一般把实例化测试客户端放在这
            self.client = app.test_client()
    
        # 该方法会在测试代码执行完后执行,方法名为固定写法
        def tearDown(self):
            pass
    
  • 写一个登录测试,要明确以下几点
    f10

    • 将被测试模块的app导入到测试模块(一般只有一个项目app,其他都是蓝图)
    • 测试的是视图函数的逻辑,通过测试客户端对指定视图发请求,断言预期返回结果
    • 即测试的目的是看能否从视图函数拿到正确结果,定位出错点
    • 测试要全面,上面的只是判断登录信息不完整,我们还需测试其他情况
  • 断言常用情景:

    assertEqual     # 如果两个值相等,则pass
    assertNotEqual  # 如果两个值不相等,则pass
    assertTrue      # 判断bool值为True,则pass
    assertFalse     # 判断bool值为False,则pass
    assertIsNone    # 不存在,则pass
    assertIsNotNone # 存在,则pass
    

测试模式

  • 前面的登录测试都是自定义json返回信息,测试时loads出返回数据(字典)

  • 如果有未json格式化的错误呢?将无法定位;因此需要打开测试模式,方便定位其他bug

  • 测试之前的图书案例数据库:看数据能否成功插入

    import unittest
    from author_book import *
    
    #自定义测试类,setUp方法和tearDown方法会分别在测试前后执行。以test_开头的函数就是具体的测试代码
    
    class DatabaseTest(unittest.TestCase):
        def setUp(self):
            # 打开测试模式
            app.config['TESTING'] = True
            app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@localhost/test1'
            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)
    
  • 运行测试的py文件,看是否OK

部署

  • 当我们执行程序时,使用flask自带的服务器,在生产环境中,自带的服务器无法满足性能要求

  • 这里采用GunicornWSGI容器,来部署flask程序

    • 还记得wsgi是什么吗?它是为Python语言定义的Web服务器和Web应用程序(框架)之间的一种简单而通用的接口(协议)
    • 简而言之,Gunicorn服从了此协议(接管了服务器和框架),我们称Gunicorn为PythonWeb服务器(业务服务器)
  • 和Django原理相同,可以将服务器分成nginx服务器、业务服务器、数据库服务器、Redis服务器
    在这里插入图片描述

  • 我的部署方式: nginx + Gunicorn + flask

    • Gunicorn(绿色独角兽)是一个Python WSGI的HTTP服务器,与各种Web框架兼容,实现非常简单,轻量级的资源消耗
    • Gunicorn直接用命令启动,不需要编写配置文件,相对uWSGI要容易很多
    • uWSGI:是实现了WSGI协议的另外一种Web服务器
    • 无论Django还是flask都是框架(代码而已),并发性能还需要运行在专门的服务器实现
    • nginx一般对应多台业务服务器,实现分流、转发、负载均衡,不然没意思了
    • 数据库的并发就是MySQL软件的事了
  • 安装Gunicorn

    pip install gunicorn
    $ gunicorn -h
    
  • 测试程序

    # main.py
    
    from flask import Flask
    app = Flask(__name__)
    @app.route('/')
    def hello():
        return '<h1>hello world Flask Gunicorn</h1>'
    
    if __name__ == '__main__':
        app.run(debug=True)
    
  • 运行

    • -w: 表示进程数(worker)
    • -b:表示绑定ip地址和端口号(bind)
    • -D 守护进程运行,不占用终端
      • 守护进程属于后台进程,但它可以脱离自己的父进程,成为自己的会话组长
    • 打开日志,记录访问的IP
    • 运行main.py下的app实例,注意要加上#-*- coding: UTF-8 -*-
    $ gunicorn -w 4 -b 192.168.43.129:5000 -D --access-logfile ./logs/log.txt main:app
    
  • 我们可以使用一台服务器开启多个端口,模拟多态服务器的场景

    $ gunicorn -w 4 -b 192.168.43.129:5001 -D --access-logfile ./logs/log1.txt main:app
    
    • 可以先不以守护进程打开,万一有错不提醒

nginx

  • 无论是Gunicorn还是nginx都是运行在服务器上的软件而已

  • 安装,使用命令安装的nginx相关文件如下:(ubuntu系统)

    $ sudo apt-get install nginx
    # 回顾Linux命令
    $ find /usr -name 'nginx'
    $ which nginx
    
    • 所有的配置文件都在/etc/nginx下,并且每个虚拟主机安排在 /etc/nginx/sites-available下
    • nginx运行程序文件在 /usr/sbin/nginx
    • 日志放在了/var/log/nginx
    • 并已经在 /etc/init.d/目录下创建了启动脚本nginx
    • 默认的虚拟主机的目录设置在了/var/www/nginx-default (有的版本 默认的虚拟主机的目录设置在了/var/www/html, 请参考/etc/nginx/sites-available里的配置)
    • 对于源代码安装的nginx,配置文件为/usr/local/nginx/conf/nginx.conf
      • 如果对配置还是不太了解,可以看我的nginx专栏,比较基础
  • 启动

    #启动
    sudo /etc/init.d/nginx start
    #查看
    ps aux | grep nginx
    # 重启
    sudo /etc/init.d/nginx reload
    
  • 配置负载均衡

    # 先将之前的配置备份
    sudo cp nginx.conf nginx.conf.django 
    
    # 在http{}中添加:
    
    # 这里就是轮流转发请求的IP和端口
    upstream flask{
    	# 这个是内网IP,因为在虚拟机里,在本地测试用的,上线肯定不行
    	server 192.168.43.129:5000;
    	server 192.168.43.129:5001;
    }
    server {
        # 监听80端口
        listen 80;
        # 本机
        server_name localhost; 
        # 默认请求的url
        location / {
            #请求转发到gunicorn服务器
            proxy_pass http://flask; 
            #设置请求头,并将头信息传递给服务器端 
            proxy_set_header Host $host; 
            # 给后端服务器请求的具体来源
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
    
    • 注意不要写错,不然可能会出现莫名其妙的错误
    • 修改任何配置文件之前都备份一下
    • 必须通过:http://localhost/index访问,什么鬼?nginx默认页面必须用127.0.0.1
    • 更多路由分发设置:https://www.jb51.net/article/165065.htm
  • 浏览器访问nginx,默认发送到nginx的80端口,nginx只需监听本地的80端口即可

  • 所有请求都将发送到nginx服务器,进行转发

  • 访问不同视图函数,通过查看日志可以发现请求是轮流转发(轮询算法)到不同业务服务器端口的

  • 以上即使用一台服务器完成nginx——Gunicorn的请求均衡配置

小结

  • 以上,是对flask框架学习过程中的总结,涉及到各方面的使用,但细节部分还是要在项目中体现
  • 下一篇结合一个小程序商城项目,系统总结flask的用法,争取入门!
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Roy_Allen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值