尚品汇项目笔记(持续更新中)

项目网络教学视频链接:尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili

目录

一、 使用vue-cli脚手架去初始化项目

 二、项目的其他配置

三、项目路由分析

四、创建Header和Footer非路由组件

五、完成路由组件的搭建

六、利用【路由元信息】实现显示或隐藏组件

七、路由传递参数

八、重写push和replace方法

 九、Home首页组件拆分业务分析

 十、完成三级联动全局组件

 十一、Home首页拆分静态组件

 十二、使用【POSTMAN工具】测试接口       

十三、对axios进行二次封装

十四、接口统一管理

十五、nprogress进度条的使用

十六、VUEX模块式开发

十七、动态展示三级联动

十八、三级联动动态背景颜色

十九、通过JS控制二三级分类的显示与隐藏

二十、引入防抖与节流

二十一、三级联动路由跳转分析

二十二、实现三级联动的路由跳转与传递参数

二十三、Search模块商品分类与过渡动画

二十四、TypeNav商品分类列表的优化

二十五、合并params和query参数

二十六、mock.js模拟数据

二十七、获取Banner轮播图数据

二十八、swiper基本使用

二十九、Banner实现轮播图(第一种解决方案)

三十、轮播图:watch+nextTick( )(第二种解决方案)

三十一、获取floor组件mooc数据

三十二、动态展示Floor组件

三十三、共用组件Carsouel(轮播图)

三十四、Search模块的静态组件

三十五、Search模块的VUEX操作

三十六、Search模块动态展示产品列表

三十七、Search模块根据不同的参数进行数据展示

三十七、Search模块中子组件动态开发

三十八、监听路由的变化再次发请求获取数据

三十九、面包屑处理分类的操作

四十、面包屑处理关键字

四十一、面包屑处理品牌信息

四十二、平台售卖属性的操作

四十三、排序操作

四十四、分页器静态组件

四十五、分页功能分析

四十六、分页器动态展示

四十七、分页器完成

四十八、滚动行为

四十九、获取产品详情数据

五十、产品详情数据动态展示

五十一、zoom放大镜展示数据

五十二、detail路由组件展示产品售卖属性

五十二、产品售卖属性值排他操作

五十三、放大镜操作

五十四、购买产品个数的操作

五十五、加入购物车

五十六、路由传递参数结合会话存储

五十七、购物车静态组件与修改

五十八、uuid游客身份获取购物车数据

五十九、购物车动态展示数据

六十、处理商品数量

六十一、删除购物车产品的操作

六十二、修改产品勾选状态

六十三、删除全部选中的商品

六十四、全部产品的勾选状态修改

六十五、注册业务实现

六十六、登录业务


一、 使用vue-cli脚手架去初始化项目

准备:提前安装好node、webpack、淘宝镜像(最好有)

1. 找到文件夹目录,输入cmd,出现下面内容,输入“ vue create 项目名”,回车确认

2. 选择vue2版本,创建好之后,使用VS打开该文件夹。

3. 分析目录组成

 二、项目的其他配置

1. 如何让浏览器自动打开这个项目?找到package.json这个文件,找到 "serve": "vue-cli-service serve",将其改为"serve": "vue-cli-service serve --open",如图所示:

2. 关闭eslint校验功能:在根目录下,创建一个vue.config.js文件。配置以下内容:

module.exports = {
  //关闭校验工具
  lintOnSave:false,
}

3. 设置src文件夹简写方式,配置别名:@。在根目录下,创建jsconfig.json文件,配置以下内容:

{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "baseUrl": "./",
    "moduleResolution": "node",
    "paths": {
      "@/*": [
        "src/*"
      ]
    },
    "lib": [
      "esnext",
      "dom",
      "dom.iterable",
      "scripthost"
    ]
  }
}

三、项目路由分析

前端路由:类似于【key--value键值对】的形式,其中key表示URL,value表示对应的路由组件

这里需要使用vue-router来实现。

项目结构主要分为上、中、下三部分

        路由组件包括:Home首页路由组件、Search搜索路由组件、login登录路由组件、注册路由组件

        非路由组件包括:Header组件【首页、搜索、登录、注册】、Footer组件【在首页、搜索页】

四、创建Header和Footer非路由组件

(1)在开发项目的时候:1. 书写静态页面(HTML+CSS)

                                 2. 拆分组件

                                 3. 获取服务器的数据动态展示

                                 4. 完成相应的动态业务逻辑

(2)那么非路由组件创建在哪里?在src文件夹下创建components文件夹,在该文件夹中分别创建Header和Footer文件夹,用于实现非路由组件。

(在创建组件时,需要注意三要素:组件结构+组件的样式+图片资源)

(3)在非路由组件文件夹中,创建vue类型的文件:index.vue

对于样式,如果采用的是less样式,浏览器不能识别less样式,需要通过less、less-loader进行处理,把less样式变为css样式,这样浏览器才能识别。

1. 先安装less-loader依赖(这里需要注意,版本不能过高,否则不能使用,这里选择5版本,如果不说明默认是最高版本)

2. 还需要在style标签的身上加上lang=lees

对于图片资源,在非路由组件文件中创建一个images文件夹,用于存放数据

(4)当组件创建好之后,就要使用该组件了,步骤为:引入----注册----使用

五、完成路由组件的搭建

(1)安装vue-router插件

 (2)通过上面分析,路由组件应该有四个:Home、Search、Login、Register,

那么路由组件通常创建在哪里呢?在src文件夹下创建pages文件夹,在该文件夹中分别创建Home、Search、Login、Register文件夹,用于实现路由组件。

(3)配置路由

在src文件夹下创建router文件夹,在该文件夹中创建一个index.js文件,用来配置路由信息

配置路由的时候,还要实现【重定向】,即在项目跑起来的时候,当访问 / 时,会立马定位到首页

 (4)接着,在main.js文件中【引入路由】和【注册路由】

     PS:当这里书写router的时候,不管是路由组件还是非路由组件,身上都拥有$route、$router属性

     $route:一般获取路由信息【路径、query、params】

     $router:一般进行编程式路由导航进行路由跳转【push | replace】

 (5)最后还要展示路由,即在App.vue文件中设置【路由组件出口的地方】

 (6)【总结】路由组件和非路由组件的区别?

1. 路由组件一般

(7)进行路由跳转

有两种形式:1.声明式导航router-link,可以进行路由的跳转

                      2.编程式导航push|replace,可以进行路由跳转

声明式导航能做的,编程式导航都能做,但是编程式导航除了可以进行路由跳转,还可以做一些其他的业务逻辑。

在【index.vue】中设置路由跳转

六、利用【路由元信息】实现显示或隐藏组件

分析Footer组件:实现它在Home、Search中显示,在Register、Login中隐藏

(1)方法一(不推荐):在上节中,我们知道这时组件已经具备$route属性,可以获取路由路径

         显示或者隐藏组件:v-if、v-show(这里采用v-show,性能更好,不频繁操作DOM)

(2)方法二(推荐):即利用【路由元信息】

这里放上有关路由元信息的官方文档内容:路由元信息 | Vue Router

找到router文件夹中的index.js文件,将【谁可以具有Footer组件的信息】通过接收属性对象的meta属性来实现,并且它可以在路由地址和导航守卫上都被访问到。

 然后在App.vue文件中,进行$route.meta.show判断,如果为真则显示,如果为假则隐藏

七、路由传递参数

我们已经了解到路由跳转有两种方式:声明式导航、编程式导航

路由进行传参时,参数一般有种写法:

        params参数:属于路径当中的一部分,在配置路由的时候需要【占位】

        query参数:不属于路径当中的一部分,类似于ajax中的queryString,不需要占位

(1)第一种路由传递参数的方式:【字符串形式】

        1.先在路由配置信息中进行占位

          2.进行路由push跳转,跳转到search页面时传递相应的【路由参数】    

        3.这时在Search页面中,通过【路由信息】就可以获取到params参数

  (2)第二种路由传递参数的方式:【模板字符串】

        1.第一步和上个方法相同

        2.和上个方法的第二部有些区别,采用模板字符串的方式

        3.接收参数和上个方法相同

 (3)第三种路由传递参数的方式:【对象】

        1. 当使用【对象】的方式进行传参,传入的参数又是params参数时,需要在路由配置信息中           为路由设置【名字】,name: "XXX"

         2.传递参数,形式如下图所示

         3.接收参数和上个方法相同

八、重写push和replace方法

        【问题】:编程式路由导航跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated的警告错误(但不影响最终的结果)?而声明式导航是没有这类问题的,因为vue-router底层就已经处理好了。

        【原因】:最新的vue-router引入了promise,即调用push方法会返回promise对象,但没有向其中传入成功的回调和失败的回调。

        【解决方法1】:在调用push方法时,就传入成功和失败的回调。(可以捕获出error看看错误类型)但是这种方法治标不治本。将来在别的组件中,不管是push还是replace,编程式导航还是有类似的错误。这样一次次解决下去太麻烦了。

         【解决方法2】:首先搞清楚上段代码中的this是什么、this.$router是什么、push是什么

                this:当前组件实例

                this.$router属性:这个属性的属性值是VueRouter类的一个实例,即当在入口文件注册路由的时候,给组件实例添加的$router和$route属性

                push:VueRouter类原型上的方法

                为了更好的理解this.$router.push( )方法,我们根据这三个的特性实现简单的伪代码

//构造函数VueRouter
function VueRouter(){

}
//原型对象上的方法
VueRouter.prototype.push = function(){
    //函数的上下位为VueRouter类的一个实例
}
//实例化一个VueRouter对象
let $router = new VueRouter();

$router.push(xxx);

        因此想要治本,必须重写VueRouter原型上的push方法。在有【路由配置信息】的文件中进行重写,因为在这个文件中,我们是可以获取到VueRouter类的

(replace方法重写和上述类似)

 九、Home首页组件拆分业务分析

【第一个组件】:因为【三级联动组件】在很多页面中都使用了,因此将其拆分成一个全局组件,哪里想用就用哪里(红色框出来的就是三级联动的展示)。

 【第二个组件】:轮播图+尚品汇快报 

【第三个组件】:今日推荐

【第四个组件】:排行榜

【第五个组件】:猜你喜欢

【第六个组件】:家用电器|手机通讯等,组件可被复用

 【第七个组件】:商品logo

 十、完成三级联动全局组件

(1)在page文件夹中的Home文件夹下,新建一个文件夹TypeNav,在该文件夹中创建index.vue文件,用来配置【三级联动组件】的内容

(2)在HTML静态资源中找到有关【三级联动】的结构代码,把代码内容放入到index.vue文件的template标签中。

(3)在css|less静态资源中找到有关【三级联动】的代码,将代码内容放入到index.vue文件的style标签中,并设置lang属性,以便能够正常处理less

 (4)将该组件注册为全局组件:找到入口文件main.js,在该文件中将【三级联动组件】注册为全局组件。

 (5)此时【三级联动组件】已经注册为全局组件,在其他地方使用它时,不需要进行引入和注册,直接使用即可。

 十一、Home首页拆分静态组件

拆分时要注意三部分:HTML、CSS、图片资源

(1)创建一个名为ListContainer的组件,按上小节的步骤对HTML和CSS进行拆分,这里需要注意的是:HTML中图片资源的路径可能已经发生了变化,需要根据目前的路径进行修改。

(2)该组件创建好之后,在Home组件中进行【引入】、【注册】和【使用】

(Recommend组件、Rank组件、TypeNav组件、Like组件的【创建、引入、注册和使用方式】和上述相同,这里不再赘述)

 十二、使用【POSTMAN工具】测试接口       

测试后端给的接口是不是可用,后端通常会给出服务器地址、请求地址、请求方式等等信息。根据这些信息,在POSTMAN工具中配置好这些信息。

十三、对axios进行二次封装

首先,搞清楚为什么要进行二次封装?因为我们想使用请求拦截器和响应拦截器

【请求拦截器】:在发请求之前可以处理一些业务

【响应拦截器】:当服务器返回数据之后,可以处理一些业务


使用前先进行安装:npm install --save axios

可以在package.json中查看是否已经安装成功,如下


在项目中通常使用API文件夹放置【axios】相关内容,因此在src文件夹中创建一个api文件夹

在api文件夹中创建一个request.js的文件,在其中实现axios的二次封装,代码如下

//对axios进行二次封装,
import axios from 'axios'

// 利用axios对象得方法create,去创建一个axios实例
// request就是axios,只不过稍微配置一下
const requests = axios.create({
    //配置对象
    //基础路径,发送请求的时候,路径当中会出现api
    baseURL:'/api',
    //代表请求超时的时间5S
    timeout:5000
});
// 请求拦截器
requests.interceptors.request.use((config)=>{
    //config:配置对象,对象里面有一个属性很重要,header请求头
    
    return config;
});
// 响应拦截器
requests.interceptors.response.use((res)=>{
    //成功的回调函数:服务器相应数据回来以后,响应拦截器可以检测到,可以做一些事情
    return res.data;
},(error)=>{
    console.log(error)
    //响应失败的回调函数
    return Promise.reject(new Error('faile'))
})

//对外暴露
export default requests;

十四、接口统一管理

如果项目规模很小,完全可以在组件的生命周期函数中发请求

如果项目规模比较大,会存在这样一种情况:有几十个组件使用了这个接口,后期接口变动了,就得一个个去修改组件当中接口的内容,很不方便。因此采用【接口统一管理】


在api文件夹中新创建一个js文件,名为index,在其中进行接口的统一管理

//当前这个模块:API进行统一管理
import requests from './request';

//三级联动接口  
//暴露这个函数,外面拿到这个函数,直接调用,就能发送请求获取数据了
export const reqCategoryList = ()=>{
    //返回的结果是promise对象 当前函数执行需要把服务器返回结果进行返回
    return requests({
        url:'/product/getBaseCategoryList',
        method:'get'
    })
}

测试之后,发现请求发生404错误,这是因为【跨域问题】

解决跨域问题的方法有很多,这里采用【代理服务器】去解决,在vue.config.js文件中进行配置

module.exports = {
  //打包时不要有map文件
  productionSourceMap:false,
  //关闭校验工具
  lintOnSave:false,
  //代理跨域
  devServer:{
    proxy:{
      '/api':{ //遇到带有api的请求,代理服务器才会将其转发
        target:'http://gmall-h5-api.atguigu.cn',
        // pathRewrite:{'^/api':''},
      }
    }
  }
}

注意:这是一个配置文件,写好之后需要重新运行一下才可以~

十五、nprogress进度条的使用

先下载nprogress进度条:npm install --save nprogress,

下载完成之后在package.json中查看是否安装成功。


nprogress进度条需要在请求拦截器和响应拦截器中去使用

先引入进度条:import nprogress from 'nprogress'

还要引入进度条样式:import "nprogress/nprogress.css"

【请求拦截器】:启动进度条 nprogress.start( )

【响应拦截器】:结束进度条nprogress.done( )

十六、VUEX模块式开发

vuex:并不是所有的项目都需要vuex,如果项目很小,则不需要;如果项目比较大,则需要使用vuex进行数据的统一管理

先安装vuex:npm install --save vuex,下载完成之后在package.json中查看是否安装成功

在src中新建一个文件夹store,用来实现vuex,创建index.js文件进行配置

import Vue from 'vue'
import Vuex from 'vuex'
//需要使用插件一次
Vue.use(Vuex)
//state:仓库存储数据的地方
const state = {}
//mutation:修改state的唯一手段
const mutation = {}
//actions:可以书写自己的业务逻辑,也可以处理异步
const actions = {}
//getters:可以理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便

//对外暴露Store类的一个实例
export default new Vuex.Store({
        state,
        mutations,
        actions,
        getters
})

还要在入口文件main.js中引入这个仓库:import store from '@/store' 并进行注册

import Vue from 'vue'
import App from './App.vue'
//三级联动组件+全局组件
import TypeNav from "@/components/TypeNav"

//第一个参数:全局组件的名字 第二个参数:哪一个组件
Vue.component(TypeNav.name, TypeNav)

//测试
//引入仓库
import store from '@/store'

new Vue({
  render: h => h(App),
  //注册路由:底下的写法KV一致省略V
  //注册路由信息:当这里书写router的时候,组件身上都拥有$route,$router属性
  router,
  //注册仓库:组件实例的身上会多一个$store属性
  store
}).$mount('#app')

接下来就要进行vuex的模块化开发了

为什么需要模块化开发?如果项目过大,组件过多,接口也很多,数据也很多,store对象会变得相当臃肿,因此可以让vuex实现模块化开发,即把一个大仓库拆分成一个个的小仓库。

可以给home、search等这样的模块单独设置一个store小模块,然后再把小模块混入到大模块中

//home模块的小仓库
const state = {};
const mutations = {};
const actions = {};
const getters = {};
export default {
    state,
    mutations,
    actions,
    getters
}
//大仓库
import Vue from 'vue'
import Vuex from 'vuex'
//需要使用插件一次
Vue.use(Vuex)
//引入小仓库
import home from './home'
import search from './search'

//对外暴露Store类的一个实例
export default new Vuex.Store({
   //实现Vuex仓库模块式开发存储数据
   modules:{
       home,
       search
   }
})

十七、动态展示三级联动

【三级联动】组件是一个全局组件,放在components文件夹中。

下面这个图就很好地展现出组件是如何获取数据的、仓库是如何去请求数据的

 对三级联动组件TypeNav进行配置

<script>
import {mapState} from 'vuex';

