flask框架基础

文章目录

    • 简介
    • 对比
    • 扩展包
    • 安装
    • 基操
      • 配置参数
      • 路由参数
    • 视图
      • 获取参数
      • 保存文件
      • 返回信息
      • cookie
      • session
      • 请求钩子
      • 扩展命令行
    • 模板
      • 变量
      • 过滤器
      • xss
      • 自定义过滤器
      • 表单
    • 模型
      • 数据库
      • 模型类
      • 查询
      • 案例
      • 数据库迁移
    • 发送邮件
    • 蓝图
      • 使用
      • 蓝图包
    • 单元测试
      • 断言
      • unittest
      • 测试模式
    • 部署
      • nginx
    • 小结

简介

  • 之前学习了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/')	# 还支持 float、path(接受 /)
    def hello_itcast(id):
        return 'hello Roy %d' %id
    
    # 默认使用字符串规则
    @app.route('/user/')	# 后面的参数会以字符串形式被接收,名为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/")
    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/")
    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
    
  • 参数都可通过此对象的属性获取,区别就在于参数的传递方法和类型了:
    flask框架基础_第1张图片

    • 表单数据:表单可以上传参数,以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遗留问题)
      flask框架基础_第2张图片
      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代表哪个用户呢?

      • 这就需要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

    
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    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>{
           {
            'hello' | 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>{
           {
            'hello' | 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指注入恶意指令代码到网页

  • 前端传递过来的可执行代码:
    flask框架基础_第3张图片

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

    <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')
    
    
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    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">
        
        {
          { form.csrf_token }}
        {
          { form.us.label }}
        <p>{
          { form.us }}p>
        
        {
          { form.ps.label }}
        <p>{
          { form.ps }}p>
        
        {% 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)
    
  • 浏览器渲染结果:
    flask框架基础_第4张图片

  • 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:[email protected]:3306/flask_test'
    
    # 自动跟踪数据库
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
    
  • 使用,这里用类对象加载配置(一般用config文件)
    flask框架基础_第5张图片

模型类

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

    类型名 python中类型 说明(mysql)
    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 二进制文件
  • 同样,完整性约束不能少:

    选项名 说明
    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:[email protected]: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='[email protected]',pswd='123456',role_id=ro1.id)
        us2 = User(name='zhang',email='[email protected]',pswd='201512',role_id=ro2.id)
        us3 = User(name='chen',email='[email protected]',pswd='987654',role_id=ro2.id)
        us4 = User(name='zhou',email='[email protected]',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
    flask框架基础_第6张图片

    • 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':'[email protected]'})
    
  • 查询删除

    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:[email protected]: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')
    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')	# /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='[email protected]')
        au_qian = Author(name='萧潜',email='[email protected]')
        au_san = Author(name='唐家三少',email='[email protected]')
        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:[email protected]: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 = '[email protected]',
        MAIL_PASSWORD = 'xxxxxxxxx',	# QQ密码
    )
    
    mail = Mail(app)
    
    @app.route('/')
    def index():
     # sender 发送方,recipients 接收方列表
        msg = Message("This is a test message ",sender='[email protected]', recipients=['[email protected]', '[email protected]'])
        #邮件内容
        msg.body = "Flask test mail"
        #发送邮件
        mail.send(msg)
        print "Mail sent"
        return "Sent Succeed"
    
    if __name__ == "__main__":
        app.run()
    

蓝图

  • 学习Flask框架,是从写单个文件,执行hello world开始的。我们在这单个文件中可以定义路由、视图函数、定义模型等等
  • 但随着业务代码的增加,将所有代码都放在单个程序文件中,是非常不合适的。这不仅会让代码阅读变得困难,而且会给后期维护带来麻烦
  • 如果自己进行模块划分,将部分视图抽出来,以模块的方式导入
    flask框架基础_第7张图片
    • 解决上述问题可以通过延迟一方导入,例如将main中的导入放到index()函数内执行
    • 或者通过装饰器的函数调用方式,还记得三层/两层装饰器吗?分别接收装饰器参数(最外层)和函数参数(最内层),这里用三层装饰器
      flask框架基础_第8张图片
  • 蓝图:用于实现单个应用(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"
    
  • 然后在主项目文件中引入蓝图包:
    flask框架基础_第9张图片

单元测试

  • 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
    
  • 写一个登录测试,要明确以下几点
    flask框架基础_第10张图片

    • 将被测试模块的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服务器
    flask框架基础_第11张图片

  • 我的部署方式: 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 '

    hello world Flask Gunicorn

    '
    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的用法,争取入门!

你可能感兴趣的:(Python,Web基础,flask,单元测试,数据库)