vue2项目

这个是因为飞书文档无法满足笔记需求,想到写博客记录笔记,前面的笔记看情况决定是不是补上
部分内容参考博主毛毛虫呜呜的笔记

笔记目录

    • git地址
    • 路由传参
    • 二次封装axios
    • 通过配置代理解决跨域问题(前端)
    • loadsh插件防抖和节流
    • 编程式导航
    • Vue路由组件销毁优化
    • 合并参数
    • mock.js模拟数据
    • 通过Vuex来实现数据存储与管理(复习,上面用过)
    • 利用swiper实现轮播图
    • floor组件
      • 父子组件通信:props
    • 将轮播图拆分为全局组件
    • Search模块vuex操作
    • getters的使用
    • Object.assign
    • 实现SearchSelector组件展示
    • 利用路由信息变化实现动态搜索
    • 面包屑分类处理
    • 全局事件总线 实现清楚面包屑,搜索框自动清除
    • 平台售卖属性
    • 排序操作
    • 分页器
    • 商品详情界面
    • 滚动行为

git地址

https://gitee.com/juneathena/vue

路由传参

路由跳转方式:
声明式导航:router-link,必须要加to:Home
编程式导航:利用组件实例对象的$router.push/replace方法,跳转之前可以书写一下自己的 业务。

query、params两个属性可以传递参数:
query参数:不属于路径当中的一部分,类似于get请求,地址栏表现为 /search?k1=v1&k2=v2
query参数对应的路由信息 path: "/search"
params参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位 ,地址栏表现为 /search/v1/v2
params参数对应的路由信息要修改为path: "/search/:keyword"这里的/:keyword就是一个params参数的占位符

路由传参:
1、字符串形式

this.$router.push("/search/" + this.keyWord + "?k=" + this.keyWord.toUpperCase());

2、模板字符串

this.$router.push(`/search/${this.keyWord}?k=${this.keyWord.toUpperCase()}`)

3、对象写法

this.$router.push({name: "search", params: {keyWord: this.keyWord}, query: {k: this.keyWord.toUpperCase()}})

路由常问问题

Q:路由传递参数(对象写法)path是否可以结合params参数一起使用?
    A:不可以,路由跳转传参的时候,对象的写法可以是name、path形式,但是需要注意的是,path这种写法不能与params参数一起使用

Q:如何指定params可传可不传?
    A: 1. 在配置路由的时候,在配置后面加一个问号 path: "/search/:keyWord?"

Q:params参数可以传递也可不传递,但是如果传递空串,如何解决?
    A: 使用undefined解决

Q:路由组件能不能传递props数据?
    A: 可以,三种写法:
       1. 布尔值写法:props: true
       2. 对象写法:props: {a:1, b:1}
       3. 函数写法:props:($router)=>{ return {keyword:$router.params.keyword, k:$router.query.k} }

二次封装axios

axios中文文档地址
Axios 是一个基于 promise 的 HTTP 库,vue比较适合Axios。
请求拦截器:可以在发送请求之前处理一些业务。
响应拦截器:可以在数据返回后处理一些事情。

// 对axios进行二次封装
import axios from "axios";import nprogress from "nprogress";
import "nprogress/nprogress.css"//1、利用axios对象的方法create去创建一个axios实例
const requests = axios.create({//配置对象// 基础路径,发请求的时候,路径中会出现/apibaseURL: "/api",// 设置超时timeout: 5000,
});// 请求拦截器:在发送请求之前,请求拦截器可以检测到,可以在发出请求之前处理一些业务
requests.interceptors.request.use((config) => {//config:配置对象,主要是对请求头headers配置//比如添加token// 进度条开始nprogress.start();return config;
})// 响应拦截器
requests.interceptors.response.use((res) => {// 进度条结束nprogress.done();// 成功回调函数return res.data;
}, (error) => {//响应失败的回调函数return Promise.reject(new Error(error));
})export default requests;  

通过配置代理解决跨域问题(前端)

通过配置代理,让请求都指向指定服务器

module.exports = {devServer: {proxy: {'/api': {target: '目标服务器',},}}
}

来自群内大佬讲解的代理
我们在发起请求的时候,如果不加请求的地址,浏览器会默认加上我们本机localhost地址,如果这样去发送请求,实际上,浏览器会默认请求,localhost:8080。跨域是受到浏览器同源策略影响,简单理解就是我们像服务端发送请求,其实别人数据给我们了,但是浏览器同源策略阻挡了服务端返回的数据,但是服务器与服务器之间是没有跨域问题的。他是通过创建一个我们本地的服务器,然后用我们本地的服务器去请求服务端服务器的内容,这样一来,我们本地服务器就能拿到服务端返回的内容了。然后我们再去拿到本地服务器的返回的数据,就把跨域问题处理了。在weback的devServe里面怎么处理的呢?他有一个配置项,叫proxy。处理跨域的核心配置就是 target,所以我们需要把需要代理的地址写上去,前提是给一个节点,这个节点就是,我在什么情况下才会去代理,所以我们需要写一个节点(上方代码里面的/api),当我80端口请求的接口中间有api的时候,webpack就知道我们需要代理了,然后80端口就会去请求target这个地址的内容。