export default {
    name:'TypeNav',

    //组建挂载完毕:可以向服务器发请求
    mounted() {
        //通知vuex发请求,获取数据,存储于仓库中
        this.$store.dispatch('categoryList') 
    },

    computed:{
        ...mapState({
            //右侧需要的是一个函数,当使用这个计算属性的时候,右侧函数会立即执行一次
            //注入一个参数state,这指的是大仓库中的数据
            categoryList:(state)=>{
                return state.home.categoryList;
            }
        })
    }
};
</script>

 找到home模块的小仓库,进行配置

import {reqCategoryList} from '@/api'; 
//home模块的小仓库
const state = {
    //state中数据默认初始值别瞎写 【根据接口的返回值去初始化】
    categoryList:[],
};
const mutations = {
    CATEGORYLIST(state,categoryList){
        state.categoryList = categoryList
    },
};
const actions = {
    //通过API里面的接口函数调用,向服务器发送请求,获取服务器的数据
    async categoryList({commit}){ //对commit进行解构赋值
        let result = await reqCategoryList();
        if(result.code === 200){
            commit("CATEGORYLIST",result.data);
        }
    }
};
const getters = {};
export default {
    state,
    mutations,
    actions,
    getters
}

通过以上步骤,三级联动组件TypeNav就已经获取到数据啦!接下来就要把数据展示到页面上了。

对代码进行分析,发现一级目录很多,如下图这样:

 因此可以只留一个,并通过v-for进行优化

<div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId">

 则一级目录的a标签名称也要改

<a href=" ">{{c1.categoryName}}</a>

二级分类也很多,同样采用v-for进行优化

<div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId" >

则二级目录的a标签名称也要改变

<a>{{c2.categoryName}}</a>

三级分类也很多,同样采用v-for进行优化

<em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">

则三级目录的a标签名称也要改变

<a>{{c3.categoryName}}</a>

十八、三级联动动态背景颜色

第一种解决方案:直接添加CSS样式(这里不用,因为很简单,来些具有挑战性的,哈哈哈)

第二种解决方案:动态添加类名

先来理一下思路:

1. 在data中定义一个变量,名为currentIndex,初始值设置为-1(不能设置为0-15之间的数,总共有16个标题)

data() {
        return {
            //存储用户鼠标移上哪一个一级分类
            currentIndex: -1
        }
    },

2. 为标题绑定一个原生JS事件mouseenter,并传入index,事件的回调函数定义在methods中,在回调函数中,将传入的值赋给currentIndex,这样就能拿到鼠标移动到的当前标题的index了

<h3 @mouseenter="changeIndex(index)">
methods:{
    enterShow(){
        this.show = true
    },
}

3. 在一级标题的循环中,判断currentIndex==index是否成立,成立的话就添加一个类,这个类就实现了添加背景色的效果。

<div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId" :class="{cur:currentIndex == index}">

实现完成之后,发现存在一个问题,鼠标移除之后还有背景颜色,这是不合理的,应该背景颜色去掉才可以。出现问题不用慌,解决就是了,再给标题添加一个鼠标移除事件喽,

但是又出现了一个问题,鼠标移到“全部商品分类”上,背景颜色应该还是存在的。(个人觉得这个实现完全没必要,看起来更像是个BUG,为了练手,还是实现一下吧)

 其实就用到了事件委派,就“全部商品分类”和“三级联动”放在同一个div中,且二者是兄弟关系

<!-- 事件的委派 -->
<div @mouseleave="leaveShow">
     <h2 class="all">全部商品分类</h2>
     <!-- 三级联动 -->
     <div class="sort">
     </div>
</div>

十九、通过JS控制二三级分类的显示与隐藏

鼠标移动到哪个标题,就展示哪个标题下的二三级分类列表

第一种解决方案:直接改变CSS样式

第二种解决方案:通过JS实现

思路:在上一节中,我们已经通过事件监听将一级标题的index传递给了data中的currentIndex变量,如果index==currentIndex,则将二三级分类的样式设置为display:'block',否则设置为“none”

<div class="item-list clearfix" :style="{display:(currentIndex == index ? 'block':'none')}">

二十、引入防抖与节流

防抖:前面的所有的触发都被取消,最后一次执行在规定时间之后才会触发,也就是说如果连续快速地触发,只会执行一次。

节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发。


实现的时候利用一个插件,叫做lodash,里面封装了防抖与节流的业务【闭包+延时器】

这里举一个防抖的小栗子:输入框输入数据时,进行Ajax请求

如果不采用防抖的话,每输入一个字就要发一次请求,假如我们输入“梅西世界杯”,会发送五次请求。这并不满足我们的实际需求,我们想要输入完这五个字,才会发送请求,因此采用防抖技术进行解决。

let input = document.querySelector('imput')
//不加防抖
input.oninput = function(){
    //这里放ajax发请求的代码
}
//加了防抖
input.oninput = _.debounce(function(){
    //这里放ajax发请求的代码
},1000);

这里举一个节流的小栗子:实现一个简单的计时器,即点击按钮,实现数字元素的增加

<h1>我是计时器<span>0</span></h1>
<button>点击我加上1</button>
....

let span = document.querySelector('span');
let button = document.querySelector('button');
let count = 0;
//未加节流
button.onclick = function(){
    count++;
    span.innerHTML = count;
}
//加了节流
button.onclick = _.throttle(function(){
    count++;
    span.innerHTML = count;
},1000);


在项目中实现节流:三级联动这里用户的交互操作可能会过快,导致浏览器反应不过来,如果当前回调函数中有一些大量业务,有可能出现卡顿现象。

vue脚手架中已经下载好了lodash,可直接全部引入lodash内容: import _ from 'lodash' 

这里我们可以按需引入,只引入节流:import throttle from 'lodash'

//未加节流的代码
changeIndex(index){
            this.currentIndex = index;
        }
//加了节流的代码
//throttle回调函数别用箭头函数,可能会出现上下文this
changeIndex:throttle(function(index){
            //index:鼠标移上某一个一级分类的元素的索引值
            //正常情况(用户慢慢地操作):鼠标进入,每一个一级分类h3,都会触发鼠标进入事件
            //非正常情况(用户操作很快):本身全部的一级分类都应该触发鼠标进入事件,但是经过测试,只有部分h3触发了
            //就是由于用户的行为过快,导致浏览器反应不过来,如果当前回调函数中有一些大量业务,有可能出现卡顿现象。        
            this.currentIndex = index;
        },50),

二十一、三级联动路由跳转分析

关于路由,我发了一篇vue-router思维导图的文章,可以帮助大家回忆起相关内容

链接在此:vue路由知识点概括--思维导图_yuran1的博客-CSDN博客


对于三级联动,用户可以点击的:一级分类、二级分类、三级分类,当我们从Home模块跳转到Search模块时,一级会把用户选中的产品(比如产品的名字、产品的ID)在路由跳转的时候进行相应的传递。

注意:这里如果使用的是声明式路由导航,可以实现路由的跳转与传递参数,但需要注意,会出现卡顿的现象,这是为什么呢?

原因:router-link可以看作是组件,当服务器的数据返回之后,由于v-for的设置,会循环出很多的router-link组件,这种方法很消耗内存,所以会出现卡顿的现象。因此这里采用编程式路由导航


但是那么多a标签,都给它们绑定click事件的回调函数的话,肯定太繁琐、太消耗内存了。

事件委派又派上用场了,我们把click事件的回调函数放在父元素身上,不用再一一绑定了。

<div class="all-sort-list2" @click="goSearch">

但是利用事件委派之后,还存在一些问题:

1. 你怎么知道点击的一定是a标签的?也有可能是div、h3等标签

2. 如何获取参数呢?【1、2、3级分类的产品的名字、id】,如何区分1、2、3级分类的标签?

解决方法看下一节

二十二、实现三级联动的路由跳转与传递参数

为了解决上述问题,这里利用【自定义属性】来解决

为解决第一个问题:为a标签加上自定义属性data-categoryName,其余的子节点是没有的。

//一级分类
<a :data-categoryName="c1.categoryName">{{ c1.categoryName }}</a>
//二级分类
<a :data-categoryName="c2.categoryName">{{ c2.categoryName }}</a>
//三级分类
<a :data-categoryName="c3.categoryName">{{ c3.categoryName }}</a>

在前面的章节中,我们可以知道goSearch( )函数中放置的是进行路由跳转的方法

我们点击子节点就可以触发goSearch( )这个回调函数,在函数中通过event.target拿到被点击的节点元素element,节点身上有一个属性dataset属性,可以获取节点的自定义属性与属性值,可以通过解构赋值取出来,如果有categoryname属性,那么被点击的就是a标签了


注意:有些同学有疑惑了,自定义属性为data-categoryName,那么判断条件应该这样写

if(data-categoryName) {......}

然而实际上是这样写的:

if(categoryname) {......}

原因是:需要在定义属性的时候在前面加上data-才能被dataset函数获取,因此data-只是一个前缀,其次浏览器会自动将属性名转化为小写。


为解决第二个问题:分别为1、2、3级的a标签加上自定义属性data-category1Id、data-category2Id、data-category3Id,其余的子节点是没有的。

<a :data-categoryName="c1.categoryName" 
   :data-category1Id="c1.categoryId"
>{{ c1.categoryName }}</a>

<a :data-categoryName="c2.categoryName" 
   :data-category1Id="c2.categoryId"
>{{ c2.categoryName }}</a>

<a :data-categoryName="c3.categoryName" 
   :data-category1Id="c3.categoryId"
>{{ c3.categoryName }}</a>

采取和判断a节点一样的方法,判断点击的节点是1级、2级还是3级,这里不再赘述了。

到此,问题就解决了,接下来就要实现在路由跳转中携带参数了,下面直接上代码:

 goSearch(event) {
      //获取到当前触发这个事件的节点,从中筛选出带有data-categoryname这样的节点
      //节点有一个属性dataset属性,可以获取节点的自定义属性和属性值
      let element = event.target;
      //获取到的变量已经不是驼峰形式了,自动改变的
      let { categoryname, category1id, category2id, category3id } =
        element.dataset;
      if (categoryname) {
        //整理路由跳转的参数
        let location = { name: "search" };
        let query = { categoryName: categoryname };
        //一级分类、二级分类、三级分类的a标签
        if (category1id) {
          query.category1Id = category1id;
        } else if    (category2id) {
          query.category2Id = category2id;
        } else {
          query.category3Id = category3id;
        }
        location.query = query;
        //路由跳转
        this.$router.push(location);
      }
    },

二十三、Search模块商品分类与过渡动画

从Home主页点击三级分类的内容,就可以跳转到Search模块

Search模块也有三级联动组件,但是它在Search模块中默认情况下是隐藏的,但是在Home模块下默认是显示的。因而这里使用v-show属性对三级联动组件进行修改,

当处于Home模块下,v-show = true;当处于Search模块下,v-show = false;(通过路由信息判断)

//三级联动
<div class="sort" v-show="show">
            ......
</div>  
.
.
.
data() {
    return {
      //存储用户鼠标移上哪一个一级分类
      currentIndex: -1,
      show: true,
    };
},
  //组建挂载完毕:可以向服务器发请求
mounted() {
    //通知vuex发请求,获取数据,存储于仓库中
    // this.$store.dispatch('categoryList') 考虑到性能将其挪到了【App.vue】
    //当组件挂载完毕,让show的属性变为false
    //如果不是Home路由组件,将typeNav进行隐藏
    if (this.$route.path != "/home") {
      this.show = false;
    }
},

但是它总不能一直隐藏吧,当鼠标移入到 “全部商品分类” 那里,就要显示三级联动的内容了,而鼠标移出后,又要隐藏了。

    <div @mouseleave="leaveShow" @mouseenter="enterShow">
        <h2 class="all">全部商品分类</h2>
        //三级联动
        <div class="sort" v-show="show">
            ......
        </div>
    </div>
    .
    .   
    .
    //当鼠标移入的时候,让商品分类列表进行展示
    enterShow() {
      this.show = true;
    },
    //当鼠标离开的时候,让商品分类列表进行隐藏
    leaveShow() {
      this.currentIndex = -1;
      //判断不是Home路由组件的时候才会执行
      if (this.$route.path != "/home") {
        this.show = false;
      }
    },

接下来实现过渡动画

前提:组件或元素务必要有v-if或v-show指令才可以进行过渡动画

   //过渡动画
   <transition name="sort">
        //三级联动
        <div class="sort" v-show="show">
                ...
        </div>
   </transition>
    //过渡动画的样式
    //过渡动画开始状态(进入)
    .sort-enter {
      height: 0px;
    }
    //过渡动画结束状态(进入)
    .sort-leave {
      height: 461px;
    }
    //定义动画时间、速率
    .sort-enter-active {
      transition: all 0.5s linear;
    }

二十四、TypeNav商品分类列表的优化

从Home模块跳转到Search模块:首先TypeNav在Home模块中挂载时,会向后台请求数据,当跳转到Search模块时,Home组件销毁,当中的TypeNav也销毁,Search组件挂载,当中的TypeNav也挂载,挂载时又要发一次请求。

综上可知,发了两次请求,性能不够好。在这个应用中,我就只想请求一次,怎么办?


先来分析一下:首先执行入口文件main.js,其中有App路由组件,她是唯一一个根组件,因此不管如何,她都只会挂载一次。那我们把TypeNav中派发action的操作(用于请求数据)放在App.vue中,就能实现仅请求一次的效果了。

如果放在main.js中可行吗?不行,因为main.js不是一个组件,而是一个js文件,派发action时,this为undefined

二十五、合并params和query参数

前面我们已经实现了点击三级联动分类,从Home主页跳转到Search模块,携带了query参数。如果这时我们在输入框输入内容进行搜索时,会发现携带的query参数没有了,只有刚刚请求的params参数了。两者是不能同时存在的,这显然不符合我们应用场景的。

假如:在三级分类中选择“手机”进入到了Search模块,这时我想在此基础上搜“华为”,如果只携带华为这个参数,那返回来的数据可能会包含华为手表、华为汽车等不相关信息。


首先,如果路由跳转的时候,带有params参数,要和query参数一起捎带过去

​
    goSearch(event) {
        .
        .
        .
        //判断:如果路由跳转的时候,带有params参数,携带参数传递过去
        if (this.$route.params) {
          location.params = this.$route.params;
          //整理完参数
          location.query = query;
          //路由跳转
          this.$router.push(location);
        }
      }
    },

然后,在head组件中,点击搜索时进行路由跳转,如果有query参数,要和params一起捎带过去

    goSearch(){
        .
        .
        .
        //如果有query也携带过去
        if(this.$route.query){
            let location = {name:'search',params:{keyword:this.keyword || undefined}}
            location.query = this.$route.query;
            this.$router.push(location)
        }
    },

二十六、mock.js模拟数据

服务器返回的数据(接口)只有商品分类菜单分类数据,对于ListContainer组件与Floor组件数据,服务器都没有提供,因此这里使用mock.js去模拟一些数据。

官网对Mock.js的解释:生成随机数据,拦截Ajax请求。


安装mock.js:cnpm install --save mock.js

使用步骤:

1. 在项目中src文件夹中创建mock文件夹

2. 准备预先设置好的JSON数据(mock文件夹中创建相应的JSON文件)

举个例子,下面是有关轮播图的JSON数据

[{
    "id": "1",
    "imageUrl": "/images/banner1.jpg"
  },
  {
    "id": "2",
    "imageUrl": "/images/banner2.jpg"
  },
  {
    "id": "3",
    "imageUrl": "/images/banner3.jpg"
  },
  {
    "id": "4",
    "imageUrl": "/images/banner4.jpg"
  }
]

注意:JSON数据需要格式化一下,别留有空格,否则跑不起来

3. 把mock数据需要的图片资源放置到public文件夹中,因为public文件夹在打包的时候,会把相应的资源原封不动地打包到dist文件夹中。

4. 开始mock,通过mockjs模块实现,在mock文件下创建一个名为mockServer.js文件

/* 
利用mockjs提供mock接口
*/
import Mock from 'mockjs'
// JSON数据格式根本没有对外暴露,但是可以引入
// webpack默认对外暴露的:图片、JSON数据格式
import floors from './floors.json'
import banners from './banners.json'

// 提供广告轮播接口  第一个参数是请求地址,第二个参数是请求数据
Mock.mock('/mock/banners', {code: 200, data: banners})//模拟首页大的轮播图的数据
// 提供floor接口
Mock.mock('/mock/floors', {code: 200, data: floors})
console.log('MockServer')

5. mockServer.js文件在入口文件main.js中引入(至少需要执行一次,才能模拟数据)

二十七、获取Banner轮播图数据

在api文件夹中创建一个名为mockAjax.js的文件,专门用来请求mock数据。

需要注意:baseURL要改为'/mock'

//对axios进行二次封装,
import axios from 'axios'
//引入进度条
import nprogress from 'nprogress'
//在当前模块中引入store
//引入进度条的样式
import "nprogress/nprogress.css"

// 利用axios对象得方法create,去创建一个axios实例
// request就是axios,只不过稍微配置一下
const requests = axios.create({
    //配置对象
    //基础路径,发送请求的时候,路径当中会出现api
    baseURL:'/mock',
    //代表请求超时的时间5S
    timeout:5000
});
//请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config)=>{
    //config:配置对象,对象里面有一个属性很重要,header请求头
    //进度条开始动
    nprogress.start();
    return config;
});
//响应拦截器
requests.interceptors.response.use((res)=>{
    //成功的回调函数:服务器相应数据回来以后,响应拦截器可以检测到,可以做一些事情
    nprogress.done();
    return res.data;
},(error)=>{
    console.log(error)
    //响应失败的回调函数
    return Promise.reject(new Error('faile'))
})

//对外暴露
export default requests;

在同文件夹下的index.js文件中写【Home首页轮播图接口】,切记url地址中不带mock,因为前面已经配置过了

