项目网络教学视频链接:尚硅谷VUE项目实战,前端项目-尚品汇(大型\重磅)_哔哩哔哩_bilibili
目录
三十、轮播图:watch+nextTick( )(第二种解决方案)
一、 使用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中文网
关于使用过程,官网给的教程非常详细,自己看看实际操作一下,这里就不再赘述了。
需要注意:
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">登 录</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。
感谢大家的支持,小菜鸡博主最近忙着找工作和修改毕业论文,这篇文章可能会更新慢一下,但不久之后一定会继续写!!