webpack/devServer中文文档
个人理解:

proxy: {// 一旦代理服务器devServer服务器接收到 /api/xxx 的请求,就会把请求转发到远端服务器服务器(3000)// 浏览器和服务器之间有跨域,但是服务器和服务器之间没有跨域'/api': {target: 'http://localhost:3000',// 重写的路径是发送到远端服务的路径// 发送请求时,请求路径重写:将 /api/todo--> /test/todo(/api重写成/test)// http://localhost:3000/api/todo -> http://localhost:3000/test/todopathRewrite: {'^/api': '/test'}}
}

loadsh插件防抖和节流

在快速划过三级导航或者搜索框输入搜索文字的时候,如果事件处理函数调用的频率无限制,会造成浏览器卡顿。因此会采用节流(throttle)防抖(debounce)

节流:在规定的时间间隔范围内不会重复触发函数的回调,只有大于这个时间间隔才会触发回调,把频繁变为少量触发
防抖:前面的所有触发都被取消,最后一次执行在规定时间之后才会触发,也就是说如果连续快速触发,只会执行一次
该项目采用第三方库实现节流:

// 按需引入throttle
import { throttle } from 'lodash/throttle.js'methods: {//采用键值对形式创建函数changeIndex: throttle(function (index){this.currentIndex = index},100),}

编程式导航

需要实现的部分的截图:(功能:点击菜单跳转链接,url带上参数)
在这里插入图片描述
实现路由跳转有两种方式:
声明式导航:router-link
编程式导航:push|replace

对于导航式路由,我们有多少个a标签就会循环出多少个router-link标签,这样当我们频繁操作时浏览器会出现卡顿现象。
对于编程式路由,我们是通过触发点击事件实现路由跳转。同理有多少个a标签就会有多少个触发函数。虽然不会出现卡顿,但是也会影响性能。
最好的解决方案就是:编程时导航+事件委派。事件委派就是把子节点的触发时间都委派给父节点,这样就可以只调用一次回调函数goSearch

利用事件委派也存在一些问题:
(1)把全部的子节点【h3、dt、dl、em】的事件都委派给了父亲节点,如何确定点击的一定是a标签?
(2)如何确定区分是一级还是二、三级的标签(一、二、三级分类产品的名字、id需要在url中传递)

解决方案:
(1)给三个a标签添加自定义属性:data-categoryName,只有这三个a标签拥有,其他子节点没有.
(2)添加自定义属性:data-category1Id:data-category2Id:data-category3Id来获取三个不同级别的a标签内的id来实现跳转。
(*)可以使用event来获取当前点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息。

        <div class="sort"><div class="all-sort-list2" @click="goSearch"><div class="item" v-for="(c1, index) in categoryList" :key="c1.categoryId" :class="{ cur: currentIndex == index }"><h3 @mouseenter="changeIndex(index)"><a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId">{{ c1.categoryName }}a>h3><div class="item-list clearfix" :style="{ display:currentIndex == index?'block':'none' }"><div class="subitem" v-for="c2 in c1.categoryChild" :key="c2.categoryId"><dl class="fore"><dt><a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{{ c2.categoryName }}a>dt><dd><em v-for="c3 in c2.categoryChild" :key="c3.categoryId"><a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId">{{ c3.categoryName }}a>em>dd>dl>div>div>div>div>

节点中有一个属性dataset属性可以获取当前节点的自定义属性与属性值

goSearch函数代码:

    goSearch(event) {let element = event.target;/*** html中会把大写转为小写* categoryname:a标签* category1id:一级类目id* category2id:二级类目id* category3id:三级类目id*///节点中有一个属性dataset(火狐、谷歌支持),可以获取节点的自定义属性与属性值let {categoryname,category1id,category2id,category3id} = element.dataset;if (categoryname) {// 整理路由跳转的参数let location = {name:'search'};let query = {categoryName:categoryname};// 一、二、三级if (category1id) {query.category1Id = category1id;} else if (category2id){query.category2Id = category2id;} else {query.category3Id = category3id;}// 完整参数location.query = query;// 路由跳转this.$router.push(location);}},},

Vue路由组件销毁优化

Vue在路由切换的时候会销毁旧路由。
我们在三级列表全局组件TypeNav中的mounted进行了请求一次商品分类列表数据。
由于Vue在路由切换的时候会销毁旧路由,当我们再次使用三级列表全局组件时还会发一次请求。
如下图所示:当我们在包含三级列表全局组件的不同组件之间进行切换时,都会进行一次信息请求。

在这里插入图片描述
每次请求的内容都是一样的,处于性能的考虑,想要只请求一次。所以我们把这次请求放在App.vue的mounted中。(根组件App.vue的mounted只会执行一次
注意:虽然main.js也是只执行一次,但是不可以放在main.js中,main.js不是组件。只有组件的身上才会有$store属性。

合并参数

合并路由中的参数:如果跳转路由的时候带有params参数,需要一起传递过去
TypeNav.vue的goSearch方法:

        // 如果跳转路由的时候带有params参数,需要一起传递过去if (this.$route.params) {// 给location添加一个params参数location.params = this.$route.params;location.query = query;// 跳转this.$router.push(location);console.log(location)}

Header.vue的goSearch方法:

    goSearch() {let location = { name: "search", params: { keyword: this.keyWord || undefined },};if (this.$route.query) {location.query = this.$route.query;}this.$router.push(location);},

mock.js模拟数据

服务器返回的数据只有商品分类,对于轮播图(ListContainer)组件和推荐商品(Floor)组件数据服务器没有提供。前端mock的数据不会和服务器进行任何通信
mock.js生成随机数组,拦截ajax请求。

使用步骤:
1、在src目录下创建一个mock文件夹
2、准备需要的json数据
3、把mock需要的图片放到public文件中
4、开始mock虚拟数据,通过mock.js模块
5、mockServe.js文件在入口文件引入(至少需要执行一次,才能模拟数据)
mock文件夹
mockServe.js

// 引入mockjs模块
import Mock from 'mockjs';
// 引入准备好的json数据
import banner from './banner.json';
import floor from './floor.json';// mock数据:第一个参数请求地址,第二个参数请求数据
Mock.mock("/mock/banner", {code:200, date:banner});
Mock.mock("mock/floor", {code:200, data:floor});

需要在入口文件main.js中引入mockServe.js

import "@/mock/mockServe"

webpack默认对外暴露的:图片、JSON数据;

通过Vuex来实现数据存储与管理(复习,上面用过)

我们会把公共的数据放在store中,然后使用时再去store中取。
以我们的首页轮播图数据为例。
1、在轮播图组件ListContainer.vue组件加载完毕后发起轮播图数据请求。

  mounted() {// 派发action,通过Vuex像Ajax发起请求this.$store.dispatch('getBannerList');}

2、请求实际是在store中的actions中完成的

const actions = {// 导航栏=======================// 轮播图async getBannerList({ commit }) {let result = await reqGetBannerList();if (result.code === 200) {commit("GETBANNERLIST", result.data);} else {console.log("code not 200, error");}},};

3、获取到数据后存入store仓库,在mutations完成

const mutations = {// 导航栏===============// 轮播图GETBANNERLIST(state, bannerList) {state.bannerList = bannerList;},
};

4、轮播图组件ListContainer.vue组件在store中获取轮播图数据。由于在这个数据是通过异步请求获得的,所以我们要通过计算属性computed获取轮播图数据。
ListContainer.vue:

  computed: {...mapState({bannerList(state){return state.home.bannerList;}})}

总结:只要是公共数据都会放在store中,之后的实现步骤就是上面的固定步骤。

利用swiper实现轮播图

其实也可以用elementUI的走马灯实现。 而且swiper并不是一个好的选择,因为直接操作了DOM

想用swiper实现轮播图主要分为以下几步:
1、安装swiper插件(5版本稳定,所以下的5 npm install --save swiper@5)
2、
由于下面还有轮播图样式都一样,所以swiper的样式在main.js中引入即可
main.js

// 引入swiper样式
import "swiper/css/swiper.css";

接下来要考虑的是什么时候去加载这个swiper,我们第一时间想到的是在mounted中创建这个实例。
但是会出现无法加载轮播图片的问题。主要是因为:

我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。

为了防止直接操作DOM,所以在html中使用ref属性

<div class="swiper-container" ref="mySwiper">

mounted():

  mounted() {// 派发action,通过Vuex像Ajax发起请求this.$store.dispatch("getBannerList");new Swiper(this.$refs.mySwiper, {loop: true,pagination: {el: ".swiper-pagination",clickable: true,},// 如果需要前进后退按钮navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},// 如果需要滚动条scrollbar: {el: ".swiper-scrollbar",},});},

解决方案1:可以加一个2000ms的定时器,等服务器数据请求完在在调用这个函数。但是存在bug:页面刷新完2s后轮播图的翻页按钮才可用,下方小点才能加载出来;不推荐,虽然工作中以解决问题为主,但是这样写会是屎一样的代码。

  mounted() {// 派发action,通过Vuex像Ajax发起请求this.$store.dispatch("getBannerList");setTimeout(()=>{new Swiper(this.$refs.mySwiper, {loop: true,pagination: {el: ".swiper-pagination",clickable: true,},// 如果需要前进后退按钮navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},// 如果需要滚动条scrollbar: {el: ".swiper-scrollbar",},});},2000);},

解决方案2:使用watch监听bannerList数组,当它不为空时候开始执行new Swiper 。但是该方法依然没有办法实现轮播效果,因为是因为在html结构代码中有v-for属性,v-for执行完需要时间,如果在watch监听到bannerList不为空后执行回调函数创建了swiper对象,v-for还没有遍历完,就会导致无法渲染出轮播图。(因为swiper对象生效的前提是html即dom结构已经渲染好了)。

  watch: {// 监听bannerList数组的变化bannerList: {handler() {// 通过watch监听bannerList属性的属性值变化// 如果执行handler方法,代表组件实例身上这个属性已经有了// 当前的函数执行,只能保证bannerList的数据已经有了,但是没法保证v-for遍历已经结束了new Swiper(this.$refs.mySwiper, {loop: true,pagination: {el: ".swiper-pagination",clickable: true,},// 前进后退按钮navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},// 滚动条scrollbar: {el: ".swiper-scrollbar",},});},},},

完美解决方案3:watch+this.$nextTick()。在下次DOM更新循环之后延迟执行------>v-for已经执行完毕,DOM结构已经全部挂载完毕;

  watch: {// 监听bannerList数组的变化bannerList: {handler() {// $nextTick()在下次DOM更新循环之后延迟执行==>v-for已经执行完毕,DOM结构已经全部挂载完毕;this.$nextTick(() => {// 当执行回调函数的时候:服务器数据肯定已经回来了,v-for执行完毕了【轮播图结构一定有了】new Swiper(this.$refs.mySwiper, {loop: true,pagination: {el: ".swiper-pagination",clickable: true,},// 前进后退按钮navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},// 滚动条scrollbar: {el: ".swiper-scrollbar",},});});},},},

floor组件

老一套:先写api请求:

// floor组件
export const reqFloorList = ()=>mockRequests.get('/floor');

然后去vuex仓库管理数据

import {reqCategoryList, reqGetBannerList, reqFloorList} from "@/api";
const state = {=============floorList: [],
};
const mutations = {=============// floor轮播图GETFLOORLIST(state, floorList) {state.floorList = floorList;}
};
const actions = {============// floor数据async getFloorList({ commit }) {let result = reqFloorList();if (result.code === 200) {commit("GETFLOORLIST", result.data);} else {console.log("getFloorList's interface code is't 200")}}
};

然后重点getFloorList's这个action在哪里派发?
需要在Floor的父组件Home中派发,因为mock的数据有两个不一样的对象,如果在floor内部派发,就会导致使用v-for遍历出来的两个组件一样。
在这里插入图片描述
Home组件

  mounted() {// 派发action,获取floor组件的数据this.$store.dispatch("getFloorList");},computed: {...mapState({floorList(state) {return state.home.floorList}})}

```v-for也可以在自定义标签上使用,例如Home``组件的html部分

<template><div>=================<Floor v-for="(floor) in floorList" :key="floor.id"/></div>
</template>

父子组件通信:props

父子组件间的通信方式:

props:用于父子组件通信
自定义事件:$on $emit 用于实现子给父通信
全局时间总线:$bus 全能,可以实现任意组件间的通信
插槽
vuex

注意!!!!!!
这次new Swiper可以放在mounted中是因为这次请求的数据不是在Floor自己内部请求的,数据是父组件Home给的,在Floor挂载完毕前,就已经拿到了父组件遍历完传过来的数据,不会因为异步请求出问题。所以不需要再使用watch.$nextTick结合使用。
Floor.vue

<template><!--楼层--><div class="floor"><div class="py-container"><div class="title clearfix"><!-- 左侧大标题 --><h3 class="fl">{{ list.name }}</h3><!-- 右侧超链接 --><div class="fr"><ul class="nav-tabs clearfix"><li class="active" v-for="(nav, index) in list.navList" :key="index"><a href="#tab1" data-toggle="tab">{{nav.text}}</a></li></ul></div></div><div class="tab-content"><div class="tab-pane"><div class="floor-1"><!-- 左上角文字 --><div class="blockgary"><ul class="jd-list"><li v-for="(keyword, index) in list.keywords" :key="index">{{ keyword }}</li></ul><img :src="list.imgUrl" /></div><!-- 轮播图 --><div class="floorBanner"><div class="swiper-container" id="floor1Swiper"  ref="cur"><div class="swiper-wrapper"><divclass="swiper-slide"v-for="carousel in list.carouselList":key="carousel.id"><img :src="carousel.imgUrl" /></div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div></div><!-- 左侧上下两个图 --><div class="split"><span class="floor-x-line"></span><div class="floor-conver-pit"><img :src="list.recommendList[0]" /></div><div class="floor-conver-pit"><img :src="list.recommendList[1]" /></div></div><!-- 中间大图 --><div class="split center"><img :src="list.bigImg" /></div><!-- 右侧上下两个图 --><div class="split"><span class="floor-x-line"></span><div class="floor-conver-pit"><img :src="list.recommendList[2]" /></div><div class="floor-conver-pit"><img :src="list.recommendList[3]" /></div></div></div></div></div></div></div>
</template><script>
import Swiper from "swiper";export default {name: "Floor",props: ["list"],mounted() {// 这次可以放在mounted中是因为这次请求的数据不是在floor自己内部请求的,数据是父组件Home给的,在floor挂载完毕前,就已经拿到了父组件穿过来的数据new Swiper(this.$refs.cur, {loop: true,pagination: {el: ".swiper-pagination",clickable: true,},navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},scrollbar: {el: ".swiper-scrollbar",},});},
};
</script>

将轮播图拆分为全局组件

因为上面写的三个轮播图样式都一样,所以可以拆分为一个公共组件。但是想要拆分为公共组件需要方法相同,在ListContainer组件中使用了watch'nextTick,所以要在Floor组件中写上相同结构的代码。

但是如果直接在Floor中直接写watch会发现不会监听到任何东西:

  watch: {list: {handler() {// 只能监听到数据已经有了,但是v-for动态渲染的结构还是没有办法确定,因此还是需要nextTickconsole.log("正在监听Floor组件的list数据")},},},

在这里插入图片描述
这是因为list数据是父组件Home给的。给的时候就是一个对象,对象里面的数据都是有的。 所以需要启动立即监听,即为immediate: true,无论是否有数据变化,启动就先启动监听。

  watch: {list: {// 无论数据是否有变化,启动先监听一次//如果不写immediante,watch监听不到list,因为list数据是父组件Home给的。给的时候就是一个对象,对象里面的数据都是有的immediate: true,handler() {console.log("正在监听Floor组件的list数据");},},},

在这里插入图片描述
但是以上写法只能监听到数据有了,但是v-for是否已经动态渲染完结构我们是无法确定的,因此还是需要使用nextTick

  watch: {list: {// 无论数据是否有变化,启动先监听一次//如果不写immediante,watch监听不到list,因为list数据是父组件Home给的。给的时候就是一个对象,对象里面的数据都是有的immediate: true,handler() {// 只能监听到数据已经有了,但是v-for动态渲染的结构还是没有办法确定,因此还是需要nextTickthis.$nextTick(() => {// 这次可以放在mounted中是因为这次请求的数据不是在floor自己内部请求的,数据是父组件Home给的,在floor挂载完毕前,就已经拿到了父组件穿过来的数据new Swiper(this.$refs.cur, {loop: true,pagination: {el: ".swiper-pagination",clickable: true,},navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},scrollbar: {el: ".swiper-scrollbar",},});});},},},

我们在components文件下新建一个Carousel/Carousel.vue用来放称为全局组件的轮播图。

Carousel.vue:
切记样式不要写在这个公共组件里面,因为ListContainerFloor的组件样式是不一样的。

<template><!-- 轮播图 --><div class="floorBanner"><div class="swiper-container" id="floor1Swiper" ref="cur"><div class="swiper-wrapper"><div class="swiper-slide" v-for="carousel in list" :key="carousel.id"><img :src="carousel.imgUrl" /></div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div></div>
</template><script>
import Swiper from "swiper";export default {name: "Carousel",// 这个list是为了下面监听props: ["list"],watch: {list: {// 无论数据是否有变化,启动先监听一次immediate: true,handler() {this.$nextTick(() => {new Swiper(this.$refs.cur, {loop: true,pagination: {el: ".swiper-pagination",clickable: true,},navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},scrollbar: {el: ".swiper-scrollbar",},});});},},},
};
</script><style lang="less" scoped></style>

main.js:

// 轮播图全局组件
import Carousel from '@/components/Carousel/Carousel.vue';
Vue.component(Carousel.name, Carousel);

然后就是去对应的组件中引用全局组件:
Floor: 因为需要拿到父组件的数据,所以props: ["list"],也不可少。
ListContainer:

Search模块vuex操作

第一步: 拆静态组件(略)
第二步: 发请求,这个接口的请求方式为POST,而且需要待参数。当前这个接口像服务器传递params,最少需要一个空对象。

// 获取搜索模块数据接口 /api/list 请求方式post  需要带参数
// 当前这个接口像服务器传递params,最少需要一个空对象
export const reqGetSearchInfo = (params) => {return requests({url: '/list',method: 'POST',// 是否带参数data: params,})
};

第三步: vuex三连环管理数据

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

getters的使用

getters:计算属性,为了简化数据而生,可以把我们将来要用的数字简化一下。
如果不使用getters属性,我们在组件获取state中的数据表达式为:this.$store.state.子模块.属性
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
**注意:**仓库中的getters是全局属性,是不分模块的。即store中所有模块的getter内的函数都可以通过$store.getters.函数名获取
search.js如果开启命名空间,getters也分模块

// 计算属性,为了简化数据而生,可以把我们将来要用的数字简化一下
const getters = {// 当前形参state是当前仓库中的state,并非大仓库中的state,所以state下一级直接就是searchListgoodsList(state) {return state.searchList.goodsList || [];},trademarkList(state) {return state.searchList.trademarkList || [];},attrsList(state) {return state.searchList.attrsList || [];},
};

Search组件:

import { mapGetters } from 'vux';
=========================computed: {...mapGetters(['goodsList'])}
};

mounted只会在组件挂在完执行一次,所以我们需要一个methods实现把请求封装为一个函数,当你需要使用的时候调用即可。
mounted:

  mounted() {this.getData();},

methods:

  methods: {//向服务器发送请求获取search模块的数据(根据参数不同返回不同的数据进行展示)getData() {this.$store.dispatch("getSearchList", {});}},

我们现在在getData中带的是一个空对象,这样是不合理的。我们需要根据参数的不同来返回不同的数据,可以写成响应式数据data,默认为空。pageNo默认数据就是1,为从第一页开始。

  data() {return {// 带给服务器的参数searchParams: {category1Id: "",category2Id: "",category3Id: "",categoryName: "",keyword: "",// 排序order: "",pageNo: 1,pageSize: 10,// 平台售卖属性props: [],// 品牌trademark: "",},};},

我们要在发请求之前(调用getData())带给服务器的参数【searchParams参数发生变化有数值带给服务器】
所以我们需要在mounted的生命周期之前获取到需要带给服务器的数据,即为beforeMount(),而这些参数是在路由中有的。
所以我们需要用Object.assign,此方法为ES6新增语法。

Object.assign

Object.assign是一个ES6新增的语法。
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性。
Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标对象的[[Set]],所以它会调用相关gettersetter。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用Object.getOwnPropertyDescriptor()Object.defineProperty()

String类型和 Symbol 类型的属性都会被拷贝。
demo:

    let searchParams = {category1Id: "",category2Id: "",category3Id: "",categoryName: "",keyword: "",// 排序order: "",pageNo: 1,pageSize: 10,// 平台售卖属性props: [],// 品牌trademark: "",};let query = {category1Id:"110", categoryName:"手机"};let parmas = {keyword: "华为"};console.log(Object.assign(searchParams, query, parmas));

在上面的例子中,searchParamsquery中的category1IdcategoryName属性,所以query中的参数覆盖掉searchParams中原有的数据,query也是如此。

实现SearchSelector组件展示

老样子,从vuex的getter拿到数据,然后去html结构动态渲染。
js部分:

import { mapGetters } from "vuex";export default {name: 'SearchSelector',computed: {...mapGetters(['trademarkList', 'attrsList'])}}

利用路由信息变化实现动态搜索

$route是组件的属性,所以watch是可以监听的(watch可以监听组件data中所有的属性)
注意:组件中data的属性包括:自己定义的、系统自带的(如 $route)、父组件向子组件传递的等等。
请添加图片描述

所以如果在search组件中想要使用$route,是不需要加this的,但是每次请求完毕需要把相映的一二三级菜单制空,让他去接收下一次操作的对应数据.不然下一次请求的数据会出问题:
在这里插入图片描述

  watch: {// 监听路由的信息是否发生变化$route() {Object.assign(this.searchParams, this.$route.query, this.$route.params);this.getData(); // 再次发请求// 每一次请求完毕,应该把相映的一二三级菜单制空,让他去接收下一次操作的对应数据this.searchParams.category1Id = "";this.searchParams.category2Id = "";this.searchParams.category3Id = "";this.searchParams.keyword = "";},},

面包屑分类处理

给X加一个点击事件@click="removeCategoryName",然后去methods中写回调函数。但是如果我们把category相关的字段置空,还是会发给服务器,从性能方面考虑,我们可以写成undefine,那么这个字段就不会发给服务器。

  methods: {===========================// 删除分类的名字removeCategoryName() {this.searchParams.categoryName = undefined;this.searchParams.category1Id = undefined;this.searchParams.category2Id = undefined;this.searchParams.category3Id = undefined;this.getData();},},

在这里插入图片描述
如上就实现了点击X取消面包屑,返回默认搜索页面,但是会发现路由并没有变成默认
在这里插入图片描述
如何解决呢,需要一个骚操作,只需要再次进行路由跳转就可以

this.$router.push({name: 'search'});

在这里插入图片描述
完整代码如下:

  methods: {===============================// 删除分类的名字removeCategoryName() {// 如果我们把category相关的字段置空,还是会发给服务器,从性能方面考虑,我们可以写成undefine,那么这个字段就不会发给服务器。this.searchParams.categoryName = undefined;this.searchParams.category1Id = undefined;this.searchParams.category2Id = undefined;this.searchParams.category3Id = undefined;this.getData();// 进行路由的跳转this.$router.push({name: 'search'});},},

但是,这么写有一些问题,我们如果带有parmas参数,也会一起删除。所以我们需要跳转路由的时候加上params参数。(个人觉得这块业务逻辑很奇怪,一二级菜单都没了,还留着params参数干啥。)

// 进行路由的跳转
this.$router.push({name: "search", params: this.$route.params});

如果按上方代码写的话,路由必定会发生改变,watch会监听到,所以上面写的mounted()就没有必要了。

全局事件总线 实现清楚面包屑,搜索框自动清除

组件通信方式:

props:父子
自定义事件:子父
veux:万能
插槽:父子
pubsub-js:万能,很少用
$bus:全局事件总线,万能

需要去main.js注册全局事件总线:

new Vue({
===========================// 全局事件总线$bus配置beforeCreate(){Vue.prototype.$bus = this;},
==========================
}).$mount('#app')

然后发送消息的组件使用this.$bus.$emit("事件名");,接受消息的组件使用this.$bus.$on("事件名")来接收事件。

平台售卖属性

平台售卖属性在子组件SearchSelector中,需要传值给父组件,所以给子组件绑定一个自定义事件@click="attrInfo(attr, attrValue)

    methods: {==============================// 平台售卖属性值attrInfo(attr, attrValue) {this.$emit("attrInfo", attr, attrValue);},},

在父组件中整理数据,我们需要的是子组件传过来的attr里面的attrId和attrName和 attrValue,发送请求:

    attrInfo(attr, attrValue) {// 1、先整理参数let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;  // ES6语法// 数组去重if (this.searchParams.props.indexOf(props) == -1) {this.searchParams.props.push(props);}// 2、发请求this.getData();},

这次的面包屑不能使用v-if,因为是一个数字,里面有很条数据。所以要采用v-for来遍历


<li class="with-x" v-for="(attrValue, index) in searchParams.props" :key="index">{{ attrValue.split(':')[1]}}<i @click="removeKeyword">xi>
li>

排序操作

1代表综合。2代表价格。
不过一般都是在后端进行排序,前端只需要注意渲染数据就可以。
li标签有active属性,才会有背景色:

  • 切换的时候通过order中是包含1(综合)还是包含2(价格)来判断谁应该有active属性。
    如果是true就展示,如果是false就不展示。所以可以通过计算属性,判断是否有1,如果有就返回true,综合就展示;没有就返回false,价格通过取反来展示,因为只有true和false两个属性,且不会变化 ,所以放在了computed里面:

      computed: {// 判断active,展示背景showActive() {if (this.searchParams.order.includes('1')) {return true;} else {return false;}},},
    
     <li :class="{  active:showActive }"><a href="#">综合a>li><li :class="{  active:!showActive }"><a href="#">价格a>li>
    

    includes
    判断判断箭头是否展示和展示向上还是向下,可以通过判断order是desc还是asc。

        // 图标上下showIcon() {if (this.searchParams.order.includes('asc')) {return true;} else {return false;}},
    
     <li :class="{ active:showActive }"><a href="#">综合 <span v-show="showActive" class="iconfont" :class="{ 'icon-angle-up': showIcon, 'icon-angle-down': !showIcon}">span>a>li><li :class="{ active:!showActive }"><a href="#">价格 <span v-show="!showActive" class="iconfont" :class="{ 'icon-angle-up': showIcon, 'icon-angle-down': !showIcon}">span>a>li>
    

    识别点击的是哪个按钮和应该显示的箭头:
    li标签添加一个点击事件:@click,如何区分这两个点击事件呢?给他们加上不同的形参:

    <li :class="{ active:showActive }" @click="changeOrder('1')"><a href="#">综合 <span v-show="showActive" class="iconfont" :class="{ 'icon-angle-up': showIcon, 'icon-angle-down': !showIcon}">span>a>
    li>
    <li :class="{ active:!showActive }" @click="changeOrder('2')"><a href="#">价格 <span v-show="!showActive" class="iconfont" :class="{ 'icon-angle-up': showIcon, 'icon-angle-down': !showIcon}">span>a>
    li>
    

    我们打印一下形参:

        // 判断点击的是哪个是按钮以及箭头方向changeOrder(flag) {  console.log(flag);},
    

    会发现点击两个不同按钮的时候,打印的参数是不一样的,flag判断点击的是综合还是价格,用户点击的时候传递过来的。
    在这里插入图片描述
    创建一个常量originFlag来存放点击之前的类型,originSort来储存点击之前是升序还是降序。
    创建一个数组orderMap来存放升序降序。
    三元表达式orderMap[originFlag !== flag ? 0 : (1 - orderMap.indexOf(originSort))]从右往左读的意思依次是:
    (1 - orderMap.indexOf(originSort)):orderMap中是否包含originSort,如果包含返回指定的字符串值在字符串中首次出现的位置,找不到返回-1。然后originFlag !== flag ? 0 : (1 - orderMap.indexOf(originSort))判断originFlag是否不等于flag,不等于返回0,等于返回0或者1,然后orderMap[三元表达式]通过结果来决定取数组orderMap中的第几个元素。

        // 判断点击的是哪个是按钮以及箭头方向changeOrder(flag) {  // flag判断点击的是综合还是价格,用户点击的时候传递过来的console.log(flag)const originFlag = this.searchParams.order.split(":")[0];const orderMap = ['desc', 'asc'];const originSort = this.searchParams.order.split(":")[1];this.searchParams.order = `${flag}:${orderMap[originFlag !== flag ? 0 : (1 - orderMap.indexOf(originSort))]}`;},
    

    分页器

    可以使用elementUI现成的组件。这里自己写一个。
    对于分页器,很重要的一个地方就是算出连续页面起始数字和结束数字
    核心属性:
    pageNo(当前页码)、pageSize、total、continues(连续展示的页码)

        // 计算出连续的页码的起始数字和结束数字startNumAndEndNum() {// ES6新语法,结构赋值// const {continues, pageNo, totalPage} = this;let start = 0, end = 0;// 少于5页if (this.continues > this.totalPage) {start = 1;end = this.totalPage;} else {start = this.pageNo - parseInt(this.continues / 2);end = this.pageNo + parseInt(this.continues / 2);// start不能为小于1的数字if (start < 1) {start = 1;end = this.continues;}// end不能大于总页码if (end > this.totalPage) {end = this.totalPage;start = this.totalPage - this.continues + 1;}}return {start, end}},
    
      <div class="pagination"><button>上一页button><button v-if="startNumAndEndNum.start > 1">1button><button v-if="startNumAndEndNum.start >2 ">···button><span v-for="(page, index) in startNumAndEndNum.end" :key="index"><button  v-if="page >= startNumAndEndNum.start">{{page}}button>span><button v-if="startNumAndEndNum.end < totalPage -1">···button><button v-if="startNumAndEndNum.end < totalPage">{{totalPage}}button><button>下一页button><button style="margin-left: 30px">共 {{total}} 条button>div>
    

    然后就需要添加点击事件向服务器发送请求:
    子传父,需要用到自定义事件:
    父亲组件Search:

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

    子组件Pagination/index.vue

    <template><div class="pagination"><button :disabled="pageNo==1" @click="$emit('getPageNo', pageNo - 1)">上一页button><button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)">1button><button v-if="startNumAndEndNum.start >2 ">···button><span v-for="(page, index) in startNumAndEndNum.end" :key="index"><button  v-if="page >= startNumAndEndNum.start" @click="$emit('getPageNo', page)" :class="{active:pageNo==page}">{{page}}button>span><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>
    

    商品详情界面

    首先要去路由中注册detail路由,而且为了让服务器知道我们点击的是哪个商品,在路由跳转的时候需要带上产品的id给详情页面。

       {path: "/detail/:skuid",name: "detail",component: Detail,meta: { showFooter: true }},
    

    当我们在search组件中点击图片的时候会跳转到detail组价。所以需要把图片部分做成router-link,以为用到了good.id所以to要写成动态,也就是:to

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

    滚动行为

    当我们从底部点击图片跳转的时候,会发现跳转过去的默认位置在浏览器底部。我们需要的效果是路由跳转默认到最上方,所以我们需要用到scrollBehavior这个方法。


    本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

  • 相关文章

    立即
    投稿

    微信公众账号

    微信扫一扫加关注

    返回
    顶部