export const reqGetBannerList = () => mockRequests.get('/banners') //简写形式

mock数据以及接口都准备完毕后,就要发送请求去获取数据啦

当ListContainer组件挂载时(mounted),派发action,通过vue发起ajax请求,将数据存储在仓库中:

mounted() {
    this.$store.dispatch('getBannerList');
}

之后在store文件夹下的home文件夹下的index.js中,进行vuex的配置

const state = {
    ...
    //轮播图的数据
    bannerList:[]
};
const actions = {
    .
    .
    .
    //获取首页轮播图的数据
    async getBannerList({commit}){
      let result = await reqGetBannerList();
      if(result.code == 200){
          commit('GETBANNERLIST',result.data)
      }
    }
};
const mutations = {
    ...
    GETBANNERLIST(state,bannerList){
        state.bannerList = bannerList;
    }
};

这个时候还没有结束哦,ListContainer组件还没拿到这个数据呢,因此可以使用mapState

import {mapState} from 'vuex';

export default {
    name:'ListContainer',
    mounted() {
        this.$store.dispatch('getBannerList');
    }
    computed:{
        ...mapState({
            bannerList:state => state.home.bannerList
        })
    },   
}

二十八、swiper基本使用

在swiper官网下载5版本:下载Swiper - Swiper中文网

关于使用过程,官网给的教程非常详细,自己看看实际操作一下,这里就不再赘述了。

教程链接:Swiper使用方法 - Swiper中文网

需要注意:

1. 在new Swiper实例之前,页面中的结构必须有,因为我们要操作DOM

2. 第一个参数可以是字符串(选择器)也可以是真实DOM节点

二十九、Banner实现轮播图(第一种解决方案)

1. 首先安装Swiper插件:选择5版本,6版本会有一些问题:npm install --save swiper@5

2. 引包(相应JS|CSS):

在组件文件中引入:import Swiper from ‘swiper’  --->引入了JS内容

对于样式来说,可以在每个相关组件中引入,但是因为很多地方都用到了轮播图,且样式是一样的,因此可以在入口文件main.js中引入样式,会更加简洁。

即:import "swiper/css/swiper.css"

注意:引入样式的时候,不用import ... from ... ,没有对外进行暴露

3.在模板语法中,我们发现目前只使用一张图片,但是轮播图却是很多张,因此需要使用v-for进行遍历

<div class="swiper-container" id="mySwiper">
    <div class="swiper-wrapper">
        <div class="swiper-slide" v-for="(carousel,indx) in bannerList" :key="carousel.id">

             <img :src="carousel.imgUrl" />

        </div>
    </div>
</div>

4. 使用Swiper

new Swiper这个过程要放在哪里写呢?放在mounted( )钩子函数中写,因为这个时候页面结构已经实现好了,符合条件。


但是写了之后,发现没有效果!那这又是因为什么呢?因为结构还不完整!

什么!结构怎么还不完整?原因就在于上面那段代码,我们使用v-for去遍历图片,图片的数据是通过axios请求获得的,涉及到了异步,只有请求数据回来了,此时的结构才能是完整的!

因此可以添加一个延迟函数,延迟使用new Swiper,但是这个方法不好用,延迟效果比较鸡肋。比如轮播图中间的小点点得等待一段时间才能够显示出来。

    setTimeout(()=>{
        var mySwiper = new Swiper(document.querySelector(".swiper-container"),{
            loop:true,
            //如果需要分页器
            pagination:{
                el:".swiper-pagination",
            },
            //如果需要前进后退按钮
            navigation:{
                nextEl:'.swiper-button-next',
                prevEl:'.swiper-button-prev',
            },
        });
    },1000)

当然,我们也可以把new Swiper放在updated( )钩子函数中,但是如果vue组件中有其他数据的话,其他数据发生改变,就要实现这个new Swiper操作,很浪费内存,不推荐使用,但是效果是正常的。


点击轮播图中的小球,不发生图片的转换,这里就要配置一个属性:clickable:true,放在pagination里。

三十、轮播图:watch+nextTick( )(第二种解决方案)

如果大家不知道nextTick( )是什么,可以看一下我之前发的相关文章

链接在这里:VUE中nextTick( )函数思维导图_yuran1的博客-CSDN博客


使用watch监听bannerList的变化,如果有变化,就会触发watch属性中的handle回调函数,我们可以把new Swiper的过程放在这个回调函数中执行。

但是运行的结果还是不行,说明new Swiper前,页面结构还是不完整的,虽然说数据获取成功了,但是不能保证v-for执行完毕。


为了解决这个问题,就要使用nextTick( )函数了

用法【官方解释】:在下次DOM更新循环结束之后,执行延迟回调。在修改数据之后,立即使用这个方法,获取更新后的DOM

三十一、获取floor组件mooc数据

1. 首先编写API接口,获取floor数据

//获取floor数据
export const reqFloorList = () => mockRequests.get('/floors')

2. 写VUEX三连环

import {reqCategoryList, reqGetBannerList,reqFloorList} from '@/api';
//home模块的小仓库
const state = {
    //state中数据默认初始值别瞎写 【根据接口的返回值去初始化】
    categoryList:[],
    //轮播图的数据
    bannerList:[],
    //floor组件的数据
    floorList:[],
};
const mutations = {
    CATEGORYLIST(state,categoryList){
        state.categoryList = categoryList
    },
    GETBANNERLIST(state,bannerList){
        state.bannerList = bannerList;
    },
    REQFLOORLIST(state,floorList){
        state.floorList = floorList
    }
};
const actions = {
    //通过API里面的接口函数调用,向服务器发送请求,获取服务器的数据
    async categoryList({commit}){ //对commit进行解构赋值
        let result = await reqCategoryList();
        if(result.code === 200){
            commit("CATEGORYLIST",result.data);
        }
    },
    //获取首页轮播图的数据
    async getBannerList({commit}){
      let result = await reqGetBannerList();
      if(result.code == 200){
          commit('GETBANNERLIST',result.data)
      }
    },
    //获取floors数组
    async getFloorList({commit}){
      let result = await reqFloorList();
      if(result.code == 200){
        commit('REQFLOORLIST',result.data)
    }
    }
};
const getters = {};
export default {
    state,
    mutations,
    actions,
    getters
}

3. 在Home组件中触发action,为什么不在Floor组件中去触发。因为Floor组件要进行复用,如果在Floor组件中通过mapState收到了返回的数据,那将无法创建出不同的Floor组件。而Home组件正是使用Floor组件的地方,可以在这里去触发action,从而拿到相应的数据,通过v-for赋给不同的Floor组件不同的数据。

<template>
  <div>
      <!-- 三级联动全局组件,已经注册为全局组件 -->
      ......
      <Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"/>
      ......
  </div>
</template>

<script>
import ........

export default {
    name:'HomeIndex',
    components:{
      ......
    },
    mounted() {
      //派发action,获取floor组件的数据
      this.$store.dispatch("getFloorList")
      ......
    },
    computed:{
      ...mapState({
        floorList:state => state.home.floorList
      })
    }
}
</script>
<style>
</style>

4. 从上面代码中可以看出父组件Home向子组件Floor传递数据 :list="floor"

子组件用props接收数据

export default {
    name:'FloorMsg',
    props:['list'],
    ......
}

三十二、动态展示Floor组件

首先通过浏览器的vue网络工具检查组件的各种属性和方法

 根据上图这些内容实现【Floor组件的动态展示】

比如:

<h3 class="fl">{{list.name}}</h3>  //标题
<li class="active" v-for="(nav,index) in list.navList" :key="index">
    <a href="#tab1" data-toggle="tab">{{nav.text}}</a>
</li>

等等,只要需要动态展示的内容都需要进行相应处理,这里不再一一进行解释


在上述处理中,我们还发现需要设置轮播图,章节二十八、二十九已经介绍过swiper具体的适用步骤,这里也不再赘述了。

但需要注意一点:上次书写Swiper的时候,在mounted( )函数中书写是不可以的,但是为什么在这里就可以了!

原因:上次书写轮播图的时候,是在当前组件内部发请求,动态渲染结构【前台至少服务器数据需要回来】,因此这里的写法在当时是不可行的。现在的这种写法为什么可以?因为请求是父组件发的,父组件通过props传递过来的,而且结构都已经都有了的情况下执行mounted( ),此时页面结构已经是完整的了。

三十三、共用组件Carsouel(轮播图)

把首页中的轮播图拆分成一个共用全局组件Carsouel,在components文件夹中新建一个名为Carsouel的文件夹,用来书写轮播图组件

<template>
  <!-- 轮播图的地方 -->
    <div class="swiper-container" ref="cur">
        <div class="swiper-wrapper">
            <div class="swiper-slide" v-for="(carousel,index) in list" :key="carousel.id">
                <img :src="carousel.imageUrl" />
            </div>
        </div>
        <!-- 如果需要分页器 -->
        <div class="swiper-pagination"></div>
        <!-- 如果需要导航按钮 -->
        <div class="swiper-button-prev"></div>
        <div class="swiper-button-next"></div>
    </div>
</template>

<script>
//引包
import Swiper from 'swiper';
export default {
    name:'Carousel',
    props:['list'],
    watch:{
        list:{
            //立即监听,不管数据有没有变化,我上来就监听一次
            //为什么watch监听不到list:因为这个数据从来没有发生过变化,父亲给的时候就是一个对象,对象里面该有的数据都是有的
            immediate:true,
            handler(){
                //只能监听到数据已经有了,但是v-for动态渲染结构我们还是没有办法确定,因此还是需要用到nextTick
                this.$nextTick(()=>{
                     var mySwiper = new Swiper(this.$refs.cur,{
                        loop:true,
                        //如果需要分页器
                        pagination:{
                            el:".swiper-pagination",
                            clickable:true
                        },
                        //如果需要前进后退按钮
                        navigation:{
                            nextEl:'.swiper-button-next',
                            prevEl:'.swiper-button-prev',
                        },
                    });
                });
            }}
    }};
</script>

需要注意的点:

1. v-for循环(v-for="(carousel,index) in list" )中的list是通过props传递过来的

2. 为什么watch监听不到list?因为这个数据从来没有发生过变化,父亲给的时候就是一个对象,对象里面该有的数据都是有的。因此设置immediate:true,即无论如何都得监测一次

3. 只能监听到数据已经有了,但是v-for动态渲染结构我们还是没有办法确定,因此还是需要用到nextTick(vue异步更新机制)


Carousel是一个全局组件,需要在全局文件main.js中引入和注册

//引入轮播图组件
import Carousel from "@/components/Carousel"
//注册轮播图组件
Vue.component(Carousel.name, Carousel)

然后回到Floor组件中,在轮播图的地方使用Carousel组件

<div class="floorBanner">
    <!-- 轮播图的地方 -->
    <Carousel :list="list.carouselList"/>
</div>

注意,要传递数据:list.carouselList

切记:以后在开发项目的时候,如果看到某一个组件在很多地方都使用,你把它变为全局组件,注册一次,可以在任意地方使用,公用的组件|非路由组件放在components文件夹中

三十四、Search模块的静态组件

先理清一下Search模块开发步骤

1. 先静态页面 + 静态组件拆分出来

2. 发请求(API)

3. VUEX(三连环)

4. 组件获取仓库数据,动态展示数据


静态组件的拆分很简单,就是把相应的html代码和css代码拆分出来,放在一个组件里。这里就不再赘述了

三十五、Search模块的VUEX操作

首先查阅api前台接口文档,确定请求方式、请求URL以及请求参数等

//当前这个函数需要接受外部传递参数
//当前这个接口,给服务器传递参数params,至少得是一个空对象
//如果连空对象都没有,那么请求会失败的
export const reqGetSearchInfo = (params) => requests(
    {
        url:"/list",
        method:'post',
        data:params
    }
) 

注意:

1. 当前这个函数需要接受外部传递参数
2. 当前这个接口,给服务器传递参数params,至少得是一个空对象。如果连空对象都没有,那么请求会失败的


在store文件夹中的search.js文件中进行【vuex三连环】

import { reqGetSearchInfo } from "@/api";
//search模块的小仓库
const state = {
    //仓库初始状态
    searchList:{},
};
const mutations = {
    GETSEARCHLIST(state,searchList){
        state.searchList = searchList
    }
};
const actions = {
    //获取search模块的数据
    async getSearchList({commit},params={}){
        //当前这个reqGetSearchInfo这个函数在调用获取服务器数据的时候,至少传递一个参数(空对象)
        //params形参,是当用户派发action的时候,第二个参数传递过来的,至少是一个空对象
        let result = await reqGetSearchInfo(params)
        if(result.code == 200){
            commit('GETSEARCHLIST',result.data)
            console.log(result.data)
        }
    }
};
export default {
    state,
    mutations,
    actions,
    getters
}

注意:仓库初始状态 searchList:{ },为什么是一个对象而不是一个数组呢?

这当然不是让我们进行凭空猜测啦,需要进行验证:在Search组件中mounted( )中去派发相应的action(getSearchList),this.$store.dispatch('getSearchList', { })

然后通过浏览器的network工具就可以查看请求回来的数据了,从而可以判断数据是什么格式

三十六、Search模块动态展示产品列表

import {mapState} from 'vuex'
computed:{
    ...mapState({
        goodsList:state => state.search.searchList.goodsList
    })
}

上述这段代码虽然可以获取到数据,但是太麻烦,写了一连串的内容,不仅容易出错还不美观


接下来使用getters进行优化,

在项目中,VUEX中的getters是为了简化仓库中的数据而生,想让其他组件捞数据的时候更简单一些,可以把我们将来在组件当中需要用的数据简化一下【将来组件在获取数据的时候就方便了】

const getters = {
    //当前形参state是当前仓库中的state,并非大仓库中的state
    goodsList(state){
        //如果网络不给力,返回的是undefined,这样不能遍历
        //计算新的属性的属性值至少是一个数组
        return state.searchList.goodsList || [];
    },
    trademarkList(state){
        return state.searchList.trademarkList || [];
    },
    attrsList(state){
        return state.searchList.attrsList || [];
    }
};
import {mapGetters} from 'vuex'
  
computed: {
    //mapGetters里面的写法:传递的数据,因为getter计算是没有划分模块【home、search】
    //补充:state是划分模块了state.home / state.search
    ...mapGetters(["goodsList", "trademarkList", "attrsList"]),
}

分析页面的结构,对于【销售产品列表】,结构都是一样的,可以使用【v-for】进行遍历

<li class="yui3-u-1-5" v-for="(good, index) in goodsList":key="good.id">

<li></li>标签内部的动态数据也需要更改,比如图片、价格等,比较简单,不再详细叙述了

三十七、Search模块根据不同的参数进行数据展示

在前面内容中,在Search模块中,我们是在mounted( )钩子函数中去dispatch  action 从而获取到相应的数据,但是这里存在一个问题,由于mounted( )钩子函数只能挂载一次,这导致只能请求一次数据,这并不符合应用的实际需求。

解决方法:在methods中创建一个函数getData( ),只要想请求数据就调用该函数,根据不同的参数返回不同的数据进行展示。

  methods: {
    //向服务器发送请求获取search模块数据(根据参数不同返回不同的数据进行展示)
    //把这次请求封装为一个函数,当你需要在调用的时候调用即可
    getData() {
      //先测试接口返回的数据模式
      this.$store.dispatch("getSearchList", this.searchParams); //dispatch是异步操作
    },
  }

由于组件挂载的时候,要获取相应的数据,因此在mounted( )去调用getData( )

(至于什么情况下再调用getData去获取数据,这里先不说,请看之后的章节)


对于请求参数而言,从项目开发文档中能发现【携带的参数】至少是10个,参数必须是可以变动的(ps:需要根据不同的参数请求不同的数据),因此把这些参数放入到data中。

下面对各个参数进行解释:

  data() {
    return {
      //带给服务器的参数
      searchParams: {
        //一级分类的id
        category1Id: "",
        //二级分类的id
        category2Id: "",
        //三级分类的id
        category3Id: "",
        //分类名字
        categoryName: "",
        //关键字
        keyword: "",
        //排序:初始状态应该是综合|降序
        order: "1:desc",
        //分页器用的:代表的是当前是第几页
        pageNo: 1,
        //代表的是每一页展示数据的个数
        pageSize: 10,
        //平台售卖属性操作带的参数
        props: [],
        //品牌
        trademark: "",
      },
    };
  },

在data中,参数是初始化的,还没有对参数进行赋值。因此需要在正式请求之前,对参数进行更新。更新这一过程需要在mounted( )之前进行,因此将放在beforeMount( )钩子函数中。

  //当组件挂载完毕之前执行一次【先与mounted之前】
  beforeMount() {
    //在发送请求之前,把接口需要传递的参数,进行整理
    //复杂的写法
    // this.searchParams.category1Id = this.$route.query.category1Id;
    // this.searchParams.category2Id = this.$route.query.category2Id;
    // this.searchParams.category3Id = this.$route.query.category3Id;
    // this.searchParams.categoryName = this.$route.query.categoryName;
    // this.searchParams.keyword = this.$route.params.keyword;
    Object.assign(this.searchParams, this.$route.params, this.$route.query);
  },

三十七、Search模块中子组件动态开发

在Search模块中有一个子组件SearchSelector,在这个子组件中通过【mapGetters】获取vuex中的数据,然后对template中的数据进行更改

三十八、监听路由的变化再次发请求获取数据

