django-9-项目实战

需求

以前后端分离的方式实现学生的增删改查操作

学生列表接口

url:/students/

请求方法:get

参数:

  • 格式:查询参数

参数名

类型

是否必传

说明

page

int

页码,默认为1

size

init

每页数据条数默认为10

name

str

根据姓名过滤

age

int

根据年龄过滤

sex

int

根据性别过滤

phone

str

根据手机过滤

channel

int

根据渠道过滤

响应:

  • 状态码:200
  • 格式:json
  • 响应示例:

 

{
    "total": 7,
    "page": 1,
    "next_page": null,
    "pre_page": null,
    "results": [
        {
            "id": 8,
            "name": "yaya",
            "age": 18,
            "sex": 1,
            "phone": null,
            "channel": "",
            "c_time": "2022-09-01T06:30:48.417Z"
        },
        {
            "id": 7,
            "name": "yaya",
            "age": 18,
            "sex": 1,
            "phone": null,
            "channel": "",
            "c_time": "2022-09-01T06:10:41.869Z"
        },
        {
            "id": 6,
            "name": "心蓝",
            "age": 20,
            "sex": 1,
            "phone": "15873061798",
            "channel": "",
            "c_time": "2022-08-31T12:21:04.068Z"
        },
        {
            "id": 5,
            "name": "小简",
            "age": 16,
            "sex": 0,
            "phone": null,
            "channel": "抖音",
            "c_time": "2022-08-23T13:10:05.317Z"
        },
        {
            "id": 3,
            "name": "张柏芝",
            "age": null,
            "sex": 1,
            "phone": null,
            "channel": "",
            "c_time": "2022-08-23T08:00:15.165Z"
        },
        {
            "id": 2,
            "name": "刘德华",
            "age": null,
            "sex": 1,
            "phone": null,
            "channel": "",
            "c_time": "2022-08-23T08:00:08.035Z"
        },
        {
            "id": 1,
            "name": "心蓝",
            "age": null,
            "sex": 1,
            "phone": null,
            "channel": "",
            "c_time": "2022-08-23T07:59:29.417Z"
        }
    ]
}

学生详情接口

url:/students/pk/

请求方法:get

参数:

  • 格式:路径参数

响应数据:

  • 状态码:200
  • 格式:json
  • 响应实例:

 

{
    "id": 1,
    "name": "心蓝",
    "age": null,
    "sex": 1,
    "phone": null,
    "channel": "",
    "c_time": "2022-08-23T07:59:29.417Z"
}

学生添加接口

url:/students/

请求方法:post

参数:

  • 格式:json

参数名

类型

是否必传

说明

name

str

姓名

age

int

年龄

sex

int

性别

phone

str

手机

channel

int

渠道id

  • 请求实例:
{
    "name": "刘德华",
    "age": 60,
    "sex": 1,
    "phone": '13888888888',
    "channel": 1
},

响应:

  • 状态码:201
  • 格式:json
  • 响应示例:
{
    "id": 8,
    "name": "yaya",
    "age": 18,
    "sex": 1,
    "phone": null,
    "channel": "",
    "c_time": "2022-09-01T06:10:41.869Z"
}

学生修改接口

url:/students/pk/

请求方法:put/patch

参数:

  • 格式:json,路径

参数名

类型

是否必传

说明

name

str

姓名

age

int

年龄

sex

int

性别

phone

str

手机

channel

int

渠道id

  • 请求实例:
{
    "name": "刘德华",
    "age": 60,
    "sex": 1,
    "phone": '13888888888',
    "channel": 1
},

响应:

  • 状态码:200
  • 格式:json
  • 响应示例:
{
    "id": 8,
    "name": "yaya",
    "age": 18,
    "sex": 1,
    "phone": null,
    "channel": "",
    "c_time": "2022-09-01T06:10:41.869Z"
}

学生删除接口

url:/students/pk/

请求方法:delete

参数:

  • 格式:路径

