一、Home首页的开发步骤
Home首页可以拆分为7个组件,分别是:TypeNav三级联动导航,ListContainer,Recommend,Rank,Like,Floor,Brand
1、导航栏TypeNav三级联动组件(全局组件)
在components下创建TypeNav组件(前面说到过全局组件创建在components中)
设置全局组件的三个步骤:
- 创建组件
- 引入组件并设置全局组件
- 使用组件,不需要导入声明
这里我遇到了一个问题:vue router路由跳转了,但是页面没有变
解决方法:App.vue文件模板里加一句<router-view></router-view>
<template> <div> <Header></Header> <router-view></router-view> <Footer v-show="$route.meta.showFooter"></Footer> </div> </template>
2、其余局部组件
- 创建组件
- 导入结构和样式还有图片
- 引入、注册、使用
二、请求服务器的准备工作
1、axios二次封装
向服务器发请求的方式有:XMLHttpRequest,fetch,JQ,axios
我们使用axios,安装:脚手架目录下 npm install --save axios
-
为什么需要进行二次封装axios?
请求拦截器、响应拦截器:请求拦截器,可以在发请求之前处理一些业务;响应拦截器:当服务器数据返回以后,可以处理一些事情 -
在项目中出现api的文件夹,一般都是放axios请求的
//对于axios进行二次封装
import axios from "axios";
//1. 利用axios对象的方法create,去创建一个axios实例
//2. request就是axios,只不过稍微配置一下
const requests = axios.create({
//配置对象
//基础路径,发请求的时候,路径当中会出现api
baseURL:"/api",
//代表请求超时的时间5s
timeout:5000,
});
//请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config)=>{
//config:配置对象,对象里面有一个属性很重要,headers请求头
return config;
});
//响应拦截器
requests.interceptors.response.use((res)=>{
//成功的回调函数:服务器响应数据回来之后,响应拦截器可以检测到,可以做一些事情
return res.data;
},(error)=>{
//响应失败的回调函数
return Promise.reject(new Error('faile'));
})
//对外暴露
export default requests;
2、api接口统一管理
// src/api/index.js
//本文件用于:API的统一管理
import requests from './request';
//三级联动(导航部分)的接口
// /api/product/getBaseCategoryList get 无参数
export const reqCategoryList = function () {
//发请求:axios发请求返回结果是Promise对象
return requests({
url: '/product/getBaseCategoryList',
method: 'get'
});
}
在main.js中测试api
import {reqCategoryList} from '@/api'
reqCategoryList();
会发现报404错误,数据无法从服务器获取
这是因为跨域的问题
从这里http://localhost:8080/#/home ----前端项目本地服务器
向这里发请求 http://gmall-h5-api.atguigu.cn ---- 后台服务器
之前说到过跨域问题,解决的方法有jsonp、CORS、代理跨域问题
这里我们使用代理的方法
在vue.config.js文件中添加:
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 关闭eslint
lintOnSave: false,
//配置代理解决跨域请求问题
devServer:{
proxy:{
'/api':{
target:'http://gmall-h5-api.atguigu.cn',
//因为此项目前面带了baseUrl都设置了带api,这里就不用设置了
// pathRewrite:{'^/api':''},
}
}
}
})
3、nprogress进度条的使用
安装脚手架:npm install --save nprogress
作用:只要项目发出请求,进度条就会开始走,服务器数据返回之后,进度条就结束
用在 请求和响应拦截器中 src/api/request.js
(1)引入进度条和进度条样式,设置发送请求时开始,和成功请求后结束
(2)修改进度条的颜色
在node_modules文件中的nprogress.css文件找到.bar的background
三、Vuex模块式开发
vuex是官方提供的一个插件,状态管理库,集中式管理项目中组件共用的数据(并不是全部项目都需要,如果项目很小,完全不需要vuex)
安装脚手架:npm i vuex@3
1、TypeNav导航三级联动
(1)当组件挂载完毕,就向服务器发请求
(2)去home仓库请求数据
//home模块的小仓库
import { reqCategoryList } from "@/api";
const state = {
//state中数据默认初始值别瞎写
categoryList:[],
};
const mutations = {
CATEGORYLIST(state,categoryList){
state.categoryList = categoryList;
}
};
const actions = {
async categoryList({commit}){
let result =await reqCategoryList();
// console.log(result);
if(result.code == 200){
commit("CATEGORYLIST",result.data);
}
}
};
(3)TypeNav接收数据
import {mapState} from 'vuex';
export default {
...mapState({
//右侧需要的是一个函数,当使用这个计算属性的时候,右侧函数会立即执行一次
//注入一个参数state,其实即为大仓库中的数据
categoryList:state=>state.home.categoryList.slice(0,16)
// categoryList:(state)=>{
// return state.home.categoryList;
// }
})
}
};
(4)渲染数据
接收来的数据就是这样的:
<div class="item" v-for="(c1,index) in categoryList" :key="c1.categoryId">
<h3>
<a href="">{{ c1.categoryName }}</a>
</h3>
<div class="item-list clearfix">
<div class="subitem" v-for="(c2,index) in c1.categoryList" :key="c2.categoryId">
<dl class="fore">
<dt>
<a href="">{{c2.categoryName}}</a>
</dt>
<dd>
<em v-for="(c3,index) in c4.categoryChild" :key="c3.categoryId">
<a href="">{{c3.categoryName}}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
(5)三级联动的防抖和节流
1、防抖和节流的介绍
- 防抖(回城技能)
前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行一次
(简单来说就是回城技能)
const result = _.debounce(function(){
},1000)
- 节流(回城cd)
在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
const result = _.throttle(function(){
},1000)
lodash插件中封装了防抖与节流的业务【闭包+延迟器】
2、三级联动节流操作
//按需引入
import throttle from '_lodash/throttle'
methods:{
changeIndex:throttle(function(index){
this.currentIndex = index;
},50)
}
(6)、三级联动的路由跳转与传参
- 需求分析
当你点击一个分类的时候,会从home模块跳转到search模块,并且把分类的名字categoryName和ID传递给search模块,然后search模块拿到这些参数向服务器发请求展示相应数据 - 解决方案
(1)路由跳转可以用<router-link></router-link>
,但是这样的话<router-view>
会生成好多组件,页面会卡
(2)如果只用编程式导航,要给每个a加点击事件
(3)最好的解决方案是编程式导航+事件委派
事件委派
给每个a标签添加@click=“goSearch”
事件委派的问题:
(1)如何确定我们点击的一定是a标签呢?如何保证我们只能通过点击a标签才跳转呢?
(2)如何获取子节点标签的商品名称和商品id(我们是通过商品名称和商品id进行页面跳转的)
解决方法:
问题1:为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。
问题2:为三个等级的a标签再添加自定义属性data-category1Id、data-category2Id、data-category3Id来获取三个等级a标签的商品id,用于路由跳转。
我们可以通过在函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息。
methods: {
goSearch() {
//第一个问题:把子节点当中a标签,我加上自定义属性data-categoryName,其余的子节点是没有的
let element = event.target;
//获取到当前触发这个事情的节点【h3/a/dt/dl】,需要带有data-categoryname这样的节点【一定是a标签】
let { categoryname, category1id, category2id, category3id } =
element.dataset;
//如果标签身上拥有categoryname一定是a标签
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);
}
},
},
(7)Search模块中三级联动的显示与隐藏
①显示与隐藏
当我们在home页面时,TypeNav导航栏三级联动要一直显示,但是在其他组件,例如search中,我们要隐藏TypeNav,让它具有隐藏和显示的功能
1、我们可以在TtpeNav组件中加入一个数据show用来决定组件的显示和隐藏,默认值为true
2、利用事件委派添加鼠标进入和鼠标离开函数的回调和show还有v-show来控制是否隐藏和展示
3、当从Home进入到Search的时候,TypeNav会再重新挂载,所以当进入Search的时候,让show变成false,而且要判断只要不是往主页跳,挂载完都要隐藏
4、添加鼠标进入和鼠标离开的回调函数
②过渡动画
前提组件或者元素务必要有v-if或者v-show指令才可以进行过渡动画
用transition标签包住带有v-show属性的标签,加一个name,然后去写css样式
// 三级导航内容部分过渡动画
//过渡动画开始的状态
.sort-enter,
.sort-leave-to {
height: 0px;
}
//过渡动画的结束状态
.sort-enter-to,
.sort-leave {
height: 461px;
}
//定义动画时间,速率
.sort-enter-active,
.sort-leave-active {
overflow: hidden;
transition: all 0.2s linear;
}
(8)TypeNav商品分类列表ajax重复请求问题
我们之前的方法是写在TypeNav组件中的mounted钩子函数中,如果来回切换,就会重复发起TypeNav中的数据请求
解决方法:把请求放入到App.vue文件中,当我们打开网页的时候,最先开启的是App.vue,这样请求只会发送一次,想用数据直接去Vuex仓库那就可以了
(8)合并params和query参数
我们往Search组件跳转有两种方式,一种是通过搜索关键字跳转,另一种是点击三级导航的链接跳转,三级导航传的是query参数,搜索传的是params参数
但是这两种方式只能传一个地方的组件,如果先三级导航跳转再搜索的话,搜索传的params参数和空query参数会覆盖三级导航传的query参数和空params参数
所以我们可以两边都通过一个判断来实现参数的合并,就是params和query参数一起到地址栏中。
if(this.$route.query){
// 代表的是有query参数也带出去
let location = {
name:"search",
params:{keyword:this.keyword || undefined}};
location.query = this.$route.query;
this.$router.push(location)
}
}
2、ListContainer、Floor组件
(1)mockjs模拟数据
安装脚手架:npm install mockjs
- 在src文件夹下创建mock文件夹,来存放json假数据
- 准备json数据
- 把mock数据需要的图片放置到public文件夹中【public文件夹在打包的时候,会把相应的资源原封不动打包到dist文件夹中】
- 创建mockServer.js通过mock.js插件实现模拟数据
- mockServer.js文件在入口文件中引入(至少需要执行一次,才能模拟数据)
//MockServer.js文件
//先引入mockjs模块
import Mock from "mockjs";
//把JSON数据格式引入进来[JSON数据格式根本没有对外暴露,但是可以引入]
//webpack默认对外暴露的:图片、JSON数据格式
import banner from './banner.json';
import banner from './floor.json';
//mock数据:第一个参数请求地址 第二个参数:请求数据
Mock.mock("/mock/banner",{code:200,data:banner});//模拟首页大的轮播图的数据
Mock.mock("/mock/floor",{code:200,data:floor});
- 在main.js中引入一下,让假的数据一上来就执行一次
//引入MockServer.js ---mock数据
import '@/mock/mockServer'
(2)mock虚拟数据的ajax请求
跟之前二次封装axios数据一样,复制一份,再配置一个,把baseUrl的属性改为/mock
在统一管理api的文件中引入并配置接口:
(3)ListContainer组件
1、获取Banner轮播图的数据
我们会把公共的数据放在store中,然后使用时再去store中取。
- 在轮播图组件ListContainer.vue组件加载完毕后发起轮播图数据请求。
mounted() {
//派发action:通过Vuex发送ajax请求,将数据存储在仓库中
this.$store.dispatch("getBannerList");
},
- 请求实际是在store中的actions中完成的
const actions = {
// 获取首页轮播图的数据
async getBannerList({commit}) {
let result = await reqGetBannerList();
console.log(result);
if (result.code == 200) {
commit("GETBANNERLIST", result.data);
}
}
};
- 获取到数据后存入store仓库,在mutations完成
const mutations = {
CATEGORYLIST(state, categoryList) {
state.categoryList = categoryList;
},
GETBANNERLIST(state,bannerList){
state.bannerList = bannerList;
}
};
- 轮播图组件ListContainer.vue组件在store中获取轮播图数据。由于在这个数据是通过异步请求获得的,所以我们要通过计算属性computed获取轮播图数据。
//ListContainer.vue
import { mapState } from "vuex";
export default {
name: "",
mounted() {
//派发action:通过Vuex发送ajax请求,将数据存储在仓库中
this.$store.dispatch("getBannerList");
},
computed: {
...mapState({
bannerList: (state) => state.home.bannerList,
}),
},
};
2、制作ListContainer轮播图
swiper安装脚手架:npm install --save swiper@5 使用文档
使用swiper分三步:
1.引入相应的包
2.页面结构必须先有(给轮播图添加静态效果)
3.有结构后再new Swiper实例(给轮播图添加动态效果)
- 引包
在main.js中引入swiper样式(main.js是全局样式,在这引入全都可以使用)
//引入swiper样式
import "swiper/css/swiper.css"
- 在ListContainer中引入swiper
//swiper
import Swiper from 'swiper';
-
搭建轮播图页面结构
v-for遍历返回的数据,把返回的数据渲染到页面中
-
添加轮播效果
如果把效果放到mounted中可以吗?
当然不可以,我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。
解决方案:
第一种:延时器
设置一个延时器,等我们的数据请求完毕后,再创建swiper
mounted() {
//派发action:通过Vuex发送ajax请求,将数据存储在仓库中
this.$store.dispatch("getBannerList");
setTimeout(() => {
var mySwiper = new Swiper(document.querySelector(".swiper-container"), {
loop: true,
//如果需要分页器
pagination: {
el: ".swiper-pagination",
},
//如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
}, 2000);
},
但是这种方法有一种弊端,就是你不知道ajax什么时候请求结束,所以你不好控制定时器的事件,所以我们不采用
第二种:使用watch监听
watch: {
//监听bannerList数据的变化,因为这条数据发生过变化---由空数组变为数组里面有四个元素
bannerList: {
handler(newValue, oldValue) {
//现在咱们通过watch监听bannerList属性的属性值的变化
//如果执行handler方法,代表组件实例身上这个属性的属性已经有了【数组:四个元素】
//当前这个函数执行,只能保证bannerList数据已经有了,但是你没办法保证v-for已经执行结束了
var mySwiper = new Swiper(document.querySelector(".swiper-container"), {
loop: true,
//如果需要分页器
pagination: {
el: ".swiper-pagination",
//点击小球的时候也切换图片
clickable: true,
},
//如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
},
},
},
使用watch监听会发现,没办法换页啊,watch监听是监听bannerList从空数组变成有数组,然后再执行下面的换页功能,但是当这个函数执行时,只能保证bannerList数据已经有了,但是你没办法保证v-for已经执行结束了,才导致功能无法显示。
第三种:watch + nextTick(完美方案)
watch
:监听bannerList数据的变化,如果执行handler方法,代表组件实例身上这个属性的属性值数组已经有了mock数据
nextTick
:在下次DOM更新 循环结束之后 执行延迟回调。在 修改数据之后 立即使用这个方法,获取更新后的DOM(简单点说就是获取了最新的数据并且渲染成功之后,再执行的方法)
watch: {
//监听bannerList数据的变化,因为这条数据发生过变化---由空数组变为数组里面有四个元素
bannerList: {
handler(newValue, oldValue) {
//现在咱们通过watch监听bannerList属性的属性值的变化
//如果执行handler方法,代表组件实例身上这个属性的属性已经有了【数组:四个元素】
//当前这个函数执行,只能保证bannerList数据已经有了,但是你没办法保证v-for已经执行结束了
this.$nextTick(() => {
var mySwiper = new Swiper(
document.querySelector(".swiper-container"),
{
loop: true,
//如果需要分页器
pagination: {
el: ".swiper-pagination",
//点击小球的时候也切换图片
clickable: true,
},
//如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
}
);
});
},
},
},
(4)Floor组件
1、获取Floor的数据
还是之前的那一套
我们的mock数据里面是有两个数据,因为我们有两个floor组件,那我们能在floor组件里创建dispath吗?那显然是不行,没办法拿数据,那怎么办呢?
解决方案:在home中dispath(做不了的事交给父亲)
但是这里有个问题,因为我们上面用dispath把数据交给了父亲,但是floor要拿到里面的数据,那要如何拿呢?
解决方案:父子组件通信
组件通信的方式有哪些?
props
自定义事件:@on @emit
全局事件总线:$bus 全能
pubsub-js:vue当中几乎不用 但是全能
插槽
vuex
在父文件中的floor组件中加入:list="floor"
在子组件中添加props
2、渲染数据
拿到数据就简单了,v-for和插值语法就可以渲染数据了
2、Floor的轮播图
mounted() {
var mySwiper = new Swiper(this.$refs.floor1Swiper, {
loop: true,
//如果需要分页器
pagination: {
el: ".swiper-pagination",
//点击小球的时候也切换图片
clickable: true,
},
//如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
},
这里为什么能直接放到mounted里面呢?
因为是父组件发请求,父组件通过props传递过来的,props已经解析完毕了,所以挂载的时候能正常获取数据
(5)封装Carsouel全局组件
在ListContainer和Floor中都用到了carsouel轮播图,我们可以把他封装成一个组件,需要的时候拿来用
在ListContainer里用的watch+$nextTick,监听bannerList变化了才执行,但在Floor中,它的数据是从父级拿过来的,当它拿到floorList的时候,已经是一个有数据的数组了,没有变化,所以用监听的方式无法获取到
解决方案:添加immediate:true(不管数据变没变先调用handler)
watch: {
list: {
//立即监听:不管你数据有没有变化,我上来立即监听一次
//为什么watch监听不到list,因为这个数据从来没有发生变化
immediate: true,
handler() {
//只能监听数据已经有了,但是v-for动态渲染结构我们还是没有办法确定,因此还是需要用nextTick
this.$nextTick(() => {
var mySwiper = new Swiper(this.$refs.floor1Swiper, {
loop: true,
//如果需要分页器
pagination: {
el: ".swiper-pagination",
//点击小球的时候也切换图片
clickable: true,
},
//如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
});
});
},
},
},