Django評論
評論復雜的地方在于需要實現點擊提交評論后評論內容需要立刻出現在下面,還要保持頁面位置不變,所以提交后不能整體刷新頁面,因為刷新以后頁面肯定在最上面,而評論一般都在最下面,所以要用到Ajax
整個過程用到了Django,Vue.js,reqwest,REST_framework,ajax
展示評論內容
展示評論內容可以直接用Django從數據庫中取出數據,然后在view中渲染到前端,但這里我想用Vue.js,為了減少django的工作量,提高效率吧
// pinglun.js
let vue = new Vue({el : "#app",data : {pinglun : [{'評論者':'zhangsan','評論日期':'2019-6-5','評論時間':'17:47:23','評論內容':'hahahha','對應文章_id':'1'},{'評論者':'zhangsan1','評論日期':'2019-6-5','評論時間':'17:48:23','評論內容':'hahffahha','對應文章_id':'1'},],},
})
<!--pinglun.html-->
<div id="app"><div class="alert alert-secondary" role="alert" id="pinglunlist"><div v-for="item in pinglun " ><h5>{{ item.評論者 }}</h5><p>{{ item.評論內容 }}</p></div></div>
</div>
這樣js中data的數據就可以渲染到html頁面了,但我們需要從數據庫中拿到數據,并且賦值給data中的pinglun
,這里需要一個reqwest模塊,需要下載
npm i reqwest
下載之后訪問json文件里面的那個網址,下載壓縮包,解壓后里面有一個reqwest.js文件,要把這個文件引入,就和用Vue要引入Vue.main.js一樣,reqwest可以從一個url請求數據,并且返回
// 這是官方api
reqwest({// 要請求的路徑url: 'path/to/html'// 請求方式, method: 'post'// 請求時要攜帶的數據, data: { foo: 'bar', baz: 100 }// 成功請求的回調函數, success: function (resp) {// reap中就包含請求來的數據qwery('#content').html(resp)}
})
reqwest({url: 'path/to/html', method: 'get', data: [ { name: 'foo', value: 'bar' }, { name: 'baz', value: 100 } ], success: function (resp) {qwery('#content').html(resp)}
})
應為需要有一個請求的url,所以還需要做一個api接口,這里有兩種辦法,一種是用Django提供的HttpResponse和json直接將序列化后的json數據渲染到頁面,但這樣只能渲染成json類型,并且存在文字編碼的問題,還可以使用django-rest-framework框架,Django REST框架是一個功能強大且靈活的構建Web api工具包
使用 HttpResponse ,不推薦
# urls.py
path('api/json',views.injson),# views.py
def injson(request):# 這里的info是手寫的假數據,若使用這種方法可以從數據庫中獲取相應數據再用json.dumps序列化info = [{'評論者':'zhangsan','評論日期':'2019-6-5','評論時間':'17:47:23','評論內容':'hahahha','對應文章_id':'1'},{'評論者':'zhangsan1','評論日期':'2019-6-5','評論時間':'17:48:23','評論內容':'hahffahha','對應文章_id':'1'},]return HttpResponse(json.dumps(info))
使用REST框架
先要安裝這個包以及依賴項
pip install djangorestframework
pip install markdown # Markdown support for the browsable API.
pip install django-filter # Filtering support
其次需要在setting.py中配置app
INSTALLED_APPS = ['django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles','myblog',# 這個就是REST依賴項'rest_framework'
]
然后就要寫api了,先把評論的model放出來
class 評論(models.Model):評論者=models.CharField(max_length=20)評論日期=models.DateField(auto_now_add=True)評論時間=models.TimeField(auto_now_add=True)評論內容=models.TextField()對應文章=models.ForeignKey('myblog.文章內容',on_delete=models.CASCADE)
然后編寫api.py
# api.py# 引入model
from .models import 評論
# REST提供的序列化工具
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.decorators import api_view
Resonse
類似于HttpResponse,用來渲染文本內容,并根據內容決定返回給用戶的數據類型
Response(data, status=None, template_name=None, headers=None, content_type=None)# data:要渲染的數據,可以是python的基本數據類型
# status:狀態碼
# template_name:模板名稱
# headers:頭部信息
# content_type:內容類型的響應
因為Response只能渲染python基本數據類型,對于復雜的數據類型,需要serializers.ModelSerializer來序列化
# api.pyclass PingLun(serializers.ModelSerializer):class Meta:depth = 1model = 評論fields = ('評論者','評論日期','評論內容')
然后就可以寫url對應的回調函數了,可以不使用api_view修飾器,但需要自己寫一個判斷來判斷請求的類型
@api_view(['GET'])
def showdata(request):id = request.GET['id']print(id)datas=評論.objects.filter(對應文章_id=id)PingLunData = PingLun(datas,many=True)return Response({'data':PingLunData.data})
這時候訪問api就可以看到優雅的數據了,完了以后完善Vue,編寫reqwest的內容
let vue = new Vue({el : "#app",data : {// 開始是一個空列表pinglun : [],},mounted(){console.log("賣個萌咋了!!!(>人<;)")this.getData()},computed : {},methods : {getData : function() {// 現在的this是window對象,等進入reqwest,this就是rewqest對象了,所以提前保存thislet self = this// 只是為了獲取當前文章的idlet myurl = window.location.hreflet id = myurl.toString().split("/").pop()reqwest({url: '/blog/api/showpinglun/?format=json', method: 'get', data: [{name: 'id',value: id}], success: function (data) {self._data.pinglun = data.data}})},}
})
到目前,就可以使用Vue從數據庫中獲取數據并渲染到前端了,總結一下:
- 要用Vue渲染數據,數據就必須在data中,但我們又不能寫死,必須從一個地方動態獲取數據
- 這個地方就是api,django有一個模塊REST專門用來建立api
- 要動態請求數據,需要用到一個框架 reqwest
- REST渲染數據用到了Resonse,但它只能渲染python基本數據,從Object.filter()得到的顯然不是,因此還要序列化數據,這里用到了serializers
- 另外,還需要api_view這個裝飾器判斷請求類型
提交評論
思路:
- 使用POST請求
- 把表單內容交給api,api再保存到數據庫
看著挺簡單,但這里面有兩個問題:
- Django要求所有POST請求進行CSRF驗證,使用正常的表單我們可以添加{{csrf_token}},Django會自動在Cookies中添加csrf驗證用的隨機序列,用reqwest怎么辦
- 一般情況下提交評論后評論會立刻顯示在下面,怎么做
解決Ajax發送POST請求的CSRF問題
這里有兩種思路
思路一:解決發現問題的人
這種思路簡單粗暴,既然問題出在了csrf驗證上,那就不讓他進行驗證就好了嘛,組織進行驗證有兩個簡單的辦法
使用裝飾器
在要取消進行csrf驗證的視圖函數上添加修飾器@csrf_exempt
from django.views.decorators.csrf import csrf_exempt
@csrf_exempt
def demo(request):pass
趕盡殺絕法
第二種是狼人的做法,比較絕,直接從setting中刪除csrf驗證的依賴項
MIDDLEWARE = ['django.middleware.security.SecurityMiddleware','django.contrib.sessions.middleware.SessionMiddleware','django.middleware.common.CommonMiddleware',# 就是這個,刪了就ok,但安全性嘛,就。。。。'django.middleware.csrf.CsrfViewMiddleware','django.contrib.auth.middleware.AuthenticationMiddleware','django.contrib.messages.middleware.MessageMiddleware','django.middleware.clickjacking.XFrameOptionsMiddleware',
]
思路二:釜底抽薪
思路一實現簡單,一勞永逸,看似不錯,但取消csrf驗證會讓網站處于很危險的境地,建議不要這樣用,第二種方法就要優雅很多,首先要知道Django是怎樣防御CSRF攻擊的,CSRF,跨站請求偽造攻擊,是攻擊者利用用戶登錄保存的cookies偽裝成用戶進行非發操作的攻擊方式,比如攻擊者在某網站留下了一個付款的鏈接www.xxxx.com/shop?money=500;to=hark(注意,這個鏈接已經設計了用戶驗證,只有正確登錄后才能付款,沒登錄直接訪問這個鏈接會被重定向到登錄界面),一個受害者在訪問這個釣魚鏈接之前正好訪問過付款的那個網站,并登錄留下了自己的cookies,這時候她再去訪問那個釣魚鏈接,瀏覽器就會檢查本地有沒有對應的cookies文件,正好有,系統就認為是他本人在付款,這就是一次csrf攻擊,csrf的特點是攻擊者并沒有拿到受害人的cookies,針對這個特點,django的處理辦法是在cookies中增加一個csrf_token字段,內容為隨機序列,同時表單提交時也把這個序列作為表單的一項同表單數據一起提交給后端做驗證,如果表單中的序列與cookies中的序列不一樣,就定義為csrf攻擊,在Debug模式下會拋出403錯誤。
根據這個,我們只要在Ajax的請求頭中加上cookies中的那個字段就可以了嘛,其實如果不懂csrf,直接在瀏覽器開發者工具里對比我們的Ajax請求頭和正常的POST請求頭就會發現我們少了X-CSRFToken這個字段,獲取本站cookies中的csrftoken字段,添加到請求頭中就可以。其實對比發現我們還缺了一項,不寫會報500錯誤,Content-Type
setRequestHeader必須寫在open之后
// js獲取cookies依賴下面的庫
<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>// 獲取cookies
let csrftoken = Cookies.get('csrftoken');// 設置請求頭
XHRObject.setRequestHeader("X-CSRFToken", csrftoken);
Ajax發送POST請求
<div id="app"><div class="alert alert-primary" role="alert"><p>評論<<</p><hr /><div class="form-group"><label for="exampleFormControlInput1">評論者:</label><input type="text" class="form-control" id="exampleFormControlInput1" placeholder="請輸入你的姓名" name="評論者" maxlength="20" required=""></div><div class="form-group"><label for="exampleFormControlTextarea1">有問題?不妨寫下了...</label><textarea class="form-control" name="評論內容" id="exampleFormControlTextarea1" rows="3"></textarea></div><hr><button type="submit" name="評論提交" οnclick="XMLDoc()"">提交評論</button></div>
function XMLDoc(){let XHRObject// 適配瀏覽器if(window.XMLHttpRequest){XHRObject = new XMLHttpRequest}else{XHRObject =new ActiveXObject("Microsoft.XMLHTTP")}// 接收XHRObject.onreadystatechange = function () {if (XHRObject.status == 200 & XHRObject.readyState == 4) {}}// 獲取文章idlet url = window.location.hreflet id = url.toString().split("/").pop()// 獲取csrftokenlet csrftoken = Cookies.get('csrftoken');// 獲取表單數據let name = document.getElementById('exampleFormControlInput1').valuelet neirong = document.getElementById('exampleFormControlTextarea1').value// 發送POST請求XHRObject.open("POST","/blog/api/postpinglun/?format=json",true)// 設置請求頭XHRObject.setRequestHeader("X-CSRFToken", csrftoken);XHRObject.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");// 發送請求,只接受一個字符串,鍵值用=連接,多個鍵值對用&連接XHRObject.send('name='+name+'&neirong='+neirong+'&id='+id.toString())document.getElementById('exampleFormControlInput1').value = ""document.getElementById('exampleFormControlTextarea1').value = ""}
api保存數據到數據庫
@api_view(['POST'])
def postdata(request):# 獲取Ajax傳來的表單信息name = request.POST['name']neirong = request.POST['neirong']id = request.POST['id']# 保存到數據庫obj=評論(評論者 = name,評論日期 = datetime.datetime.now().strftime('%Y-%m-%d'),評論時間 = datetime.datetime.now().strftime('%H:%M:%S'),評論內容 = neirong,對應文章_id = id)obj.save()
提交數據時更新下方評論列表
要在提交時更新,就要綁定兩個單擊事件,一個是Ajax的,用來保存數據,另一個是Vue的,用來更新數據,這里可以直接調用之前的getData函數
<button type="submit" name="評論提交" onclick="XMLDoc()" @click="getData()">提交評論</button>