为了可以【再次】发请求获取不同的数据,这里首先要确定【再次发请求】的时机:也就是说当路由发生变化的时候,说明需要再次发请求了。因此需要对路由的变化进行监测,即使用【watch】

  //数据监听:监听组件实例身上的属性的属性值变化
  watch: {
    //监听路由的信息是否发生变化,如果发生变化,则再次发送请求
    $route(newValue, oldValue) {
      //再次发送请求之前整理带给服务器的参数
      Object.assign(this.searchParams, this.$route.params, this.$route.query);
      //再次发起ajax请求
      this.getData();
      //每一次请求完毕,应该把相应的1、2、3级分类的id置空,让他接受下一次的相应1、2、3id
      //分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
      this.searchParams.category1Id = "";
      this.searchParams.category2Id = "";
      this.searchParams.category3Id = "";
    },
  },
};

PS:这里老师说关键字keyword不需要置空,但是从真正使用上来说,应该要置空的,否则会影响用户的体验。(京东就对此进行了清空)

三十九、面包屑处理分类的操作

面包屑总共有四类:【分类的面包屑】、【关键字的面包屑】、【品牌的面包屑】、【平台的售卖的属性值展示】

此外,面包屑这部分不应该是死的,应该是动态的。


在Search模块中通过searchParams可以拿到【商品分类】的数据,可作为分类面包屑

在这里通过v-if进行显示判断

<!-- 分类的面包屑 -->
<li class="with-x" v-if="searchParams.categoryName">
    {{searchParams.categoryName}}
    <i @click="removecategoryName">×</i>
</li>

上面代码中给 i标签添加了一个点击事件,即删除该面包屑,那么就要重新去请求数据了

    //删除分类的名字
    removecategoryName() {
      //把带给服务器的参数置空了,还需要向服务器发请求
      //带给服务器参数的说明是可有可无的,属性值为空的字符串还是会把相应的字段带给服务器
      //但是你把相应的字段变为undefined。当前这个字段不会带给服务器,减少带宽消耗
      this.searchParams.categoryName = undefined;
      this.searchParams.category1Id = undefined;
      this.searchParams.category2Id = undefined;
      this.searchParams.category3Id = undefined;
      this.getData();
      //地址栏也需要修改,进行路由的跳转(现在的路由跳转只是跳转到自己这里)
      //严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着params参数
      if (this.$route.params) {
        this.$router.push({ name: "search", params: this.$route.params });
      }
    },

四十、面包屑处理关键字

【关键字面包屑】和【分类面包屑】的实现原理是一样的

首先通过v-if进行显示判断

<!-- 关键字的面包屑 -->
<li class="with-x" v-if="searchParams.keyword">
    {{ searchParams.keyword }}
    <i @click="removeKeyword">×</i>
</li>

再给 i标签 绑定一个监听事件,即去除这个面包屑时,需要重新请求数据

    //删除关键字
    removeKeyword() {
      //给服务器带的参数searchParams的keyword置空
      this.searchParams.keyword = undefined;
      this.getData();
      if (this.$route.query) {
        this.$router.push({ name: "search", query: this.$route.query });
      }
      //将搜索框中的内容置空,同级组件之间进行通信
      //通知兄弟组件Header删除关键字
      this.$bus.$emit("clear");
    },

从上面代码中可以看出:为了将搜索框中的内容清空,需要search组件和home组件进行通信,

这两个组件属于兄弟组件,可以使用【全局事件总线】进行通信

  //Home组件
  mounted() {
      //通过全局事件总线清楚关键字
      this.$bus.$on('clear',() => {
          this.keyword = " ";
      })
  },

四十一、面包屑处理品牌信息

这部分和前两部分有一些区别,

首先需要注意,品牌这部分内容不在Search组件中,而是在Search组件的子组件SearchSelector中。先给各个品牌绑定一个点击事件tradeMarkHandler,并传入参数trademark

<ul class="logo-list">
          <li v-for="(trademark,index) in trademarkList" :key="trademark.tmId" 
              @click="tradeMarkHandler(trademark)">{{trademark.tmName}}
          </li>
</ul>
    methods: {
      //品牌的事件处理函数
      tradeMarkHandler(trademark){
        //点击了品牌,还是需要整理参数,向服务器发送请求获取相应的数据,并进行展示
        //为什么是Search发请求,为什么呢?因为父组件中searchParams参数是带给服务器的,子组件把你 
        //点击的品牌的信息给父组件传递过去
        this.$emit('trademarkInfo', trademark);
      },
    }

从上面的代码中可以看出子向父通信使用自定义事件,子组件通过$emit触发自定义事件trademarkInfo,并传递相应的参数


而在父组件Search中绑定自定义事件,并设置自定义事件的回调函数,并接收传递过来的参数

<SearchSelector @trademarkInfo="trademarkInfo" />
    //自定义事件的回调
    trademarkInfo(trademark) {
      //整理品牌字段的参数(按照固定的格式)
      this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;
      //需要再次发送请求,获取
      this.getData();
    },

除此之外,还要将【品牌面包屑】进行展示,首先通过v-if进行显示判断

<!-- 品牌的面包屑 -->
<li class="with-x" v-if="searchParams.trademark">
    {{ searchParams.trademark.split(":")[1]}}
    <i @click="removetrademark">×</i>
</li>

再给 i标签 绑定一个监听事件,即删除这个品牌面包屑后,需要重新发请求去获取数据

    //删除品牌
    removetrademark() {
      this.searchParams.trademark = undefined;
      this.getData();
    },

四十二、平台售卖属性的操作

【平台售卖属性】这部分的内容不在Search组件中,而是在Search的子组件SearchSelector中,

先给平台售卖属性绑定一个点击事件,并传入两个相应的参数(attr, attrvalue)

<li v-for="(attrvalue,index) in attr.attrValueList" :key="index" 
    @click="attrInfo(attr,attrvalue)" >
       
    <a>{{attrvalue}}</a>

</li>
methods: {

      ....

      //平台售卖属性值的点击事件
      attrInfo(attr,attrvalue){
        //["属性ID:属性值:属性名"]
        this.$emit("attrInfo",attr,attrvalue)
      }
},

从上面的代码中可以看出子向父通信使用自定义事件,子组件通过$emit触发自定义事件attrInfo,并传递相应的参数。

而在父组件Search中绑定自定义事件,并设置自定义事件的回调函数,并接收传递过来的参数

<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo" />
    //收集平台属性的回调函数(自定义事件)
    attrInfo(attr, attrvalue) {
      //参数的格式先整理好
      let props = `${attr.attrId}:${attrvalue}:${attr.attrName}`;
      //数组去重----常见面试题
      if (this.searchParams.props.indexOf(props) == -1) {
        this.searchParams.props.push(props);
      }
      //再次发送请求
      this.getData();
    },

从上面代码中可以看到,进行了【数组去重】操作,为什么这么做呢?

因为如果不进入数组去重的话,多次点击同一个平台售卖属性,会出现多个重复的面包屑。


除此之外,还要将【平台售卖属性】面包屑进行展示,注意这里不再使用v-if,而是使用v-for,因为props是一个数组

<!-- 平台的售卖的属性值展示 -->
<li class="with-x" v-for="(attrvalue, index) in searchParams.props" :key="index">
    {{ attrvalue.split(":")[1] }}
    <i @click="removeAttr(index)">×</i>
</li>

再给 i标签 绑定一个监听事件,即删除这个平台售卖属性后,需要重新发请求去获取数据

    //removeAttr删除售卖的属性
    removeAttr(index) {
      //再次整理参数
      this.searchParams.props.splice(index, 1);
      //再次发送请求
      this.getData();
    },

四十三、排序操作

分析api接口文档,发现searchParams中【order参数】就是用来指定排序方式的,下面讲讲它的具体含义

1表示综合;2表示价格;asc表示升序;desc表示降序,因此有如下四种组合:

1:asc        2:desc        1:desc        2:asc        (注意:初始状态为1:desc)


 上图是关于排序的两个位置(综合/价格),点击哪个位置,哪个位置就有对应的样式。由此我们知道,这个类是动态添加的。那怎么实现呢?这就需要用到【v-bind指令】喽~当isOne或isTwo为true时,li元素才拥有active这个类。


 上图中出现了箭头,先不考虑箭头的指向,什么时候箭头才出现呢?谁有类名active,谁就有箭头呗。根据这种关系,可以考虑使用【v-if】或者【v-show】

箭头可以使用【阿里巴巴矢量图标库】中的素材,具体的使用方法可以进行百度


但是上述过程只能通过手动修改order参数,才能控制显示效果,这肯定不能满足实际需求

因此还是要为【综合】和【价格】两个a标签绑定点击事件,


下面是具体实现的代码,需要注意的事项在注释中写出,一定要好好理解,

<li :class="{ active: isOne }" @click="changOrder('1')">
    <a>综合<span
               v-show="isOne"
               class="iconfont"
               :class="{
                   'icon-xiangshang': isAsc,
                   'icon-paixu': isDesc,
               }">
           </span>
    </a>
</li>
<li :class="{ active: isTwo }" @click="changOrder('2')">
    <a>综合<span
               v-show="isTwo"
               class="iconfont"
               :class="{
                   'icon-xiangshang': isAsc,
                   'icon-paixu': isDesc,
               }">
           </span>
    </a>
</li>
computed: {
    ......
    isOne() {
      return this.searchParams.order.indexOf("1") != -1; //返回值为布尔值
    },
    isTwo() {
      return this.searchParams.order.indexOf("2") != -1; //返回值为布尔值
    },
    isAsc() {
      return this.searchParams.order.indexOf("asc") != -1; //返回值为布尔值
    },
    isDesc() {
      return this.searchParams.order.indexOf("desc") != -1; //返回值为布尔值
    },
    ......
  },
methods:{
    //排序的操作
    changOrder(flag) {
      //flag形参:它是一个标记,代表用户点击的是综合还是价格 (用户点击的时候传递过来的)
      //这里获取的是最开始的状态【需要根据初始状态去判断接下来做什么】
      let originFlag = this.searchParams.order.split(":")[0];
      let originSort = this.searchParams.order.split(":")[1];
      //准备一个新的order属性值
      let newOrder = "";
      //这个语句能够确定这次点击和上次点击的地方是【同一个】,将排序颠倒过来
      if (flag == originFlag) {
        newOrder = `${originFlag}:${originSort == "desc" ? "asc" : "desc"}`;
      } else {
        //这次点击和上次点击的地方【不是同一个】,默认排序向下
        newOrder = `${flag}:${"desc"}`;
      }
      //将新的order赋予searchParams【重新赋值】
      this.searchParams.order = newOrder;
      //再次发送请求
      this.getData();
    },
}

四十四、分页器静态组件

因为分页器不止在一个地方使用,所以需要将分页器的内容作为全局组件来使用

因此在components文件夹下新建一个文件夹【Pagination】,用来存放分页器组件

并在main.js文件中引入该组件并注册,且在Search组件中使用该组件

分页器静态组件的内容在api开发接口文档中已经给出,直接使用就可以了,有些小地方需要进行修改,比较简单,这里就不再进行赘述了

四十五、分页功能分析

为什么很多项目采用分页功能?因为电商平台同时展示的数据有很多(上万条)

我们知道ElementUI实现了分页器,使用起来非常简单,但是在这个项目中不使用它,因为想锻炼一下自身是否掌握了【自定义分页器】的功能


实现分页器之前,先思考分页器都需要哪些数据(条件)呢?

1. 需要知道当前是第几页:pageNo字段代表当前页数

2. 需要知道每页需要展示多少条数据:pageSize字段

3. 需要知道分页器一共有多少条数据:total字段--【获取另外一条信息:一共多少页】

4. 需要知道分页器连续的页码个数:continues字段,一般是5或者7,为什么是奇数呢?因为对称,比较好看

举个栗子🌰:每一页有3条数据,一共91条数据,那么一共有30+1页


分页器组件是Search组件的一个子组件,上述这些数据需要Search组件传递给分页器组件,分页器拿到这些数据之后再去展示。父向子进行通信,这里使用props

注意:在开发的时候先自己传递假的数据进行调试,调试成功之后再用服务器的数据

<!-- 分页器 -->
<Pagination
   :pageNo="***"
   :pageSize="***"
   :total="***"
   :continues="***"
/>
export default {
    name: "Pagination",
    props:['pageNo','pageSize','total','continues'],
    ...
}

在上面的代码中


分页器组件拿到数据后,就可以利用这些数据进行页面数据的动态展示,比如“共X条”

<button style="margin-left: 30px">共{{total}}条</button>

