前后端分离开发和混合开发的区别还是很大的。前后端分离我们需要遵循restful规范,先介绍什么是restful api规范
a.同一种数据的操作,只设置一个url路由。也就是根据请求方法来区分具体的处理逻辑。而不再设置多个增删改查的路由。
(1)可以基于FBV来通过请求方法的不同,处理不同的逻辑。
url(r'^order/', views.order),
def order(request):
if request.method == 'GET':
return HttpResponse('获取订单')
elif request.method == 'POST':
return HttpResponse('创建订单')
elif request.method == 'PUT':
return HttpResponse('更新订单')
elif request.method == 'DELETE':
return HttpResponse('删除订单')
(2)可以基于CBV来实现处理不同的逻辑
url(r'^order/', views.OrderView.as_view()),
class OrderView(View):
def get(self,request,*args,**kwargs):
return HttpResponse(json.dumps(ret),status=201)
def post(self,request,*args,**kwargs):
return HttpResponse('创建订单')
def put(self,request,*args,**kwargs):
return HttpResponse('更新订单')
def delete(self,request,*args,**kwargs):
return HttpResponse('删除订单')
而两种方式中,最建议使用CBV的方式去写接口,更加简洁,不用判断了。
b. 域名建议
为了对用户使用的url和网页中使用的接口api进行区别,设置如下规则
(1)子域名的方式区分(需要解决跨域的问题):
www.baidu.com (用户在浏览器中输入的地址,可以访问网站页面)
但是网页需要到后台请求接口,获取数据,那么接口的api应该如何命名呢?
api.baidu.com/v1/login.json
用户一看到域名是以api开头的,就知道是接口,返回的是json数据。
(2)URL的方式进行区分(不需要解决跨域问题):
www.baidu.com (用户使用的URL)
www.baidu.com/api/v1/login.json
不管使用哪种方式,就是为了能够一眼区分出来这是一个api接口。
两种方式哪一种更好呢?
答案是第二种,因为第一种可能会出现跨域请求,也就是当域名不同或者端口不同的时候,都会出现跨域请求,而第二种保证了域名和端口的一致性,只是url不一样而已。
跨域:因为浏览器的同源策略,当你通过浏览器向www.baidu.com前端页面发送请求的时候,网页需要向后台请求接口,但是如果接口的域名和当前的域名不一致,就会出现跨源请求的错误,无法访问到页面。而跨源是网页向api发送请求之后,服务器响应了这个请求,但是是浏览器端把这一次请求的响应给阻止了,并不是在请求不同域名的接口时,服务端不会响应这个请求。跨源是浏览器端的阻止行为,而不是服务器端的。
c. 版本规则
两个版本共存的时候,应该将API的版本号放入URL。
api.example.com/api/v1/
另一种做法是,将版本号放在HTTP头信息中,但不如放入URL方便和直观。Github采用这种做法。
d. 面向资源编程
将网络中的任何东西都看作是资源,对资源可以进行增删改查的操作,但是资源表示的是一个名称,如果一个url后面跟的是一个名词(单复数都可以),所用的名词往往与数据库的表格名对应,就表示要对这个资源进行增删改查的操作了。而get/post/delete/put是动词,所以url中不建议出现动词。
www.baidu.com/api/v1/order/ (遵循规范)
www.baidu.com/api/v1/orders/ (遵循规范)
www.baidu.com/api/v1/get_order/ (没有遵循规范)
e. HTTP方法规范
GET:从服务器上获取一个或者多个资源
POST:在服务器上新建一个资源
PUT:在服务器跟新全部资源
PATCH:在服务器更新部分资源
比如用户表就有用户名,密码,性别,如果是PUT就全部更新。如果是PATHCH就只更新密码。
f. 过滤规范
www.baidu.com/api/v1/orders/?status=1&page=2&offset
g. 状态码规范(状态码+code码)
后台提供的状态码,供前端使用。
200系列,300系列表示重定向,400系列表示客户端错误,500系列表示服务端错误(后台代码错误)。
但是只有状态码还是不够的,请求的状态太多,所以除了使用状态码表示状态以外,还应该有code码来表示更加详细的请求情况。
比如:支付宝的code码,20000,20001等
def get(self,request,*args,**kwargs):
ret = {
'code':1000,
'msg':'没有携带cookie'
}
return HttpResponse(json.dumps(ret),status=201)
h. 请求的返回值规范
GET /order/:返回资源对象的列表(数组)
GET /order/1/:返回单个资源对象
POST /order/:返回新生成的资源对象
PUT /order/1/:返回完整的资源对象
PATCH /order/1/:返回完整的资源对象
DELETE /order/1/:返回一个空文档
i. Hypermedia API超链接规范
希望在请求结果中包含这一个资源的详细信息的api。
比如,我们请求商品列表信息得到如下的结果:
[
{
"id": 1,
"name": "袜子"
},
{
"id": 2,
"name": "裤子"
},
{
"id": 3,
"name": "鞋子"
}
]
如果你想查看id=1的商品的详细信息,你需要使用这个id拼接详情页的url地址,并发出请求,但是restful中希望这个详情页的url直接包含在json数据中,不用再单独进行拼接了。
[
{
"id": 1,
"name": "袜子",
"url": "http://www.baidu.com/api/v1/1/"
},
{
"id": 2,
"name": "裤子"
"url": "http://www.baidu.com/api/v1/2/"
},
{
"id": 3,
"name": "鞋子"
"url": "http://www.baidu.com/api/v1/2/"
}
]
根据上面的规范来进行实例的操作:
url路由的定义应该尽量使用通用视图:
from django.contrib import admin
from django.urls import path
from stuapp.views import *
urlpatterns = [
path('admin/', admin.site.urls),
path('token/', get_csrftoken),
path('students/', StudentView.as_view()),
path('inspect/', StudentInspectView.as_view()),
]
views.py文件中实现通用视图类函数,并将相关数据传给前端
# CBV和FBV
# CBV:基于类的视图 class StudentView(View):
# FBV:基于函数的视图 def student(request):
class StudentView(View):
@method_decorator(allow_origin)
def get(self, request):
"""
学生数据查询接口
功能:分页返回学生数据,前端传递page=1就返回第一页的数据;
?page: 页码
?size: 每页的数据个数
:param request:
:return:
"""
error = ''
# 1. 查询所有的学生数据
stus = StuModel.objects.all()
# 2. 根据前端ajax传递的page&size参数开始做分页数据
size = int(request.GET.get('size', '2'))
page_number = int(request.GET.get('page', '1'))
paginator = Paginator(stus, size)
try:
page = paginator.page(page_number)
except (EmptyPage, PageNotAnInteger, InvalidPage):
error = '已经是最后一页了'
page = paginator.page(paginator.num_pages)
page_number = paginator.num_pages
# 3. 开始做分页
# 假设分页器上只显示5个页码,分页器出现滚动之后,当前页始终在中间,当前页前后各两个页码;
if paginator.num_pages <= 5:
# 全部展示,将当前所有页码的值返回给前端
page_nums = [x for x in range(1, paginator.num_pages + 1)]
elif page_number < 4:
# 如果总页数超过5页了,但是当前页的页码小于4的时候,分页器是同样不会滚动的。
# 1 2 3 4 5
# 2 3 4 5 6
# 3 4 5 6 7
page_nums = [x for x in range(1, 6)]
elif page_number - 4 >= 0 and page_number <= paginator.num_pages - 2:
# 如果总页数超过5页了,分页器需要滚动
page_nums = [x for x in range(page_number - 2, page_number + 3)]
else:
# 超过5页,但是已经到最后一页了,页面不再滚动
page_nums = [x for x in range(paginator.num_pages - 4, paginator.num_pages+1)]
#4. 向前端返回json数据
previous = page.has_previous()
next = page.has_next()
data = {
'code': 100,
'status': 'ok',
'error': error,
# 总的数据个数
'total_pages': len(stus),
# 是否有上一页
'has_previous': previous,
'previous_url': page_number-1 if previous else None,
# 是否有下一页
'has_next': next,
'next_url': page_number+1 if next else None,
'page_nums': page_nums,
# 当前页的数据列表
'results': object_to_json(page.object_list),
'current_page': page_number
}
# response = JsonResponse(data)
# 允许所有的源,向这个接口发送请求并得到响应。(改变浏览器默认的禁止跨域,此时就是允许跨域。)
# response['Access-Control-Allow-Origin'] = '*'
return data
在这我们使用了一个装饰器@method_decorator(allow_origin),它的作用就是解决跨域的问题,//前后端分离下,经常出现跨域(CORS)问题,这个问题是浏览器的原因造成的,跟后台没有关系。
//一般在当前页面中,请求了和当前域名及端口不一致的url,就会出现跨域;
// 页面:http://localhost:63342 接口:http://localhost:8000
// 浏览器默认情况下阻止跨域:主要是为了保护网站的安全性的一种同源策略,在不确定安全性的前提下,不允许访问和网站本身端口域名不相同的地址的。
// 跨域是浏览器向后台接口发送请求,并且后台也响应了这个请求,但是浏览器的同源策略将这个跨域的响应拦截了。具体内容如下
from django.http import JsonResponse
def allow_origin(func):
def _func(*args, **kwargs):
data = func(*args, **kwargs)
response = JsonResponse(data)
response['Access-Control-Allow-Origin'] = '*'
return response
return _func
另外还使用了object_to_json()函数,它的作用是将QuerySet对象转为一个字典,因为前端不能识别QuerySet对象,实现代码如下:
from django.db.models.query import QuerySet
def object_to_json(model, ignore=None):
"""
函数的作用就是将ORM中的Model对象,转化成json对象,再返回给前端
:param model:
:param ignore:
:return:
"""
if ignore is None:
ignore = []
if type(model) in [QuerySet, list]:
json = []
for element in model:
json.append(_django_single_object_to_json(element, ignore))
return json
else:
return _django_single_object_to_json(model, ignore)
def _django_single_object_to_json(element, ignore=None):
return dict([(attr, getattr(element, attr)) for attr in [f.name for f in element._meta.fields if f not in ignore]])
前端的js代码:
function loadData(page) {
// 该函数是根据page的值,加载当前页数据的函数;
// page: 表示当前页的页码
list_url = 'http://127.0.0.1:8000/students/?page=' + page + '&size=3';
$.get(list_url, function(data){
// 解析后台接口返回的json数据
// 因为loadData(page)是循环调用的,所以在每次append()之前,先将上一次的数据清空,然后再append()新的数据。
$('tbody').empty();
for (var index in data.results){
var student = data.results[index];
tr = $('');
// 向 标签中添加三个,分别是姓名、ID、年龄
tr.append($(' ').text(student.name), $(' ').text(student.id), $(' ').text(student.age));
// 继续向 标签中添加编辑图标和删除图标
tr.append($('').append($('').attr({
'class': 'glyphicon glyphicon-edit',
'data-toggle': 'modal',
'data-target': '#editModal'
})));
tr.append($(' ').append($('').attr({'class': 'glyphicon glyphicon-remove'})));
$('tbody').append(tr);
}
// 开始设置分页
$('.pagination').empty();
if (data.has_previous){
// 有上一页
$('.pagination').append($('').append($('').attr({'aria-label': 'Previous', 'href': 'javascript:loadData(' + data.previous_url + ');'}).text('<<')));
}
// [2 3 4 5 6]
for (var index in data.page_nums){
var page_number = data.page_nums[index];
var li = $('');
if (data.current_page == page_number){
//如果当前请求的页码的值current_page和遍历出来的page_number的值相等,将这个页码标记为选中状态。
li.attr('class', 'active');
}
// 向这个li内部添加一个a标签
li.append($('').attr({'href': 'javascript:loadData(' + page_number + ');'}).text(page_number));
// 再将这个li添加到ul标签中;
$('.pagination').append(li);
}
// 下一页
if (data.has_next){
$('.pagination').append($('').append($('').attr({'aria-label': 'Next', 'href': 'javascript:loadData(' + data.next_url + ');'}).text('>>')));
}
});
}
loadData(1);
前后端分离中CSRF的问题:
- 在前后端分离中,接口的调用本身就是一种跨站请求,因为这个接口既要被安卓端的站点访问,又要被苹果端的站点访问,所以在这种模式下,CSRF的认证就失去作用了。解决方案:取消CsrfMilldeware中间件对于CSRF的认证;
- 如果在前后端分离中,必须要进行csrf认证,也可以实现;
CSRF工作原理:
FORM表单提交POST请求时:
每次渲染页面,在Html模版中,{{ csrf_token }}都会加载一个随机字符串,每次的值都是不一样的;这个值会放在请求体Form Data中,提交至后台;
AJAX提交POST请求时:
不一定非得通过{{ csrf_token }}来认证csrf,也可以通过在请求头中,添加X-CSRFToken: csrftoken字段,同样也能通过csrf认证;
由于前后端分离:前端页面无法识别{{ csrf_token }},那么前后端分离如何通过csrf的认证呢?通过在请求头中,添加X-CSRFToken: csrftoken字段
不需要csrf认证时的配置:
csrf_exempt: 对某一个FBV的视图取消csrf认证;不能用户CBV
csrf_protect: 对某一个FBV的视图添加csrf认证;不能用户CBV
如果想让整个项目都取消csrf认证,去settings.py中关闭中间件。
在CBV中怎样设置呢?
很简单,如下,在urls里配置就可
from django.views.decorators.csrf import csrf_exempt
path('inspect/', csrf_exempt(StudentInspectView.as_view()) ),
在前后端分离情况下需要csrf认证又怎么设置呢
安装第三方跨域包:django-cors-headers
解决:
1. 基本的跨域请求能够实现;
2. 能够获取跨域请求返回的响应头中的所有字段(默认只返回Content-Type);
3. 能够发起跨域请求的时候携带Cookie(默认不允许带Cookie);
配置settings.py文件:
INSTALLED_APPS = [
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# 设置为False,如果设置为True,则请求无法携带Cookie了。
CORS_ORIGIN_ALLOW_ALL = False
# 设置请求是否允许携带Cookie,必须和xhrFields: {withCredentials: true,}同时使用。
CORS_ALLOW_CREDENTIALS = True
# 跨域源的白名单,需要跨域的源设置在这里
CORS_ORIGIN_WHITELIST = [
'localhost:63342',
]
# 指定哪些方法可以发送跨域请求
CORS_ALLOW_METHODS = (
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
)
# 指定请求头中允许携带的字段
CORS_ALLOW_HEADERS = (
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
'cookie',
)
后台views.py配置:
class StudentInspectView(View):
"""
检查学员学号是否唯一的CBV视图类接口。
"""
def post(self, request):
stu_id = request.POST.get('sid')
data = {}
if StuModel.objects.filter(id=stu_id):
# 学号已经存在
data['is_exist'] = 1
data['message'] = '该学号已经存在'
else:
data['is_exist'] = 0
data['message'] = '该学号可以使用'
# 需要返回Response对象
return JsonResponse(data)
前端js配置:
// 检查学号是否已经存在
$('#sid').blur(function () {
//在数据没有合法之前,"提交按钮" 不能点击;
$('#add').attr('disabled', true);
$.ajax({
url: 'http://localhost:8000/inspect/',
type: 'POST',
data: {
'sid': $('#sid').val()
},
xhrFields: {
withCredentials: true
},
headers: {
'X-CSRFToken': $.cookie('csrftoken'),
},
success: function (data, status) {
$('#sid').next().text(data.message);
}
});
});
// 发送ajax请求,在页面刷新的时候,就获取csrftoken的值
$(function () {
$.ajax({
url: 'http://localhost:8000/token/',
// 默认情况下,跨域请求的请求头是不允许携带Cookie的,另外一方面,跨域请求的响应头中默认只包含content-type,后台的响应中只返回这一个字段。
xhrFields: {
withCredentials: true,
}
});
});
你可能感兴趣的:(Django,前后端分离)