最新接口请求地址

- http://gmall-h5-api.atguigu.cn/

	- 示例: http://gmall-h5-api.atguigu.cn/api/product/getBaseCategoryList

axios二次封装

- 前端请求方法大概有这几种: XMLHttpRequest,fetch,JQuery,axios(推荐)

- 为什么要二次封装axios,为了'请求拦截器'和'响应拦截器'

	- 请求拦截器: 可以在请求发送之前,处理一些业务
	- 响应拦截器: 当服务器返回数据时,处理一些业务
	
- 安装 axios: npm install axios
  • 二次封装配置
### src.api.request.js

import axios from 'axios'

const requests = axios.create({ // 创建一个 axios实例并配置
	baseURL:'/api', // 以后发请求,无需再写'/api'路径
	timeout:5000, // 超时5秒即视为请求失败
})

requests.interceptors.request.use((config)=>{
	return config // config里面有一个headers请求头,是重要参数
})

requests.interceptors.response.user((res)=>{
	return res.data
},(error)=>{
	return Promise.reject(new Error('faile'))
})

export default requests // 最后导出

API统一管理

  • 引入场景: 当组件很多而且请求也多的时候,比如服务器的路径变了,得去组件一个个修改请求路径,显得不方便.所以,进行统一的API管理方便以后维护

    类似django的settings配置项,各个模块引入.当修改settings配置时,各个模块无需再修改

  • api新建index.js,把API的逻辑放在里面

### api.index.js

import requests from './request'
// 使用 reqCategoryList 变量来保存 匿名函数对象
// axios 返回的是一个 Promise对象
export const reqCategoryList = ()=>requests({url:'/product/getBaseCategoryList',method:'get'})
  • 要向后端发请求,先配置代理服务器解决跨域问题
### vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  // 关闭eslint
  lintOnSave:false,
  devServer: {

      // 以下三个配置项: 启动项目的时候,就自动打开浏览器
      open:true,
      host:'localhost',
      port:8080,

      proxy: {
          "/api": {
              // 服务器目的地址
              target: "http://gmall-h5-api.atguigu.cn",
              // 重写路径
              // pathRewrite: {"^/api": ""}
              // changeOrigin: true,
          },
      },
  },
})

  • 测试: 我们在main.js测试效果
### main.js
......
import {reqCategoryList} from './api'
var res = reqCategoryList()
console.log(res) // res保存的是一个Promise对象,里面包含后端发过来的数据

new Vue({
  ......
}).$mount('#app')

进度条nprogress的使用

  • 安装
npm install nprogress
  • 使用: 在请求开始请求结束中运行
### api.request.js

import axios from 'axios'
import nprogress from 'nprogress' // 引入 nprogress
import 'nprogress/nprogress.css' // 引入 nprogress 样式(可以修改源码的进度条颜色)

const requests = axios.create({ 
	......
})

requests.interceptors.request.use((config)=>{
	nprogress.start(); // 开始发请求就显示进度条
	return config 
})

requests.interceptors.response.use((res)=>{
	nprogress.done(); // 请求发送结束,进度条也结束
	return res.data
},(error)=>{
	......
})

export default requests

如果不引入nprogress.css,网页是没有进度条效果的

只有在发请求的时候,才有进度条效果(比如路由组件之间切换的时候,是不会有进度条效果的)

VueX: N多组件之间,共享数据的仓库

  • 如果组件很少,完全可以不用,可以使用$bus来搞定即可
### store.index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  
  actions: { // 接收组件传过来的数据,提交给mutations;也可以与后端交互
	  
  },
  
  mutations: { // 加工数据
	  
  },
  
  state: { // 存放数据的仓库
	  
  },
  getters: { // 计算属性:简化运算
	  
  },
  
  modules: {
  }
})

  • 简单测试(点击按钮,变量自加1)
### store.index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  
  actions: { 
	  add({commit}){
		  commit('ADD') // 提交给mutations
	  }
  },
  
  mutations: { 
	  ADD(state){
		  state.num++; // 自加1
	  }
  },
  
  state: { 
	  num:1 // 测试数据
  },
  ......
})

### Home.vue
......
<template>
	<div id="">
		......
		<h1>当前num的值为: {{$store.state.num}}</h1>
		<button type="button" @click="add">num自加</button> <!--绑定事件-->
	</div>
</template>

<script>
	......
	
	export default {
		name:'Home',
		components:{
			......
		},
		methods:{
			add(){
				this.$store.dispatch('add') // 分发
			}
		},
	}
</script>

<style>
</style>

  • VueX模块化分工(便于日后维护各个组件的数据)
- store.home.index.js(search一样的)

const actions = {}
const mutations = {}
const state = {}
const getters = {}

export default {
	actions,
	mutations,
	state,
	getters
}

- store.index.js(注册模块)

import Vue from 'vue'
import Vuex from 'vuex'
import home from './home/index.js'
import search from './search/index.js'

Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
	  home,
	  search
  }
})

项目规范: 把TypeNav扔到components目录(全局组件一般丢这里)

  • main.js重新导入路径
......
import TypeNav from '@/components/TypeNav' // 重新导入
......

new Vue({
......
}).$mount('#app')

三级联动获取服务器数据

  • typeNav组件挂载完毕,就向服务器发请求,获取数据
### typeNav.index.vue
......
<script>
	export default {
		name:'TypeNav',
		mounted(){
			this.$store.dispatch('categoryList') // 进行事件分发
		}
	}
</script>

### store.home.index.js
import {reqCategoryList} from "@/api" // 导入之前封装好的api接口

const actions = { // 向后端发请求的逻辑,一定是在 actions处理
	categoryList(){ // 事件处理
		var res = reqCategoryList() // 发请求
		console.log(res) // 接收
	}
}

- 返回结果是一个 Promise()对象,缺点是数据不够直观

    Promise {<pending>}
    [[Prototype]]: Promise
    [[PromiseState]]: "fulfilled"
    [[PromiseResult]]: Object // data数据藏在这里...
    
- 直观的返回data可以这么写: 

    const actions = {
        async categoryList(){ // 用 async 修饰 categoryList函数对象
            var res = await reqCategoryList() // 用 await 修饰 Promise对象
            console.log(res)
        }
    }
    
    - 注意事项: async和await一定是成对出现的
    - 返回结果如下:
    	{code: 200, message: '成功', data: Array(17), ok: true}

  • index.js的完整逻辑如下
import {reqCategoryList} from "@/api"

const actions = {
	async categoryList({commit}){
		var res = await reqCategoryList()
		if(res.code == 200){ // 响应成功,才提交本次数据
			commit('CATEGORYLIST',res.data)
		}
	}
}
const mutations = { // 赋值
	CATEGORYLIST(state,categoryList){
		state.categoryList = categoryList
	}
}
const state = {
	categoryList:[] // 初始值请不要瞎写,后端返回的是[...],就写[]
}
const getters = {}