除此之外,还可以知道最后一页的页数,利用Math的向上取整函数ceil( )

    computed:{
      //计算总共多少页
      totalPage(){
        //向上取整
        return Math.ceil(this.total/this.pageSize)
      },

对于分页器而言,很重要的一个地方是连续页面的【起始数字】和【结束数字】

举个栗子🌰:如果当前是第8页,连续页面数为5,那么起始数字和结束数字是6和10

下面代码中给出了两个非正常情况的处理过程,需要多理解

      //计算出连续的页码的起始数字与结束数字【连续页码的数字:至少是5】
      startNumAndEndNum(){
        const {continues,pageNo,totalPage} = this;
        //先定义两个变量存储起始数字和结束数字
        let start = 0,end = 0;
        //连续页码数字是5【至少5页】,如果出现不正常的现象【内容不够5页】
        //这是不正常的现象
        if(continues > totalPage){
          start = 1;
          end = totalPage;
        }else{
          //正常现象【连续页码5:但是你的页码一定是大于5】
          //起始数字
          start = pageNo - parseInt(continues/2);
          //结束数字
          end = pageNo + parseInt(continues/2);
          //把出现不正常的现象进行纠正【start数字出现0|负数】
          if(start < 1){
            start = 1;
            end = continues;
          }
          //把出现不正常的现象【end数字大于总页码】纠正
          if(end > totalPage){
            end = totalPage;
            start = totalPage - continues + 1;
          }
        }
        return {start,end};
      },

四十六、分页器动态展示

先补充一个知识点:v-for可以遍历【数组】、【数字】、【字符串】、【对象】

整个分页器分为【上中下】三个部分,如下图所示:

这里要分析几个特殊的情况:

1. 第一页什么时候才能出现

2. 第一个“...”什么时候才能出现

3. 最后一页什么时候才能出现

4. 最后一个“...”什么时候才能出现

<template>
  <div class="pagination">
    <button :disabled="pageNo == 1" @click="$emit('getPageNo',pageNo-1 )">上一页</button>
    <button v-if="startNumAndEndNum.start > 1" >1</button>
    <button v-if="startNumAndEndNum.start > 2">···</button>

    <!-- 中间部分 使用v-for的结果就是end之前的数字全部遍历出来了,因此需要使用v-if进行显示判断-->
    <button v-for="(page,index) in startNumAndEndNum.end" :key="index" 
            v-if="page >= startNumAndEndNum.start">{{page}}</button>

    <button v-if="startNumAndEndNum.end < totalPage - 1" >···</button>
    <button v-if="startNumAndEndNum.end < totalPage" >{{totalPage}}</button>
    <button :disabled="pageNo == totalPage" >下一页</button>

    <button style="margin-left: 30px">共{{total}}条</button>
  </div>

</template>

四十七、分页器完成

在前面,我们是先传递假的数据进行调试,现在调试已经完成,就需要使用服务器的数据了

首先Search组件向Pagination组件传递相应的数据

<!-- 分页器 -->
<Pagination
   :pageNo="searchParams.pageNo"
   :pageSize="searchParams.pageSize"
   :total="total"
   :continues="5"
/>

其中pageNo与pageSize可以从searchParams中获取,而total在searchParams中找不到,它是存放在search的小仓库vuex中的,因此可以通过mapState映射为组件身上的属性

computed:{
    ...
    //获取search模块展数产品一共有多少数据,这些数据存储在vuex中
    ...mapState({
      total: (state) => state.search.searchList.total,
    }),
}

在Pagination组件中,点击哪一页,就把相应的页数发送给Search组件,然后再由Search组件去请求对应页数的数据。这里涉及到子组件向父组件传递数据,可以考虑使用【自定义事件】。首先给Pagination组件添加一个自定义事件getPageNo,并设置响应的回调函数

<Pagination
     :pageNo="searchParams.pageNo"
     :pageSize="searchParams.pageSize"
     :total="total"
     :continues="5"
     @getPageNo="getPageNo"
/> 
methods:{
    ...
    //自定义事件的回调函数----获取当前点击的是第几页
    getPageNo(pageNo) {
      //整理带给服务器的参数
      this.searchParams.pageNo = pageNo;
      //再次发送请求
      this.getData();
    },
}

然后,Pagination子组件要触发自定义事件,这里需要明白哪些地方会触发自定义事件,即请求页面数据。比如上一页、下一页,各个页码,但要注意:省略号...不需要触发

这里要考虑一个问题:当页面处于第一页的时候,是没有“上一页”这个按钮的;当页面处于最后一页的时候,是没有“下一页”这个按钮的,那怎么去解决呢?这里就用到了【v-bind:disabled】

还需要考虑动态样式的问题,点击的地方会具备相应的样式,这里使用v-bind处理

  <div class="pagination">
    <button :disabled="pageNo == 1" @click="$emit('getPageNo',pageNo-1 )">上一页</button>
    <button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo',1)" :class="{active:pageNo==1}">1</button>
    <button v-if="startNumAndEndNum.start > 2">···</button>

    <!-- 中间部分 -->
    <button v-for="(page,index) in startNumAndEndNum.end" :key="index" v-if="page >= startNumAndEndNum.start" @click="$emit('getPageNo',page)" :class="{active:pageNo==page}">{{page}}</button>

    <button v-if="startNumAndEndNum.end < totalPage - 1" >···</button>
    <button v-if="startNumAndEndNum.end < totalPage" @click="$emit('getPageNo',totalPage)" >{{totalPage}}</button>
    <button :disabled="pageNo == totalPage"  @click="$emit('getPageNo',pageNo+1)" >下一页</button>

    <button style="margin-left: 30px">共{{total}}条</button>
  </div>

注意代码中的【$emit】和【:disabled】、【:class】相关内容,这是重点

四十八、滚动行为

首先在pages文件夹中创建一个详情页的组件Detail,再将其注册为路由组件

    //相关路由配置信息
    import Detail from "@/pages/Detail"
    
    {
        path:'/detail/:stuid',
        component:Detail, 
        meta:{show:true}
    },

当点击商品图片的时候,跳转到详情页面,在路由跳转的时候需要带上产品的ID

<div class="p-img">
                    
     <router-link :to="`/detail/${good.id}`">
         <img v-lazy="good.defaultImg" />
     </router-link>
          
</div>

这时有一个问题,当我们点击图片进入到详情页的时候,滚轮却不在页面的最顶部。那怎么才能控制滚轮在最顶部?

打开router文件夹中的index.js文件,发现VueRouter类的实例中:routes配置项的内容太多了,可以另外创建一个文件【routes.js】存放这些路由配置信息,然后再引入即可。

滚动行为相关的知识点可以在vue官网中找到,vue-Router下的进阶部分。滚动函数和路由配置信息是平级的,直接写在routes后面即可

//index.js文件
import routes from './routes'

//配置路由 对外暴露VueRouter类的实例
let router = new VueRouter({
    //配置路由 (k,v一致,可以省略v)
    routes,
    //滚动行为
    scrollBehavior(to,from,savedPosition){
        //返回的这个y=0,代表的滚动条在最上方
        return {y:0}
        
    }
});

export default router;

四十九、获取产品详情数据

第一步:先去api文件夹下的index.js文件中写获取产品详情数据的接口

//获取产品详情信息的接口 url:/api/item/{skuId} 请求方式:get
export const reqGoodsInfo = (stuId)=>requests(
    {
        url:`/item/${stuId}`,
        method:'get'
    }
)

第二步:实现vuex,获取产品详情数据

vuex中还需要新增一个模块detail(之前就介绍过该项目的vuex是由几个小模块的vuex组成的)

import {reqGoodsInfo} from "@/api"; 

const state = {
    goodInfo:{},
}
const mutations = {
    GETGOODSINFO(state,goodInfo){
        state.goodInfo = goodInfo
    }
}
const actions = {
    //获取产品信息的action,需要传递参数ID
    async getGoodInfo({commit},stuId){
       let result = await reqGoodsInfo(stuId)
       if(result.code == 200){
           commit('GETGOODSINFO',result.data)
       }
    },
}
export default{
    state,
    actions,
    mutations,
    getters
}

写完detail模块的vuex,不要忘记还要去大仓库的vuex进行【合并】

import Vue from 'vue'
import Vuex from 'vuex'
//需要使用插件一次
Vue.use(Vuex)
//引入小仓库
import home from './home'
import search from './search'
import detail from './detail'
//对外暴露Store类的一个实例
export default new Vuex.Store({
   //实现Vuex仓库模块式开发存储数据
   modules:{
       home,
       search,
       detail
   }
})

现在仓库中还没有数据,即goodInfo是一个空对象,因为没有派发(dispatch)action

在Detail组件挂载完成后就派发action,去获取相应的产品详情数据。其中的参数是通过路由传递过来的,可以通过$route获取。完成之后,detail模块的vuex就已经有请求回来的数据了

  mounted() {
    //派发action 获取商品的详细信息
    this.$store.dispatch("getGoodInfo", this.$route.params.stuid);
  },

五十、产品详情数据动态展示

获取到产品详情数据后,就需要进行动态展示了

在detail组件中有一些子组件,下图可以展示出detail组件内容的主要结构


 为了简化数据,这里使用getters

//这是在detail模块的vuex仓库中写的
//getters一般是为了简化数据
const getters = {
    //路径导航数据的简化
    categoryView(state){
        return state.goodInfo.categoryView
    },
    //产品信息数据的简化
    skuInfo(state){
        return state.goodInfo.skuInfo
    },
    //产品售卖属性的简化
    spuSaleAttrList(state){
        return state.goodInfo.spuSaleAttrList
    }
}
//这是在detail组件中写的,通过mapGetters将这些数据映射为组件自身的计算属性
import { mapGetters } from "vuex";

computed: {
    ...mapGetters(["categoryView", "skuInfo", "spuSaleAttrList"]),
},

下面就要使用这些数据进行动态展示了,例如【导航路径区域】,使用v-show来进行显示判断,如果有相应数据的话,我就展示,没有就不展示

<div class="conPoin">
  <span v-show="categoryView.category1Name">{{categoryView.category1Name}}</span>
  <span v-show="categoryView.category2Name">{{categoryView.category2Name}}</span>
  <span v-show="categoryView.category3Name">{{categoryView.category3Name}}</span>
</div>

虽然代码可以正常运行,但是控制台有警告信息,这是为什么呢?因为下面这段代码出现问题喽!

从代码中可以看出,categoryView函数return结果是:state.goodInfo.categoryView,但是state.goodInfo的初始状态是一个空对象,这时没有categoryView属性,会返回undefined,再去调用undefined的categoryXName属性,肯定会有报错。当服务器的数据回来之后,state.goodIfno不再是一个空对象了,也具备了categoryView属性,这时又正常了,但控制台会有警告信息出现。

为了不出现警告信息,返回值要改为这种形式。如果state.goodInfo的初始状态是一个空对象,则需要返回一个空对象,调用空对象的categoryXName属性,结果是undefined,好歹不会出现警告信息了。

//getters一般是为了简化数据
const getters = {
    //路径导航数据的简化
    categoryView(state){
        // 比如:state、goodInfo初始状态空对象,空对象的categoryView属性值undefined
        return state.goodInfo.categoryView || {}
    },
    //产品信息数据的简化
    skuInfo(state){
        return state.goodInfo.skuInfo || {}
    },
    //产品售卖属性的简化
    spuSaleAttrList(state){
        return state.goodInfo.spuSaleAttrList || {}
    }
}

接下来对其他数据再进行动态展示,方法和上述差不多,这里就不再进行赘述了

五十一、zoom放大镜展示数据

在前面的内容中,我们在detail组件中已经通过mapGetters从vuex中拿到了skuInfo数据,这是有关放大镜的数据

观察放大镜,可以发现它由两个部分组成:放大镜效果和小图列表。为了书写方便,将这两个部分拆分为两个子组件,分别是【Zoom组件】和【ImageList组件】

这两个子组件需要展示图片,那图片数据就在skuInfo中,此时父组件detail已经拿到了skuInfo。这时就涉及到了父组件向子组件传递数据了,这里使用【props】进行传递

<div class="previewWrap">
    <!--放大镜效果-->
    <Zoom :skuImageList="skuImageList" />
    <!-- 小图列表 -->
    <ImageList :skuImageList="skuImageList" />
</div>
computed: {
    ...mapGetters(["categoryView", "skuInfo", "spuSaleAttrList"]),
    //给子组件的数据
    skuImageList() {
      return this.skuInfo.skuImageList;
    },
},

Zoom组件通过props接收传递过来的数据

export default {
    name: "Zoom",
    props:['skuImageList'],
}

然后根据传递过来的数据,对zoom组件中的图片进行展示

  <div class="spec-preview">
    <img :src="skuImageList[0].imgUrl" />
    <div class="event"></div>
    <div class="big">
      <img :src="skuImageList[0].imgUrl" />
    </div>
    <!-- 遮罩层 -->
    <div class="mask"></div>
  </div>

此时页面虽然是正常的,不影响正常运行,但控制台出现了错误信息:这是因为刚开始时,组件挂载完毕后(mounted),skuImageList是undefined,所以获取undefined[0]的imgurl属性值就会报错。然后经过一段时间,skuImageList获取回来了,此时可以正常获取skuImageList[0]的imgurl属性值了

解决方法:detail组件给zoom子组件传递过来的数据至少是一个空数组,获取空数组的索引项,好歹会返回undefined,不会报错。但是这里要注意:如果skuImageList[0]的结果是undefined,那么skuImageList[0].imgurl还是会报错。因此如果获取skuImageList[0],至少要返回一个空对象。就算获取空对象的imgurl,也不会报错,结果是undefined。(有些套娃了,哈哈哈哈哈)

computed: {
    ...mapGetters(["categoryView", "skuInfo", "spuSaleAttrList"]),
    //给子组件的数据
    skuImageList() {
      //如果服务器数据没有回来,skuInfo这个对象是空对象
      return this.skuInfo.skuImageList || {};
    },
},
<template>
  <div class="spec-preview">
    <img :src="imgObj.imgUrl" />
    <div class="event"></div>
    <div class="big">
      <img :src="imgObj.imgUrl"/>
    </div>
    <!-- 遮罩层 -->
    <div class="mask"></div>
  </div>
</template>

<script>
  export default {
    name: "Zoom",
    props:['skuImageList'],
    computed:{
      imgObj(){
        //当data中的值发生改变时,计算属性会重新进行计算
        return this.skuImageList[this.currentIndex] || {}
      }
    },
  }

五十二、detail路由组件展示产品售卖属性

同样利用【props】,由detail组件向ImageList组件传递数据,这里就不再赘述了

在ImageList组件中需要实现【轮播图】,轮播图swiper相关的知识点在前面已经介绍过


在前面内容中,detail组件已经通过mapGetters拿到了vuex中的【spuSaleAttrList】数据了,里面包含产品售卖属性的相关数据。然后detail组件根据这些数据进行动态展示。

注意:样式class:active不应该写死;给定key值(不要被这么长的名称吓坏了,只是变量名比较复杂而已,逻辑上很容易理解,没有难度的)

<div class="chooseArea">
     <div class="choosed"></div>
     <dl v-for="(spuSaleAttr, index) in spuSaleAttrList" :key="spuSaleAttr.id">
         <dt class="title">{{ spuSaleAttr.saleAttrName }}</dt>
         <dd changepirce="0"
             :class="{ active: spuSaleAttrValue.isChecked == 1 }"
             v-for="spuSaleAttrValue in spuSaleAttr.spuSaleAttrValueList"
             :key="spuSaleAttrValue.id"
             @click="changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)">
             {{ spuSaleAttrValue.saleAttrValueName }}
         </dd>
     </dl>
</div>

五十二、产品售卖属性值排他操作

排他操作:当我们点击某个售卖属性时会出现高亮的效果,但是同类其他售卖属性却没有高亮效果

首先使用v-for遍历出全部的产品售卖属性,并为它们添加click点击事件,绑定相应的回调函数changeActive( ),并传入两个参数:一个是具体属性,例如:“8+128G”,另一个是包含所有属性的数组,例如:["6+128G", "8+128G"]

      <div class="chooseArea">
              <div class="choosed"></div>
              <dl
                v-for="(spuSaleAttr, index) in spuSaleAttrList"
                :key="spuSaleAttr.id"
              >
                <dt class="title">{{ spuSaleAttr.saleAttrName }}</dt>
                <dd
                  changepirce="0"
                  :class="{ active: spuSaleAttrValue.isChecked == 1 }"
                  v-for="spuSaleAttrValue in spuSaleAttr.spuSaleAttrValueList"
                  :key="spuSaleAttrValue.id"
                  @click="changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)"
                >
                  {{ spuSaleAttrValue.saleAttrValueName }}
                </dd>
              </dl>
      </div>

在methods中设置回调函数,先遍历全部产品售卖属性,将属性值isChecked设置为0,即此时都没有高亮,然后将点击的那个具体属性的isChecked属性值设置为1,即有了高亮效果

        methods: {
              //产品的售卖属性值切换高亮
              changeActive(saleAttrValue,arr){
                  //遍历全部售卖属性值isChecked为零,都没有高亮了
                  arr.forEach(item => {
                      item.isChecked = 0;
                  });
                  //点击的那个售卖属性值设置为高亮
                  saleAttrValue.isChecked = 1;
              },
        }

五十三、放大镜操作

首先完善轮播图,Swiper的具体使用步骤在前面已经讲过了,这里就不再进行叙述。

这里使用watch+nextTick( )方法

watch: {
    //监听数据:可以保证数据一定是ok的,但是不能保证v-for遍历结构是否完成
    skuImageList(newValue, oldValue) {
      this.$nextTick(() => {
        new Swiper(this.$refs.cur, {
          // 如果需要前进后退按钮
          navigation: {
            nextEl: ".swiper-button-next",
            prevEl: ".swiper-button-prev",
          },
          //显示几个图片的设置
          slidesPerView :3,
          //每次切换图片的个数
          slidesPerGroup : 1
        });
      });
    },
},

还要实现一个效果,就是点击哪个小图片,就给哪个小图片添加一个高亮的边框,表示选中状态

这里不考虑直接添加CSS样式去解决,而是使用JS的方式去实现这个效果:即点击谁,就给谁添加一个样式(这和三级联动中“移动到哪个分类,哪个分类就有高亮效果”是差不多的实现方式)

首先在data中添加一个名为:currentIndex 的数据,用来表征点击数据

然后为img标签添加一个动态类名,只有满足currentIndex==index时,才拥有active这个类。

并为img标签添加click事件,绑定回调函数changeCurrentIndex( ),并传入参数index

<img :src="slide.imgUrl" 
     :class="{active:currentIndex==index}" 
     @click="changeCurrentIndex(index)"/>

在methods中设置回调函数,修改响应式数据currentIndex,这时就可以实现点击谁,谁就有高亮的边框效果了

methods:{
    changeCurrentIndex(index){
      //修改响应式数据
      this.currentIndex = index
},

还没结束,还没结束......这时我们点击小图,但是放大镜不跟着变化,原因是二者没有建立必要的数据联系。二者还是兄弟关系,所以涉及到兄弟组件的通信机制,这里使用【全局事件总线】进行通信。在ImageList组件中通知兄弟组件Zoom:当前的索引值是多少

methods:{
    changeCurrentIndex(index){
      //修改响应式数据
      this.currentIndex = index
      //通知兄弟组件:当前的索引值为多少
      this.$bus.$emit('getIndex',this.currentIndex);
    }
},

兄弟组件Zoom接收传递过来的数据,并修改响应式数据currentIndex

mounted() {
      //全局事件总线:获取兄弟组件传递过来的索引值
      this.$bus.$on('getIndex',(index)=>{
        //修改当前响应式数据
        this.currentIndex = index
      })
},

然后对放大镜的图片内容进行修改

<div class="big">
      <img :src="imgObj.imgUrl" ref="big"/>
</div>


computed:{
      imgObj(){
        //当data中的值发生改变时,计算属性会重新进行计算
        return this.skuImageList[this.currentIndex] || {}
      }
},

那么放大镜的效果怎么实现呢?如下图所示,即将小方块的区域进行放大,且小方块是会随着鼠标而移动的。

首先给【原始大图所在的div】添加mousemove事件,绑定回调函数handler,并在methods中设置相应的回调函数,回调函数的内容:让蒙版跟着鼠标而移动。因此需要修改蒙版(图中绿色区域)的信息,所以给【蒙版所在的div】添加ref属性,可以方便地获取和修改蒙版的信息。此外,我们还需要根据蒙版的内容修改放大镜的内容,因此还要给【放大镜所在的div】添加ref属性,可以方便地获取和修改到放大镜的信息。

<template>
  <div class="spec-preview">
    <img :src="imgObj.imgUrl" />
    <!-- 绑定事件的地方 -->
    <div class="event" @mousemove="handler"></div>
    <!-- 放大的区域 -->
    <div class="big">
      <img :src="imgObj.imgUrl" ref="big"/>
    </div>
    <!-- 遮罩层(蒙版) -->
    <div class="mask" ref="mask"></div>
  </div>
</template>

 怎么测量出蒙版的具体位置呢?这里给出一张图帮助大家更好理解代码

。。。。。。(有空再画,哈哈哈)

methods: {
      handler(){
        //获取遮罩层
        let mask = this.$refs.mask
        let big = this.$refs.big
        //计算出left和top
        let left = event.offsetX - mask.offsetWidth/2;
        let top = event.offsetY - mask.offsetHeight/2;
        //约束范围
        if(left <= 0) left = 0;
        if(left >= mask.offsetWidth) left = mask.offsetWidth;
        if(top <= 0) top = 0;
        if(top >= mask.offsetHeight) top = mask.offsetHeight;
        //修改元素的left|top属性值
        mask.style.left = left + 'px';
        mask.style.top = top + 'px';
        big.style.left = -2 * left +'px'
        big.style.top = -2 * top + 'px'
      }
 },

五十四、购买产品个数的操作

这部分内容属于detail组件,先在data中创建一个名为skuNum的数据,表示购买产品的个数

在input中使用v-model,实现数据的双向绑定;然后对于“+”、“-”绑定点击事件,实现skuNum的加减操作,但需要注意:“-”操作时,如果skuNum<1,则不能再进行“-”操作了;

对input表单元素添加change事件(表单内容改变),这么操作主要是为了判断用户输入是否合法。

<div class="controls">
        <input autocomplete="off" class="itxt" v-model="skuNum" @change="changeSkuNum"/>            
        <a href="javascript:" class="plus" @click="skuNum++">+</a>
        <a href="javascript:" class="mins" @click="skuNum>1?skuNum--:skuNum=1">-</a>
</div>

在methods中设置change事件的回调函数,其中

methods:{
    ...

    //表单元素修改产品个数
    changeSkuNum(event){
      //用户输入进来的文本 * 1
      let value = event.target.value * 1
      //如果用户输入进来的是非法的:出现NAN或者小于1
      if(isNaN(value) || value < 1){
        this.skuNum = 1
      }else{ //如果用户输入的是合法的,value*1的结果仍然是value,不会产生额外的影响
        //结果必须是整数
        this.skuNum = parseInt(value)
      }
    }
}

PS:这里补充一下,在看视频时,发现弹幕中很多人提出这样的疑问:input输入框的内容不是和skuNum已经实现双向数据绑定了吗,为什么这里还要判断输入框的内容然后再赋值给skuNum呢?

看似这里有些矛盾,其实一点都不矛盾。这里是对用户输入的数据进行合法性判断,并将处理后的结果反映在input输入框中。如果这里没有实现数据的双向绑定,那么更改后的结果怎么传递给input输入框呢?

五十五、加入购物车

首先确定接口信息,通过查看api前台接口文档,有一个接口就是用来实现【添加到购物车】这个功能的,但是这个接口还有一个用途【对已有物品进行数量改动】

在api文件夹中index.js文件中配置这个接口的信息

//将产品添加到购物车中(获取更新某一个产品的个数)
export const reqAddOrUpdateShopCart =(stuId,skuNum) =>requests(
    {
        url:`/cart/addToCart/${stuId}/${skuNum}`,
        method:'post'
    }
)

接下来,就是万变不离其宗的【vuex三连环】,这部分属于detail组件,所以vuex内容要放在detail的vuex小模块中。

注意:这里其实并不进行完整的vuex三连环,因为服务器不返回数据,所以也就没有必要存储了

import {reqGoodsInfo,reqAddOrUpdateShopCart} from "@/api"; 
const state = {...}
const mutations = {...}
const actions = {
    ...
    //将产品添加到购物车中 || 修改某一个产品的个数
    async addOrUpdateShopCart({commit},{stuId,skuNum}){
       //点击“加入购物车”返回的结果
       //加入购物车之后(发请求),前台将参数带给服务器
       //服务器写入数据成功,并没有返回其他的数据,只是返回code=200, 代表这次操作成功
       //没有返回别的数据,因此仓库中不需要三连环存储数据
        let result = await reqAddOrUpdateShopCart(stuId,skuNum)
        //当前这个函数的执行结果是Promise对象
        if(result.code === 200){
            return "ok"
        }else{
            //代表加入购物车失败
            return Promise.reject(new Error('faile'));
        }
        
    }
}
//getters一般是为了简化数据
const getters = {...}
export default{
    state,
    actions,
    mutations,
    getters
}

PS:这里小伙伴可能会有疑惑:既然该请求不返回数据,也就没有必要存储数据了,那为什么还要将这部分的内容费劲写在vuex中?可以直接在click事件的回调函数中去发送请求,不是也ok吗?

这是因为官方建议所有的异步都要写在actions中,可以方便管理,不然的话,随着业务的扩展,组件代码变得越来越长。当然对于一些简单的非主任务来说,可以写在methods中


然后就需要触发dispatch actions,给detail组件的相关a标签绑定点击事件,并设置回调函数

<div class="add">
     <!-- 点击加入购物车,进行路由跳转之前,发送请求-->
     <!-- 把你购买的产品信息通过请求的形式通知服务器,服务器进行相应的存储 -->
     <a @click="addShopcar">加入购物车</a>
</div>

在加入购物车这个方法中,需要判断是否成功加入了购物车。

1. 如果成功了,则进行路由跳转,并携带skuNum参数。(创建addcartsuccess组件,以及注册路由的内容,比较简单,这里就不再进行赘述了)

2. 如果失败了,则给用户提示信息

有两种解决思路:

1. 将请求得到的result结果存储在detail小仓库里面,然后在detail组件的methods方法中可拿到这个值。根据这个值,就可以判断加入购物车是否成功了

2. 涉及到Promise知识点,代码中的this.$store.dispatch(...)返回的结果其实是一个【Promise对象】,且该对象的状态要不是成功的,要不是失败的。因此就可以根据这个状态来判断加入购物车是否成功了。(所以需要在actions()中写明白返回值是什么,上面的有关actions代码就是完整的)

methods:{
    ...
    //加入购物车
    async addShopcar(){
      // 1.发请求----将产品加入到数据库(通知服务器)
      // 需要判断加入购物车是成功了还是失败了
      try{
          await this.$store.dispatch('addOrUpdateShopCart',{
          stuId:this.$route.params.stuid,
          skuNum:this.skuNum
          });
          //路由跳转
          //在路由跳转的时候,还需要将产品的信息带给下一级的路由组件
          //一些简单的数据:通过query形式给路由组件传递过去
          //一些复杂的数据:通过会话存储
          sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo))
          this.$router.push({name:'addcartsuccess',query:{skuNum:this.skuNum}});
          //浏览器的存储功能是HTML5新增的,分为本地存储和会话存储
          //本地存储:持久化---5M
          //会话存储:非持久化---会话结束数据就消失
          //不管是本地存储还是会话存储,都不能存储对象,一般存储字符串
      }catch(error){
        alert(error.message)
      }
      // 2.服务器存储成功----进行路由跳转
      // 3.失败,给用户进行提示
    }
}