响应:

  • 状态码:204
  • 格式:无响应内容

后端代码

视图

class StudentView(View):

    def serialize(self, item):

        return {
                'id': item.id,
                'name': item.name,
                'age': item.age,
                'sex': item.sex,
                'phone': item.phone,
                'channel': item.channel.title if item.channel else '',
                'c_time': item.c_time
            }

    def get(self, request, pk=None):
        if pk is not None:
            #  查看学生详情
            obj = self.get_obj(pk)
            data = self.serialize(obj)
            return JsonResponse(data)

        # 1. 获取查询参数
        query_params = {key: value for key, value in request.GET.items()}
        # 2. 获取分页参数
        page = int(query_params.pop('page', 1))
        size = int(query_params.pop('size', 10))
        # 3. 获取查询集
        queryset = Student.objects.all()
        for key, value in query_params.items():
            try:
                queryset = queryset.filter(**{key: value})
            except:
                pass
        # 4. 分页处理
        # 数据总条数
        total_num = queryset.count()
        # 总页数
        total_page = math.ceil(total_num / size)
        # 下一页
        absolute_url = self.request.build_absolute_uri()
        next_url = None
        pre_url = None
        if page < total_page:

            if 'page' in absolute_url:
                next_url = re.sub(r'page=\d*', 'page={}'.format(page+1), absolute_url)
            else:
                next_url = absolute_url + '&page={}'.format(page+1)
        # 上一页
        if page > 1:

            if 'page' in absolute_url:
                pre_url = re.sub(r'page=\d*', 'page={}'.format(page-1), absolute_url)
            else:
                pre_url = absolute_url + '&page={}'.format(page-1)
        # 分页过滤
        queryset = queryset[(page - 1) * size:page * size]
        # 5.序列化
        students = [
            self.serialize(item) for item in queryset
        ]
        data = {
            'total': total_num,
            'page': page,
            'next_page': next_url,
            'pre_page': pre_url,
            'results': students
        }
        # 6. 返回响应
        return JsonResponse(data)

    def post(self, request):
        # 1.接受参数
        create_data = json.loads(request.body)
        # 2.实例化表达
        form = StudentForm(create_data)
        # 3.校验
        if form.is_valid():
            instance = form.save()
            # 4.序列化
            data = self.serialize(instance)
            return JsonResponse(data, status=201)
        else:
            # 5.错误信息
            data = {'errors': form.errors}
            return JsonResponse(data, status=400)

    def get_obj(self, pk):
        obj = get_object_or_404(Student, pk=pk)
        return obj

    def put(self, request, pk):
        # 1. 获取对象
        obj = self.get_obj(pk)
        # 2. 接收参数
        update_data = json.loads(request.body)
        # 2. 实例化表单
        form = StudentForm(update_data, instance=obj)
        # 3. 校验
        if form.is_valid():
            instance = form.save()
            # 4. 序列化
            data = self.serialize(instance)
            return JsonResponse(data, status=200)
        else:
            # 5.错误信息
            data = {'errors': form.errors}
            return JsonResponse(data, status=400)

    def delete(self, request, pk):
        # 1. 获取对象
        obj = self.get_obj(pk)
        # 2. 删除对象
        try:
            obj.delete()
            return HttpResponse(status=204)
        except Exception as e:
            return JsonResponse(data={'errors': str(e)}, status=400)

路由

path('students/', views.StudentView.as_view(), name='student-list-create'),
    path('students/<int:pk>/', views.StudentView.as_view(), name='student-retrieve-update-delete')

前端代码

列表页面