export default {
	actions,
	mutations,
	state,
	getters
}

  • components.TypeNav.index.vue组件测试数据
<div class="type-nav">
	{{categoryList}} <!--测试数据-->
......
<script>
	import {mapState} from 'vuex' // 使用 mapState 简化代码
	
	export default {
		name:'TypeNav',
		computed:{
			...mapState({ // 解构的写法
				categoryList:state=>state.home.categoryList // 返回 categoryList
			})
		},
		mounted(){
			this.$store.dispatch('categoryList')
		}
	}
</script>

  • components.TypeNav.index.vue组件渲染数据
<template>
    <!-- 商品分类导航 -->
    <div class="type-nav">
        <div class="container">
            <h2 class="all">全部商品分类</h2>
            <nav class="nav">
                <a href="###">服装城</a>
                <a href="###">美妆馆</a>
                <a href="###">尚品汇超市</a>
                <a href="###">全球购</a>
                <a href="###">闪购</a>
                <a href="###">团购</a>
                <a href="###">有趣</a>
                <a href="###">秒杀</a>
            </nav>
            <div class="sort">
                <div class="all-sort-list2">
          			
          			<!--渲染第一层数据-->
                    <div class="item" v-for="(item1,index1) in categoryList" :key="item1.categoryId">
                        <h3>
                            <a href="">{{item1.categoryName}}</a>
                        </h3>
                        <div class="item-list clearfix">
                        	<!--渲染第二层数据-->
                            <div class="subitem" v-for="(item2,index2) in item1.categoryChild" :key="item2.categoryId">
                                <dl class="fore">
                                    <dt>
                                        <a href="">{{item2.categoryName}}</a>
                                    </dt>
                                    <dd>
                                    	<!--渲染第三层数据-->
                                        <em v-for="(item3,index3) in item2.categoryChild" :key="item3.categoryId">
                                            <a href="">{{item3.categoryName}}</a>
                                        </em>
                                    </dd>
                                </dl>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
    import {mapState} from 'vuex'
    
    export default {
        name:'TypeNav',
        computed:{
            ...mapState({
                categoryList:state=>state.home.categoryList
            })
        },
        mounted(){
            this.$store.dispatch('categoryList')
        }
    }
</script>

<style lang="less" scoped>
    ......
</style>

  • 类似三级联动这种数据格式,套路如下
[
	{
		id:1
		name:'xxx'
		categoryChild:[...] // 继续上面的套路,包裹一个个对象,里面再包裹[]...
	}
	{...}
	{...}
]

增加一级导航背景色高亮样式

  • 当鼠标移动到一级导航时,立马显示背景色高亮效果
- 解决方式1: 一行css代码搞定
	### index.vue
	......
	.item:hover {
    	background-color: skyblue;
    }
  • 本项目主要为了练习js,所以这次我们使用js来实现这个功能
### index.vue

 <div class="item" v-for="(item1,index1) in categoryList" :key="item1.categoryId" :class="{cur:currentIndex==index1}"> <!--新增:class样式,条件判断-->
 						<!--鼠标进入||鼠标离开-->
                        <h3 @mouseenter="changeIndex(index1)" @mouseleave="leaveIndex">
                            ......