五十六、路由传递参数结合会话存储

在addcartsuccess组件中,需要展示购买产品的详细信息。在上节中,我们已经知道从detail组件跳转到该组件时,会携带路由参数skuNum,即产品数量。

有的小伙伴会有疑问:只携带路由参数skuNum,感觉不够用啊,还需要产品的其他信息,比如产品名称、颜色、内存等。正好detail的vuex仓库中有这些数据,在skuInfo字段中保存着。可以不可以把skuInfo对象也作为参数传递过去呢?

不可以,因为如果将skuInfo对象作为路由参数传递过去,能正常拿到数据,如下图所示:

 但是地址栏的内容是这样的,如下图所示,有些难看,不够美观。


如果想要美观的话,接下来的方法就涉及到【会话存储】的相关内容了,即路由传递参数仅携带skuNum,产品信息进行会话存储(不持久化,会话结束就消失,没必要进行持久化存储)

但是有一个问题,产品信息skuInfo是一个对象,但不管是本地存储还是会话存储,一般存储的是字符串,存储不了对象。如果存储的是一个对象,组件最终拿到的数据是这样的:[object, Object]那怎么解决呢?可以使用JSON.stringify()将对象转化为【字符串类型】的数据。

PS:上述内容的代码实现在上节中已经实现,可以参考上小节中给出的代码内容和注释

addcartsuccess组件拿到数据之后,进行数据的动态展示,这部分很简单,不再进行赘述

五十七、购物车静态组件与修改

在addcartsuccess购物车组件中,有两个按钮:【查看商品详情】和【去购物车结算】

对于【查看商品详情】这个按钮,要实现:点击后跳转到【商品详情页】,即detail组件

这里使用声明式路由导航:<router-link></router-link>,注意:需要携带产品的【id属性】

<div class="right-gocart">
     <!-- <a href="javascript:" class="sui-btn btn-xlarge">查看商品详情</a> -->
     <router-link class="sui-btn btn-xlarge" :to="`/detail/${skuInfo.id}`">查看商品详情
     </router-link>
</div>

对于【去购物车结算】这个按钮,点击后需要跳转到购物车组件,目前这个组件还没有,需要进行创建以及配置相关的路由信息。这部分很简单就不再详述了

同样这里使用声明式路由导航,且不需要携带任何参数

<div class="right-gocart">
          <!-- <a href="javascript:" class="sui-btn btn-xlarge">查看商品详情</a> -->
          <router-link class="sui-btn btn-xlarge" :to="`/detail/${skuInfo.id}`">查看商品详情</router-link>
          <!-- <a href="javascript:" >去购物车结算 > </a> -->
          <router-link class="sui-btn btn-xlarge" to="/shopcart">去购物车结算</router-link>
</div>

我们发现购物车静态组件的列表结构存在一些问题,需要进行修改:调整css让每个项目对齐,并删除第三项内容(“语音升级版”、“黑色版本”,“墨绿色”等)。下图是改变之后的页面结构。

(PS:这里不用特别纠结,主要是改变CSS样式,和项目的整体逻辑没有多大关系)

五十八、uuid游客身份获取购物车数据

上面的购物车中的列表数据肯定不是凭空捏造出来的,还是需要向服务器发请求获取数据。因此这里又要用到【写接口】和【vuex三连环】的内容了

首先从api前台接口文档中,找到请求地址、请求方法、参数类型等,设计该接口

//获取购物车列表数据的接口
export const reqCartList = ()=>requests({url:'/cart/cartList',method:'get'})

然后在store文件夹中新建一个名为shopcart.js的文件,表示是ShopCart购物车组件的vuex小仓库

import { reqCartList } from "@/api";
const state = {};
const actions = {
    //获取购物车列表的数据
    async getCartList({commit}){
        let result = await reqCartList()
        if(result.code == 200){
            commit("GETCARTLIST",result.data)  
        }
    },
};
const getters = {};
export default{
    state,
    mutations,
    actions,
    getters
}

(注意:这只是一个小仓库,还需要回到大仓库(store文件夹下的index.js中)进行合并,实现模块化开发)

在ShopCart组件中需要派发dispatch actions,不仅在mounted中需要派发,而且当产品数量发生变化时也需要派发。因此在methods中创建一个getData( )函数,用来dispatch actions,从而获取个人购物车的列表数据

export default {
    name: "ShopCart",
    mounted() {
       this.getData();
    },
    methods: {
       //获取个人购物车的数据
       getData() {
         this.$store.dispatch("getCartList");
       },
    }
}

但是经过测试发现,返回的数据竟然是空的!!!为什么是空的呢?这也不难理解:在前面的内容中,我们加入购物车时,仅仅携带了skuNum(产品数量)属性值,并且将skuInfo(产品信息)进行会话存储,实现数据的传递。但是这些数据中没有能够表明用户身份的信息!因此当你去获取购物车数据的时候,我怎么知道哪些数据是你的呢!

为了解决这个问题,可以采用token(令牌),但是这里先不用,而是采用uuid来标识【临时游客身份】,因此在点击”加入购物车“按钮的时候,还要告诉浏览器“你是谁?”,就是把创建出来的uuid传递给服务器。

而要使用uuid,得需要先进行下载。我们发现在node_modules文件夹中已经包含了有关uuid的文件夹(可能是因为别的依赖在使用uuid,所以也一同进行了下载)。所以我们无需下载了


我们查看api前台开发接口文档,发现加入购物车的相关接口只允许传入两个参数,分别是skuId和skuNum。那么用来标识用户身份的uuid怎么进行传递呢?答案:【使用请求头】!!!

请求头信息也可以被用来传递信息,因此在请求拦截器这里,为请求头添加uuid信息


具体的uuid创建出来后,需要进行持久化存储,,不能每次访问页面都使用不同的身份

因此在detail的vuex模块中,在state中设置一个叫做uuid_token的变量,然后通过调用uuid()函数,得到一个独一无二的值,赋给uuid_token变量(注意:这种方法会导致每次执行程序时,都会拥有一个新的uuid,这与我们的开发需求是违背的,所以这种方法是不可行的)

const state = {
    ...省略部分
    //游客的临时身份
    uuid_token:uuid() //uuid()就是用来生成一个独一无二的id的
}

解决方法:在src文件夹中新创建一个名为【utils】的文件夹,用来存放一些常用的功能模块,比如:正则表达式、临时身份uuid等。因此我们把uuid相关的内容存放在utils文件夹中,并暴露一个函数getUUID( ),这个函数会返回一个随机字符串,且这个随机字符串不能再变。

我们在detail的vuex模块中引入这个函数,并使用它,代码如下

//封装游客身份模块uuid--->生成一个随机的字符串(不能再变了)
import {getUUID} from '@/utils/uuid_token';
const state = {
    ...省略部分
    //游客的临时身份
    uuid_token:getUUID()
}

接下来就要实现getUUID( )函数了,其中重要的部分是要解决uuid不能每次函数执行都发生变化,可以先查看本地存储中是否已经有了,如果没有,则把刚开始创建出来的uuid存储在localStorage中(localStorage是持久化存储),如果有的话,则直接读取这个数据。最后将这个数据返回。

import {v4 as uuidv4} from 'uuid'
//要生成一个随机的字符串,并且每次执行不能发生变化,游客身份持久存储
export const getUUID  = ()=>{
    //先从本地存储获取uuid(看一下本地存储里面是否有)
    let uuid_token = localStorage.getItem('UUIDTOKEN');
    //如果没有怎么办
    if(!uuid_token){
        //我生成游客临时身份
        uuid_token = uuidv4();
        //本地存储一次
        localStorage.setItem('UUIDTOKEN',uuid_token);
    }
    //切记封装的函数要有返回值,否则返回undefined
    return uuid_token;
}

独一无二的uuid创建好之后,就可以在请求拦截器那里,给请求头信息携带上uuid了

首先request.js文件(请求拦截器所在的文件)需要获取store中的uuid数据,因此需要在文件中引入store(因为store本身就实现了暴露)

这里需要注意:在请求头中添加一个字段,这个字段必须和后台开发人员商量好,不能自己直接给一个字段,这样是不行的

//在当前模块中引入store
import store from '@/store'

//请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config)=>{
    //config:配置对象,对象里面有一个属性很重要,header请求头
    //进度条开始动
    nprogress.start();
    if(store.state.detail.uuid_token){
        //请求头中添加一个字段(userTempId),已经和后台老师商量好了
        config.headers.userTempId = store.state.detail.uuid_token
    }
    //需要携带token,将其带给服务器
    if(store.state.user.token){
        config.headers.token = store.state.user.token
    }
    return config;
});

五十九、购物车动态展示数据

经过上述的操作,此时已经能获取用户购物车的数据了,接下来就要进行数据的动态展示了

 通过上图,可以看到返回来的数据格式是比较复杂的:数组里面套了个对象,而对象其中一个属性值是一个数组,这个数组中存放的内容才是购物车的真实数据。


先把shopcart的vuex模块实现完整吧,拿到购物车数据之后,要进行存储,涉及到vuex三连环,这部分已经写过很多类似的了,不是很难,需要注意的就是数据结构的问题,因为数据结构比较复杂,所以需要搞清楚自己获取的是哪部分的数据,可以通过getters进行简化

import { reqCartList} from "@/api";
const state = {
    cartList:[],
};
const mutations = {
    GETCARTLIST(state,cartList){
        state.cartList = cartList;
    }
};
const actions = {
    //获取购物车列表的数据
    async getCartList({commit}){
        let result = await reqCartList()
        if(result.code == 200){
            commit("GETCARTLIST",result.data)
            
        }
    }
};
const getters = {
    cartList(state){
        return state.cartList[0] || {}
    }
};
export default{
    state,
    mutations,
    actions,
    getters
}

回到ShopCart组件中,就要使用vuex中的数据了。我们知道cartList其实是一个对象,而该对象的一个属性值才是真正的购物车数据,所以需要进一步处理

import { mapGetters } from "vuex";
...(省略部分)
computed: {
    ...mapGetters(["cartList"]),
    //购物车数据
    cartInfoList() {
      return this.cartList.cartInfoList || [];
    },
}

接下来就要进行ShopCart组件中数据的动态展示了,展示内容如下图所示:

 在template中,只保留了一个列表项(一件商品),然后通过v-for进行循环遍历,

<div class="cart-body">
   <ul class="cart-list" v-for="(cart, index) in cartInfoList" :key="cart.id>
   ......省略部分
   </ul>
</div>

对于复选框要不要勾选的情况,需根据isChecked的值进行判断,如果为1,则表示勾选;如果为0,则表示不勾选。因此需要给input元素动态添加checked属性

<li class="cart-list-con1">
    <input type="checkbox" name="chk_list"
           :checked="cart.isChecked == 1" @change="updateChecked(cart, $event)"/>
</li>

接下来,动态展示产品图片、产品标题以及产品价格

<li class="cart-list-con2">
    <img :src="cart.imgUrl" />
    <div class="item-msg">{{ cart.skuName }}</div>
</li>
<li class="cart-list-con4">
    <span class="price">{{ cart.skuPrice }}</span>
</li>

对于产品数量,要把默认值给去掉,其数据也是动态的,通过v-bind进行绑定

<li class="cart-list-con5">
     <input autocomplete="off" type="text" minnum="1" 
            class="itxt" :value="cart.skuNum"/>
</li>

对于小计(元),服务器返回来的数据是不包含这个数据,需要通过前端开发人员进行计算

<li class="cart-list-con6">
     <span class="sum">{{ cart.skuNum * cart.skuPrice }}</span>
</li>

对于总价,服务器返回来的数据也是不包含这个数据,需要通过前端开发人员进行计算,在computed中添加一个名为totalPrice()的计算属性,用来计算总价

<div class="sumprice">
   <em>总价(不含运费) :</em>
   <i class="summoney">{{ totalPrice }}</i>
</div>

computed: {
    ...mapGetters(["cartList"]),
    //购物车数据
    cartInfoList() {
      return this.cartList.cartInfoList || [];
    },
    //计算购买产品的总价
    totalPrice() {
      let sum = 0;
      this.cartInfoList.forEach((item) => {
        sum += item.skuNum * item.skuPrice;
      });
      return sum;
    }
 }

对于全选复选框,要判断是否勾选:如果每一个产品的isChecked都为1,则要勾选全选复选框。在computed中添加一个名为isAllCheck()的计算属性,用来判断复选框是不是全部选中

<div class="select-all">
        <input class="chooseAll" type="checkbox" :checked="isAllCheck"/>
        <span>全选</span>
</div>

computed: {
    ...mapGetters(["cartList"]),
    //购物车数据
    cartInfoList() {
      return this.cartList.cartInfoList || [];
    },
    //计算购买产品的总价
    totalPrice() {
      let sum = 0;
      this.cartInfoList.forEach((item) => {
        sum += item.skuNum * item.skuPrice;
      });
      return sum;
    },
    //判断复选框是不是全部选中
    isAllCheck() {
      //遍历数组里面的元素,只要全部元素isChecked属性都为1=====>真
      //只要有一个不是1=======>假
      return this.cartInfoList.every((item) => item.isChecked == 1);
    },
  },