<!-- student_list_single.html -->
<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>Bootstrap 101 Template</title>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
      integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">

    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
    <!--[if lt IE 9]>
    <script src="https://fastly.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
    <script src="https://fastly.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
    <![endif]-->
    <style>
      [v-cloak] {
        display: none;
      }
    </style>
  </head>
  <body>
    <div class="container" style="width: 1000px" id="app">
      <h1>学生列表</h1>
      <a class="btn btn-success" style="float: right" href="./student_detail_single.html">添加</a>
      <table class="table table-hover table-bordered table-condensed" v-cloak>
        <thead>
          <tr>
            <th>序号</th>
            <th>姓名</th>
            <th>性别</th>
            <th>年龄</th>
            <th>phone</th>
            <th>渠道</th>
            <th>创建时间</th>
            <th>操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="(stu, index) in students">
            <td>{{ index }}</td>
            <td>{{ stu.name }}</td>
            <td>{{ stu.sex }}</td>
            <td>{{ stu.age }}</td>
            <td>{{ stu.phone }}</td>
            <td>{{ stu.channel }}</td>
            <td>{{ stu.c_time }}</td>
            <td style="text-align: center;padding: 0.75em 0"><a :href="'./student_detail_single.html?id='+stu.id"
                                                               class="btn btn-primary btn-sm">编辑</a>&nbsp;
              <button class="btn btn-danger btn-sm" @click="delStudent(stu.id, index)">删除</button>
            </td>
          </tr>
        </tbody>
      </table>
      <nav aria-label="...">
        <ul class="pager">
          <li class="previous" :class="{ disabled: !pre_url }"><a
                                                                 href="{{ students.pre_url }}"><span
                                                                                                 aria-hidden="true">&larr;</span> 上一页</a></li>
          <li class="next" :class="{ disabled: !next_url }"><a
                                                              href="{{ students.next_url }}">下一页 <span
                                                                                                   aria-hidden="true">&rarr;</span></a>
          </li>
        </ul>
      </nav>
    </div>


    <!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
    <script src="https://fastly.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"
      integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ"
      crossorigin="anonymous"></script>
      <!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
      <script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"
      integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd"
      crossorigin="anonymous"></script>
      <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
      <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
      <script>
      let baseUrl = 'http://127.0.0.1:8000'
      const app = new Vue({
      el: '#app',
      data: {
      students: [],
      next_url: null,
      pre_url: null,
      },
      computed: {

      },
      methods: {
      delStudent(sid, index) {
      axios.delete(baseUrl + '/crm/students/' + sid + '/').then(res => {
      this.students.splice(index, 1)
      })
      }
      },
      created() {
      axios.get(baseUrl + '/crm/students/').then(res => {
      this.students = res.data.results
      this.next_url = res.data.next_url
      this.pre_url = res.data.pre_url
      })
      }
      })
      </script>
      </body>
      </html>

详情页

<!-- student_detail_single.html -->
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
    <title>Bootstrap 101 Template</title>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
          integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">

    <!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
    <!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
    <!--[if lt IE 9]>
    <script src="https://fastly.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
    <script src="https://fastly.jsdelivr.net/npm/respond.js@1.4.2/dest/respond.min.js"></script>
    <![endif]-->
    <style>
        [v-cloak]{
              display: none;
          }
    </style>
</head>
<body>
<div class="container" style="width: 800px" id="app" v-cloak>
    <h1>{{ title }}</h1>
    <form class="form-horizontal">
        <div class="form-group">
            <label for="name" class="col-sm-2 control-label">姓名</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="name" name="name" v-model="stu.name" placeholder="姓名">
            </div>
        </div>
        <div class="form-group">
            <label for="sex" class="col-sm-2 control-label">性别</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="sex" name="sex" v-model="stu.sex" placeholder="性别">
            </div>
        </div>
        <div class="form-group">
            <label for="age" class="col-sm-2 control-label">年龄</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="age" name="age" v-model="stu.age" placeholder="年龄">
            </div>
        </div>
        <div class="form-group">
            <label for="qq" class="col-sm-2 control-label">qq</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="qq" name="qq" v-model="stu.qq" placeholder="qq">
            </div>
        </div>
        <div class="form-group">
            <label for="phone" class="col-sm-2 control-label">手机号码</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="phone" name="phone" v-model="stu.phone"
                       placeholder="手机号码">
            </div>
        </div>
        <div class="form-group">
            <label for="channel" class="col-sm-2 control-label">渠道</label>
            <div class="col-sm-10">
                <select name="channel" id="channel" class="form-control" v-model="stu.channel">
                    <option value="">--------</option>
                    <option v-for="channel in channels" :value="channel.id">{{ channel.name }}</option>
                </select>
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-offset-2 col-sm-10">
                <button class="btn btn-info float-right"
                        @click.prevent="btnSubmit(stu)">{{ btnName }}</button>
            </div>
        </div>


    </form>
