目錄
1.新增購物車+驗證登入狀態
1.新增購物車的整體思想
使用者在課程詳情頁面點選了加入購物車:
拿到當前課程的課程id 到資料庫把課程id所對應的資訊(需要在購物車顯示的)加工成一個字典,然後json序列化成字串儲存到redis中
如何實現點選新增購物車,將購物車資料新增到redis中??????
2.新增購物車-後端介面
1.建立一個cart應用,並配置INSTALLAPP
python3 ../../ manage.py startapp cart
2.總路由中新增cart
# lyapi/urls.py path('cart/', include("cart.urls") ),
3.cart/urls.py
from django.urls import path,re_path from . import views urlpatterns = [ path('add_cart/', views.AddCartView.as_view({'post':'add'})) ]
4.cart/views.py
from rest_framework.viewsets import Viewset from django_redis import get_redis_connection from course import models from rest_framework.response import Response class AddCartView(ViewSet): def add(self,request): course_id = request.data.get('course_id') user_id = 1 # 先把使用者id寫死 # 去redis裡存資料 conn = get_redis_connection('cart') # 校驗一下課程id是否合法 try: models.Course.objects.get(id=course_id) except: return Response({'msg':'課程不存在'},status=400) '''選擇用集合的資料型別去儲存''' conn.sadd('cart_%s' % user_id,course_id) # vheader右方的購物車小紅數字顯示 cart_length = conn.scard('cart_%s' % user_id) # 獲取商品數量 return Response({'msg':'新增成功','cart_length',cart_length})
5.單獨給購物車使用一個redis庫
# dev.py CACHES = { ...... "cart":{ "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/3", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, } }
3.新增購物車-前端
前端點選新增購物車,向後端傳送請求
<!-- html --> <div class="add-cart" @click="addCart"><img src="/static/img/cart-yellow.svg" alt="">加入購物車</div>
注意:新增購物車要驗證使用者是否已經登入
所以現在我們需要將兩個請求變為同步請求,讓token驗證之後再進行別的操作
非同步改成同步還是比較麻煩的,所以我們直接將驗證token的操作寫在addCart新增購物車方法裡
// js addCart(){ // 獲取前端儲存的token值 let token = localStorage.token || sessionStorage.token; // 如果token值存在 if (token){ // 驗證token this.$axios.post(`${this.$settings.Host}/users/verify/`,{ token:token, }).then((res)=>{ // 驗證通過,可以新增購物車 this.$axios.post(`${this.$settings.Host}/cart/add_cart/`,{ // 獲取課程id course_id:this.course_id, }).then((res)=>{ // 新增購物車成功,列印新增成功的資訊 this.$message.success(res.data.msg); }) // 驗證沒有通過(token錯誤或者token過期) 提示使用者讓使用者去登入 }).catch((error)=>{ this.$confirm('您還沒有登入!!!?', '31s', { confirmButtonText: '去登入', cancelButtonText: '取消', type: 'warning' }).then(() => { this.$router.push('/user/login'); }) // 將過期的token清理掉 sessionStorage.removeItem('token'); sessionStorage.removeItem('username'); sessionStorage.removeItem('id'); localStorage.removeItem('token'); localStorage.removeItem('username'); localStorage.removeItem('id'); }) } // token獲取不到 else { this.$confirm('您還沒有登入!!!?', '31s', { confirmButtonText: '去登入', cancelButtonText: '取消', type: 'warning' }).then(() => { this.$router.push('/user/login'); }) } },
我們在後端已經將購物車的長度返回了,在前端我們就可以拿到購物車的長度
// Detail.vue this.$axios.post(`${this.$settings.Host}/cart/add_cart/`,{ // 獲取課程id course_id:this.course_id, }).then((res)=>{ // 新增購物車成功,列印新增成功的資訊 this.$message.success(res.data.msg); // 獲取到後端傳送過來的購物車長度 this.cart_length = res.data.cart_length })
第一種思路:vheader元件是detail元件的子元件,我們可以通過vue的父子傳值來實現。
那問題就來了
如果我們訪問實戰課頁面 也就是/course,在course元件是不也要顯示那個購物車小紅圓圈?
但是在course元件我們根本就沒有去獲取購物車的長度。所以紅圓圈數字根本顯示不出來。所以父子傳值這個思路行不通。
3.Vuex
因為對於一些資料,需要在多個元件中即時共享,所以根據上述的問題,我們引出vuex
1.安裝Vuex
npm install -S vuex
2.把vuex註冊到vue中
在src目錄下建立store目錄,並在store目錄下建立一個index.js檔案,index.js檔案程式碼
// store/index.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { // 資料倉儲,類似vue裡面的data cart_length: 0 // 購物車資料 }, mutations: { // 資料操作方法,類似vue裡面的methods add_cart (state, cart_length) { state.cart_length = cart_length; // 修改購物車的商品總數 } } })
3.掛載store物件
把上面index.js中建立的store物件註冊到main.js的vue中。
// main.js import Vue from 'vue' import App from './App' import router from './router' import store from './store'; // 引入 new Vue({ el: '#app', router, store, // 掛載 components: { App }, template: '<App/>' })
4.Vheader元件讀取store的資料(讀取購物車長度)
在Vheader.vue頭部元件中,直接就可以讀取store裡面的資料
<router-link to="/"> <b>{{$store.state.cart_length}}</b> <img src="@/assets/shopcart.png" alt=""> <span>購物車 </span> </router-link>
5.Detail元件修改store的資料(修改購物車長度)
當使用者點選新增購物車時,觸發addCart中的post方法,將購物車的長度進行修改
this.$axios.post(`${this.$settings.Host}/cart/add_cart/`,{ course_id:this.course_id, }).then((res)=>{ this.$message.success(res.data.msg); // 從後端獲取到的購物車長度不存在當前元件的資料屬性中了,而是存到vuex中 this.$store.commit('add_cart', res.data.cart_length) ; // commit用來觸發mutation中宣告的方法 })
6.關於頁面重新整理,vuex資料丟失問題
解決方式:使用者點選重新整理時,我們可以監聽使用者重新整理的這個動作,可以在重新整理之前對頁面做一些動作。
當點選重新整理時,我們先把資料存到sessionStorage或localStorage中,
頁面重新整理完成之後,再把資料取回來放到vuex中。這樣的話就可以做到頁面重新整理了,資料也沒有丟。
1.點選重新整理,將資料存到sessionStorage中
// app.vue <script> export default { name: 'App', created() { // 頁面重新整理之前把cart_length資料存到了sessionStorage中 window.addEventListener('beforeunload',()=>{ console.log('頁面要重新整理啦!!!,趕緊儲存資料!!!!'); sessionStorage.setItem('cart_length',this.$store.state.cart_length); }) } } </script>
2..頁面重新整理完成之後,將資料從sessionStorage取出來放到vuex中
// vheader.vue created() { if (this.$store.state.cart_length === 0) { let cart_length = sessionStorage.getItem('cart_length'); this.$store.commit('add_cart', cart_length); } },
7.關於redis的異常捕獲
為了保證系統的日誌記錄可以跟進redis部分的,我們還可以在之前自定義異常處理中增加關於 redis的異常捕獲
# utils/exceptions.py from rest_framework.views import exception_handler from django.db import DatabaseError from rest_framework.response import Response from rest_framework import status from redis import RedisError # 引入redis異常 import logging logger = logging.getLogger('django') def custom_exception_handler(exc, context): """ 自定義異常處理 :param exc: 異常類 :param context: 丟擲異常的上下文 :return: Response響應物件 """ # 呼叫drf框架原生的異常處理方法 response = exception_handler(exc, context) if response is None: view = context['view'] # 錯誤出現的那個函式或者方法 if isinstance(exc, DatabaseError) or isinstance(exc, RedisError): # 資料庫異常/redis異常 logger.error('[%s] %s' % (view, exc)) response = Response({'message': '伺服器內部錯誤'}, status=status.HTTP_507_INSUFFICIENT_STORAGE) return response
4.購物車頁面展示-後端介面
1.新增購物車-課程有效期
2.新增購物車時將有效期也存到redis中:之前的redis資料儲存結構是集合,但是現在集合已經滿足不了我們的需求了。要使用雜湊資料型別儲存。
雜湊資料型別結構如下所示:
''' user_id:{ course_id:expire, course_id:expire, } '''
# cart/views.py class AddCartView(ViewSet): def add(self,request): ...... expire = 0 # 有效期:表示永久有效 '''存使用者對應的課程id和有效期''' conn.hset('cart_%s' % user_id,course_id,expire) '''存放使用者的購物車長度''' cart_length = conn.hlen('cart_%s' % user_id) ......
在上面的程式碼中,我們可以看到一共建立了兩次conn連線,這樣並不是很好,所以我們藉助一個redis的管道pipe
pipe = conn.pipeline() # 建立管道 pipe.multi() # 將下面兩個指令放到管道里面 '''存使用者對應的課程id和有效期''' pipe.hset('cart_%s' % user_id,course_id,expire) '''存放使用者的購物車長度''' cart_length = pipe.hlen('cart_%s' % user_id) pipe.execute() # 執行上面兩條指令
2.購物車列表-後端介面
class AddCartView(ViewSet): def cart_list(self,request): user_id = 1 # 使用者id先寫死 conn = get_redis_connection('cart') # 獲取cart對應的redis庫物件 # 將當前使用者所對應的課程id從redis中取出來 ret = conn.hgetall('cart_%s' % user_id) # 封裝成了字典{課程id,有效期},dict {b'1': b'0', b'2': b'0'} cart_data_list = [] try: for cid, eid in ret.items():# cid:課程id eid:有效期 '''redis中存的是位元組 所以要解碼''' course_id = cid.decode('utf-8') expire_id = eid.decode('utf-8') course_obj = models.Course.objects.get(id=course_id) ''' 前端所需要的購物車資料包括 1.課程名稱 2.課程封面圖 3.課程價格 4.課程有效期 so 我們自己建立一個資料結構去儲存前端所需要的內容 ''' cart_data_list.append({ 'name':course_obj.name, 'course_img':contains.SERVER_ADDR + course_obj.course_img.url , # 圖片路徑是相對路徑,我們將其變為絕對路徑 'price':course_obj.price, 'expire_id':expire_id }) except Exception: logger.error('獲取購物車資料失敗') return Response({'msg':'後臺資料庫出問題了,請聯絡管理員'},status=status.HTTP_507_INSUFFICIENT_STORAGE) # 將資料響應給前端 return Response({'msg':'xxx','cart_data_list':cart_data_list})
5.購物車頁面展示-前端
1.購物車前端的初始介面
...
2.將cart元件註冊到路由上
import Vue from 'vue' import Cart from '@/components/Cart' Vue.use(Router) export default new Router({ mode:'history', routes: [ { path: '/cart/', component: Cart }, ] })
3.關於cart元件和cartitem元件
在購物車頁面中,整個購物車是一個元件(cart元件),然後要展示的每條購物車資料又是一個子元件(cartitem元件)
<!-- cart.vue html部分 --> <div class="cart_course_list"> <CartItem v-for="(value,index) in cart_data_list" :key="index" :cart="value"></CartItem> <!-- 001 :cart 父元件往子元件傳值 --> </div>
// cart.vue js部分 <script> import CartItem from "./common/CartItem" export default { name: "Cart", data(){ return { cart_data_list:[], } }, methods:{ }, created() { let token = sessionStorage.token || localStorage.token; if (token){ this.$axios.get(`${this.$settings.Host}/cart/add_cart/`) // 獲取購物車資料 .then((res)=>{ this.cart_data_list = res.data.cart_data_list }) .catch((error)=>{ this.$message.error(error.response.data.msg); }) }else { this.$router.push('/user/login'); } }, components:{ CartItem, } } </script>
父元件拿著自己的值 cart_data_list 傳遞給每個子元件進行渲染(父元件往子元件傳值)
// cartitem.vue <template> <div class="cart_item"> <div class="cart_column column_1"> <el-checkbox class="my_el_checkbox" v-model="checked"></el-checkbox> </div> <div class="cart_column column_2"> <img :src="cart.course_img" alt=""> <span><router-link to="/course/detail/1">{{cart.name}}</router-link></span> </div> <div class="cart_column column_3"> <el-select v-model="cart.expire_id" size="mini" placeholder="請選擇購買有效期" class="my_el_select"> <el-option label="1個月有效" value="30" key="30"></el-option> <el-option label="2個月有效" value="60" key="60"></el-option> <el-option label="3個月有效" value="90" key="90"></el-option> <el-option label="永久有效" value="0" key="0"></el-option> </el-select> </div> <div class="cart_column column_4">¥{{cart.price}}</div> <div class="cart_column column_4">刪除</div> </div> </template> <script> export default { name: "CartItem", data(){ return { checked:false, } }, props:['cart', ] // 002:子元件接受父元件傳過來的值 } </script>
1.頁面重新整理導致的vuex資料重置
解決方法:頁面重新整理前將資料存到SessionStorage
// App.vue <script> export default { name: 'App', created() { window.addEventListener('beforeunload',()=>{ sessionStorage.setItem('cart_length',this.$store.state.cart_length); }) } } </script>
2.不同頁面顯示的購物車小紅圓圈數量不一致
在元件載入的時候,會執行vheader中的created方法,拿到sessionStorage的值
let cart_length = sessionStorage.getItem('cart_length'); this.$store.commit('add_cart',cart_length);
其中有一點:只有頁面重新整理的時候,才會拿到sessionStorage的值存放到vuex中,
如果頁面不重新整理,使用者點選新增購物車(此時vuex存的購物車長度因為新增購物操作已經發生了變化),
我們元件再載入時,如果拿的是sessionStorage的值,其實拿的還是原來的那個值。
我們應該把拿值操作放到重新整理頁面之後
created(){ if (this.$store.state.cart_length === 0){ // 如果購物車沒有資料 let cart_length = sessionStorage.getItem('cart_length'); // 就去sessionStorage中拿資料 this.$store.commit('add_cart',cart_length); // 並將資料存放到vuex中 } },