六十、处理商品数量

在购物车部分,当我们输入产品数量以及点击商品数量的 +、- 时,需要向服务器发请求,告知服务器数据是怎么变化的。

有同学会有疑问:有必要这么麻烦吗,直接更改产品数量不就好了。这么做的原因是如果不发请求,只是改变数据的话,页面刷新后显示的还是原来的数据,这肯定不行的呀。

请求的接口和添加购物车的接口是同一个,需要携带skuId和skuNum两个参数,这里的skuNum并不是指产品数量了,而是现有数量与原有数量的【差值】(整数代表增加,负数代表减少)。比如:产品数量原本是10,在输入框改为16,那么skuNum为6;这时如果点击“-”,则skuNum为-1,如果点击“+”,则skuNum为1。

在前面接口的配置已经完成了,这里不再赘述。


不管是改变输入框的内容,还是点击“+”、“-”,都触发一个回调函数handler( ),用来派发actions向服务器发请求,以修改产品的个数。因此handler()函数要能够区分上述三种事件类型,因此在触发回调函数时可以传入一个类型参数type(mins, change, plus), 

handler()函数传入三个参数,分别是

1. 【type】:为了区分这三个操作的类型

2. 【disNum】:变化量(1); 变化量(-1);input最终的个数(并不是变化的量,放在回调函数内部进行处理)

3. 【cart】:确定是哪一个产品(身上有id)

<li class="cart-list-con5">
    <a href="javascript:void(0)" class="mins" @click="handler('mins', -1, cart)">-</a>
    <input autocomplete="off" type="text" minnum="1" class="itxt" :value="cart.skuNum"
           @change="handler('change', $event.target.value * 1, cart)" />
    <a href="javascript:void(0)" class="plus" @click="handler('plus', +1, cart)" >+</a>
</li>
methods:{
   handler(type,disNum,cart){
     switch (type) {
        //加号
        case "plus":
          //带给服务器变化的量
          disNum = 1;
          break;
        case "mins":
          // //判断产品的个数大于1:才可以传递给服务器-1
          // if(cart.skuNum > 1){
          //   disNum = -1;
          // }else{
          //   //产品的个数小于等于1
          //   disNum = 0;
          // }
          disNum = cart.skuNum > 1 ? -1 : 0;
          break;
        case "change":
          //用户输入的最终量,是非法的(带有汉字),带给服务器数字零
          if (isNaN(disNum) || disNum < 1) {
            disNum = 0;
          } else {
            //属于正常情况:如果是小数则取整,带给服务器变化的量,用户输入进来的量-产品的起始个数
            disNum = parseInt(disNum) - cart.skuNum;
          }
          break;
      }
   }
   //派发action
   this.$store.dispatch("addOrUpdateShopCart", {stuId:cart.skuId, skuNum:disNum});

}

但是有个问题,当我们更改数量之后,发现页面没有变动!这是因为我们只是向服务器发送请求,告知服务器产品数量是如何发生变化的。但是该接口不返回数据,因此我们只能再重新去请求购物车的数据。上述代码中派发actions的内容就可以修改为以下代码:

      //派发action
      try {
        //代表修改成功
        await this.$store.dispatch("addOrUpdateShopCart", {
          stuId: cart.skuId,
          skuNum: disNum,
        });
        //再一次获取服务器最新的数据进行展示
        this.getData();
      } catch (error) {
        console.log(error.message);
      }

六十一、删除购物车产品的操作

该部分的内容和上面的操作几乎是一样的,操作步骤为:

【写api接口】 ------> 【vuex模块开发】 ------> 【派发actions(发请求)】


首先,写api接口,请求方式为delete,发送请求时需要携带【skuId】参数

//删除购物车产品的接口
export const reqDeleteCartById = (skuId) => requests({
    url:`/cart/deleteCart/${skuId}`,
    method:'delete'
})

然后,进行vuex模块化开发,在shopcart组件的小仓库中实现。这里需要注意,该请求是没有结果的,因此不用进行vuex三连环,即不用改变仓库数据(不用写mutations和states两个部分)

import { reqCartList,reqDeleteCartById} from "@/api";

....

const actions = {
    ...省略部分
    //删除购物车的某一个产品
    async deleteCartListById({commit}, skuId){
        let result = await reqDeleteCartById(skuId)
        if(result.code == 200){
            return 'ok';
        }else{
            return Promise.reject(new Error('faile'));
        }
    },
}

接着,就需要在点击“删除”时,派发dispatch actions,向服务器发请求。首先给删除所在的a标签添加click事件,并设置相应的回调函数deleteCartById( ),参数为cart,即点击的那行购物车的信息。回调函数放在methods中。

<li class="cart-list-con7">
   <a class="sindelet" @click="deleteCartById(cart)">删除</a>
   <br />
   <a href="#none">移到收藏</a>
</li>
methods:{
    //删除某一个产品的操作
    async deleteCartById(cart) {
      try {
        //如果删除成功,再次发送请求获取新的数据进行展示
        await this.$store.dispatch("deleteCartListById", cart.skuId);
        this.getData();
      } catch (error) {
        alert(error.message);
      }
    },
}

但现在还有一个问题,当我们点击“-”太快时会出现0或者负数的现象,这里就要考虑使用【节流】技术。在第二十小节中已经介绍过防抖与节流技术,可以去看看哦。

步骤:引入lodash中的节流函数,再使用节流函数就可以了

import throttle from "lodash/throttle";

methods:{
    //修改某一个产品的个数【节流】
    handler: throttle(async function (type, disNum, cart) {
      
      ...省略部分
      
    }, 500),
}

六十二、修改产品勾选状态

产品勾选还要涉及到发请求,因为产品的勾选状态是保存在服务器数据中的,所以要把产品新的状态值传递给服务器,让它更新数据。

经典步骤:【写api接口】 ------> 【vuex模块开发】 ------> 【派发actions(发请求)】


首先,写api接口。请求方式为get,携带两个参数【skuId】和【isChecked】

//修改商品选中的状态
export const reqUpdateCheckedById =(skuId,isChecked) => requests({
    url:`/cart/checkCart/${skuId}/${isChecked}`,
    method:'get'
})

然后,进行vuex模块化开发,在shopcart组件的小仓库中实现。这里需要注意的是,同样地,该请求是没有结果的,因此不用进行vuex三连环,即不用改变仓库数据(不用写mutations和states两个部分)

import { reqCartList,reqDeleteCartById,reqUpdateCheckedById} from "@/api";

....

const actions = {

    ...省略部分
    //修改购物车某一个产品的选中状态
    async updateCheckedById({commit}, {skuId,isChecked}){
        let result = await reqUpdateCheckedById(skuId,isChecked)
        if(result.code==200){
            return 'ok';
        }else{
            return Promise.reject(new Error('faile'));
        }
    },
}

接着,在对勾选框进行操作时,派发dispatch actions,向服务器发请求。首先给勾选框input添加change事件,并设置相应的回调函数updateChecked( ),参数为cart和event。回调函数放在methods中。

<li class="cart-list-con1">
    <input type="checkbox" name="chk_list" :checked="cart.isChecked == 1"
           @change="updateChecked(cart, $event)"/>
</li>
methods:{
    //修改某一个产品的勾选状态
    async updateChecked(cart, event) {
      //带给服务器的参数不是布尔值,是0或者1
      try {
        //如果修改数据成功
        let checked = event.target.checked ? "1" : "0";
        await this.$store.dispatch("updateCheckedById", {
          skuId: cart.skuId,
          isChecked: checked,
        });
        this.getData();
      } catch (error) {
        //如果失败,进行提示
        alert(error.message);
      }
    },
}

六十三、删除全部选中的商品

其实并没有一次性删除很多产品的接口,但是有通过ID删除产品的接口(一次只能删一个)。因此,当我们多次调用接口就能实现删除多个产品了

(PS:正常情况下,项目的接口应该是设计好的,能实现一次性删除多个。这里主要是来考察promise.all( )方法)


先给【删除选中的商品】所在的a标签绑定点击事件,并设置回调函数deleteAllCheckedCart( ),在methods中实现

<div class="option">
    <a @click="deleteAllCheckedCart">删除选中的商品</a>
    <a href="#none">移到我的关注</a>
    <a href="#none">清除下柜商品</a>
</div>
    //删除全部选中的产品
    //这个回调函数没有办法收集到一些有用的数据
    async deleteAllCheckedCart() {
      //派发一个action
      try {
        await this.$store.dispatch("deleteAllCheckedCart");
        //再次发送请求
        this.getData();
      } catch (error) {
        alert(error.message);
      }
    },

上述代码中,我们可能会想着在deleteAllCheckedCart( )函数中,收集选中产品的ID,然后进行删除。但是产品的ID信息不经过额外操作是无法收集到的。因此我们可以考虑在shopcart的vuex模块中实现删除产品。


接下里就实现vuex开发了。在前面我们就已经分析过了,删除多个产品的接口实际是没有的,我们需要多次去调用删除单个产品的接口(即deleteCartListBySkuId接口,已实现)来实现这个功能。也就是说,我们要在一个action里派发另一个action了。

//删除全部勾选的产品
    deleteAllCheckedCart({dispatch,getters}){
         //context:小仓库 commit【提交mutations修改state】 getters【计算属性】dispatch【派发action】state【当前仓库数据】
         let PromiseAll = []
         getters.cartList.cartInfoList.forEach(item=>{
              //每次都返回一个Promise对象,只要其中一个失败,全部都失败
              let promise = item.isChecked==1? dispatch('deleteCartListById',item.skuId):'';
              //将每一次返回的Promise对象添加到数组当中
              PromiseAll.push(promise)
         });
         //z只要全部的结果都成功,结果就是成功的,如果有一个失败,返回即为失败的结果
         return Promise.all(PromiseAll)
    },

六十四、全部产品的勾选状态修改

先简单分析一下功能需求:当全选框勾选后,所有产品都被勾选,不管之前是勾选状态还是未勾选状态。这时如果再取消勾选,则所有产品都是未勾选状态


首先给全选框绑定一个change事件,并设置回调函数updateAllCartChecked( )

<div class="select-all">
    <input class="chooseAll" type="checkbox" 
           :checked="isAllChec"
           @change="updateAllCartChecked" />
    <span>全选</span>
</div>

接下来,创建一个变量表示全选框的状态,分别有0和1两种状态。当状态为0时,将所有产品勾选框的状态也设置为0;当状态为1时,将所有产品勾选框的状态也设置为1。派发action时,需要把这个变量传递过去。

    //修改全部产品的选中状态
    async updateAllCartChecked(event) {
         try {
              let ischecked = event.target.checked ? "1" : "0";
              //派发action
              await this.$store.dispatch("updateAllCartIsChecked", ischecked);
              this.getData();
         } catch (error) {
              alert(error.message);
         }
    },

然后,进行shopcart的vuex模块开发。与上节内容相同,需要在一个action里派发另一个action,遍历购物车的产品数据,将所有产品的勾选状态设置为传过来的那个变量,这个变量代表的就是全选框的勾选状态

    //修改全部产品的状态
    updateAllCartIsChecked({dispatch,state},isChecked){
          //数组
          let promiseAll = []
          state.cartList[0].cartInfoList.forEach(item=>{
              let promise = dispatch('updateCheckedById',{skuId:item.skuId,isChecked})
              promiseAll.push(promise)
          });
          //最终返回的结果
          return Promise.all(promiseAll)
    }

但这个时候,还有一个问题:此时全选框为勾选状态,我们删除所有产品后,此时购物车没有产品了,但全选框依旧为勾选状态,应该为不勾选状态才对。因此全选框为勾选状态时还有一个条件:购物车的数据必须大于0

<div class="select-all">
    <input class="chooseAll" type="checkbox" 
           :checked="isAllCheck&&cartInfoList.length > 0"
           @change="updateAllCartChecked" />
    <span>全选</span>
</div>

六十五、注册业务实现

(题外话:【登录与注册的功能】与【git】是前端开发人员必会技能)

这里先不做表单验证(比如说手机号格式对不对,输入的两次密码相同与否),主要实现整体的业务逻辑


注册页面的核心部分是这样的,如下图所示:

 首先,用户会输入手机号,点击“验证码”按钮,就会把手机号发送给服务端,然后由服务端去给用户的手机发送验证码。因此我们需要获取用户输入的手机号,可以使用【v-model】。此外,用户获取验证码之后,填入到页面的表单中,再把验证码发送给服务器,由服务器判断验证码是否一致。因此验证码也需要进行双向数据绑定。

<div class="content">
        <label>手机号:</label>
        <input type="text" placeholder="请输入你的手机号" v-model="phone" />
        <span class="error-msg">错误提示信息</span>
</div>
<div class="content">
        <label>验证码:</label>
        <input type="text" placeholder="请输入你的验证码" v-model="code" />
        <button style="width: 100px; height: 38px">获取验证码</button>
        <span class="error-msg">错误提示信息</span>
</div>

export default {
  name: "Register",
  data() {
    return {
      //手机号
      phone: "",
      //验证码
      code: ""
    };
  },

}

然后,设置获取验证码的接口,请求方式为get请求,需要传入一个参数:手机号

//获取验证码
export const reqGetCode = (phone)=>requests({
      url:`/user/passport/sendCode/${phone}`,
      method:'get'
})

接下来,创建登录与注册模块的vuex小仓库,不要忘记还要在vuex大仓库进行合并,前面章节中已经写过相关内容,比较简单,这里就不再进行赘述了。在小仓库中进行vuex三连环,设置获取验证码的action,mutation以及state

​
//登录与注册的模块
import { reqGetCode } from '@/api';
const state = {
    code:'',
};
const mutations = {
    GETCODE(state,code){
        state.code = code
    }
};
const actions = {
    //获取验证码
    async getCode({commit},phone){
        //获取验证码的这个接口,把验证码返回了,但是正常情况是后台把验证码发到用户手机上【省钱】
       let result = await reqGetCode(phone);
       if(result.code === 200){
           commit("GETCODE",result.data);
           return 'ok'
       }else{
           return Promise.reject(new Error('faile'));
       }
    }
};
const getters = {};

export default{
    state,
    mutations,
    actions,
    getters
}

​

然后,给“获取验证码”绑定点击事件,并在methods中设置回调函数getCode( ),在其中派发action

