一、准备工作
1、全局安装了pipenv工具,然后在项目文件夹下用pipenv install命令为项目安装虚拟环境(项目绑定)
输入pipenv shell 进入所创建的虚拟环境,在虚拟环境中没有安装任何包。相当于隔离。
2、为项目所绑定的虚拟环境安装各种所需要的包。如安装flask 输入pipenv install flask
3、常用命令:进入虚拟环境pipenv shell
退出虚拟环境:exit
卸载包:pipenv uninstall …
查看包依赖关系:pipenv graph
查看虚拟环境安装目录 pipenv —venv
4、为pycharm配置所创建的虚拟环境,在pycharm中导入项目,并选择python解释器为虚拟环境
二、创建一个web项目并运行
1、实例化Flask对象
2、定义一个函数并用浏览器运行
3、启动服务器(在虚拟环境下直接运行实例化了Flask的py文件)
from flaskimport Flask
app = Flask(__name__)
#这个hello函数是视图函数,相当于是mvc中的控制器,除此之外还有基于类的视图,叫即插视图
#视图函数相对于普通函数,返回值是经过flask封装的一个response对象,如会返回状态码,content-type,http
#header ,content-type是告诉客户端浏览器解析视图函数返回文本的方式
#路由注册的常用方法
@app.route('/hello')
def hello():
#这里修改了返回的response对象的content-type,让客户端解析成json格式,用于设计api的时候
#本质上web服务器的返回值都是字符串,只不过可以通过修改content-type来让客户端解析成不同格式
headers = {
'content-type' : ‘application/json'
}
#自定义response对象
response = make_response('<html></html>',201)
#将修改的header添加至自定义response对象中
# response.headers = headers
# return response
# 也可以换一种方式,如(这种常用):这种不需要前面的自己创建response对象,但是实质上是
#通过将这三个存入一个元组,然后在变成response对象返回
return '<html></html>’,201,headers
#全局导入配置文件,这里写配置文件(模块)路径,这种方式要求文件中参数必须全部大写
app.config.from_object('config')
#另一种注册路由的方式,当你使用基于类的视图就用这种注册方式
app.add_url_rule('/hello',view_func=hello)
# app启动参数配置
# host配置外部可访问的ip地址,配置0.0.0.0就是接受外网访问
# debug进入调试模式,Flask就可以自动监听文件改动,自动重启服务器,调试模式还可以详细的报错信息
# port改端口号
if __name__ == '__main__':#确保if里的语句只在入口文件中执行,保证在生产环境中不会执行
#因为在生产环境中是nginx作为接受请求然后转发至uwsgi,然后由uwsgi加载我们的py文件,此时
#开发环境的入口文件就只是一个模块文件,如果没有这个if,则也会执行run语句
app.run(host = '0.0.0.0',debug = app.config['DEBUG’])
三、Flask深入理解
1、路由机制
问:如何通过url来访问到不同的视图函数?flask是通过字典的方式,url对应的是字典中的key,而
视图函数则是值,当不同的url访问时,则对应字典中的值。而若是要通过视图函数反向构建url时
flask提供了一个叫endpoint的东西。
问:如何算是成功注册了路由?首先url_map对象中必须要有我们url向endpoint的指向,其次
view_functions里必须要记录endpoint指向的视图函数
2、蓝图(blueprint)
用来解决视图函数注册的问题,可以让视图函数注册在蓝图中,而不是直接注册在app核心对象中。
类似于这种第三方插件,都必须实例化对象然后注册在flask核心对象中,才可以使用
在__init__.py中注册蓝图,并导入相关视图函数模块。
3、在路由注册路径中使用?传递参数
使用flask中的request,直接导入,在视图函数中使用。
request必须是由视图函数或者HTTP请求触发,才会成为想要的不可变的字典。
4、对访问url路径的参数校验: 使用第三方插件WTForms进行参数校验,通过创建类且实例化对象来
调用方法完成参数校验,获取参数时,使用form.data的方法获取。
组合使用验证器(函数设计规范):可以使将不同的功能函数写成小函数,然后组合使用
5、拆分配置文件,一般分为两个secure和setting,secure放机密信息如数据库配置参数和app key,
setting放非机密信息,一般放生产环境和调试环境不改变的参数。
6、使用Flask经典错误:No application found. Either work inside a view function or push an application
context。
AppContext、RequestContext、Flask与Request之间的关系:
Flask的上下文环境:1、应用上下文AppContext(封装了Flask和外部的一些参数和方法如pop())
2、请求上下文RequestContext (封装了Request和外部一些方法)。(都是对象)
若想操作Flask和Request核心对象,请从对应的上下文对象中间接获取核心对象(通过LocalProxy
代理来完成)
3、 当Flask接收到一个请求,会让RequestContext创建一个对象封装这个请求,然后准备将这个对象
入栈,入栈前会检查app核心对象的栈顶元素是否为app,若不是,则新建一个app核心对象入栈,
然后才会将RequestContext对象入栈。在使用代理时,current_app和request一直都是指向两个栈的栈顶
所以就是间接操作栈顶元素,只有当栈顶为空时,使用代理时才会报错。解决方法:手动创建context对象
然后调用push()入栈。使用后再出栈。(一般做,离线应用,单元测试时候用)若有请求,则Flask会
自动将app_context入栈
4、使用with语句来代替手动将app核心对象入栈的操作(相当于语法糖,省略了push和pop操作):
with app.app.context():
a = current_app
上下文管理器:实现了__enter__和__exit__方法的对象,如AppContext
上下文表达式:必须要返回一个上下文管理器,如app_context()
with的其他使用场景:类似于操作数据库,可以将连接数据库放在__enter__中,将
编写sql语句等其他逻辑放在with的代码块中,将释放数据库资源放在__exit__中
如class A():
def __enter__(self): # 返回值会赋值给as 后的变量
def __exit__(self): # 这里用来释放资源和处理异常。返回True || False,默认为False
# 返回False意味着当出现异常在with中不再需要处理异常,True则需要
with A() as obj_a:
pass
这里A()是上下文表达式
7、使用@contextmanager(可以将类变成上下文管理器)
with只能搭配上下文管理器使用,但是上下文管理器要实现__enter__和__exit__方法,很繁琐
所以可以使用装饰器@contextmanager来装饰某一个函数,在这个函数内部写__enter__和__exit__
代码,然后配合yield关键字(可以让程序中断后继续执行,相当于加强版的return)如下:
class MyResource:
def query(self):
print("query")
@contextmanager
def make_mysource():
print("connected!")
yield MyResource() # 这里yield后接实例化对象是因为后面的as r 的r要调用query()
print("disconnected")
with make_mysource()as r:
r.query()
四、Flask操作数据库
1、数据库表的创建方式:
1)DataBase First :传统的创建表的方式,按照字段添加
2)Model First :使用工具创建表的结构模型,然后自动生成表
3)Code First : 特点:专注于业务模型设计,而不是数据库设计模型层 使用sqlalchemy为数据库自
动创建表,需在配置文件中配置mysql的uri
2、关于聚合操作group_by:
对于GROUP BY聚合操作,如果在SELECT中的列,没有在GROUP BY中出现,那么这个SQL是
不合法的,因为列不在GROUP BY从句中。简而言之,就是SELECT后面接的列必须被GROUP BY
后面接的列所包含。如:
select a,b from table group by a,b,c; (正确)
select a,b,c from table group by a,b; (错误)
这个配置会使得GROUP BY语句环境变得十分狭窄,所以一般都不加这个配置
所以要使用distinct,可以将group_by查询的关键字放在distinct中。
3、一般查询模型内(一张表)的数据,用filter_by即可,若涉及到跨表查询则用db.session.filter,
更加灵活,括号中写表达式,func.count搭配group_by可完成分组统计,使用in_查询一张表中的
字段值和一张列表中相等的值,像这样使用链式调用的方式,要用关键字触发:
count_list = db.session.query(func.count(Wish.isbn),Wish.isbn).filter(
Wish.launched ==False,
Wish.isbn.in_(isbn_list),
Wish.status ==1).group_by(Wish.isbn).all()
# 这里的count_list是一个元组,在函数中别返回元组,尽量返回对象或者字典,
# 返回对象可以用nametuple,这里用返回字典的方式,使用列表推导式
count_list = [{'count':w[0],'isbn':w[1]}for win count_list]
return count_list
4、Flask中的first_or_404()方法:
1、在Flask中查询数据库时经常用到链式调用,而链式调用要用first(),all()等触发,而当查询结果没有
数据时,也就是first()为空,first_or_404()这个方法就会帮我们自动抛出一个异常,并且跳转到404
页面,而实现这个方法,flask内部通过对first的封装,还有调用了_abort对象的一个方法,要实现
把对象当成方法调用,则这个类内部必须实现__call__方法,实质上也就是调用这个__call__()方法。
意义:1、简化了函数调用,可以直接调用实例化对象。2、统一函数调用接口
2、那么如果first_or_404()抛出了异常,那页面是如何跳转至404页面的?
原因在于response读取了NotFound(HttpException)
3、装饰器app_errorhandler:(基于AOP思想(面向切片编程,将所有可能出现相同的情况集中处理))
可以帮我们实现当任意页面抛出异常的时候,监听到这个异常,并且按照我们自定义的逻辑去执行。
在web.init文件中定义如下:(监听到了404状态码的异常)
@web.app_errorhandler(404)
def not_found(e): # 从e中读取异常信息
return render_template('404.html'),404
4、设计数据库时,要合理利用数据冗余记录历史状态:
例如完成一笔订单交易,对于这个订单交易表的设计,就要重新增加字段来存储相关信息,如
交易人的nickname等,就算这个字段和User表中的字段冗余。因为nickname是可以修改的,如果
和user表中关联,则交易记录中的数据也被修改,这是不能接受的
五、Flask中的多线程与线程隔离
1、进程是竞争计算机资源的基本单位。
2、线程是进程的一部分,进程与线程关系可以是1:1,也可以是1:n
线程的引入是因为进程的粒度太大,不能有效利用cpu资源,进程之间的切换消耗很大的资源,
所以引入一个轻量,小巧的资源调度的单元—线程
3、进程主要用来分配资源,如cpu,内存;
而线程主要是利用cpu来执行代码(不可以拥有资源,但可以访问进程资源),所以线程是属于进程
4、一个文件默认有个主线程来执行,但是可以自己新增一个线程来执行其他函数
new_t = threading.Thread(target=接函数名,name='自定义新增线程名')
new_t.start() # 启动新增线程
5、 Python不能很好地利用cpu多线程的优势,原因在于,pyhton的GIL机制(全局解释器锁)
即同一时间只允许同一个线程执行。jpython中没有这个解释器。
但是python对于IO密集型程序(如查询数据库,请求网络资源)有一定意义,python可以在
一个线程执行请求IO的时候等待返回的那段时间将cpu腾出来给别的线程用。
6、Flask的线程隔离:
假设客户端发送了许多不同的request请求给服务器,由于每个发来的request都需要实例化一个Request对象
来保存请求信息,所以我们的request变量不知道具体指向哪一个实例化的Request对象。解决思路,分别将
不同实例化的Request对象赋值给不同的request变量,这样就可以一一对应就可以知道,但是这样操作不
合理,所以我们就采用字典的方式来解决:用不同线程的id号来做key,将实例化的Request做value。
Flask中解决线程隔离,思想如上,本质采用字典的形式来保存,然后将其封装成Local对象(线程隔离对象)
操作原理:使用Local对象操作线程中的属性,和其他线程的属性互不干扰。
7、Flask中的线程隔离栈LocalStack
Local 和 LocalStack和字典的关系:(都是线程隔离对象)
被线程隔离的对象AppContext,RequestContext/Request,session,g
Local是用字典来实现了线程隔离,
LocalStack是将Local对象当成自己内部属性,从而实现了一个线程隔离的栈。
LocalStack的用法:和Local差不多,实例化LocalStack对象,然后调用push方法入栈的元素和
其他线程属性互不干扰。
8、使用线程隔离的意义:使当前线程可以正确的引用到他自己所创建的对象,而不是引用到其他
线程所创建的对象
9、flask全局只有一个app核心对象(可以多个),current_app只是对app的引用,所以对current_app隔离没有意义
而request对象随着线程的创建都会实例化新的,所以要对request对象进行线程隔离。
AppContext把Flask核心对象当做属性保存起来,RequestContext把Request对象封装保存起来。
current_app指向的是(LocalStack.top = AppContext top.app = Flask)
request指向的是(LocalStack.top = RequestContext top.request = Request)
六、业务开发
1、ViewModel:由于向服务器请求api所获得的数据并不能完全符合页面所显示的数据,所以引入
ViewModel层,在VM层中对获得的数据进行裁剪、修改、合并等操作来满足页面所需要的数据
2、python可以直接序列化字典,但是不可以直接序列化对象
python序列化对象:只序列化对象的变量和数据,不序列化方法
解决:
books.fill(yushu_book,q)
# 这里若是直接返回books会报错 return(books),因为python不可以序列化一个对象,但是可以序列化字典,
所以想到了将对象中的内置__dict__
# 返回,但是又有可能对象中还有对象,所以采用函数式编程思想,用函数将一个不能序列化的对象转化成
可以序列化的字典,再将这个函数作为参数传入dumps方法
return json.dumps(books, default=lambda o: o.__dict__)
在这个编写函数的时候,我们有时候可以不必考虑参数具体设置,将代码的解释权交给调用函数方
ps:单页面和多页面网站的区别:
单页面中的数据是在客户端处理后渲染的,多页面网站是在服务器端渲染的。
3、用户注册
1)使用request.form获取用户提交的表单数据,并用实现了WTForm的类来接收得到验证后的form
2)开始校验,实例化User对象,并将表单数据填入User对象
3)调用db.session方法,将数据存入数据库,用户密码要加密后存入数据库
4)注册成功重定向到登录页面
4、用户登录
1)同上,也是获取表单数据,并且得到验证后的form
2)通过query.filter_by()查询数据库中的数据和对应填写数据是否一致,密码要进行加密后再通数据库对比。
3)所有数据校验通过,使用login_user插件管理登录状态,会自动将登录信息存入cookie
ps:用户访问权限控制:未登录状态访问需要登录才能访问的视图函数(被login_required装饰),会
跳转至登录页面,在login_manager初始化时指定
5、发送邮件
使用flask提供的mail插件来发送邮件,在配置文件secure中,配置如下:
# Email配置
MAIL_SERVER ='smtp.qq.com'
MAIL_PORT =465
MAIL_USE_SSL =True
MAIL_USE_TSL =False
MAIL_USERNAME ='' #这是发送者邮箱
MAIL_PASSWORD =‘’ #这是授权码
1)实例化并且在app中注册 2)在模块中导入 3)
def send_mail():
msg = Message('title', sender=‘填MAIL_USERNAME', body='邮件正文',
recipients=['接收邮箱'])
mail.send(msg) 4)调用send_mail()即可发送
6、重置密码
访问相应view_func,然后填写重置密码的电子邮箱,若查询到表中有,则向这个电子邮箱发送
重置密码的邮件,通过附带token信息(将用户id序列化存入)。打开邮件,邮件是一段url,访问这个url
可加载一个重置密码的页面,将token解序列化,获取user id,然后新密码校验成功后,修改数据库信息
7、设计ViewModel:viewmodel主要是为了在不同场景展示不同的数据形式
8、在交易中的不同状态定义时,可以用枚举类来定义,如下:
设计viewmodel时基本上都要涉及到单体和集合,可以设计一个单体,然后再设计一个集合,然后
通过循环将单体存入集合中,设计单体时,可以将单体当做类处理,也可以将单体当成字典返回,
字典的优势在于代码简单,而类的话可以将数据进行特别处理,所以推荐用类处理。遇到比较多的字段
的viewmodel,可以结合两者的特点,单体中用字典封装。
七、Flask静态文件,模板文件访问原理
1、静态文件默认存储位置在app/static,直接浏览器访问路径即可,若要修改static位置,则在注册app核心对象
或者蓝图的时候就要指定static_folder或static_url_path
2、模板文件如(html模板)默认放在app/templates中,若要修改位置,操作和app一样,在注册时,修改
template_folder参数
八、使用Jinja2来将数据解析和展示至html模板中
1、 在View_func()中用render_template()将数据渲染至指定模板页面,然后在模板页面中使用{{}解析和展示数据
2、在Jinja2中读取字典和对象,操作方式都一样,都可使用.和[]的形式访问
3、使用if 和 for in 控制语句 都需要加上{% %} 如{% if %},而且需要闭合语句,如下:
(可结合使用python的比较和逻辑运算符)
{%if data.age ==21 and data.name =='leaveye' %}
<span style="font-style:oblique "> {{data.name }}</span>
{%endif %}
4、使用for遍历字典
{%for key,value in data.items() %}
<ul>
<li>
{{key }}:{{value }}
</li>
</ul>
{%endfor %}
5、模板继承
定义一个总的基础模板,Base.html,然后让子模板继承,此时子模板可不写那些html声明
,只需要写和父模板不同的文件即可,然后将子模板插入到父模板对应block中既可
{%extends 'layout.html' %}
{%block content %}
{{super() }} //super()是为了不覆盖父模板中的内容
{# 使用jinja2模板语言解析和展示数据 #}
{%for key,value in data.items() %}
{{key }}:{{value }}
{%endfor %}
{%endblock %}
6、Jinja2使用filter(类似于管道命令)
1、default过滤器:当访问了一个不存在的值,default会将其替换成自定义的值
{{data.school |default(data.school) |default('不存在的变量')}}
访问了data中一个不存在的school属性,则将其传递给|后的表达式,default给出
自定义值data.school还是不存在,则继续传递给下个管道,最终显示最后一个管道赋的值,
当这个值存在,就不会传递给下个管道
7、反向构建url:为了在html中加载css,js,图片等静态文件,通过endpoint和url_for来完成,如:
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="{{url_for('static',filename='test.css') }}">
</head>
这里的{{url_for(‘endpoint名’,filename=‘要加载的文件名')
8、Flask的消息闪现(要添加secret_key)
可以在视图函数中使用flash(‘给模板传递的消息’,category=‘分类名’)将你自定义分类的消息
传递给模板,模板接收方式:
{%set messages =get_flashed_messages(category_filter=[‘分类名']) %}
{{messages }} //由flash传递的消息是一个数组(列表)
这里用set接收的变量的作用域在整个block,而用with接收,就局限在with和 endwith中