Python: Flask-Web后端框架及其他
- TAGS: Python
Flask框架简单使用
Celery调度+Redis安装
Celery是一个使用Python开发的 分布式任务调度模块 ,因此对于大量使用Python构建的系统,使用起来方便。
Celery目前爸爸4.x,仅支持Django 1.8以上版本。 Celery3.1只可以支持Django1.8一下版本。
- Celery官网https://docs.celeryq.dev/en/stable/
- Celery帮助文档http://www.celeryproject.org/docs-and-support/
优点:
- 简单:调用接口简单易用
- 高可用:客户端、Worker之间连接自动重试,Broker自身支持HA
- 快速:单个Celery进程每分钟可用数以百万计的任务
- 灵活:Celery的每一个部分都能扩展,或直接使用,或用户自己定义。
常用应用
Celery可用支持实时异步任务处理,也支持任务的定时调度。
- 异步发邮件
- cleery执行队列
- 间隔半小时同步天气信息等
- celery定时操作
角色
- 任务Task: 对应一个Python函数
- 队列Queue: 待执行任务的队列
- 工人Worker: 一个新的进程,负者执行任务
- 代理Broker: 负者调度,在任务环境中使用RabbitMQ、Redis等
Celery需要依靠RabbitMQ等作为消息代理,同时也支持Redis甚至是Mysql、Mongo等,当然,官方默认推荐的是RabbitMQ,如果使用Redis需要配置。
本次采用Redis来作为Broker,也是Redis存储任务结果
安装
pip install celery #pip install celery==5.5.3 #安装对redis的支持,并自动升级相关依赖 pip install -U "celery[redis]"
测试
Celery库使用前,必须初始化,所得示例叫做”应用application或app”。应用是线程安全的。不同应用在同一进程中,可以使用不同拍照、不同组件、不同结果
#t2.py from celery import Celery app = Celery('mytask') # broker='pyamqp://guest@localhost//' @app.task def hello(): return 'hello world' app.conf.broker_url = 'redis://localhost:6379/0' print('='*30) print(app) # <Celery mytask at 0x1c0e87a4ec0> print(hello, hello.name) # <@task: mytask.hello of mytask at 0x1c0e87a4ec0> mytask.hello print(app.tasks) # {'mytask.hello': <@task: mytask.hello of mytask at 0x1c0e87a4ec0>, 'celery.chain': ...省略} print(app.timezone) # UTC print(*app.conf.items(), sep='\n')
默认使用amqp链接到本地
amqp://guest:**[cite/t:@localhost:5672/]/
本次使用Redis
Redis安装配置
docker run -d -p 6379:6379 redis
使用Epel源的rpm安装
#redis安装,使用提供的rpm安装,redis依赖jemalloc yum install redis #yum install jemalloc-3.6.0-1.el7.x86_64.rpm redis-3.2.12-2.el7.x86_64.rpm # rpm -pql redis-3.2.12-2.el7.x86_64.rpm /etc/logrotate.d/redis /etc/redis-sentinel.conf /etc/redis.conf /usr/bin/redis-cli /usr/bin/redis-sentinel /usr/bin/redis-server /usr/lib/systemd/system/redis-sentinel.service /usr/lib/systemd/system/redis.service # 编辑redis配置文件 # vi /ect/redis.conf port 6379 #启动时的默认端口 bind 10.0.0.152 #redis启动时的主机 protected-mode no #是否开启保护模式
启动、停止redis服务
# 启动服务 systemctl start redis # 停止服务 systemctl stop redis # 添加开机启动 systemctl enable redis
redis desktop manager 是图形界面管理工具
broker配置使用
redis链接字符串格式 redis://password@hostname:port/db_number
#指定服务器的redis端口6379,使用0号库 app.conf.broker_url = 'redis://10.0.0.152:6379/0'
Celery使用
生成任务
# t2.py from celery import Celery import time app = Celery('t2') app.conf.broker_url = 'redis://10.0.0.152:6379/0' # 0号库存执行任务队列 # 重复执行问题解决 # 如果超过visibility_timeout,Celery会认为此任务失败 # 会重分配其他worker执行该任务,这样会造成重复执行。visibility_timeout这个值大一些 # 注意,如果慢任务执行时长超过visibility_timeout依然会多执行 app.conf.broker_transport_options = {"visibility_timeout":3600*12} #12 hours 任务超时时间 app.conf.result_backend = 'redis://10.0.0.152:6379/1' # 1号库存执行结果 app.conf.update( enable_utc = True, timezone = "Asia/Shanghai" ) #@app.task # 按名字,函数名就是名字 @app.task(name="my_add") # 修改任务名称 #@app.task(ingore_result=True) # 不关心执行结果 def add(x,y): print("start run add x={},y={}".format(x,y)) ret = x+y time.sleep(5) print("end run ret = {}".format(ret)) return ret if __name__ == "__main__": # 添加任务到Broker中 print('in main. Send task') add.delay(4,5) add.apply_async((10,20),countdown=5) #派发一个任务,延迟5秒后执行 print('end~~~~)
# 将任务放到redis中等待被执行 python mytask.py
目录在新版中,代码中Celery('xx') 中名称必须和当前模块名xx.py一样。
注意:上面代码执行,使用add.delay等加入任务到Redis中,在启动celery命令消费Redis的任务,执行并返回结果到Redis中。
#增加任务的常用方法 T.delay(arg, kwarg=value) always a shortcut to .apply_async. T.apply_async((arg,),{'kwarg': value}) T.apply_async(countdown=10) executes 10 seconds from now.
执行任务
如果在Linux下能出现下面问题,可如下配置
from celery import platforms # Linux下,默认不允许root用户启动celery,可使用下面的配置 platforms.C_FORCE_ROOT = True
使用命令执行Redis中的任务
# 全局设置 -A APP, --app App #指定app名称,App是模块名 # worker 指定work工作 -l, --loglevel #指定日志级别 DEBUG/INFO/WARNING/ERROR/CRITICAL/FATAL -n 名称 #%n指主机名 --concurrency #指定并发多进程数,缺省值是CPU数 celery -A my_add worker --loglevel=INFO --concurrency=5 -n worker1@%n celery -A my_add worker -l info -c 5 -n worker1@%n
test1是模块名,里面有celery app。
变量 | 说明 | 举例 | 表现 |
---|---|---|---|
%h | 主机host | worker1@%h | [email protected] |
%n | 主机名hostname | worker1@%n | worker1@george |
%d | 域domain | worker1@%d | [email protected] |
windows10 的环境,python3.8环境,使用celery 5.x 的版本,发现任务接收了,但是一直没执行,无返回结果。
- 早期的celery 3.x版本是可以支持windows 平台的,但是跟python3.8 不兼容了。
#安装eventlet解决问题 pip install eventlet -P ,--pool 指定进程池实现,默认prefork,windows下使用eventlet #重新执行任务 celery -A my_add worker -P eventlet --loglevel=INFO --concurrency=5 -n worker1@%n
发邮件
在用户注册完激活时,或修改了用户信息,遇到故障等情况时,都会发送邮件或发送短信息,这些业务场景不需要一直阻塞等待这些发送任务完成,一般都会采用异步执行。也就是说,都会向队列中添加一个任务后,直接返回。
发邮件帮助可以参考 https://docs.djangoproject.com/en/5.2/topics/email/
Django中发送邮件需要在settings.py中配置如下
# SMTP ## qq邮件配置 EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = "smtp.qq.com" EMAIL_PORT = 465 #缺省25,SSL的异步465 EMAIL_USE_SSL = True #缺省False EMAIL_USE_TLS = False #缺省值False EMAIL_HOST_USER = "[email protected]" EMAIL_HOST_PASSWORD = "*************" # 密码为授权码。同时需要在qq邮箱设置红开通POP3/SMTP服务 服务
注意:不同邮箱服务器配置不太一样。
邮件发送测试代码如下:
# user/urls.py 增加users/sendmail 路由 from django.urls import path from .views import reg, user_login, user_logout, get_captcha, test_filter, get_user, send_email urlpatterns = [ path('', reg), path('login', user_login, name='userlogin'), path('logout', user_logout), path('getcaptcha', get_captcha), path('tf', test_filter), # /users/tf path('<int:id>', get_user), # /users/2 path('sendmail', send_email) ]
# 这个函数测试时,可以写在任意模块中,需要时被调用就可以了 # user/views.py from django.http import HttpRequest, HttpResponse, JsonResponse ..... from django.core.mail import send_mail from django.conf import settings def email(request): print('测试邮件开始发送============') send_mail( "测试邮件", "test", settings.EMAIL_HOST_USER, # 借用哪一个邮箱背后的server发送 ["[email protected]"], # 目标邮箱们 fail_silently=False, html_message=""" <h1>注册邮件</h1><br> <a href="http://xuchangwei.com/users/active/?token=xxxx&uid=123">激活</a> """ # 利用HTML的邮件正文 ) print('测试邮件发送完毕============') # 测试函数 def send_email(request): try: email() # 发送邮件时会阻塞 except Exception as e: print(e) return JsonResponse({"error":"邮件发送失败"},status=400) return HttpResponse("发送成功",status=201)
Celery在Django中的集成方法
https://docs.celeryq.dev/en/stable/django/index.html
目录结构
- proj/ - manage.py - blog/ #Django全局目录 - __init__.py - settings.py - celery.py - urls.py - app1 #应用程序目录 - __init__.py - tasks.py - view.py - models.py
在Django全局目录中(settings.pys所在目录)
1、定义一个celery.py文件
# blog/celery.py import os from celery import Celery # Set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings') #必须修改模块名 app = Celery('blog') # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. # - namespace='CELERY' means all celery-related configuration keys # should have a `CELERY_` prefix. app.config_from_object('django.conf:settings', namespace='CELERY') # Load task modules from all registered Django apps. app.autodiscover_tasks() app.conf.broker_url = 'redis://10.0.0.152:6379/2' app.conf.broker_transport_options = {'visibility_timeout': 3600*24} # 24 hour. app.conf.result_backend = 'redis://10.0.0.152:6379/3' app.conf.update( timezone='Asia/Shanghai', enable_utc=True, )
2、修改 __init__.py
文件
from .celery import app as celery_app __all__ = ('celery_app',)
在user应用下 1、新建文件tasks.py中新建任务
# user/tasks.py from django.core.mail import send_mail from django.conf import settings from blog.celery import app import datetime import time @app.task # 变成任务 def real_send_mail(): print('测试邮件开始发送============') time.sleep(3) send_mail( "测试邮件", "test", settings.EMAIL_HOST_USER, # 借用哪一个邮箱背后的server发送 ["[email protected]"], # 目标邮箱们 fail_silently=False, html_message=""" <h1>注册邮件</h1><br> {} <br> <a href="http://xuchangwei.com/users/active/?token=xxxx&uid=123">激活</a> """.format(datetime.datetime.now()) # 利用HTML的邮件正文 ) print('测试邮件发送完毕============')
2、views.py视图函数中调用
# user/views.py #... from .tasks import real_send_mail def send_email(request): try: # real_send_mail() 这样写没有用到celery real_send_mail.delay() # 发任务, 不需要等待 except Exception as e: print(e) return JsonResponse(Messages.BAD_REQUEST) return HttpResponse('邮件已经发送,请5分钟后去邮箱查收激活,有效时间2小时')
以后放在注册函数中发送邮件。
注意:目前在Python3.7部署时,发邮件可能会报wrap_socket()错。可以使用python3.6版本
3、urls.py路由视图函数
# user/urls.py from django.urls import path from .views import reg, user_login, user_logout, get_captcha, test_filter, get_user, send_email urlpatterns = [ path('', reg), path('login', user_login, name='userlogin'), path('logout', user_logout), path('getcaptcha', get_captcha), path('tf', test_filter), # /users/tf path('<int:id>', get_user), # /users/2 path('sendmail', send_email) ]
访问 http://localhost:8000/users/sendmail 会调用send_mail视图函数,执行real_send_mail.delay(),会在redis中增加任务。
执行任务,下面命令会从redis中拿任务执行
# 在项目根目录下执行 # windows 添加 -P eventlet celery -A blog worker -P eventlet worker -l info -c 4 -n worker@%n
思考:
- Post业务的pub发布功能,它真的是直接把数据提交到数据库当中吗?
- 这个功能是否适合使用Celery,为什么?
异步的,分库分表。
flask
安装
#python -m venv .venv #.venv\Scripts\Activate.ps1 pip install flask
官方网站:https://flask.palletsprojects.com/
Flask快速入门:
快速构建
mkdir webapps/static -pv touch webapps/__init__.py touch t1.py
t1.py
from webapps import app if __name__ == "__main__": app.run(debug=True) #app.run("127.0.0.1",port=8080,debug=True)
webapps/__init__.py
from flask import Flask, Response, Request, jsonify, render_template import io # 创建应用 app = Flask(__name__) # 路由和视图函数 @app.route("/") def hello_world(): # return "<p>Hello, World!</p>" return Response("Hello, flask", status=200) #@app.route("/json", methods=["GET"]) #列表中指定多个方法 GET、POST @app.route("/json") #列表中指定多个方法 GET、POST def get_json(): d = {'a':1, 'b':['a', 123, 'xyz'], 'c':(1,2,3)} return jsonify(d) # 注册2个路径 @app.route("/info") @app.route("/i") def get_info(): sio = io.StringIO() for i in [ app.root_path, app.url_map, app.static_folder, app.static_url_path, app.view_functions, app.template_folder, app.__dict__.items() ]: print(i) # 静态路径 /static/<filename> print(i, file=sio, end='<br>') # path => view function return Response(sio.getvalue(), content_type='text/html')
python t1.py # 默认5000端口
Map([<Rule '/static/<filename>' (OPTIONS, HEAD, GET) -> static>, <Rule '/' (OPTIONS, HEAD, GET) -> hello_world>, <Rule '/json' (OPTIONS, HEAD, GET) -> get_json>, <Rule '/info' (OPTIONS, HEAD, GET) -> get_info>]) D:\project\pyprojs\trae\flask10\webapps\static /static {'static': <function Flask.__init__.<locals>.<lambda> at 0x000001B88AF34A40>, 'hello_world': <function hello_world at 0x000001B88D956E80>, 'get_json': <function get_json at 0x000001B88D956F20>, 'get_info': <function get_info at 0x000001B88D956FC0>} templates dict_items([('import_name', 'webapps'), ('_static_ ...省略}
在项目根目录下构建:
- webapps包目录,存放flask代码,包内有
__init__.py
文件 - templates目录,存放模板文件
- static目录,存放js,css等静态文件。其下建立js目录,放入jquery、echarts的js文件
- app.py入口文件
说明
- 应用:创建出来提供WEB服务的实例,也是wsgi的入口
- 视图函数:执行内部代码输出响应的内容
- 路由:通过route装饰器创建path到视图函数的映射关系
- jsonify(*args, **kwargs) 封装数据返回Json响应报文,但是args和kwargs传参只能选择一种,否则抛异常。
模板
https://flask.palletsprojects.com/en/stable/quickstart/#rendering-templates
Flask使用jinja2模板。
对于应用app来说其模板是,根目录下的templates,其下新建index.html
/templates/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> 这是模板<br> {{test}}~~~<br> {% for i in range(1, 10) %} {{ i }}<br> {% endfor %} {% for i in range(1, 10) %} {% for j in range(1, 10) %} {{ i }}*{{ j }}={{ "%s"|format(i*j) }}<br> {% endfor %} {% endfor %} </body> </html>
修改 /webapps/__init__.py
使用模板渲染函数。
from flask import Flask, Response, Request, jsonify, render_template import io app = Flask(__name__) @app.route("/") def hello_world(): # return "<p>Hello, World!</p>" return Response("Hello, flask", status=200) @app.route("/json") def get_json(): d = {'a':1, 'b':['a', 123, 'xyz'], 'c':(1,2,3)} return jsonify(d) @app.route("/info") def get_info(): sio = io.StringIO() for i in [ app.root_path, app.url_map, app.static_folder, app.static_url_path, app.view_functions, app.template_folder, app.__dict__.items() ]: print(i) # 静态路径 /static/<filename> print(i, file=sio, end='<br>') # path => view function return Response(sio.getvalue(), content_type='text/html') @app.route('/t') def test_template(): return render_template('index.html', test='abc')
jinja2和Django模板语法一致,这里不阐述
在app.jinja_loader属性中,有下面的语句
@locked_cached_property def jinja_loader(self): if self.template_folder is not None: return FileSystemLoader(os.path.join(self.root_path, self.template_folder))
- 说明:不管是app,还是bluepoint,都是使用自己的root_path和模板路径拼接成模板路径。
- 可以通过app或者bluepoint查看
app.jinja_loader.searchpath
模板搜索路径。 - 可以在jinja2.loaders.FileSystemLoader.get_source函数中下断点看搜索路径。
蓝图
Flask中,基本上都是route装饰器和视图函数的映射,如果函数很多,代码组织结构会非常乱。
蓝图Blueprint,就是Flask中 模块化 技术
新建webapps/books.py蓝图文件
#webapps/books.py文件 # 创建蓝图 from flask import Blueprint, jsonify # 相当于这是一个应用 # 可以单指定模板路径,如果是相对路径,则表示从项目根目录开始 books = Blueprint('books', __name__, template_folder='temp', url_prefix='/books') @books.route('/') def get_all(): d = [ (1, 'java', 20), (2, 'python', 30), (3, 'c++', 40), ] return jsonify(d) @books.route('/bookinfo') def get_book_info(): for x in [books.root_path, books.static_folder, books.static_url_path, books.template_folder, books.name, books.url_prefix ]: print(x) print() return jsonify({'a':'ok'})
修改 /webapps/__init__.py
文件如下:在app中注册新建的蓝图文件
#/webapps/__init__.py from flask import Flask, Response, Request, jsonify, render_template import io app = Flask(__name__) @app.route("/") def hello_world(): # return "<p>Hello, World!</p>" return Response("Hello, flask", status=200) @app.route("/json") def get_json(): d = {'a':1, 'b':['a', 123, 'xyz'], 'c':(1,2,3)} return jsonify(d) @app.route("/info") def get_info(): sio = io.StringIO() for i in [ app.root_path, app.url_map, app.static_folder, app.static_url_path, app.view_functions, app.template_folder, app.__dict__.items() ]: print(i) # 静态路径 /static/<filename> print(i, file=sio, end='<br>') # path => view function return Response(sio.getvalue(), content_type='text/html') @app.route('/t') def test_template(): return render_template('index.html', test='abc') from .books import books app.register_blueprint(books, url_prefix='/xyz') # 注册优先
注册完成后,启动=/main.py=文件
Blueprint构造参数
- name: 蓝图名称,注册在app的蓝图字典中用的key
- import_name: 用来计算蓝图模块所在路径,一般写
__name__
- root_path: 指定蓝图模块所在路径,如果None,使用import_name计算得到
- template_folder
- 相对于蓝图的root_path,假设就是books.py的目录path1,那么上面的模板文件路径path1/temp/xxx.html
- 模板是一批搜索路径,会依次搜索这些模板路径,找到第一个返回。如果在找到本蓝图模板之前,有一个同名的模板,那么本蓝图模板文件就不能被找到了
- url_prefix: 指定本蓝图模块的路径前缀,app.register_blueprint注册蓝图时,也可以对当前蓝图指定url_prefix,将覆盖蓝图中的定义。
特别注意,输出的root_path路径,说明蓝图有自己一套路径。
最后app.register_blueprint(bpbooks,url_prefix="/bookss"),url_prefix一定要以 /
开始,否则报错,最后路径以注册的 url_prefix为准
jQuery
2006年初发布的一个JavaScript库,极大简化了JS编程。 jQuery库包含以下功能:
- HTML 元素选取
- HTML 元素操作
- CSS操作
- HTML DOM 遍历和修改
- HTML事件函数
- JavaScript特效和动画
- AJAX
- Utilities
除了上述功能外,它之所以流行,是官方、非官方提供了海量的插件。
安装
下载iquery,解压后,一个开发用的,一个min是压缩后的的部署用。
<script src="jquery.js"></script>
Flask静态部署
('root_path', 'E:\\ClassProjects\\test') ('_static_folder', 'static') ('url_map', Map([ ..., <Rule '/static/<filename>' (OPTIONS, HEAD, GET) ->static>]))
将js文件、CSS文件一般都是放到static目录中。
本次js放到/static/js/jquery.js。
修改templates/index.html如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>测试</title> <script src="js/jquery.js"></scr </head> <body> <h2id="root">测试jQuery</h2> </body> </html>
路径写法
js/jquery.js表示从当前站点访问路径开始的相对路径,http://127.0.0.1:8000/js/jquery.js ,找不到返回404。
/static/js/jquery.js表示站点根目录下开始,http://127.0.0.1:8000/staticjs/jquery.js ,找到返回200。
但是上面这种写法是固定死的,可以使用url_for处理。
url_for第一个参数写函数,通过函数对象,反查路径映射中的路径。即通过(OPTIONS, HEAD, GET) ->static>查逻辑路径'/st/<filename>'
<script src={{url_for('static', filename="js/jquery.js"")}}></script>
上面这一句的意思就是通过static反查路径,<Ru1e'/st/<fFilename>' (OPTIONS, HEAD, GET) ->static>])
在Flask中,可以指定静态目录。
app = Flask('web', static_folder='static', static_uurl_path='/st')
- static_url_path: 指的是url中访问路径,可以不写,默认就是/<static_folder>
- static_folder: 指的是静态文件的物理放置目录。注意这个目录不要写错。如若没有配置,默认static。如果没有配置static_url_path,默认就是/static
基本语法
$(selector).action()
- 美元符号定义jQuery
- 选择符(selector)"查询"和"查找"HTML元素
- jQuery的action()执行对元素的操作
文档就绪函数
为了防止文档加载完之前就允许执行jQuery代码,需要一个网页加载完的才调用的函数。
$(document).ready(function(){ /* jQuery functions go here });
可以简写为下面形式
$(function(){ /* jQuery functions go here准备就绪回调函数 */ });
jQuery元素选择器
jQuery使用CSS选择器来选取HTML元素。
选择器 | 含义 |
---|---|
$("р") | 选取<p>元素 |
$("p.intro") | 选取所有class="intro"的<p>元素 |
$("p#demo") | 选取所有id="demo"的<p>元素 |
jQuery属性选择器
jQuery使用XPath表达式来选择带有给定属性的元素。
选择器 | 含义 |
---|---|
$("[href]") | 选取所有带有href属性的元素 |
$("[href='#']") | 选取所有带有href值等于"#"的元素 |
$("[href!='#']") | 选取所有带有href值不等于"#"的元素 |
$("[href$='.jpg']") | 选取所有href值以".jpg"结尾的元素 |
元素包装
console.log($('#root'));// jquery对象 console.log(document.getElementById('root')); // HTML元素 console.log($(document.getElementById('root'))) // 包装成jquery对象 console.log($('div'))// div标签选择器,注意和下面的不同之处 console.log($('<div/>')) // HTML元素包装成jquery对象
CSS操作
var r = $('#root') // 选择 console.log(r.css(color'));// 读 r.css('color', '#F00')/ // 写 r.css({'background-color':'#f2f2f2f2', 'color': #00F'});
HTML操作
var r = $('#root'); console.log(r.html());// 读 r.html('<span>'+ r.html()+ '</span>'); // 写 console.log(r.html());// 读 console.log(r.text());//只有文本
增加节点
内部追加
var r = $('#root'); r.append('<img />');
增加兄弟
after外部增加兄弟,append内部末尾增加
var r = $('#root'); r.append('<img />'); r.after('<hr>'); r.append('<ul><li></li><li></li><li></li></ul>');
attr属性操作
var r = $('#root'); r.append('<img />'); r.after('<hr>'); r.append('<ul><li>行1</li><li>行2</li><li>行3</li></ul>') $('#root img').attr({ height:60, width:120, alt: 'cicilogo', src: 'http://www.cici.com/wp-content/uploads/2018/112/2018122312035677.png' }); console.log($('#root img').attr('alt'));
removeAttr是移除属性函数。
第几个
:odd 奇数,:even偶数,:first第一个
$('#root ul li:first').css('color', '#f00');// $('#root ul li:nth-child(2)').css('color', '#Ofo');//匹配1i的父ul下的1i的第几个
jQuery事件函数
为jQuery对象增加事件回调函数
注意
$('button.testclick').click()//调用click事件回调函数 $('button.testclick').click(function(){}}} // i设置click事件回调函数
Ajax
jQuery对XMLHttpRequest组件的调用接口实现了封装,更加方便调用。默认是异步请求。
GET请求
flask中蓝图代码改动如下
from flask import Blueprint, jsonify, render_template, request books = Blueprint('bp', _name_, url_prefix="/ooo', template_folder='temp') # 可以有自己的静态路径、模板路径、root路径 @books.route('/') def all(): books = [ {'id': O, 'name': 'python'}, {'id': 1, 'name': 'C++}, {'id': 2, 'name': 'java'} ] print(*dir(request), sep='\'n') print(request.ur1) # http://127.0.0.1:5000/bodks/?k1=1&k2=3 print(request.full_path) # /books/?k1=1&k2=3 print(request.method) # GET print(request.cookies) print(request.args) print(request.form) print(request.is_json, request.get_json, requesst.json) print(request.query_string) # b'k1=1&k2=3' return jsonify(books)
GET请求跨域
如果你发起HTTP请求时,使用了不同的域名或端口,这就不是同域了。
为了演示效果,使用不同域来访问。
如果浏览器地址栏使用127.0.0.1,那么Ajax请求就使用localhost。总之两个不一样就行。
点击"Ajax GET请求"按钮,发现数据返回了,但是控制台中的数据不能打印了,并报了下面的错误
这是HTTP请求跨域访问产生的。
请求头如下
GET /books/?k1=1&k2=3 HTTP/1.1 Host: localhost: 5000 Connection: keep-alive Accept: */* Origin: http://127.0.0.1:5000 User-Agent: Możilla/5.0 (windows NT 6.1) ApplewebKit/537.36(KHTML, like Gecko)Maxthon/5.0 Chrome/55.0.2883.75 Safari/537.36 DNT: 1 Referer: http://127.0.0.1:5000/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN
上面是GET请求,跨域请求后,其实数据都已经返回到了浏览器端,但是被浏览器拒绝了。
跨域CORS
Cross Origin Resource Sharing跨域资源共享,它是使用额外的HT「P头来告诉浏览器准许访问一个与当前所在origin(Domain) 不同源 的服务器上特定的资源。
不同源: 指的是不同的协议,或不同域,或不同端口。
当浏览器从当前所在资源所在源,对不同源的资源发起HTTP请请求的时候,这个HTTP请求就是跨域HTTP请求。
简单请求
简单请求:
- 如果请求方式是,GET、POST、HEAD三种方法之一
- Content-Type是,text/plain、multipart/form-data、application/x-www-form-urlencoded三种值之一
简单请求解决方案比较简单,只需要在服务器端响应报文中增加一个首部。
简单请求跨域解决
浏览器端请求报文中的首部发送字段Origin首部,服务器端响应首部增加Access-Control-Allow-Origin首部允许即可。
#蓝图中修改如下 @books.route('/')#或者/all def all(): books = [ (0,"python',40.0), (1, 'C++', 50.0), (2,"Java', 35)] ret = jsonify(books) ret.headers['Access-Control-Allow-Origin'] = '*' return ret
Access-Control-Allow-Origin: *意思是什么域都被允许; Access-Control-Allow-origin: http://cici.com只允许指定域。 res.headers['Access-Control-Allow-Origin'] = 'http://127.0.0.1:5000
发送的数据到了Flask中,使用request.form即可提取。
预检请求
POST请求跨域
简单请求
预检请求
POST请求提交JSON数据
直接点击Ajax POST请求提交Json数据按钮,在浏览器控制台出现如下错误。
在jQuery官方有这么一句
contentType (default: 'application/x-www-form-urlencoded; charseet=UTF-8 Type: Boolean or String When sending data to the server, use this content type. Default is"application/x-www-form urlencoded; charset=UTF-8", which is fine for most cases. If you eexplicitly pass in a content-type to $. aj ax (), then it is always sent to the server (even if no data is sentof jQuery 1.6 you can pass false to tell jQuery to not set any content type header. Note: TheW3C XMLHttp Request specification dictates that the charset is always UTF-8; specifyinganother charset will not force the browser to change the encoding. Note: For cross-domain requests, setting the content type to anything other than application/x-www-form-urlencoded, multipart/form-data, Or text/plain will trigger the browser to send a preflight OPTIONS request to the serveer.
也就是说跨域访问如果设置不是这3种Content-Type,也就是不是简单请求,那么会触发preflight OPTIONS请求(预检请求)。
请求头如下
OPTIONS /books/?k1=v1&k2=v2%&k2=v3 HTTP/1.1 Host: localhost: 5000 Connection: keep-alive Access-Control-Request-Method: POST Origin: http://127.0.0.1:5000 X-DevTools-Emulate-Network-Conditions-Client-:Id: a7899476-5a03-4a5e-b588- f8e1118e6d14 User-Agent: Mozilla/5.0 (windows NT 6.1) Applewebkit)/537.36 (KHTML, like Gecko)Maxthon/5.0 Chrome/55.0.2883.75 Safari/537.36 Access-Control-Request-Headers: content-type Accept: */* DNT: 1 Referer: http://127.0.0.1:5000/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN
正常请求头如下
解决方案
- 不跨域,使用同域来访问网页即可
- 修改视图函数,增加OPTIONS方法支持,并返回相应的访问空制头
跨域访问成功了,注意是2次HTTP请求,第一次就是提起预检请求OPTIONS方法,如果对方允许,才发起第二次POST请求。
Json数据处理
服务器端并没有成功的解析出lson数据,观察发现发过来的数据不是Ison。
其他
jsonp
使用XHR不能跨域,前面实验看到了,除非做跨域的处理或者preflight等。
但是<script>等标签可以使用 不同域 的文件。
为了便于客户端使用数据,逐渐形成了一种非正式传输协议,人们把它称作JSONP,该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。
使用nginx
站点主目录中部署index.html和a.js
//a.js文件中写下面语句 alert('test js');
这种访问没有跨域问题,注意script标签发起一个新的http连接请求js文件
现在把a.js部署到另一个域,修改index.html
<script src='http://node1.cici.com/a.js'></script>
依然可以访问,没有任何问题。但是XHR跨域就需要一些处理了。
修改index.html
<script> function jsonpcallback(data){/本地函数 console.log('返回处理的数据'); console.log(data); </script> <script src='http://node1.cici.com/a.js?callback=jsonpcallback'></script> </head> <body> <h1>Welcome to Jsonp</h1> </body> </html>
<script src='http://node1.cici.com/a.js?callback=jsonpcallback'></script>告诉服务器端,返回的函数名是什么
// a.js // jsonpcallback jsonpcallback({a:100, b:"abc"})
这样就解决了Ajax跨域访问的问题了。
把上例中的约定回调函数、增加script标签访问其他域的代码抽出来,就可以更加灵活的使用了。
jQuery的Ajax就提供了这样的处理方式。
<!DOCTYPE html> <html> <head> <title>cici</title> <script src="jquery.min.js"></script> <script> $(function(){ $.ajax({ url: 'http://node1.cici.com/a.js', dataType:'jsonp', //jsonp在jQuery中默认形式为http://nodel.cici.com/a.js6?callback=jQuery随机数 //jsonp:'cb',//修改参数名callback jsonpCallback:'jsonpcallback',//指定返回后调用的函数数名 success:function(data){/拿回的数据再传递给成功回调使用 console.log(data); console.log('-'.repeat(30));
注意:Jsonp只能使用GET方法,因为它内部使用了script标签的src发起http请求,这不能是POST方法的请求。
Ajax使用XHR组件通信,出于安全,一般要求同源策略。
JSONP实际上是网页内部动态增加了script标签,并利用了使用src引用静态资源时不受跨域限制的机制。事先写好一个回调函数放在网页中,并在发起HTTP请求时,将回调函数名告知服务器端。服务器端使用这个回调函数名,按照JavaScript语法把数据放在回调函数中。然后将这些内容作为脚本的内容返回浏览器端,浏览器端就可以运行该脚本。jQuery提供了JSONF语法糖,简化了操作。
Django静态配置
Django的静态文件配置,如下 1、settings中INSTALLED_APPS确保有django.contrib.staticfiles
2、settings中定义静态路径,STATIC_URL='static
3、settings中定义
STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ]
练习
Ajax方式提交GET方法,从服务器查回数据,使用表格显示,要求使用JS对表格进行动态追加。