</div>

<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://fastly.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"
        integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ"
        crossorigin="anonymous"></script>
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"
        integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd"
        crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
    let baseUrl = 'http://127.0.0.1:8000'
    const app = new Vue({
        el: '#app',
        data: {
            title: '',
            stu: {},
            channels: [{name: '百度', id: 1}, {name: '抖音', id: 2}],
            btnName: ''
        },
        methods: {
            delStudent(sid, index) {
                axios.delete(baseUrl + '/crm/students/' + sid + '/' ).then(res => {
                    this.students.splice(index, 1)
                })
            },
            getParams() {
                let query = window.location.search.substring(1)
                let vars = query.split("&");
                let data = {}
                for (let v of vars) {
                    v = v.split('=');
                    data[v[0]] = v[1]
                }
                return data

            },
            saveStu(sid, stu){
                axios.put(baseUrl + '/crm/students/' + sid + '/' , stu).then(res=>{
                    location.href = 'student_list_single.html'
                })
            },
            createStu(stu){
                axios.post( baseUrl + '/crm/students/', stu).then(res=>{
                    location.href = 'student_list_single.html'
                })
            },
            btnSubmit(stu){
                let sid = this.getParams().id;
                if (sid){
                    this.saveStu(sid, stu)
                }else {
                    this.createStu(stu)
                }
            }

        },
        created() {
            let sid = this.getParams().id;
            if (sid) {
                this.title = '学生详情';
                this.btnName = '保存';
                axios.get(baseUrl + '/crm/students/' + sid + '/' ).then(res => {
                    this.stu = res.data
                })
            } else {
                this.title = '添加学生';
                this.btnName = '添加'
            }

        }
    })
</script>
</body>
</html>

CORS

后端服务写好后,通过postman可以正常访问,但通过浏览器时,发送ajax请求会报CORS错误。

要理解CORS需要先了解几个概念

同源策略

同源策略是一个重要的安全策略,它是浏览器最核心最基本的安全功能。

它限制web应用程序只能从加载应用程序的同一个域请求HTTP资源。

当向不同的域请求HTTP资源时就发生了跨域,默认请情况下浏览器会阻止跨域的请求。

那如何判断是否同源呢?

如果两个URL的协议,端口和主机都相同的话,则这两个URL是同源。

例如:以下所有资源都具有相同的来源:

http://example.com/

http://example.com:80/

http://example.com/path/file

每个url都有相同的协议,主机和端口号。

而以下每个资源都与其他不同源:

http://example.com/

http://example.com:8080/

http://www.example.com/

https://example.com:80/

https://example.com/

http://example.org/

http://ietf.org/

所以,所谓的同源策略简单的理解就是,打开某个页面后,这个页面上的ajax请求默认只能向和页面同源的url发送http请求。

同源策略固然保证了安全,但同时也限制了应用的灵活性,所以出现了CORS.

什么是CORS

CORS是一个W3C标准,全称是"跨域资源共享"(cross-origin resource sharing)。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于前端开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

CORS原理

跨域请求

浏览器将跨域请求分为两类:简单请求非简单请求。

只要同时满足一下两个条件,就属于简单请求:

  1. 请求方法是一下三种方法之一:
    • head
    • get
    • post
  1. http请求的头信息不超出以下几种字段:
    • accept
    • accept-language
    • content-language
    • Last-Event-ID
    • Content-Type的值仅限于下列三者之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理是不一样的。