<script>
    import {mapState} from 'vuex'
    
    export default {
        name:'TypeNav',
		data(){
			return {
				currentIndex:-1 // 准备数据,值为-1的时候,不等于任何索引
			}
		},
		methods:{
			changeIndex(index){
				this.currentIndex = index // 当满足索引相同的时候,点亮元素的样式
			},
			leaveIndex(){
				this.currentIndex = -1 // 鼠标离开的时候,取消样式
			}
		},
        computed:{
            ......
        mounted(){
            ......
        }
    }
</script>

<style>
	.....
	.cur {
    	background: skyblue; // 新增样式
    }
</style>
  • 修复小样式: 当鼠标从一级目录移动到全部商品分类的时候,此时一级目录应该维持高亮效果

    我们新增div元素,把之前的结构包裹进去,从而实现这一效果

 <div class="type-nav">
        <div class="container">
			<div @mouseleave="leaveIndex"> <!--新增div结构,把鼠标离开事件丢这里-->
				<h2 class="all">全部商品分类</h2>
				<div class="sort">
				......
			<nav class="nav">
                <a href="###">服装城</a>
                <a href="###">美妆馆</a>
                <a href="###">尚品汇超市</a>
                <a href="###">全球购</a>
                <a href="###">闪购</a>
                <a href="###">团购</a>
                <a href="###">有趣</a>
                <a href="###">秒杀</a>
            </nav>

功能: 通过js控制二,三级商品分类的显示与隐藏

- 最开始的时候,是通过CSS样式 display: block|none 显示与隐藏二,三级商品分类
  现在注释掉这些样式,我们使用js来实现
	
	.item-list {
        display: none; // 默认隐藏
        ......
    &:hover { // 鼠标移动过来就展示
        .item-list {
        display: block;
    }
  • 模仿前面的套路
......
								<!--新增display取值的判断-->
<div class="item-list clearfix" :style="{display:currentIndex==index1?'block':'none'}">
				                <div class="subitem" v-for="(item2,index2) in item1.categoryChild" :key="item2.categoryId">
				                    <dl class="fore">

浏览器的卡顿现象

- 当用户操作'一级菜单',鼠标移动速度很快的时候,导致浏览器来不及反应,部分'一级菜单'的效果没有呈现出来
  如果里面包含大量的业务逻辑,这种情况显然是不允许发生的

函数的防抖和节流

  • 原理: 闭包 && 延时器
- 防抖: 频繁发生的事情,最终只发生一次(以最后一次点击为准)

- 节流: 频繁发生的事情,由多次变成少次(即减少次数,以最开始一次点击为准)

- 防抖: 就是回城,被打断就要重新来

- 节流: 就是技能CD,没好就用不了技能
  • 防抖demo演示: 搜索框用户输入内容的时候,有两种发请求的场景
- 只要搜索框的内容一改变,立马发请求到后端(先演示这个)

- 等用户输入完内容,再向后端发请求(推荐)
- 效果: 输入框的内容一旦发生变化,马上触发函数体里面的逻辑

<!DOCTYPE html>
<html>
	<head>
		......
	</head>
	<body>
		<p>
			搜索内容: <input type="text" name="" id="" value="" />
		</p>
		
		<script type="text/javascript">
			var inputElement = document.querySelector('input');
			inputElement.oninput = function(){ // oninput事件,实时监听输入框内容的变化
				console.log('输入框的内容被改变了')
			}
		</script>
	</body>
</html>

  • 处理防抖节流的逻辑,我们使用第三方库lodash来完成,使用方式和JQuery类似
<!DOCTYPE html>
<html>
	<head>
		......
		<!--引入-->
		<script src="./js/lodash.js" type="text/javascript" charset="utf-8"></script>
	</head>
	<body>
		......
		<script type="text/javascript">
			console.log(_) // 使用方式,就是一个'_'
			......
		</script>
	</body>
</html>

  • 使用_.debounce实现防抖功能
- 效果: 用户的不断的输入,但是过5s以后才会触发函数体的逻辑,即多次触发,实际只执行一次

<!DOCTYPE html>
<html>
	<head>
		......
		<script src="./js/lodash.js" type="text/javascript" charset="utf-8"></script>
	</head>
	<body>
		<p>
			搜索内容: <input type="text" name="" id="" value="" />
		</p>
		
		<script type="text/javascript">
			var inputElement = document.querySelector('input');
			inputElement.oninput = _.debounce(function(){ // 防抖
				console.log('输入框的内容被改变了')
			},5000) // 延迟时间为5s
		</script>
	</body>
</html>

  • 节流demo引入场景: 每点击一次按钮,变量就自加1(计数器实例)
- 效果: 每点击一次按钮,就执行一次加1逻辑

<!DOCTYPE html>
<html>
	<head>
		......
	</head>
	<body>
		
		<h1>我是计数器:<span>0</span></h1>
		<button type="button">点我加1</button>
		
		<script type="text/javascript">
			
			var spanElement = document.querySelector('span')
			var buttonElement = document.querySelector('button')
			var count = 0
			buttonElement.onclick = function(){
				count++;
				spanElement.innerHTML = count;
			}
		</script>
	</body>
</html>

  • 使用_.throttle来控制节流
- 效果: 点击一次按钮以后,立即执行一次,5s内不管再点击多少次,都不会再被执行

<!DOCTYPE html>
<html>
	<head>
		......
		<script src="./js/lodash.js" type="text/javascript" charset="utf-8"></script>
	</head>
	<body>
		<h1>我是计数器:<span>0</span></h1>
		<button type="button">点我加1</button>
		
		<script type="text/javascript">
			var spanElement = document.querySelector('span')
			var buttonElement = document.querySelector('button')
			var count = 0
			buttonElement.onclick = _.throttle(function(){ // 调用
				count++;
				spanElement.innerHTML = count;
				console.log('自加1了')
			},5000)
		</script>
	</body>
</html>

  • 使用_.debounce来演示:一模一样的代码,只不过调用换成了_.debounce
- 效果1: 只要一直点击按钮,函数体逻辑是不会被触发的
- 效果2: 以最后一次点击为准,等待5s后再执行函数体逻辑

<!DOCTYPE html>
<html>
	<head>
		......
		<script src="./js/lodash.js" type="text/javascript" charset="utf-8"></script>
	</head>
	<body>
		<h1>我是计数器:<span>0</span></h1>
		<button type="button">点我加1</button>
		
		<script type="text/javascript">
			var spanElement = document.querySelector('span')
			var buttonElement = document.querySelector('button')
			var count = 0
			buttonElement.onclick = _.debounce(function(){
				count++;
				spanElement.innerHTML = count;
				console.log('自加1了')
			},5000)
		</script>
	</body>
</html>
  • 小结: 节流防抖效果最终区别
- 节流: 用户单击按钮,马上执行一次,设置延迟时间,在延迟时间内,不会再触发逻辑

- 防抖: 用户单击按钮,以最后一次点击为准,设置延迟时间,等延迟时间到了,再执行逻辑(如果一直点击,无法触发逻辑直到最后一次点击)

lodash引入项目

### TypeNav.index.vue
......
<script>
	......
	import _ from 'lodash' // node_modules自带有,无需再npm安装
    
    export default {
        ......
		methods:{
			// 不能再使用简写形式了
			changeIndex:_.throttle(function(index){
				this.currentIndex = index
				console.log(index)
			},50),
			leaveIndex(){
				......
			}
		},
        computed:{
            ......
        },
        mounted(){
            ......
        }
    }
</script>
  • 注意事项: import _ from 'lodash'这种形式相当于引入全部,最好的引入方式是按需引入
### TypeNav.index.vue
......
<script>
   ......
	import throttle from 'lodash/throttle' // 引入
	
    export default {
       ......
		methods:{
			changeIndex:throttle(function(index){ // 稍改
				this.currentIndex = index
			},50),
			leaveIndex(){
				this.currentIndex = -1
			}
		},
        ......
    }
</script>

throttle(func)最好写成function的形式,使用箭头函数会有this指向问题

三级联动的跳转和传参

  • 场景一: 点击一级目录的时候,url是这样
http://sph-list.atguigu.cn/list.html?category1Id=1
  • 场景二: 点击二级目录的时候,url是这样
http://sph-list.atguigu.cn/list.html?category2Id=22
  • 场景三: 点击三级目录的时候,url是这样
http://sph-list.atguigu.cn/list.html?category3Id=178
  • 搜索的时候,url是这样
..../categoryName=手机&categoryId=2
  • 使用rounter-link跳转,可能出现卡顿现象
<router-link>是一个组件,当服务器数据返回以后,若循环出很多<router-link>组件,有可能一下子占用很多内存,造成卡顿
  • 三级联动的跳转,我们使用编程式导航来实现,可以这弄
- 为每个<a>标签绑定跳转事件goSearch

### TypeNav.vue
......
<a @click="goSearch">{{item1.categoryName}}</a>
<a @click="goSearch">{{item2.categoryName}}</a>
<a @click="goSearch">{{item3.categoryName}}</a>
......
  • 更好的解决办法是利用事件委托来实现
- 不再为每个a标签绑定跳转事件,而是委托给div块来处理

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

- 面临的问题
	- 在div块中,一定要点到a标签才能跳转
	- 传参如何实现?(比如如何区分一级,二级,三级标签),实现类似这样的url: .../search?categoryName=电子书&category3Id=1
	
- 解决办法:
	- 给a标签绑定两个自定义属性: <a :data-categoryName="item1.categoryName" :data-category1Id="item1.categoryId">{{item1.categoryName}}</a>
	- 当用户点击div块时,获取元素,若该元素拥有 categoryname 属性,则继续判断 categoryId
	  最后构造路由参数传给"$router.push()"
### TypeNav.index.vue
......
<div class="all-sort-list2" @click="goSearch"><!--委托div块处理-->
    ......
    <!--为一级,二级,三级目录绑定两个自定义属性 data-categoryName && data-category1Id-->
    <a :data-categoryName="item1.categoryName" :data-category1Id="item1.categoryId">{{item1.categoryName}}</a>
    ......
    <a :data-categoryName="item2.categoryName" :data-category2Id="item2.categoryId">{{item2.categoryName}}</a>
    ......
    <a :data-categoryName="item3.categoryName" :data-category3Id="item3.categoryId">{{item3.categoryName}}</a>
    ......
<script>
	......
	methods:{
		......
		goSearch(event){
				var element = event.target; // 获取用户点击的目标元素
				// 解构获取四个变量值(注意全部是小写,而不是驼峰的命名方式,想用驼峰就不要用解构赋值)
				let {categoryname, category1id, category2id, category3id} = element.dataset;
				// 若存在 categoryname,则说明用户点击的肯定是a标签
				if(categoryname){
					var location = {name:"search"} // 构造路由参数
					var query = {categoryName:categoryname}
					
					if(category1id){ // 判断 categoryId
						query.category1Id = category1id
					}else if(category2id){
						query.category2Id = category2id
					}else{
						query.category3Id = category3id
					}
					
					location.query = query; // 收集完数据,丢给push()
					this.$router.push(location);
				}
			}
	}
</script>

- 最终效果举例: ....../search?categoryName=notepad&category3Id=178

Search组件引入全局组件TypeNav

  • 效果: 页面进入 Search组件的时候,导航菜单是不显示的,等鼠标移动进入,再展示菜单

    等鼠标离开菜单的时候,导航菜单重新被隐藏

- 挂载 TypeNav 组件的时候

	mounted(){
            this.$store.dispatch('categoryList')
			if(this.$route.path != '/home'){ // 只有 home 页才一直展示"导航菜单"
				this.show = false
			}
        }
        
<!-- <div @mouseleave="leaveIndex"> -->
<!--鼠标进入就展示,离开就隐藏-->
<div @mouseleave="leaveShow" @mouseenter="enterShow">
    ......
    <div class="sort" v-show="show"> <!--使用布尔变量show来控制菜单的展示/隐藏-->
......
export default {
      ......
		data(){
			return {
				......
				show:true // 默认true
			}
		},
		methods:{
			......
			enterShow(){
			    // 挂载的时候,show为false,当鼠标进来的时候,就变更为true
				if(this.$route.path != '/home'){
					this.show = true
				}
			},
			leaveShow(){
				this.currentIndex = -1; // 继承之前的逻辑
				if(this.$route.path != '/home'){
					this.show = false // 鼠标离开的时候,就隐藏(除了home页)
				}
			},
  • 一级导航菜单添加动画效果: 使用 transition name=xxx
  • 使用动画的前提条件: 组件或者元素必须包含v-show/if指令
### TypeNav
......
<div @mouseleave="leaveShow" @mouseenter="enterShow">
    ......
    <transition name="sort"> <!--使用<transition name="sort">包裹起来-->
    	<div class="sort" v-show="show">
    </transition>
</div>
......
.sort-enter{ // 开始的状态(进入)
				height: 0px;
			}
.sort-enter-to{ // 结束的状态(进入)
            	height: 461px;
    		}
.sort-enter-active{ // 动画的时间,速率
            	transition: all .5s linear;
            }
  • 还可以用动画的旋转效果
.sort-enter{
				height: 0px;
				transform: rotate(0deg); // 开始旋转0度
			}
.sort-enter-to{
                height: 461px;
                transform: rotate(360deg); // 后面旋转360度
            }
.sort-enter-active{
            	transition: all .5s linear;
            }

请求次数的优化

  • TypeNav组件挂载完毕的时候,我们向后端发请求,获取导航条数据
......
mounted() {
			this.$store.dispatch('categoryList') // 向后端发起请求
			......
		}
  • TypeNav组件在Search组件也有用到,所以当页面跳到Search组件的时候,会再发一次请求

    其实,我们只需要发一次请求就够了,怎么处理,

    • 可以把数据丢到浏览器,但这其实也是一种请求
    • 答案是放到'App.vue',根组件只会被执行一次,然后把数据保存在vuex仓库
    • 为什么不能扔到main.js(也只执行一次),因为js文件没有this.$store.dispatch
### App.vue
......
<script type="text/javascript">
	......
	
	export default {
		name:'App',
		......
		mounted(){
			this.$store.dispatch('categoryList') // 发请求的逻辑丢这里
		}
	}
</script>

- 再测试效果: 数据只在加载Home页的时候请求一次,就不会再请求

queryparams参数一起传

  • 实现点击typeNav菜单和Header搜索时,把queryparams参数一起传(不单单只传一个)
### typeNav.vue
......
<div class="all-sort-list2" @click="goSearch"> <!--点击分类菜单-->
......
goSearch(event) {
    var element = event.target;
    let {
        categoryname,
        category1id,
        category2id,
        category3id
    } = element.dataset;
    if (categoryname) {
        var location = {
            name: "search"
        }
        var query = {
            categoryName: categoryname
        }
        if (category1id) {
            query.category1Id = category1id
        } else if (category2id) {
            query.category2Id = category2id
        } else {
            query.category3Id = category3id
        }
        
        if(this.$route.params){ // 增加 params参数判断
            location.params = this.$route.params
        }
        
        location.query = query;
        this.$router.push(location);
        
        
    }
}

### Header.index.vue
......
<button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch"> <!--点击搜索-->
......
goSearch(){
    let location = {name:'search',params:{keyword:this.keyword || undefined}}
    if(this.$route.query){ // 增加 query 判断
        location.query = this.$route.query
    }
    this.$router.push(location,()=>{},()=>{})
    
    // this.$router.push({
    //  name:'search',
    //  params:{keyword:this.keyword},
    //  query:{k:this.keyword.toUpperCase()}
    // },()=>{},()=>{})
}

mock.js: 模拟后端发假数据

  • 安装
npm install mockjs
  • 配置注意事项
- mock.banner.json 文件中: 别留有空格(空格会导致跑不起来)
	- 数据格式: [{...},{...},{...}] # 无需对外暴露
- public新建images目录,把需要的图片都丢里面(打包的时候会原封不动,要的就是这个效果)
### mock.mockServe.js
import Mock from "mockjs" // 导入 Mock
import banner from './banner.json' // webpack默认暴露 JSON数据,图片
import floor from './floor.json'

Mock.mock('/mock/banner',{code:200,data:banner}) // 构造请求对象: 路径+数据
Mock.mock('/mock/floor',{code:200,data:floor})

### main.js 中执行一次
......
import '@/mock/mockServe' // 把整个模块导进来

### api.mockAjax.js(配置请求: 把 request.js的逻辑拷贝过来,小改一下即可)
import axios from 'axios'
import nprogress from 'nprogress'
import 'nprogress/nprogress.css'

const requests = axios.create({ // 创建一个 axios实例并配置
	baseURL:'/mock',
	timeout:5000,
})

requests.interceptors.request.use((config)=>{
	......
})

requests.interceptors.response.use((res)=>{
	......

export default requests

### index.js
import requests from './request'
import mockRequests from './mockAjax'

export const reqCategoryList = ()=>requests({url:'/product/getBaseCategoryList',method:'get'})

export const reqGetBannerList = ()=>mockRequests.get('/banner') // 发请求
  • ListContainer组件发送请求并接收数据
### ListContainer.index.vue
......
<script>
	import {mapState} from 'vuex'
	
	export default {
		name: 'ListContainer',
		mounted(){
			this.$store.dispatch('getBannerList') // 分发
		},
		computed:{
			...mapState({
				bannerList:state=>state.home.bannerList // 接收数据
			})
		}
	}
</script>

### store.home.index.js
import {reqCategoryList,reqGetBannerList} from "@/api" // 导入

const actions = {
	async categoryList({commit}){
		......
	},

	async getBannerList({commit}){
		let res = await reqGetBannerList();
		if(res.code == 200){
			commit('GETBANNERLIST',res.data) // 提交数据
		}
	}
}
const mutations = {
	CATEGORYLIST(state,categoryList){
		......
	},
	GETBANNERLIST(state,bannerList){
		state.bannerList = bannerList // 绑定数据
	}
}
const state = {
	categoryList:[],
	bannerList:[] // 初始化数据
}
......

swiper专门用来处理轮播图(不管移动端还是PC端都适用)

  • 官方网址
https://swiper.com.cn/
  • demo演示(三个步骤)
    • 引入swiper cssjs文件
    • 引入swiper页面结构
    • 生成 swiper 实例
    • 注意事项: 如果 swiper页面结构不完整(后面js渲染数据就有这种问题),是不会有轮播图效果的
<!DOCTYPE html>
<html>
	<head>
		......
		<!--引入swiper css和js文件-->
		<script src="js/swiper.min.js"></script>
		<link rel="stylesheet" type="text/css" href="./css/swiper.min.css"/>
	</head>
	<body>
		<div class="swiper"> <!--引入swiper页面结构-->
		    <div class="swiper-wrapper">
		        <div class="swiper-slide">Slide 1</div>
		        <div class="swiper-slide">Slide 2</div>
		        <div class="swiper-slide">Slide 3</div>
		    </div>
		    <!-- 如果需要分页器 -->
		    <div class="swiper-pagination"></div>
		    
		    <!-- 如果需要导航按钮 -->
		    <div class="swiper-button-prev"></div>
		    <div class="swiper-button-next"></div>
		    
		    <!-- 如果需要滚动条 -->
		    <div class="swiper-scrollbar"></div>
		</div>
	</body>
	
	<script>
      // 生成 swiper 实例
	  var mySwiper = new Swiper ('.swiper', {
	    // direction: 'vertical', // 垂直切换选项
	    loop: true, // 循环模式选项
	    
	    // 如果需要分页器
	    pagination: {
	      el: '.swiper-pagination',
	    },
	    
	    // 如果需要前进后退按钮
	    navigation: {
	      nextEl: '.swiper-button-next',
	      prevEl: '.swiper-button-prev',
	    },
	    
	    // 如果需要滚动条
	    scrollbar: {
	      el: '.swiper-scrollbar',
	    },
	  })        
	  </script>
</html>

  • vue引入swiper
- npm install swiper@5 // 项目使用的是5的版本
- main.js 引入全局 css 样式(组件使用的时候,就不必再导入css样式了)
	......
	import 'swiper/css/swiper.css'
- 组件中,引入js
	......
	<script>
	......
	import Swiper from 'swiper'
### ListContainer.index.vue
.....
<div class="center">
    <!--banner轮播-->
    <div class="swiper-container" ref="cur">
    	<!--swiper页面结构-->
        <div class="swiper-wrapper">
        								<!--遍历bannerList-->
            <div class="swiper-slide" v-for="(carousel,index) in bannerList" :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>
......
<script>
    ......
    import Swiper from 'swiper' // 导入

    export default {
        name: 'ListContainer',
        mounted() {
            this.$store.dispatch('getBannerList')
            
            setInterval(() => { // 放入计时器,延迟2s再展示
                // 生成 swiper 实例
                var mySwiper = new Swiper('.swiper-container', {
                    // direction: 'vertical', // 垂直切换选项
                    loop: true, // 循环模式选项

                    // 如果需要分页器
                    pagination: {
                        el: '.swiper-pagination',
                        clickable: true // 点击小球也切换图片
                    },

                    // 如果需要前进后退按钮
                    navigation: {
                        nextEl: '.swiper-button-next',
                        prevEl: '.swiper-button-prev',
                    },

                    // 如果需要滚动条
                    // scrollbar: {
                    //  el: '.swiper-scrollbar',
                    // },
                })
            }, 2000)
        },
        
        computed: {
            ...mapState({ // 数据源
                bannerList: state => state.home.bannerList
            })
        }
    }
</script>
  • 注意事项: 为什么要把swiper实例丢到计时器?
- 如果丢到 mounted()处理,由于涉及到v-for遍历数据,导致页面结构还不完整,new Swipe实例就不会生效

- mounted 的定义是,等组件挂载完毕才执行一系列逻辑(前提是没有涉及到类似v-for这种遍历)
  • 使用watch来解决也有问题: 数据是发生变化了,但是v-for的遍历结束了吗?没结束的话,页面结构依然不完整,所以swiper实例化失败
watch:{
    bannerList:{
        handler(newVal,oldVal){
            var mySwiper = new Swiper('.swiper-container', {
               
                loop: true,
                pagination: {
                    el: '.swiper-pagination',
                    clickable: true 
                },
            
               
                navigation: {
                    nextEl: '.swiper-button-next',
                    prevEl: '.swiper-button-prev',
                },
            
              
            })
        }
    }
}
  • 最佳解决方案: watch + vm.$nextTick
- vm.$nextTick: 循环结束,数据被修改时候生效
  • 最终解决方案
watch:{
    bannerList:{
        handler(newVal,oldVal){
            this.$nextTick(()=>{ // 把生成swiper实例的逻辑,丢到 $nextTick的回调里(等页面结构循环加载完毕后,再渲染swiper实例)
                var mySwiper = new Swiper('.swiper-container', {
                
                    loop: true,
              
                 
                    pagination: {
                        el: '.swiper-pagination',
                        clickable: true 
                    },
                
                  
                    navigation: {
                        nextEl: '.swiper-button-next',
                        prevEl: '.swiper-button-prev',
                    },
                
                   
                })
            })
        }
    }
}

Floor组件的数据渲染

  • 实现的套路,和banner类似
  • 封装发送请求
### api.index.js
.....
export const reqFloorList = ()=>mockRequests.get('/floor')

- mock数据如下: list里面,两组对象

[
    {
        "id":"001",
        "name":"家用电器",
        "keywords":[
            "节能补贴",
            "4K电视",
            "空气净化器",
            "IH电饭煲",
            "滚筒洗衣机",
            "电热水器"
        ],
        "imgUrl":"/images/floor-1-1.png",
        "navList":[
            {
                "url":"#",
                "text":"热门"
            },
            {
                "url":"#",
                "text":"大家电"
            },
            {
                "url":"#",
                "text":"生活电器"
            },
            {
                "url":"#",
                "text":"厨房电器"
            },
            {
                "url":"#",
                "text":"应季电器"
            },
            {
                "url":"#",
                "text":"空气/净水"
            },
            {
                "url":"#",
                "text":"高端电器"
            }
        ],
        "carouselList":[
            {
                "id":"0011",
                "imgUrl":"/images/floor-1-b01.png"
            },
            {
                "id":"0012",
                "imgUrl":"/images/floor-1-b02.png"
            },
            {
                "id":"0013",
                "imgUrl":"/images/floor-1-b03.png"
            }
        ],
        "recommendList":Array[4],
        "bigImg":"/images/floor-1-4.png"
    },
    Object{...}
]
  • store.home.index处理数据
import {......reqFloorList} from "@/api" // 导入请求对象

const actions = {
	async categoryList({commit}){
		......
	},

	async getBannerList({commit}){
		......
	},
	
	async getFloorList({commit}){
		let res = await reqFloorList(); // 请求并获取数据
		if(res.code == 200){
			commit('GETFLOORLIST',res.data)
		}
	}
}
const mutations = {
	CATEGORYLIST(state,categoryList){
		......
	},
	GETBANNERLIST(state,bannerList){
		......
	},
	GETFLOORLIST(state,floorList){
		state.floorList = floorList // 存储数据
	}
}
const state = {
	......
	floorList:[] // 初始化
}
......

  • 轮播图示例中,请求是在listContainer中发送,然后数据返回来,再渲染,本次把数据放在父组件Home中,数据由父传子
### home.index.vue

<template>
	<div id="">
		......
		<!--list有两组数据,故 Floor组件会被复用两次,把对象floor通过list参数传过去-->
		<Floor v-for="(floor,index) in floorList" :key="floor.id" :list="floor"></Floor>
		<!-- <Floor></Floor> -->
	</div>
</template>

<script>
	import ListContainer from '@/pages/Home/ListContainer'
	import Recommend from '@/pages/Home/Recommend'
	import Rank from '@/pages/Home/Rank'
	import Floor from '@/pages/Home/Floor'
	import Like from '@/pages/Home/Like'
	
	import {mapState} from 'vuex'
	
	// import {reqCategoryList} from '../../api/index.js'
	
	export default {
		name:'Home',
		components:{
			ListContainer,
			Recommend,
			Rank,
			Floor,
			Like
		},
		// mounted(){
		// 	console.log(reqCategoryList);
		// 	var res = reqCategoryList();
		// 	console.log(res)
		// }
		methods:{
			// add(){
			// 	this.$store.dispatch('add')
			// }
		},
		computed:{
			...mapState({
				floorList:state=>state.home.floorList
			})
		},
		mounted(){
			this.$store.dispatch('getFloorList')
		}
		// mounted(){
		// 	console.log(this.$store)
		// }
	}
</script>

<style>
</style>

......
<script>
	......
	export default {
		name:'Home',
		......
		mounted(){
			this.$store.dispatch('getFloorList') // 请求分发
		},
		computed:{
			...mapState({
				floorList:state=>state.home.floorList // 接收数据
			})
		},
	}
</script>
  • Floor组件接收数据并渲染
<template>
	<!--楼层-->
	        <div class="floor">
	            <div class="py-container">
	                <div class="title clearfix">
	                    <h3 class="fl">{{list.name}}</h3> <!--渲染name-->
	                    <div class="fr">
	                        <ul class="nav-tabs clearfix">
	                            <li class="active" v-for="(nav,index) in list.navList"> <!--渲染导航数据-->
	                                <a :href="nav.url" 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.bigImg" />
	                            </div>
	                            <div class="floorBanner">
	                                <div class="swiper-container" ref="cur"> <!--小轮播图结构-->
	                                    <div class="swiper-wrapper">
	                                        <div class="swiper-slide" v-for="(carousel,index) 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>
	                                <!--以下数据不能v-for,因为数据结构不一样-->
	                                <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>
	                          ......
</template>

<script>
	import Swiper from 'swiper'
	
	export default {
		name:'Floor',
		props:['list'], // 接收数据
		mounted(){
			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>
  • 轮播图结构注意事项
- 第一次写轮播图的时候,是在当前组件内部发请求,动态渲染数据(数据需要从服务器返回来),所有需要watch+$nextTick
- 本例中,请求是父组件发的,子组件通过props接收,所以'轮播图结构'已经有了,可以写在mounted
  • 组件之间的通信方式review
- props: 父==>子组件通信
- 自定义事件: @on,@emit 子==>父
- 全局事件总线: $bus(全能)
- pubsub-js: vue中几乎不用(全能)
- 插槽
- vuex(一般用这个)

轮播图搞成全局组件

  • home页有三个地方用到,其他组件也有可能用到

  • 注意事项

- 只有 template 结构,样式一样,才能这样拆分出来

	- 有时候会看到 结构一致,但是大小不一样,这是正常的,通过样式去控制一下就可以了,不要以为不能拆分
  • floor组件的轮播图js逻辑改造成listContainer组件的轮播图js逻辑
- 注意事项: list的数据是父组件home传过来的,没有发生变化,所以单纯写watch是检测不到数据变化的
  所以配置了 immediate为true,不管数据有没有发生变化,先立即执行一次,从而实现需求

<script>
	export default {
		name:'Floor',
		......
		watch:{
			list:{
				immediate:true, // 必须加
				handler(){
					this.$nextTick(()=>{
						var mySwiper = new Swiper(this.$refs.cur, {
							......
						})
					})
				}
			}
		}
	}
</script>
  • 轮播图搞成全局组件,新建Carousel组件
### components.Carousel.index.vue
<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.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'], // 接收父组件传过来的数据
		watch:{ // 之前Floor组件的逻辑剪过来
			list:{
				immediate:true,
				handler(){
					this.$nextTick(()=>{
						var mySwiper = new Swiper(this.$refs.cur, {
						......
						})
					})
				}
			}
		}
	}
</script>

<style scoped>
</style>

  • main.js注册
......
import Carousel from '@/components/Carousel' // 导入并注册
Vue.component(Carousel.name,Carousel)
......
  • 各个组件引用(注意:服务器返回的数据可能不太一样,要精准定位数据源,否则数据渲染出错,导出图片出不来)
### listContainer.index.vue
<template>
	<!--列表-->
	<div class="list-container">
		......
			<div class="center">
				<!--传bannerList过去即可-->
				<Carousel :list="bannerList"></Carousel>
			</div>
			......
	</div>
</template>

### Home.Floor.vue
......
<div class="floorBanner">
	<!--传carouselList,轮播图的图片地址包含在里面-->
	<Carousel :list="list.carouselList"></Carousel>
</div>

模块开发流程

  • 先静态页面+拆分组件
  • 发请求(API)
  • vuex三连环
  • 组件获取仓库数据,动态展示数据

Search组件开发

  • 先静态页面+拆分组件(现成的,因为之前拆分组件的动作已经演示过了,所以这次不再演示)

    • 把文件拷贝到Search组件目录,替换原来的index.vue
  • 发请求以及vuex三连环

### api.index.js
......
import requests from './request'
......

export const reqGetSearchInfo = (params)=>requests({
	url:'/list',
	method:'post',
	data:params // 提交给服务器的数据
})

- 可以在main.js中,测试请求是正常响应
......
import {reqGetSearchInfo} from '@/api'
console.log(reqGetSearchInfo({})) // 注意传入一个空对象"{}",不加这个参数,无法收到服务器数据(之前已定义的形参)
......

- 在Search组件分发请求
......
mounted(){
	this.$store.dispatch('getSearchList',{})
},

- store.search.index.js 处理数据

import { reqGetSearchInfo } from "@/api" // 导入请求

const actions = {
	async getSearchList({commit},params={}){ // 发送请求并接收数据
		let res = await reqGetSearchInfo(params)
		if(res.code == 200){ // 响应成功就提交
			commit('GETSEARCHLIST',res.data)
		}
	}
}
const mutations = {
	GETSEARCHLIST(state,searchList){
		state.searchList = searchList // 最终将数据存储到store
	}
}
const state = {
	searchList:{}
}
const getters = {}

export default {
	actions,
	mutations,
	state,
	getters
}

  • 渲染数据之前,先使用getters简化代码
### store.search.index.js

......

const actions = {
	.......
}
const mutations = {
	......
}
const state = {
	searchList:{}
}
const getters = {
	goodsList(state){ // state 是上面的 state, 不是全局的state
		return state.searchList.goodsList || []
	},
	trademarkList(state){
		return state.searchList.trademarkList || [] // 断网状态下,返回空[],避免报"attrs error"
	},
	attrsList(state){
		return state.searchList.attrsList || []
	}
}

export default {
	......
}

  • 组件使用
### search.index.vue
......
<script>
  ......
  import {mapState,mapGetters} from 'vuex'
  export default {
    ......
	mounted(){
		this.$store.dispatch('getSearchList',{})
	},
	computed:{
		......
		// 映射三个 getters
		...mapGetters(['goodsList','trademarkList','attrsList'])
	}
  }
</script>

  • 渲染到网页
......
<ul class="yui3-g">
						<!--遍历-->
  <li class="yui3-u-1-5" v-for="(good,index) in goodsList" :key="good.id">
    <div class="list-wrap">
      <div class="p-img">						<!--渲染图片-->
        <a href="item.html" target="_blank"><img :src="good.defaultImg" /></a>
      </div>
      <div class="price">
        <strong>
          <em>¥</em>
          <i>{{good.price}}</i> <!--渲染价格-->
        </strong>
      </div>
      <div class="attr">
        <a target="_blank" href="item.html" title="促销信息,下单即赠送三个月CIBN视频会员卡!【小米电视新品4A 58 火爆预约中】">{{good.title}}</a> <!--渲染标题-->
      </div>
      <div class="commit">
        <i class="command">已有<span>2000</span>人评价</i>
      </div>
      <div class="operate">
        <a href="success-cart.html" target="_blank" class="sui-btn btn-bordered btn-danger">加入购物车</a>
        <a href="javascript:void(0);" class="sui-btn btn-bordered">收藏</a>
      </div>
    </div>
  </li>
</ul>

search组件根据不同的参数获取数据并展示

<script>
  ......
  export default {
    name: 'Search',
	data(){
		return {
			searchParams: { // 新增数据对象,接收typeNav和'搜索框'传递过来的参数
			  // 一级分类的ID
			  "category1Id": "",
			  // 二级分类的ID
			  "category2Id": "",
			  // 三级分类的ID
			  "category3Id": "",
			  // 分类的名字
			  "categoryName": "",
			  // 关键字
			  "keyword": "",
			  // 排序   应该有初始值
			  "order": "",
			  // 当前分页
			  "pageNo": 1,
			  // 每一页所带的数据数
			  "pageSize": 10,
			  // 平台售卖属性操作所带的数据
			  "props": [],
			  // 品牌
			  "trademark": ""
			}
		}
	},
    components: {
      ......
    },
	beforeMount(){ // 组件挂载完毕之前,填充 searchParams 数据对象
		// 复杂写法
		// this.category1Id = this.$route.query.category1Id;
		// this.category2Id = this.$route.query.category2Id;
		// this.category3Id = this.$route.query.category3Id;
		// this.categoryName = this.$route.query.categoryName;
		// this.keyword = this.$route.params.keyword;
		
		// 简便写法: Object.assign()用于合并对象
		// 把query和params值 合并到 searchParams
		Object.assign(this.searchParams,this.$route.query,this.$route.params)
		
	},
	methods:{
		getData(){ // 打包成方法,方便其他模块随时调用
			this.$store.dispatch('getSearchList',this.searchParams)
		},
	},
	mounted(){ // 挂载完毕以后,带着参数派发请求
		this.getData() 
	},
	computed:{
		......
	}
  }
</script>

Search子组件数据的渲染(品牌,具体属性)

### SearchSelector.vue

<template>
	<div class="clearfix selector">
		<div class="type-wrap logo">
			<div class="fl key brand">品牌</div>
			<div class="value logos">
				<ul class="logo-list">
					<!--渲染品牌数据-->
					<li v-for="(item,index) in trademarkList" :key="item.tmId">{{item.tmName}}</li>
				</ul>
			</div>
			<div class="ext">
				<a href="javascript:void(0);" class="sui-btn">多选</a>
				<a href="javascript:void(0);">更多</a>
			</div>
		</div>
								<!--渲染属性-->
		<div class="type-wrap" v-for="(attr,index) in attrsList" :key="attr.attrId">
			<div class="fl key">{{attr.attrName}}</div>
			<div class="fl value">
				<ul class="type-list">
					<!--渲染具体属性值-->
					<li v-for="(attrValue,index) in attr.attrValueList" :key="index">
						<a>{{attrValue}}</a>
					</li>
				</ul>
			</div>
			<div class="fl ext"></div>
		</div>
	</div>
</template>

<script>
	import {mapGetters} from 'vuex'
	
	export default {
		name: 'SearchSelector',
		computed:{
			...mapGetters(['trademarkList','attrsList']), // 映射仓库两组数据
		}
	}
</script>

监视路由的变化,路由一旦发生变化,再次发起请求

### search.index.vue
......
watch:{
		$route(newVal,oldVal){
			// 一旦路由发生变化,重新刷新 searchParams值
			Object.assign(this.searchParams,this.$route.query,this.$route.params);
			// 再次发起请求
			this.getData();
			// 请求完毕以后,清空原来的参数(否则有残留)
			// undefined的好处在于,不会被打包到请求里面去,避免发送空参数给服务端(节省性能)
			this.searchParams.category1Id = undefined
			this.searchParams.category2Id = undefined
			this.searchParams.category3Id = undefined
		}
	}

面包屑导航的处理

### search.index.vue
......
<div class="bread">
  <ul class="fl sui-breadcrumb">
    <li>
      <a href="#">全部结果</a>
    </li>
  </ul>
  <ul class="fl sui-tag">
  	<!--有传categoryName就显示,没有传就不显示(比如"搜索"这种情况)-->
    <li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}</li>
  </ul>
</div>

  • 当用户点击x的时候,清空查询参数,返回默认的页面
### search.index.vue
......
<!--绑定点击事件来处理-->
<li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCategoryName">x</i></li>
......
methods:{
		getData(){
			......
		},
		removeCategoryName(){
			// 先置空发送服务器的数据
			this.searchParams.categoryName = undefined;
			this.searchParams.category1Id = undefined;
			this.searchParams.category2Id = undefined;
			this.searchParams.category3Id = undefined;
			// 在向服务器发送请求
			this.getData();
			// 地址栏也需要改   进行路由跳转 自己跳转自己
			// if (this.$route.params) {
			if (this.$route.query || this.$route.params) {
			  this.$router.push({name: "search", params: this.$route.params})
			}
		}
	},

  • 关键字的面包屑处理(和三级导航目录一样的套路)
......
<div class="bread">
  ......
  <ul class="fl sui-tag">
    <li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCategoryName">x</i></li>
    <!--一样的套路-->
    <li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}}<i @click="removeKeyword">x</i></li>
  </ul>
</div>
......
methods:{
		getData(){
			......
		},
		removeCategoryName(){
			......
		},
		removeKeyword(){
			this.searchParams.keyword = undefined;
			this.getData();
		}
	},
  • 现存代码的问题: 当X掉重新发送请求以后,搜索框keyword依然存在,而实际需求必须清空

    keyword关键字存在于兄弟组件Header.vue,所以涉及到兄弟组件的通信操作

- props: 父=>子
- 自定义事件:子=>父
- vuex: 万能
- 插槽: 父=>子
- pubsub-js: 万能(很少人用)
- $bus: 万能
  • 本次使用$bus来实现,先绑定$bus
### main.js
......
new Vue({
  ......
  beforeCreate(){
	  Vue.prototype.$bus = this; // 绑定$bus
  }
}).$mount('#app')
  • 项目应用: 当用户点击keyword X的时候,由Search组件通知Header组件清空keyword
### Search.vue
......
removeKeyword(){
      this.searchParams.keyword = undefined;
      this.getData();
      this.$bus.$emit('clear') // 触发 clear 事件
}

### Header.vue
......
mounted(){
      this.$bus.$on('clear',()=>{ // 响应clear事件,清空keyword
            this.keyword = '';
      })
}
  • 最后,路由跳转自己,保留query参数
### Search.vue
......
removeKeyword(){
      this.searchParams.keyword = undefined;
      this.getData();
      this.$bus.$emit('clear')
      if (this.$route.query) { // 路由自己跳转自己
        this.$router.push({name: "search", query: this.$route.query})
      }
}
  • 面包屑处理品牌信息: 当用户点击品牌的时候,下方的商品列表要展示对应的品牌商品

    涉及子传父通信,本次使用自定义事件来处理

### SearchSelector.vue
......
<div class="value logos">
  <ul class="logo-list">
  	<!--当用户点击品牌的时候,绑定事件-->
    <li v-for="(item,index) in trademarkList" :key="item.tmId" @click="trackmarkHandler(item)">{{item.tmName}}</li>
  </ul>
</div>
......
methods:{ // 把参数传给父组件,让父组件带着参数,发请求
  trackmarkHandler(trackmark){
    this.$emit('trackmarkInfo',trackmark)
  },
  ......
},

### Search.vue
......
<!--响应子组件的自定义事件-->
<SearchSelector @trackmarkInfo="trackmarkInfo" /> 
......
methods:{
    ......
    trackmarkInfo(trackmark){ // 接收参数并赋值
      this.searchParams.trademark = `${trackmark.tmId}:${trackmark.tmName}` // trademark:'1:苹果'
      this.getData();
    }
  },
  • 品牌的面包屑展示
### Search.vue
......
<ul class="fl sui-tag">
  ......
  <li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}}<i @click="removeKeyword">x</i></li>
  <!--一样的套路-->
  <li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(':')[1]}}<i @click="removeTrackmark">x</i></li>
</ul>
......
removeTrackmark(){
      this.searchParams.trademark = undefined;
      this.getData();
}

售卖属性的操作

  • 要求的数据格式
- props:["属性ID:属性值:属性名"]

- 示例: ["1:6~6.24英寸:屏幕尺寸"]
  • 实现需求: 当用户点击属性值时,页面展示对应属性值的商品列表

    比如用户选择运行内存:8G,页面返回对应规格的手机商品

  • 思路分析

- 把用户选择的商品属性参数,发给父组件,父组件自己填充searchParams参数,然后发请求获取相关商品属性的数据
### SearchSelector.vue
......
<div class="type-wrap" v-for="(attr,index) in attrsList" :key="attr.attrId">
      <div class="fl key">{{attr.attrName}}</div>
      <div class="fl value">
            <ul class="type-list">
                  <li v-for="(attrValue,index) in attr.attrValueList" :key="index" @click="attrInfo(attr,attrValue)"> <!--绑定点击事件并传参-->
                        <a>{{attrValue}}</a>
                  </li>
            </ul>
      </div>
      <div class="fl ext"></div>
</div>
......
methods:{
      ......
      attrInfo(attr,attrValue){
            this.$emit('attrInfo',attr,attrValue) // 触发事件
      }
      
},

### Search.index.vue
......
<!--新增响应子组件事件-->
<SearchSelector @trackmarkInfo="trackmarkInfo" @attrInfo="attrInfo"/>
......
methods:{
      ......
      attrInfo(attr,attrValue){
      		// 构造str数据,插入props列表项,最后发请求
            let props = `${attr.attrId}:${attrValue}:${attr.attrName}`
            this.searchParams.props.push(props)
            this.getData();
      }
},
  • 售卖属性面包屑展示和删除
### Search.index.vue
......
......
<ul class="fl sui-tag">
      ......
      <li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(':')[1]}}<i @click="removeTrackmark">x</i></li>
      <!--不再使用v-if,而是使用v-for处理,要展示的属性值很多(空list是不会展示的)-->
      <!--所以使用v-for就具有v-if的功能,而且实现了遍历-->
      
      <!--同时绑定点X事件-->
      <li class="with-x" v-for="(attrValue,index) in searchParams.props" :key="index">{{attrValue.split(':')[1]}}<i @click="removeAttrValue(index)">x</i></li>
</ul>
......
......
attrInfo(attr,attrValue){ // 面包屑展示
      let props = `${attr.attrId}:${attrValue}:${attr.attrName}`
      // 增加'数组去重'的判断,重复的元素不再展示
      if(this.searchParams.props.indexOf(props) == -1) this.searchParams.props.push(props)
      this.getData(); // 不能把这句丢到上面的if里面,因为同一属性值的后端数据,可能更新
},

removeAttrValue(index){ // 面包屑删除
      this.searchParams.props.splice(index,1); // 不要写成 slice,完全不一样
      this.getData();
}