<button style="width: 100px; height: 38px" @click="getCode">获取验证码</button>
methods: {
    //获取验证码
    async getCode() {
      //简单判断一下---至少用数据
      try {
        //如果获取到验证码
        const { phone } = this;
        phone && (await this.$store.dispatch("getCode", phone));
        //将组件的code属性值变为仓库中的验证码
        this.code = this.$store.state.user.code;
      } catch (error) {
        alert(error.message);
      }
      console.log();
},

手机和验证码的业务已经实现,接下来就要实现【登录密码】和【确认密码】了。同样这两个内容都是要实现数据的双向数据绑定,因此采用v-model。而“同意协议并注册”这里,也需要获取用户是否同意,因此在data中设置一个名为agree的变量,用来表征。

  <div class="content">
        <label>登录密码:</label>
        <input type="text" placeholder="请输入你的密码" v-model="password" />
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="content">
        <label>确认密码:</label>
        <input type="text" placeholder="请输入你的确认密码" v-model="password1" />
        <span class="error-msg">错误提示信息</span>
      </div>
      <div class="controls">
        <input type="checkbox" v-model="agree" />
        <span>同意协议并注册《尚品汇用户协议》</span>
        <span class="error-msg">错误提示信息</span>
  </div>
export default {
  name: "Register",
  data() {
    return {
      //手机号
      phone: "",
      //验证码
      code: "",
      //密码
      password: "",
      //确认密码
      password1: "",
      //是否同意
      agree: true,
    };
  }
}

当上述四个部分都填入正确格式的数据后,当我们点击“完成注册”按钮,则会跳转到登录页面。因此要给这个按钮绑定一个点击事件,并在methods中设置相应的回调函数。并且在回调函数中需要发送请求,把表单的数据传递给服务器,实现用户数据的写入。

<div class="btn">
    <button @click="userRegister">完成注册</button>
</div>
methods:{
    userRegister() {
        //去发请求,其实是派发action
    }
}

实现请求接口,请求方式为post类型,携带请求体参数data

//用户注册 --->携带请求体参数
export const reqUserRegister = (data)=>requests({
    url:'/user/passport/register',
    data, //简写(k,v一致,省略v)
    method:'post'
})

在登录与注册的vue小仓库中实现vuex三连环,需要注意的是,这里并不进行完整的三连环,只需要实现action就可以了,因为该请求并不返回数据

//登录与注册的模块
import { reqGetCode, reqUserRegister} from '@/api';
const state = {
    code:'',
};
const mutations = {
    GETCODE(state,code){
        state.code = code
    }
};
const actions = {
    //获取验证码
    async getCode({commit},phone){
        //获取验证码的这个接口,把验证码返回了,但是正常情况是后台把验证码发到用户手机上【省钱】
       let result = await reqGetCode(phone);
       if(result.code === 200){
           commit("GETCODE",result.data);
           return 'ok'
       }else{
           return Promise.reject(new Error('faile'));
       }
    },
    //用户注册
    async userRegister({commit},user){
       let result =  await reqUserRegister(user);
       if(result.code==200){
           return 'ok';
       }else{
           return Promise.reject(new Error('faile'));
       }
    },
};
const getters = {};

export default{
    state,
    mutations,
    actions,
    getters
}

在点击事件的回调函数中派发action,如果请求成功,则进行路由跳转,否则提示错误信息

methods:{
    async userRegister() {
        try {
            //如果成功----路由跳转
            const { phone, code, password, password1 } = this;
            (phone&&code&&password==password1)&& await this.$store.dispatch("userRegister", { phone, code, password });
            //注册成功进行路由的跳转
            this.$router.push("/login");
        } catch (error) {
            alert(error.message);
        }
    },
}

六十六、登录业务

先理一下业务逻辑,用户输入账号和密码,点击登录按钮后,前端获取表单数据后发送给服务器,服务器判定是否存在该用户,以及密码是否正确。如果判断是对的,则跳转到home页面,否则进行错误提示。


首先,在登录组件中,要获取表单数据,实现双向数据绑定,因此要用到v-model

<div class="input-text clearFix">
     <span></span>
     <input type="text" placeholder="邮箱/用户名/手机号" v-model="phone">
</div>
<div class="input-text clearFix">
     <span class="pwd"></span>
     <input type="text" placeholder="请输入密码" v-model="password">
</div>
export default {
    name: 'Login',
    data() {
      return {
        phone: '',
        password:'',
      };
    }
}

然后,实现登录请求接口,请求方式为post类型,携带请求体参数data

//用户登录
export const reqUserlogin = (data)=>requests({
    url:'/user/passport/login',
    data, //简写(k,v一致,省略v)
    method:'post'
})

接着,在登录与注册的vue小仓库中实现vuex三连环。当用户登录成功的时候,服务器为了区分这个用户是谁,因此服务器下发token【令牌:用户唯一标识符】,它是一个随机字符串。

服务器返回的数据如下图所示,这个接口实现得并不完美,一般情况下,登录接口请求回来的数据只有token字段,其他用户信息是不会给的。前端需要持久化存储token(***),再携带oken去服务器请求用户信息。这里直接把token和用户信息都返回了。但是我们还是根据较好的思路来实现,即先获取token,再借助token去请求获取用户信息

(PS:目前有很多网站都使用了token,例如码云,GitHub)

//登录与注册的模块
import { reqGetCode ,reqUserRegister,reqUserlogin} from '@/api';
const state = {
    code:'',
    token:''
};
const mutations = {
    GETCODE(state,code){
        state.code = code
    },
    USERLOGIN(state,token){
        state.token = token
    }
};
const actions = {
    //获取验证码
    async getCode({commit},phone){
        //获取验证码的这个接口,把验证码返回了,但是正常情况是后台把验证码发到用户手机上【省钱】
       let result = await reqGetCode(phone);
       if(result.code === 200){
           commit("GETCODE",result.data);
           return 'ok'
       }else{
           return Promise.reject(new Error('faile'));
       }
    },
    //用户注册
    async userRegister({commit},user){
       let result =  await reqUserRegister(user);
       if(result.code==200){
           return 'ok';
       }else{
           return Promise.reject(new Error('faile'));
       }
    },
    //用户登录【token】
    async userLogin({commit},user){
        let result = await reqUserlogin(user);
        //服务器下发token,用户唯一标识符(uuid)
        //将来经常通过带token找服务器要用户信息进行展示
        if(result.code==200){
            //用户已经登录成功并且获取到token
            commit("USERLOGIN",result.data.token);
            return 'ok';
        }else{
            return Promise.reject(new Error('faile'));
        }
    }
};
const getters = {};

export default{
    state,
    mutations,
    actions,
    getters
}

然后,在login组件中派发action,先给“登录”按钮绑定一个点击事件,并在methods中设置回调函数(注意:要阻止form表单的默认行为,使用prevent)。当我们点击按钮时,如果请求成功,就能跳转到home页面,并且vuex小仓库能获取到token值

<button class="btn" @click.prevent="userLogin">登&nbsp;&nbsp;录</button>

methods: {
      //登录的回调函数
      async userLogin(){
        try {
          //登录成功
          const {phone,password} = this;
          (phone && password) && await this.$store.dispatch('userLogin',{phone,password})
          //跳转到home首页
          this.$router.push("/home")
        } catch (error) {
          alert(error.message)
        }
      }
    },

但是!!!vuex仓库中的数据不是持久化存储的,当我们刷新页面后,获取到的token值就没了,为空。此外,如果登录成功了,home主页左上端应该显示【用户名|退出】,但是现在还是显示【请登录|免费注册】

我们先一个个去解决上述这个问题,先解决home主页左上端显示问题。根据token获取用户信息,要先设计api接口,请求方式为get类型,不需要携带参数,token则在请求头中携带着发送过去(在请求拦截器中实现)。

//获取用户的信息【携带token】
export const reqUserInfo = ()=>requests({
    url:'/user/passport/auth/getUserInfo',
    method:'get'
})
//请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config)=>{
    //config:配置对象,对象里面有一个属性很重要,header请求头
    //进度条开始动
    nprogress.start();
    if(store.state.detail.uuid_token){
        //请求头中添加一个字段(userTempId),已经和后台老师商量好了
        config.headers.userTempId = store.state.detail.uuid_token
    }
    //需要携带token,将其带给服务器
    if(store.state.user.token){
        config.headers.token = store.state.user.token
    }
    return config;
});

(PS:有些同学可能会有疑惑:为什么发送token要放在请求拦截器中实现?)

答:我自己理解起来主要有两点原因,仅供参考

1. 因为一旦有了token,接下来一些页面的内容就得根据这个特定用户进行展示,因此我们需要在一些请求中携带token(用户标识符),来获取用户独一无二的数据。放在请求拦截器中,不管需不需要token,只要有,我就发给服务器。这样统一处理更加方便

2. 在项目中,我们一般都是对axios进行二次封装,二次封装主要是为了实现请求/相应拦截器,在请求拦截器中有一个非常重要的配置对象属性,即请求头,而token正好是是放在请求头中携带发送出去的,使用起来很方便。


接下来就要实现vuex三连环了,

//登录与注册的模块
import { reqGetCode ,reqUserRegister,reqUserlogin,reqUserInfo} from '@/api';

const state = {
    ...
    userInfo:''
};
const mutations = {
    ...
    GETUSERINFO(state,userInfo){
        state.userInfo = userInfo
    }
};
const actions = {
    ...
    //获取用户信息
    async getUserInfo({commit}){
        let result = await reqUserInfo();
        if(result.code==200){
            commit('GETUSERINFO',result.data);
            return 'ok';
        }else{
            return Promise.reject(new Error('faile'));
        }
    }
};
const getters = {};

export default{
    state,
    mutations,
    actions,
    getters
}

注意:在getUserInfo中,需不需要对【能否获取用户信息】进行条件判断。其实都可以,,这里是实现了的,如果不实现的话,之后我们去使用数据时发现并没有,也可以判断为“未能成功获取用户信息”。另外,有些同学查看了reqUserInfo( )接口地址,发现其中并没有携带token信息,这是因为在请求拦截器中我们已经实现了把token发给服务器了。

那么上述的action什么时候派发合适呢?即在home组件挂载时,派发action,获取用户信息

export default {
    name:'HomeIndex',
    ...
    mounted() {
      //获取用户信息,并在首页进行展示
      this.$store.dispatch('getUserInfo');
    },
    ...
}

当用户登录成功后,页面要发生一些变动,此时下图中的“请登录|免费注册”应该改为用户名

 去Header组件中进行修改,使用【v-if】根据情况来判断是否显示“请登录|免费注册”,还要使用【v-else】来判断是否显示“用户名|退出”。当然,用户的信息是存在user小仓库中,header需要从仓库中读取。

<div class="loginList">
    <p>尚品汇欢迎您!</p>
    <!-- 没有用户名,需要登录 -->
    <p v-if="!userName">
        <span>请</span>
        <!-- 声明式导航:务必要有to属性 -->
        <router-link to="/login">登录</router-link>
        <router-link class="register" to="/register">免费注册</router-link>
    </p>
    <!-- 登录成功了 -->
    <p v-else>
        <a>{{userName}}</a>
        <a class="register" @click="logout">退出</a>
    </p>
</div>

export default {
  name:"HeaderIndex",
  ...
  computed:{
      //用户名信息
      userName(){
        return this.$store.state.user.userInfo.name;
      }
  }
}

至此,第一个问题解决了,还剩另一问题:vuex仓库中的数据不是持久化存储的,当我们刷新页面后,获取到的token值就没了,为空。因此我们要持久化存储token,说到持久化存储就要想到【localStorage】喽,在user小仓库中修改登录action:增加一个额外步骤,即把token存储在localStorage里。

const actions = {
    ...
    //用户登录【token】
    async userLogin({commit},user){
        let result = await reqUserlogin(user);
        //服务器下发token,用户唯一标识符(uuid)
        //将来经常通过带token找服务器要用户信息进行展示
        if(result.code==200){
            //用户已经登录成功并且获取到token
            commit("USERLOGIN",result.data.token);
            //持久化存储token
            localStorage.setItem("TOKEN", result.data.token)
            return 'ok';
        }else{
            return Promise.reject(new Error('faile'));
        }
    }
};

有些程序员不会直接这样写,会把有关token的内容专门放在一个文件中。于是,我们在utils中创建一个名为token.js的文件,专门用来存放有关token的内容,暴露函数,以供外面使用。

//对外暴露一个函数
//存储token
export const setToken = (token)=>{
    localStorage.setItem("TOKEN",token)
};

//获取token
export const getToken = () =>{
    return localStorage.getItem("TOKEN")
};

userLogin action中的代码就可以修改成下面这样

import {setToken} from '@/utils/token'

const actions = {
    ...
    //用户登录【token】
    async userLogin({commit},user){
        let result = await reqUserlogin(user);
        //服务器下发token,用户唯一标识符(uuid)
        //将来经常通过带token找服务器要用户信息进行展示
        if(result.code==200){
            //用户已经登录成功并且获取到token
            commit("USERLOGIN",result.data.token);
            //持久化存储token
            setToken(result.data.token)
            return 'ok';
        }else{
            return Promise.reject(new Error('faile'));
        }
    }
};

但是现在一刷页面,token还是丢失了,这是因为我们现在只是持久化了token,但是没有用它呢!页面刷新后的请求是没有携带token的。找到user小仓库,在此进行分析

const state = {
    code:'',
    token:'',
    userInfo:''
};

我们能够发现,state中token字段的初始值为空,也就意味着:当我们刷新页面时,仓库中的值会进行初始化,token值为' ',因此无法获取用户信息。因此在这里,我们要获取持久化存储的token

const state = {
    code:'',
    token:getToken(),
    userInfo:''
};

还没有结束,还存在问题!哈哈哈,如果我们从home主页跳转到其他页面,比如详情页,这时我们再刷新页面,还是未登录状态,token还是丢失了。

这是因为在home组件中,在mounted( )中去派发action,获取用户信息,这就意味着:页面刷新后,组件重新挂载后会去获取用户信息,而token是持久化存储,不会丢失的。而在detail组件中,当组件重新挂载时,并没有再去派发action去请求数据,就算token持久化存储了,但是也没去获取它,因而token为空。

解决方法:

第一种:像home组件一样去处理,但是我们需要一一去处理很多组件,非常繁琐,不推荐

第二种:让APP组件挂载(mounted)时派发action,获取用户信息并持久化存储,这样我们不用单独为很多组件派发action了。但是这种方法有缺陷。就是当我们登录后,其实也是显示未登录状态,且也拿不到token。这时只需要点击刷新按钮就可以获取到token了,且是登录状态。

(PS:其实这个问题是没有得到完美解决的,这里先不说了,之后会再给出好的解决方案)

六十七、退出登录

当用户退出时,需要向服务器发送请求,服务器需要清除用户数据。

先找到“登录”所在的a标签,绑定点击事件,并在methods中设置回调函数logout。

<p v-else>
     <a>{{userName}}</a>
     <a class="register" @click="logout">退出</a>
</p>
   
methods: {
      logout(){
         //退出登录需要做的事情
         //1.需要发请求,通知服务器退出登录【清除一些数据:token】
         //2.清除项目砀中的数据【比如用户信息userInfo、token】
      }
      
   },

然后设计接口,请求方式为get类型,且不需要携带参数

//退出登录
export const reqLogout = ()=>requests({
    url:'/user/passport/logout',
    method:'get'
});

接着在user小仓库中实现vuex三连环,注意:虽然请求并不返回数据 ,但这里还需要实现完整的vuex三连环,因为要清空state中已有的token数据。清除token的操作放在token.js中实现和暴露,这里直接引入和使用就可以了

//对外暴露一个函数
//存储token
export const setToken = (token)=>{
    localStorage.setItem("TOKEN",token)
};

//获取token
export const getToken = () =>{
    return localStorage.getItem("TOKEN")
};

//清除本地存储的token
export const removeToken = ()=>{
    localStorage.removeItem("TOKEN")
}
//登录与注册的模块
import { reqGetCode ,reqUserRegister,reqUserlogin,reqUserInfo,reqLogout} from '@/api';
import {setToken,getToken,removeToken} from "@/utils/token"

const state = {
    code:'',
    token:getToken(),
    userInfo:''
};
const mutations = {
    ...
    CLEARUSERINFO(state){
        //把仓库中相关用户信息清空
        state.token = '';
        state.userInfo = {};
        //本地存储数据清空
        removeToken();
    }
};
const actions = {
    ....

    //退出登录
    async userLogout({commit}){
        //只是向服务器发起一次请求,通知服务器清除token
        let result = await reqLogout();
        //action里面不能操作state,提交mutation修改state
        if(result.code == 200){
            commit("CLEARUSERINFO");
            return 'ok';
        }else{
            return Promise.reject(new Error('faile'));
        }
    }
};
const getters = {};

export default{
    state,
    mutations,
    actions,
    getters
}

在logout函数中中派发action,需要返回成功或失败。为什么需要判断是不是成功或失败呢?这是因为如果成功的话需要返回首页,而不是停留在当前页面。因此在userLogout( )函数需要有返回值,且返回值是promise类型。

methods: {
    ...

    //退出登录
    async logout(){
        //退出登录需要做的事情
        //1.需要发请求,通知服务器退出登录【清除一些数据,token】
        //清除项目当中的数据
        try {
            //如果退出成功
            await this.$store.dispatch('userLogout');
            //回到首页
            this.$router.push('/home')
        } catch (error) {
            alert(error.message)
        }
    }
}

六十七、导航守卫

对【导航守卫】不太清楚的同学,可以看一下我的另一篇笔记,包含路由很多的知识点,其中就有导航守卫 。链接地址:vue路由知识点概括--思维导图_yuran1的博客-CSDN博客


要解决的问题:

1. 未登录状态下,用户是不能访问购物车页面的

2. 登录状态下,用户是不能访问登录页面的


首先,在全局前置守卫中,限制登录状态下用户是不能访问登录页面。那怎么才能判断用户是否登录了呢?只要vuex中有了token,就代表用户已经登陆了。因此我们需要在全局前置守卫中拿到token。于是引入store,

import store from '@/store'

......

//全局守卫:前置守卫(在路由跳转之间进行判断)
router.beforeEach(async(to,from,next)=>{
    //to:可以获取到你要跳转的那个路由信息
    //from:可以获取到你从哪个路由而来的信息
    //next:放行函数,有时候要加入一些条件才可以放行,
    //next(path):放行到指定的路由
    //next(false):中断
    //为了测试,先全部放行
    // next();
    
    //用户登陆了,才会有token,未登录一定不会有token
    let token = store.state.user.token
    //用户的信息
    //空对象的布尔值永远是1,因此不能直接用空对象进行判断,要用值去判断
    // let userInfo = store.state.user.userInfo
    let name = store.state.user.userInfo.name
    //用户已经登录了
    if(token){
        //用户已经登录了,还想去login---不可以,让其停留在首页
        if(to.path=='/login'){
            next('/')//根页面,也就是主页
        }else{//用户已经登录了,但是去的不是login页面
            //如果用户名已经有了
            if(name){
                next();
            }else{
                //没有用户信息,派发action让仓库存储用户信息再跳转
                try {
                    //获取用户信息成功
                    await store.dispatch('getUserInfo')
                    //放行
                    next()
                } catch (error) {
                    //如果获取用户信息失败,token失效了(如身份过期等原因)
                    //清除token
                    await store.dispatch('userLogout');
                    next('/login')
                    alert(error.message)
                }
            }  
        }
    }else{
        //未登录,不能去交易相关的、支付相关的、个人中心
        //未登录状态去上面这些路由----应先登录
        let toPath = to.path
        if(toPath.indexOf('/trade')!=-1 || toPath.indexOf('/pay')!=-1 || toPath.indexOf('/center')!=-1 ){
            //把未登录的时候想去但是没有去成的信息,存储在地址栏中【路由】
            next('/login?redirect='+toPath)
        }else{
            next();
        }
        //去的不是上面这些路由(home|search|shopCart),应该放行
    }
})

需要注意:有了token,并不代表有了用户信息。这里就要解决这个问题了:如果我们从home主页跳转到其他页面,比如详情页,这时我们再刷新页面,还是未登录状态,token还是丢失了。

这是因为在home组件中,在mounted( )中去派发action,获取用户信息,这就意味着:页面刷新后,组件重新挂载后会去获取用户信息,而token是持久化存储,不会丢失的。而在detail组件中,当组件重新挂载时,并没有再去派发action去请求数据。因而用户信息为空。

刷新其实就是自己跳自己。因此我们就利用【全局前置守卫】在跳转到其他组件之前去判断有没有用户信息,如果没有的话则派发action。


感谢大家的支持,小菜鸡博主最近忙着找工作和修改毕业论文,这篇文章可能会更新慢一下,但不久之后一定会继续写!!

  • 13
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值