简单请求CORS步骤

对于简单请求CORS的基本流程如下:

第一步:客户端(浏览器)请求

当浏览器发出跨域请求时,该浏览器会添加一个包含当前源(协议,主机和端口)的Origin头。

1686047563657-4101e207-04ac-4897-9f4f-b690bb5c9352.png

第二步:服务器响应

在服务器,当服务器看到Origin头并想要允许访问时,就需要在响应中加入一个Access-Control-Allow-Origin响应头来指定请求源(例如加入*表示允许任何源)

1686047563680-202c46f2-577e-47a9-816e-900eb2789c02.png

第三步:浏览器接受响应

当浏览器看到带有响应Access-Control-Allow-Origin响应头的响应时,即允许与客户端网站共享响应数据。否则抛出CORS异常。

注意: 同源策略只是浏览器遵守的规则,使用别的工具进行请求不会遵循同源策略的影响。

复杂请求CORS步骤

第一步:发送预检请求

浏览器会根据需要创建预检请求。该请求是一个options请求,会在实际请求消息之前被发送。

1686047701337-c1e93b1a-225e-427a-a53c-a3d455618690.png

预检请求中关键请求头是origin表示请求来自哪个源。除了origin字段,预检请求头的信息还包含两个特殊字段。

  1. access-control-request-method该字段是必须的,用来列出接下来的CORS请求会用到哪些HTTP方法,上面图片中的是PATCH
  2. access-control-request-headers这个字段是一个逗号分隔的字符串,指定接下来的CORS请求还会携带哪些额外的字段,上面图片中的是content-type

第二步:响应预检请求

服务器收到预检请求后,检查origin,access-control-request-method,access-control-request-headers字段后,就可以返回响应。

1686047701394-6c1ff0d6-fd2c-4097-abef-1c75c0a8b363.png

响应中的access-control-allow-origin字段表示允许跨域的源,*表示允许任意跨域请求。其他CORS相关响应头如下:

  1. Access-Control-Allow-Methods逗号分隔的一个字符串,表明服务器允许的跨域请求方法
  2. Access-Control-Allow-Headers逗号分隔的一个字符串,表明服务器支持的头字段
  3. Access-Control-Max-Age该字段可选,用来指定本次预检请求的有效期,单位为秒。上图中的有效期是一天(86400秒),在此期间不用发出另一条预检请求。

注意:如果服务器否定了预检请求,也会返回一个正常的HTTP响应,但是不包含任何CORS相关的响应头。

第三步:发送跨域请求

一旦服务器通过了预检请求,以后每次浏览器正常的CORS请求,就跟简单请求一样,会有一个origin头字段。服务器的回应,也会有一个access-control-allow-origin头信息字段。

1686047701370-a888f22d-0769-4e41-9b34-2a6c141cd25e.png

cookie跨域

出于隐私原因,CORS请求默认不带cookie。如果想要在使用CORS时发送cookie,就需要发送请求时携带cookie并且服务器也同意。

请求

ajax请求需要打开withCredentials属性才可以携带cookie:

 

const request = axios.create({
    baseURL: 'http://127.0.0.1:8000',
    timeout: 5000,
    withCredentials: true // 设置为true 跨域时会携带cookie
})

响应

如果要接受cookie跨域,access-control-allow-origin就不能设置为星号,必须指定明确,并且响应头中必须包含字段Access-Control-Allow-Credentials,值为true。

同时cookie依然遵循同源策略,只有服务器指明的域名的cookie才会上传。

1686047701372-06c78f8b-8227-46e2-8877-7ce16a5619a7.png

django-cors-headers

在django项目中要实现CORS可以手写(重复造轮子),也可以使用成熟插件(推荐)。

django-cors-headers是一个处理跨域资源共享(CORS)所需服务器器头信息的django应用。

它的使用非常简单:https://www.yuque.com/gululu-lhhoz/ln9o1q/cd1u2epfbogk2pzk

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值