day83:luffy:新增購物車&導航欄購物車數字顯示&購物車頁面展示

Poke發表於2020-11-09

目錄

1.新增購物車+驗證登入狀態

2.右上方購物車圖示的小紅圓圈數字

3.Vuex

4.購物車頁面展示-後端介面

5.購物車頁面展示-前端

6.解決一個購物車數量顯示混亂的bug

1.新增購物車+驗證登入狀態

1.新增購物車的整體思想

購物車資料要存到redis中的:要存使用者id,課程id

使用者在課程詳情頁面點選了加入購物車:

拿到當前課程的課程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>

注意:新增購物車要驗證使用者是否已經登入

axios傳送的是非同步請求,setting.js裡的check_login()函式和Detail.vue中的addCart是同步執行的,但是addCart新增購物車時需要驗證登入狀態的,

會出現:我這邊還沒有驗證token呢,新增購物車那邊就已經執行到檢測token的程式碼位置了。

所以現在我們需要將兩個請求變為同步請求,讓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');
              })
        }



      },

2.右上方購物車圖示的小紅圓圈數字

我們在後端已經將購物車的長度返回了,在前端我們就可以拿到購物車的長度

// 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
              })

現在就會有一個問題,我們這個cart_length資料屬性是在Detail元件裡面的,但是那個右上方購物車小紅圓圈是在vheader元件裡面的。不同元件之間的資料不是互通的

第一種思路: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資料丟失問題

問題:vuex中的資料是存放在記憶體中的,頁面一重新整理,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的異常捕獲

day83:luffy:新增購物車&導航欄購物車數字顯示&購物車頁面展示
# 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
redis的異常捕獲以及記錄錯誤日誌

4.購物車頁面展示-後端介面

1.新增購物車-課程有效期

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>

6.解決一個購物車數量顯示混亂的bug

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中
    }
  },

 

相關文章