假設你的博客已經順利部署到了線上。你寫了很多好文章,和粉絲們互動並感受成就感。
現在你想更進一步,努力提高文章質量,使其更受讀者歡迎,打造圈內一流博客。問題是該如何判斷一篇文章是“受歡迎的”?靠瀏覽量是個方法,但是並不能區分出內容花拳繡腿的標題黨。靠評論數也是個好方法,但個人博客通常讀者不多,好文章零評論是很正常的。
這時候**“點贊”**功能就顯得重要了。如果大部分讀者都給了一個贊,那就表明文章確實還不錯。
動手之前的思考
點贊功能可不簡單,實現途徑非常的多。別急着動手,耐心思考:我們的博客到底需要什麼樣的點贊?
**首先,點贊是否要求用戶必須登錄?**要求登錄的好處是可以精確的記錄是哪些用戶、對哪些文章點過贊(多對多關係),以便進行細緻的數據分析。壞處是登錄這個要求很笨重,會屏蔽掉大部分的遊客用戶。博主傾向於不要求用戶登錄,畢竟小站通常用戶就不多,提高參與度纔是點贊最核心的任務。
如果某天你的小站火了,就把要求用戶登錄的交互功能讓給“收藏”吧!
**其次,用戶是否可以重複點贊?**很多視頻平臺的用戶可以對某個喜歡的女主播瘋狂點贊,以表達自己非常非常的喜歡。這對用戶較多的平臺是沒問題的,因爲用戶數量多了之後,你點幾百個贊也只是九牛一毛。但博客網站這樣做很容易造成某些文章點贊爲零,某些文章點贊數又出奇的高。顯然這不代表文章質量的差異。
好了,目前我們的策略是不要求用戶登錄,也不允許用戶重複點贊。**下一個問題是,在哪裏記錄用戶的點贊相關的數據呢?**點贊數量毫無疑問要保存在數據庫裏,以便隨時取出數據並呈現出來。
但問題是**校驗用戶是否已點讚的記錄保存在哪?**在數據庫中記錄用戶的IP地址是個方法,但你得處理好記錄IP和記錄登錄用戶的關係,稍微有點麻煩。另外每次用戶的點贊都需要向後端發送校驗請求,增加了服務器的負擔。
既然數據保存在後端數據庫裏不好,**那能不能保存在瀏覽器端呢?**答案是可以的,並且有 Cookie 和 LocalStorage 都可以讓你保存數據。它兩的主要區別如下:
特性 | Cookie | LocalStorage |
---|---|---|
生命週期 | 可設置失效時間,默認是關閉瀏覽器後失效 | 除非被清除,否則永久保存 |
存儲空間 | 4K左右 | 一般爲5MB |
與服務器通信 | 每次都會攜帶在HTTP頭中 | 不參與服務器的通信 |
易用性 | 源生接口不友好 | 源生接口可以接受 |
比較下來會發現 LocalStorage 可以永久保存數據,存儲空間大,也不參與服務器通信,很適合點讚的需求。由於數據保存在瀏覽器中,所以也不需要區分用戶有沒有登錄了:實際上每次請求點贊時,校驗的是當前這個瀏覽器是否已經點過讚了,而不是用戶!
可能你會反駁說,那要是用戶換一個瀏覽器不就可以重複點讚了嗎,更何況瀏覽器端的數據是非常容易篡改的。但這又有什麼關係呢?點贊數據並不需要非常精確,隨他去吧。
所有的現代瀏覽器都支持 LocalStorage 功能。如果你還在用 IE6 ,趕緊考慮升級瀏覽器吧。
總結一下,我們的點贊功能如下:
- 不要求用戶登錄
- 不允許重複點贊
- 點贊數保存在服務器數據庫中
- 點贊校驗數據保存在瀏覽器的 LocalStorage 中
當用戶點贊時,前端腳本會在 LocalStorage 裏校驗是否已經點過讚了;如未點過贊,纔會向服務器發送點贊請求,並記錄數據。
想清楚需求,難題就迎刃而解了。接下來就是代碼實現。
需要說明的是,以上分析並不代表其他方法不好,僅僅是在博客小站的環境下,博主覺得合適的技術路徑而已。如果你心中住着另一個哈姆雷特,請想辦法去實現它。
代碼實現
準備工作
本章的重點工作在前端,因此先把簡單的後端代碼寫了,權當熱身。
有的讀者聽到前端就覺得頭疼。你的痛苦我明白,但也是必不可少的。光寫 Python 是做不出漂亮網站的。
由於點贊數需要保存在數據庫中,因此修改文章模型是必須的了:
article/models.py
...
# 文章模型
class ArticlePost(models.Model):
...
# 新增點贊數統計
likes = models.PositiveIntegerField(default=0)
...
遷移數據:
(env) > python manage.py makemigrations
(env) > python manage.py migrate
繼續用類視圖:
article/views.py
...
# 點贊數 +1
class IncreaseLikesView(View):
def post(self, request, *args, **kwargs):
article = ArticlePost.objects.get(id=kwargs.get('id'))
article.likes += 1
article.save()
return HttpResponse('success')
功能是讓點贊數增加1個,並且返回 success
。至於爲什麼是 success
後面再講。
最後就是路由了:
article/urls.py
...
urlpatterns = [
...
# 點贊 +1
path(
'increase-likes/<int:id>/',
views.IncreaseLikesView.as_view(),
name='increase_likes'
),
]
很簡單吧。剩下的就是專心寫前端代碼了。
JS與Ajax
由於校驗數據保存在瀏覽器中,因此前端的工作較多。
先把完整代碼貼出來(講解在後面):
templates/article/detail.html
...
<!-- 已有代碼,文章正文 -->
<div class="col-12">
<p>{{ article.body|safe }}</p>
</div>
<!-- 新增點贊按鈕 -->
<div style="text-align:center;" class="mt-4">
<button class="btn btn-outline-danger"
type="button"
onclick="validate_is_like(
'{% url 'article:increase_likes' article.id %}',
{{ article.id }},
{{ article.likes }}
)"
>
<span>點贊</span>
<span>
<i class="fas fa-heart"></i>
</span>
<span id="likes_number">
{{ article.likes }}
</span>
</button>
</div>
...
{% block script %}
...
<!-- 以下均爲新代碼 -->
<!-- csrf token -->
<script src="{% static 'csrf.js' %}"></script>
<script>
// 點贊功能主函數
function validate_is_like(url, id, likes) {
// 取出 LocalStorage 中的數據
let storage = window.localStorage;
const storage_str_data = storage.getItem("my_blog_data");
let storage_json_data = JSON.parse(storage_str_data);
// 若數據不存在,則創建空字典
if (!storage_json_data) {
storage_json_data = {}
};
// 檢查當前文章是否已點贊。是則 status = true
const status = check_status(storage_json_data, id);
if (status) {
layer.msg('已經點過讚了喲~');
// 點過贊則立即退出函數
return;
} else {
// 用 Jquery 找到點贊數量,並 +1
$('span#likes_number').text(likes + 1).css('color', '#dc3545');
}
// 用 ajax 向後端發送 post 請求
$.post(
url,
// post 只是爲了做 csrf 校驗,因此數據爲空
{},
function(result) {
if (result === 'success') {
// 嘗試修改點贊數據
try {
storage_json_data[id] = true;
} catch (e) {
window.localStorage.clear();
};
// 將字典轉換爲字符串,以便存儲到 LocalStorage
const d = JSON.stringify(storage_json_data);
// 嘗試存儲點贊數據到 LocalStorage
try {
storage.setItem("my_blog_data", d);
} catch (e) {
// code 22 錯誤表示 LocalStorage 空間滿了
if (e.code === 22) {
window.localStorage.clear();
storage.setItem("my_blog_data", d);
}
};
} else {
layer.msg("與服務器通信失敗..過一會兒再試試唄~");
}
}
);
};
// 輔助點贊主函數,驗證點贊狀態
function check_status(data, id) {
// 嘗試查詢點贊狀態
try {
if (id in data && data[id]) {
return true;
} else {
return false;
}
} catch (e) {
window.localStorage.clear();
return false;
};
};
</script>
{% endblock script %}
代碼內容很多,接下來拆分講解。
<!-- 新增點贊代碼 -->
<div style="text-align:center;" class="mt-4">
<button class="btn btn-outline-danger"
type="button"
onclick="validate_is_like(
'{% url 'article:increase_likes' article.id %}',
{{ article.id }},
{{ article.likes }}
)"
>
<span>點贊</span>
<span>
<i class="fas fa-heart"></i>
</span>
<span id="likes_number">
{{ article.likes }}
</span>
</button>
</div>
上面的 HTML 代碼功能很簡單,提供一個點讚的按鈕,點擊此按鈕時會觸發叫做validate_is_like
的 JavaScript 函數。特別需要注意的是 '{% url 'article:increase_likes' article.id %}'
都是用的單引號,這裏千萬不能用雙引號,原因請讀者思考一下。
<script src="{% static 'csrf.js' %}"></script>
還記得csrf.js嗎?我們在多級評論章節已經將它引入了,目的是讓 Ajax 也能通過 csrf 校驗。如果還沒有這個文件的請點擊鏈接下載。
接下來就是佔據最多版面的函數validate_is_like()
,我們來拆分裏面的內容。
// 取出 LocalStorage 中的數據
let storage = window.localStorage;
const storage_str_data = storage.getItem("my_blog_data");
let storage_json_data = JSON.parse(storage_str_data);
// 若數據不存在,則創建空字典
if (!storage_json_data) {
storage_json_data = {}
};
瀏覽器裏面, window
對象指當前的瀏覽器窗口。它也是當前頁面的頂層對象(即最高一層的對象),所有其他對象都是它的下屬,localStorage
也是如此。
要校驗數據,首先必須取出數據。這裏用localStorage.getItem()
接口取出了數據。
雖然 LocalStorage 的存儲方式爲標準的鍵值對類型(類似Python的字典),但是很怪的是存儲的值只支持字符串類型。所以這裏要用 JSON.parse()
將字符串還原爲對象。
用戶第一次點贊時,LocalStorage 中肯定是沒有任何數據的,所以 if
語句的作用是創建一個空的字典待用。
// 檢查當前文章是否已點贊。是則 status = true
const status = check_status(storage_json_data, id);
if (status) {
layer.msg('已經點過讚了喲~');
// 點過贊則立即退出函數
return;
} else {
// 用 Jquery 找到點贊數量,並 +1
$('span#likes_number').text(likes + 1).css('color', '#dc3545');
}
接下來馬上調用函數 check_status
檢查用戶是否已經對本文點過讚了。如果點過了,就彈窗提示,並且用 return
馬上終止 validate_is_like
函數,後面的代碼就不執行了;如果還沒點過,就讓按鈕的點贊數 +1。
但這時候其實後臺數據庫的點贊數並沒有更新。接着往下看。
// 用 ajax 向後端發送 post 請求
$.post(
url,
// post 只是爲了做 csrf 校驗,因此數據爲空
{},
function(result) {
if (result === 'success') {
// 嘗試修改點贊數據
try {
storage_json_data[id] = true;
} catch (e) {
window.localStorage.clear();
};
const d = JSON.stringify(storage_json_data);
// 嘗試存儲點贊數據到 LocalStorage
try {
storage.setItem("my_blog_data", d);
} catch (e) {
// code 22 錯誤表示 LocalStorage 空間滿了
if (e.code === 22) {
window.localStorage.clear();
storage.setItem("my_blog_data", d);
}
};
} else {
layer.msg("與服務器通信失敗..過一會兒再試試唄~");
}
}
);
這裏開始嘗試與後端通信並更新點贊數。整塊代碼被 $.post()
包裹,它其實就是 Ajax 的 post 請求。function(result) {...}
是請求成功時才執行的回調函數,參數 result
是後端的返回值。如果通信成功,則嘗試將點讚的校驗數據保存到 LocalStorage 中。期間發生任何錯誤(特別是 LocalStorage 存儲已滿的錯誤),都會清除 LocalStorage 中的所有數據,以便後續的數據記錄。
可以看出,博主採用的數據結構比較簡單,像這樣:
{
2: true,
31: true
...
}
鍵代表文章的 id
,布爾值代表點讚的狀態。上面數據的意思是 id
爲 2 和 31 的文章已經點過讚了。讀者以後可能會希望文章、評論以及其他內容都可以點贊,那就需要設計更加複雜的數據結構。
// 輔助點贊主函數,驗證點贊狀態
function check_status(data, id) {
// 嘗試查詢點贊狀態
try {
if (id in data && data[id]) {
return true;
} else {
return false;
}
} catch (e) {
window.localStorage.clear();
return false;
};
};
至於 check_status()
函數就很簡單了,作用是查詢是否已經點過讚了,是則返回 true,否則返回 false。
整個 JavaScript 腳本就完成了。
調試接口
讀者在調試時可能會出現各種問題,請按 Ctrl + Shift + I
打開瀏覽器控制檯的 Console
界面,利用以下命令 debug:
- localStorage:查看 LocalStorage 的數據
- localStorage.clear():清除所有數據
- localStorage.getItem():獲取某個數據
- localStorage.setItem():保存某個數據
測試
代碼講完了,接下來就打開文章詳情頁面測試一下:
點擊點贊按鈕,點贊數 +1;再次點擊點贊按鈕,點贊數不會增加,並且會彈窗提示用戶已經點過了。
你可以隨意嘗試關閉頁面或瀏覽器,保存的點贊校驗數據是不會消失的。
這樣就完成了一個簡單的點贊功能。
當然還可以繼續往下優化:
- 沒點讚的愛心應該顯示爲灰色,點過的顯示爲紅色,這樣才人性化
- 最好再來一點酷炫的點贊動畫,或者提示性文字
- 要不要給被點贊人發一條通知信息呢?
- …
教程篇幅有限,不打算再深入下去了,就當做讀者朋友的課後作業吧,要用心完成哦。給你點贊!
第一條的提示:初始加載頁面時,愛心統一顯示爲灰色,然後調用 JavaScript 腳本比對 LocalStorage 中的數據,靈活運用 Jquery ,將點過讚的愛心顏色修改爲紅色。
總結
我們的博客項目現在擁有了層次分明的用戶交互結構:瀏覽量數據最輕巧,評價文章類型的受歡迎度;點贊數據比較平衡,評價文章內容的受歡迎度;評論數據最笨重,但價值也最高。讀者以後在開發功能的時候,也要像這樣把核心需求想清楚才行。
另一個需要提出的是,只有非敏感、不重要的數據才保存在 LocalStorage,不要對它太過依賴。
再一次提醒,教程爲了便於講解,代碼文件已經變得越來越龐大。請在適當的時候把它分割成多個更小的組件,方便維護和重用。
- 有疑問請在杜賽的個人網站留言,我會盡快回復。
- 或Email私信我:[email protected]
- 項目完整代碼:Django_blog_tutorial