一、Home模块拆分
通过对Home模块页面的分析,Home模块可包括7个组件
1. 三级联动组件TypeNav
由于这个组件在Home页面、Search页面、Detail页面中都使用了,所以把该组件注册为全局组件。
好处:只需要注册一次,就可以在任何地方使用
全局注册:Vue.component('组件名',组件)
2. 其余组件
拆分的时候注意样式CSS、结构HTML及图片资源
记录一个Bug,在ListContainer组件里,右侧的广告图片不显示(宋仲基那个图)
原因是浏览器安装的屏蔽广告插件把这个广告屏蔽了,所以报错。
参考博客:GET http://localhost:8080/***.png net::ERR_BLOCKED_BY_CLIENT 无法加载图片_localhost图片加载不出来-CSDN博客
二、发送请求的准备工作
1. axios的二次封装
(1)、为什么要对axios进行二次封装
对axios二次封装可配置响应拦截器、请求拦截器:分别在收到服务器数据时或进行发送请求操作之前进行一些业务逻辑处理。
(2)、二次封装
-
安装axios:
npm install axios
-
创建
src/api/request.js
文件夹,进行二次封装// 本文件用于对axios二次封装 import axios from 'axios' // 1. 利用axios方法create创建一个axios实例 let requests = axios.create({ // 配置实例属性 // 1.1 基础路径,因为请求路径里都有api,所以这里设置基础路径 baseURL: '/api', // 1.2 设置请求超时的时间。请求时间超过5s,则请求失败 timeout: 5000 }); // 2. 配置请求拦截器 // 请求拦截器检测到请求,在请求发出去之前做一些事情 requests.interceptors.request.use((config) => { // config:配置对象,其中header请求头属性很重要 return config; }) // 3. 配置响应拦截器 requests.interceptors.response.use((res) => { // 请求成功的回调函数 return res.data }, (error) => { // 响应失败的回调 return Promise.reject(new Error('falie')) }) // 暴露axios实例 export default requests
对于axios,可参考NPM中关于axios的文档
2. 统一管理接口API----跨域
创建文件src/api/index.js
,封装好发送请求的方法,发送请求时直接调用方法即可。
// src/api/index.js
// 本文件对于API接口进行统一管理
import requests from './request'
// 三级联动
// 请求地址:/api/product/getBaseCategoryList,请求方法:get
export const reqCategoryList = () => {
// 发请求:axios发送请求返回结果Promise对象;
//axios二次封装里以及写了basedUrl,所以这里就不用写/api了
return requests({ url: '/product/getBaseCategoryList', method: 'get' })
测试方法:
// main.js
import { reqCategoryList } from './api'
reqCategoryList()
(1)、测试遇到的跨域问题:
跨域是指请求的 协议、域名、端口号不同
前端项目本地服务器:http://localhost:8080/#/home
后台服务器:http://gmall-h5-api.atguigu.cn
解决跨域 (具体细节回顾博客:配置代理服务器):
// vue.config.js
devServer: {
proxy: {
'/api': {
target: 'http://gmall-h5-api.atguigu.cn',
}
}
}
请求成功:
3. nprogress进度条
nprogress:页面加载进度条。只要项目当中发请求,进度条就开始往前动,服务器数据返回之后,进度条结束。
- 安装进度条插件:
npm install nprogress
statrt()
:进度条开始done()
:进度条结束- 在请求及响应拦截器中使用(进度条的颜色等样式可以修改,直接在css文件里修改即可)
三、 vuex模块开发
Vuex具体细节看这篇博客:Vuex
这个项目中的vuex采用模块化开发,分别建立home
和search
两个小仓库,然后放入store
这个大仓库里。
// 小仓库
// store/home/index.js store/search/index.js
export default {
// 开启命名空间
namespaced: true,
state: {},
actions: {},
mutations: {},
getters: {}
}
store
大仓库:
main.js
引入Vuex
// 引入Vuex
import store from '@/store'
new Vue({
// KV一致时省略V
router,
store,
render: h => h(App),
}).$mount('#app')
四、TypeNav三级联动组件开发
1. 动态展示三级联动数据
向后台发送请求,获取三级联动的数据,渲染到页面上
TypeNav
组件挂载完毕,就发送请求
mounted () {
// 通知vuex发送请求,获取数据,存储在store中;dispatch('模块名/方法名')
this.$store.dispatch('home/categoryList')
},
home
小仓库里,将请求来的数据保存在state
里:
//home/index.js
state: {
categoryList: []
},
actions: {
// 获取三级联动的列表
async categoryList (context) {
let result = await reqCategoryList()
if (result.code === 200) {
// 请求成功
context.commit('CATEGORTLIST', result.data)
}
}
},
mutations: {
CATEGORTLIST (state, categoryList) {
// 将获取成功的数据保存起来
state.categoryList = categoryList
}
},
TypeNav
组件读取vuex中请求来的数据
import { mapState } from 'vuex'
computed: {
// 获取State数据
...mapState('home', ['categoryList'])
},
通过循环遍历的方式将categoryList
渲染到页面上
这里发现一个小bug;一级分类的数据多了一个,视频里请求到16个,结果获取到17个了,多了一个样式就变了;
解决:只用categoryList
里的前16个,一级v-for
的代码改成:
v-for="(c1, index) in categoryList.slice(0, 16)"
2. 三级联动 动态背景
需求1:鼠标移动到哪个词条,哪个词条背景颜色改变。离开时,背景颜色消失。
需求2:但是当鼠标由第一个词条图书、音像..
移动到全部商品分类
时,第一个词条的颜色不变。
(1)、方式一:CSS样式
.item:hover{
background-color:skyblue
}
这种方式能够实现需求1,但实现不了需求2
(2)、方式二:JS
设置mouseenter
与mouseleave
事件及currentIndex
属性,该属性默认值是-1;
当鼠标移入词条时,获取当前词条的index
,存入currentIndex
。当index === currentIndex
时,给当前词条添加样式。
当鼠标移除词条时,再次将currentIndex
设为-1;
<div
class="item"
v-for="(c1, index) in categoryList.slice(0, 16)"
:key="c1.categoryId"
@mouseenter="changeBg(index)"
:class="{ cur: index === currentIndex }"
>
<!----一级分类a标签---->
<!----二级分类内容---->
<!----三级分类内容---->
</div>
<script>
data () {
return {
currentIndex: -1
}
},
methods: {
changeBg (index) {
this.currentIndex = index
},
leaveIndex () {
this.currentIndex = -1
}
}
</script>>
<style>
.cur {
background-color: skyblue;
}
</style>
为实现需求2,将全部商品分类与下方三级联动数据包裹在一个div里,将鼠标移出事件添加在这个div里
<div @mouseleave="leaveIndex">
<h2 class="all">全部商品分类</h2>
<div>..三级联动数据...</div>
</div>
3. 控制二三级数据隐藏与显示–绑定style样式
4. 三级联动的节流和防抖
1、什么是节流和防抖
事件触发的非常频繁时,每一次的触发,回调函数都要去执行。但是如果触发的十分频繁而回调函数内部有计算等其他内容,则很可能出现浏览器卡顿,导致只有几次触发得到响应。
节流:在规定的间隔时间范围内不会重复触发回调,把频繁触发变为少量触发。
防抖:前面的所有的触发都被取消,也就是如果有连续快速的触发,只会执行一次。
应用:某些轮播图,快速点击切换图片时,可进行防抖或节流。
防抖:用户操作频繁,无关规定时间长短,期间只执行一次。
节流:用户操作很频繁,但是把频繁的操作变成少量操作【可以给浏览器充裕的时间解析代码】即规定时间执行一次。
具体看JS博客
2、三级联动节流
当鼠标快速从上往下移动时(即用户操作频繁),并不是所有词条的mouseenter事件都被触发。
节流采用lodash库提供的函数,由于vue中包含了lodash,所以不用提前安装
// 全部功能函数引入
import _ from 'lodash'
// 或采用 按需引入
import throttle from 'lodash/throttle'
methods:{
/*
正常情况(用户慢慢操作):鼠标进入每一个一级分类的h3,都会触发鼠标进入事件
非正常情况(用户操作很快):只有部分触发进入事件,这是由于用户行为过快,导致浏览器反应不过来。
如果当前回调函数中有一些大量业务,则有可能出现卡顿
*/
// 这里的throttle回调函数别用箭头函数,可能出现上下文this问题
changeBg: throttle(function (index) {
this.currentIndex = index
console.log('鼠标进入词条' + index);
}, 50)
}
5. 三级联动路由跳转及传参
需求:用户点击三级联动中的一级分类、二级分类、三级分类时,Home
模块跳转到Search
模块,并且在路由跳转时,将categoryName
和categoryId
携带过去
1、路由跳转的方式
(1)、声明式导航router-link
。
router-link
本质上是一个组件,当服务器的数据返回之后,会循环创建、渲染出很多的router-link
实例组件。由于三级联动中链接较多,一瞬间创建很多组件实例十分消耗内存。因此采用router-link
会出现卡顿现象。
(2)、编程式导航
可通过添加监听点击事件实现路由跳转。但是每个a标签都要绑定自己的回调,这样不太好。
只绑定一个回调要比绑很多回调要好,所以考虑采用事件委托的方式。事件委托是将点击事件添加到父节点中,这样任意子节点的事件都会冒泡到父节点,进而父节点的触发点击事件、调用回调函数。回顾事件委托:WebAPI(二)、事件委托
综上,采用编程式导航+事件委托的方式实现路由跳转。而这种方式存在两个问题。
(1)、编程式导航+事件委托–自定义属性
问题:
1.事件委托是把全部的子节点【h3、dt、dl】的事件都委派给父节点,如何确定点击的一定是a标签呢
2.即使确定点击的是a标签,如何区分是一级、二级、三级分类的标签。
解决方法:采用自定义属性
回顾自定义属性:WebAPI(一)、自定义属性
给a标签加上自定义属性,通过event.target.dataset
获取到这些自定义属性
选择div父节点,绑定点击事件
// 跳转search页面
goSearch (event) {
// 解构赋值,只有a标签才有这些自定义属性
const { categoryname, category1id, category2id, category3id } = event.target.dataset
// 组装参数
let location = { name: 'search' }
let query = { categoryName: categoryname }
// 当前这个if语句:一定是a标签才会进入
if (categoryname) {
if (category1id) { // 一定是a标签:一级分类
query.category1Id = category1id
} else if (category2id) { // 一定是a标签:二级分类
query.category2Id = category2id
} else { // 一定是a标签:三级分类
query.category3Id = category3id
}
}
// 给location对象配置上query属性
location.query = query
this.$router.push(location)
}
6、 Search模块三级联动显示与隐藏
Search页面也有一个三级联动,且刚进入这个界面时,三级联动数据并不展示。鼠标移入全部商品分类
时展示数据,离开时隐藏数据。
(1)、Search模块使用TypeNav组件
由于TypeNav
组件是一个全局组件,直接使用即可
(2)、控制TypeNav的显示与隐藏
可以在TypeNav中使用v-show
来决定TypeNav是显示还是隐藏。新增一个属性isShow
当由Home页面进入Search页面时,TypeNav组件会再重新挂载,所以在TypeNav挂载时(即钩子函数mounted),通过当前的路由信息,就决定这个组件是该显示还是隐藏。TypeNav
是非路由组件,其$route
信息是当前路由组件的信息。如果当前路由不是Home模块,即路径不是/home
时,则隐藏。
(3)、鼠标移入移出与TypeNav的显示与隐藏
当鼠标移入全部商品分类
这一大块内容时,无论是home页面还是search页面,TypeNav都是显示的,所以isShow=true
;
当鼠标移出这部分时,home页面中,这部分不隐藏。其他页面中,还是要隐藏起来。所以鼠标事件的回调函数如下图所示:
(4)、三级联动显示与隐藏的动画效果
transition
标签:
// 过渡
// 进入的起点,离开的终点
.sort-enter,
.sort-leave-to {
opacity: 0;
}
// 进入及离开的过程
.sort-enter-active,
.sort-leave-active {
transition: all 0.3s linear;
}
// 进入的终点,离开的起点
.sort-enter-to,
.sort-leave {
opacity: 1;
}
7、 TypeNav重复发送请求的优化
由于多个页面都使用了TypeNav
组件,当进入Home页面时,TypeNav
加载一次。进入Search页面时又加载一次。而三级联动数据的请求写在了TypeNav
组件的mounted函数里,所以切换页面时会重复发送请求,而数据都是一样的,发一次请求就够了,没必要多次发送。
解决方法:请求写在根组件(App.vue)里
根组件只加载一次,所以只发送一次请求。
8. 合并Search页面的query与params参数
目前跳往Search页面的情况有两种:
Header
组件中,输入关键词,点击搜索。跳转到Search页面(携带params参数)TypeNav
组件中,点击分类,跳转到Search页面(携带query参数)。
但是当先通过点击三级联动的链接跳转到搜索页面(有query参数),再通过关键词搜索(携带params参数)跳转Search页面时,三级联动的参数则会不见。
比如通过三级联动点击"手机",跳转到搜索页面,路径显示为:
localhosr:8080/#/search?categoryName=手机&category3Id=61
然后再通过搜索框搜索"华为":
localhosr:8080/#/search/华为
解决方法:将Header.vue里的回调函数加上query参数
// Header.vue
goSearch () {
this.$router.push({
name: 'search',
params: {
keyWord: this.keyWord || undefined
},
query: this.$route.query
})
}
这里本来还有一个if判断,判断query是否为空,但是不起作用,具体原因博主DantinZhang有写过; 参考博客:Vue2电商前台项目(二):完成Home首页模块业务_电商开源项目vue-CSDN博客
六、Home首页的ListContainer和Floor组件
1. 搭建mock
(1)安装mock:npm install mockjs
,并在src文件夹下新建mock文件夹,方便进行mock的配置
(2)创建数据文件:
- 轮播图banner数据文件
src/mock/banner.json
- 楼层数据文件:
src/mock/floor.json
(3)把数据文件里用到的图片资源放到src/images
文件夹里。
(4) 在mock文件夹下创建mockServe.js
,进行配置
(5) 在main.js文件里引入 mockServe.js
,让假数据一上来就先执行一下
import './mock/mockServer'
2. 搭建ListContainer组件
(1)、 mock虚拟数据的ajax请求
- 创建
api/mockRequest.js
之前二次封装的axios的用来给后台服务器发送请求的。对于向mock虚拟数据发送请求,同样先对axios进行二次封装。不同的是baseURL
的值改为在mockServe.js
里设置的请求路径
- 放入
api/index.js
中,统一进行api管理
import mockRequests from './mockRequest'
// 获取轮播图数据的api
export const reqBannerList = () => {
return mockRequests({ url: '/banner', method: 'get' })
(2)、 获取轮播图数据
Listcontainer
组件发送请求,获取数据,存到Vuex里,组件获取Vuex的数据
(3)、 搭建轮播图
-
安装Swiper:
npm install swiper@5
- Swiper基本使用:Swiper中文网-轮播图幻灯片js插件,H5页面前端开发
-
引包:
import Swiper from 'swiper'
-
引样式:
import 'swiper/css/swiper.css'
; 由于页面中不只这一处使用轮播图,所以样式可以从主文件(main.js)中引入。此时轮播图只是静态样式,原因是还未创建实例
- 创建Swiper实例的讨论
创建实例之后,轮播图才有动态效果。问题是应该在什么时候,在哪里创建Swiper实例?
1、放在mounted函数?
mounted () {
console.log('listContainer--Mounted加载');
// 发送请求并将数据存储在vuex里
console.log('发送ajax请求');
this.$store.dispatch('home/getBannerList')
// 创建实例
console.log('new Swiper实例');
new Swiper('.swiper-container',{...})
}
轮播图仍未静态的。new Swiper实例之前,页面中的结构应该要完整。mounted里有一个异步操作,会导致创建实例先执行,当new Swiper实例时,该组件还未获取到数据,v-for渲染结构并不完全,也就是此时的结构是不完整的。所以此时仍是静态效果
3、采用setTimeout异步函数
在mounted函数里,存在的问题是先创建了实例,后有的数据,所以才不起作用。那考虑如何现有数据,再创建实例------采用异步函数
mounted () {
console.log('listContainer--Mounted加载');
// 发送请求并将数据存储在vuex里
this.$store.dispatch('home/getBannerList')
// 设置定时器,等组件获取到请求的数据之后,再实例化Swpier
setTimeout(() => {
console.log('new Swiper实例');
new Swiper('.swiper-container', {
...
}, 1000)
}
这种方式可以,但不是最优,因为发送ajax请求的时间不确定,所以定时器时间不好把握
4、watch+$nextTick(最优)
- 数据监听watch:监听已有数据变化
$nextTick:
在DOM更新结束后,立即调用这个$nextTick
的回调函数。也就是执行这个回调的时候,能够保证服务器的数据回来了,v-for执行完毕了(即轮播图的结构已经完成了)。$nextTick可以保证页面中的结构一定是有的,经常和很多插件一起使用。要是还不明白,回顾博客 Vue(九) nextTick。
watch: {
// 监听bannerList数据的变化,因为这条数据发生过变化---由空数组变为数组里有4个元素
bannerList: {
handler () {
this.$nextTick(() => {
new Swiper('.swiper-container', {
loop: true, // 循环模式
// 如果需要分页器
pagination: {
el: '.swiper-pagination'
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
}
})
})
}
}
}
watch监听bannerList
属性的属性值的变化,当handler函数执行时,只能保证bannerList
的数据已经有了,不再是空数组,但是没办法保证v-for已经将这些数据渲染完成。当v-for渲染完成后,结构才完整。因此采用nextTick
3. 搭建Floor组件
经典流程:封装api => 组件挂载完毕时发送请求、获取数据 => 数据存到Vuex中(vuex三件套state、actions、mutations) =>渲染页面
(1)、封装api、发送请求
api/index.js
里封装api
// Floor数据
export const reqFloorList = () => {
return mockRequests({ url: '/floor', method: 'get' })
}
Vuex保存数据及Home组件获取数据
(回顾Home页面,Home页面中有两个<Floor/>
标签,需要通过v-for来遍历生成floor组件,所以需要在Home组件中进行dispatch。)
(2)、 动态数据渲染页面
Home组件将数据传递给子组件Floor
(高频面试题,父组件给子组件传值的方式/组件通信的方式有哪些?
props、自定义事件:Vue(八) props 自定义事件
全局事件总线、消息订阅与发布:Vue(九)全局事件总线、消息订阅与发布
插槽:Vue(十一) 插槽
Vuex: Vue(十二) Vuex
)
<!-- Home.vue -->
<Floor v-for="f in floorList" :key="f.id" :list="f"></Floor>
<!-- Floor.vue -->
props: ['list']
(3)、 Floor组件里的轮播图
在Floor里,new Swiper实例可以放在mounted里,因为Floor里的数据是父组件Home传递过来的,Floor里没有任何异步操作,所以挂在完毕之后页面结构就会现有,所以创建Swiper实例是可行的。(之前ListContainer组件内发送了异步请求,当创建Swiper实例时,由于数据不全,所以轮播图的结构并不完整)
(4)、 封装轮播图
由于轮播图在多个组件中用到,所以可将轮播图封装为全局组件。前提是保证目前用到轮播图的地方,轮播图的代码一致。根据上边可知,ListContainer组件和Floor组件的区别是new Swiper
实例的地方不一致,ListContainer
用的是watch+$nextTick
, Floor
用的是mounted。
让Floor组件向ListContainer组件看齐,将new Swiper
实例放到watch监听里:
list: {
// 因为Floor中的list数据是父组件传过来的,没有发生过变化,所以需要上来立即执行一次监听,创建Swiper实例。
immediate: true,
handler () {
// 数据已经有了,但是v-for动态渲染结构还是没有办法确定,因此还是需要nextTick
this.$nextTick(() => {
new Swiper(this.$refs.mySwiper, {
loop: true, // 循环模式
pagination: { // 如果需要分页器
el: '.swiper-pagination',
clickable: true
},
navigation: { // 如果需要前进后退按钮
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
}
})
})
}
}
最终封装为Carousel组件:
<!--src/components/Carousel/index.vue-->
<template>
<div class="swiper-container" ref="mySwiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(s, index) in list" :key="index">
<img :src="s.imgUrl" />
</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'],
// 监听List数据的变化,
//ListContainer组件中的这条数据发生过变化(由空数组变为数组里有4个元素),所以可用watch监听
watch: {
list: {
// 因为Floor中的list数据是父组件传过来的,没有发生过变化,所以需要上来立即执行一次监听,创建Swiper实例。
immediate: true,
handler () {
// 数据已经有了,但是v-for动态渲染结构还是没有办法确定,因此需要nextTick
this.$nextTick(() => {
new Swiper(this.$refs.mySwiper, {
loop: true,
pagination: { // 如果需要分页器
el: '.swiper-pagination',
clickable: true
},
navigation: { // 如果需要前进后退按钮
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
}
})
})
}
}
}
}
</script>