当前位置: 首页 > news >正文

仿京东 项目笔记1

目录

  • 项目代码
  • 1. 项目配置
  • 2. 前端Vue核心
  • 3. 组件的显示与隐藏用v-if和v-show
  • 4. 路由传参
    • 4.1 路由跳转有几种方式?
    • 4.2 路由传参,参数有几种写法?
    • 4.3 路由传参相关面试题
      • 4.3.1 路由传递参数(对象写法)path是否可以结合params参数一起使用?
      • 4.3.2 如何指定params参数可传可不传?
      • 4.3.3 params参数可传可不传,若传递为空串,如何解决?
      • 4.3.4 路由组件能否传递props数据?
    • 4.4 编程式路由跳转到当前路由(参数不变),多次执行会弹出NavigationDuplicated的警告错误
  • 5. 接口统一管理
    • 5.1 二次封装Axios
    • 5.2 前端通过代理解决跨域问题
    • 5.2 请求接口统一封装
    • 5.3 async和await
    • 5.4 nprogress请求加载进度条
  • 6. 事件委派
  • 7. 卡顿现象
    • 7.1 函数的防抖和节流
    • 7.2 三级联动表单的节流
    • 7.3 三级联动菜单路由跳转
  • 8. Vue路由切换的请求优化
  • 9. Mock插件
    • 9.1 Mock使用
    • 9.2 利用mockjs提供模拟数据
  • 10. Swiper轮播图
    • 10.1 Swiper的基本使用
    • 10.2 父传子数据+轮播图Swiper的使用
    • 10.3 将轮播图Swiper模块提取为公共组件
  • 11. vuex中getters的使用
  • 12. Object.asign实现对象拷贝
  • 13. 利用路由信息变化实现动态搜索
  • 14. 面包屑相关操作
  • 14.1 面包屑添加/删除分类属性(query)
    • 14.2 面包屑添加/删除搜索关键字(params)
    • 14.3 SearchSelector子组件传参关联面包屑
  • 15. 商品排序
  • 16. 手写分页器
  • 17. 路由滚动行为
  • 18. undefined细节
  • 19. 商品放大镜
  • 20. 加入购物车成功路由
  • 21. 购物车组件开发
    • 21.1 临时游客的uuid
    • 21.2 购物车商品数量更改
    • 21.3 购物车单个商品状态修改和删除
    • 21.4 购物车删除选中的全部商品和全选商品
  • 22 CSS样式中使用@符号

项目代码

项目代码1

1. 项目配置

  1. 项目运行起来时,让浏览器自动打开
    在package.json文件中设置 --open
"scripts": {"serve": "vue-cli-service serve --open","build": "vue-cli-service build","lint": "vue-cli-service lint"},
  1. 关闭eslint校验工具
    vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({transpileDependencies: true,lintOnSave: false
})
  1. src文件夹简写方法,配置别名
    因为项目大的时候src(源代码文件夹):里面目录会很多,找文件不方便,设置src文件夹的别名的好处,找文件会方便一些
    在文件根目录下,跟vue.config.js同级目录下,新建jsconfig.json文件,[@代表的是src文件夹]
{"compilerOptions": {"baseUrl": "./","paths": {"@/*": ["src/*"]}},"exclude": ["node_modules","dist"]
}

创建的vue2项目,默认是有jsconfig.js文件,文件配置选项说明,可参考这个blog

  1. 路由分析 Vue-Router
    前端路由:
    K即为URL(网络资源定位符)
    V即为相应的路由组件
    确定项目结构顺序:上中下 -----只有中间部分的V在发生变化,中间部分应该使用的是路由组件
    项目路由分析:2个非路由组件和四个路由组件
    两个非路由组件:Header 、Footer
    路由组件 : Home、Search、Login(没有底部的Footer组件,带有二维码的)、Register(没有底部的Footer组件,带二维码的)

  2. 路由组件和非路由组件区别:

  • 非路由组件放在components中,路由组件放在pages或views中
  • 非路由组件通过标签使用,路由组件通过路由使用
  • 在main.js注册完路由,所有的路由和非路由组件身上都会拥有$router $route属性
  • $router:一般进行编程式导航进行路由跳转
  • $route: 一般获取路由信息(name path params等)

2. 前端Vue核心

开发一个前端模块可以概括为以下几个步骤:
(1)写静态页面、拆分为静态组件;
(2)发请求(API);
(3)vuex(actions、mutations、state三连操作);
(4)组件获取仓库数据,动态展示;

3. 组件的显示与隐藏用v-if和v-show

v-if:频繁操作DOM、耗性能
v-show: 通过样式将元素显示或隐藏,性能更好

场景: footer组件在登录注册页面是不存在的,所以要隐藏,v-if 或者 v-show
那么条件判断是什么?
根据组件身上的 $route.path 判断

<Footer v-show="$route.path == '/login' || $route.path == '/register'" ></Footer>

问题: 当组件数量增多时,判断过于冗余
解决: 利用路由元信息meta,在路由的元信息中定义show属性,用来给v-show赋值,判断是否显示footer组件

//router/idnex.js
{path: '/register',component: Register,meta: {showFooter: false} },

判断:

<Footer v-show="$route.meta.showFooter" ></Footer>

在这里插入图片描述

4. 路由传参

详细学习: Vue-Router

4.1 路由跳转有几种方式?

  1. 声明式导航:router-link(务必要有to属性)
  2. 编程式导航:主要利用的是组件实例的$router.push | replace方法,可以书写一些自己的业务

4.2 路由传参,参数有几种写法?

  1. params参数:属于路径中的一部分,在配置路由的时候需要占位
  2. query参数:;不属于路径的一部分,类似于Ajax中的querystring ,不需要占位 /home?k=v&kv=
    情况说明: 当点击搜索按钮之后 将home页面跳转到search页面,输入内容后,需要将内容传递给search页面
    params:
    占位:(注意冒号:)
path: '/search/:keyWord',

params传参:

第一种:字符串

this.$router.push('/search/'+this.keyWord)

query传参: (不需要占位)

第一种:字符串

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

第二种方法:模板字符串

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

第三种:对象写法(常用)

this.$router.push({name: 'search',           //使用params 不能使用 path: '/Search'params: {keyWord: this.keyWord}, query: {k:this.keyWord.toUpperCase()}})

以对象方式传参时,如果我们传参中使用了params,只能使用name(而且需要使用命名路由),不能使用path,如果只是使用query传参,可以使用path

{path: '/search/:keyWord',name: 'search',           //命名路由component: Search, meta: {showFooter: true}},

在这里插入图片描述

4.3 路由传参相关面试题

4.3.1 路由传递参数(对象写法)path是否可以结合params参数一起使用?

不可以,以对象方式传参时,对象写法可以是namepath形式,但需要注意的是,path这种写法不能与params参数一起,如果只是使用query传参,可以使用path

//无效
this.$router.push({path: '/search', params: {keyWord: this.keyWord}, query: {k:this.keyWord.toUpperCase()}
})
//有效
this.$router.push({path: '/search', query: {k:this.keyWord.toUpperCase()}
})//有效
this.$router.push({name: 'search',           //使用params 不能使用 path: '/Search'params: {keyWord: this.keyWord}, query: {k:this.keyWord.toUpperCase()}
})

效果:params参数获取不到,只有query参数获取到了
在这里插入图片描述

4.3.2 如何指定params参数可传可不传?

配置路由时已占位(params参数),但是路由跳转时不传递参数,路径会存在问题,详情如下:

1. Search路由项的path已经指定要传一个keyword的params参数,如下所示:
path: "/search/:keyword",    2. 执行下面进行路由跳转的代码:
this.$router.push({name:"Search",query:{k:this.keyword}})
当前跳转代码没有传递params参数
此时的url路径为:`http://localhost:8080/?k=asd`
此时的地址信息少了 `/search`
正常的地址栏信息: `http://localhost:8080/search?k=asd`

解决方法:
可以通过改变path,在后面加个问号来指定params参数可传可不传 ,

path: "/search/:keyword?",        //?表示该参数可传可不传

4.3.3 params参数可传可不传,若传递为空串,如何解决?

问题:

this.$router.push({name: 'search',          params: {keyWord: ''}, query: {k:this.keyWord.toUpperCase()}
})此时的url路径有问题:`http://localhost:8080/?k=SAD`

解决方法:
使用undefined解决,params参数可以传递也可不传递(空字符串)

this.$router.push({name: 'search',          params: {keyWord: '' || undefined}, query: {k:this.keyWord.toUpperCase()}
})
此时的url路径为:`http://localhost:8080/search?k=SAD`

4.3.4 路由组件能否传递props数据?

可以,有三种写法。
布尔值写法:只能传递params参数。
对象写法:额外的给路由组件传递一些props
函数写法(常用):params、query参数都可传递
具体用法看 之前总结的 Vue Router 路由 里的路由props配置

4.4 编程式路由跳转到当前路由(参数不变),多次执行会弹出NavigationDuplicated的警告错误

**问题:**多次点击搜索按钮会出现(编程式导航,$route.push()
**注意:**声明式导航不会出现该问题,因为vue底层已解决

let res = this.$router.push({name: 'search', params: {keyWord: this.keyWord}, query: {k:this.keyWord.toUpperCase()}
})
console.log(res);

执行上面的代码,会出现下面的结果

在这里插入图片描述

**原因:**最新的"vue-router": "^3.5.3"引入了promise,编程式导航具有其返回值,失败成功的回调
push是一个promise,promise需要传递成功和失败两个参数,我们的push中没有传递。

**解决方法:**给push方法添加两个回调参数

let res = this.$router.push({name: 'search', params: {keyWord: this.keyWord}, query: {k:this.keyWord.toUpperCase()}},()=>{},                             //执行成功回调(error)=>{console.log(error);}      //执行失败回调
)
console.log(res);

点击两下搜索按钮,确实捕获到当前错误,但这种方法治标不治本,将来在别的组件中push|replace,编程式导航还是会有类似错误,这个方法只解决了单个编程式导航。
在这里插入图片描述

根治方法:
push是VueRouter.prototype的一个方法,在router/index.js文件中 重写 该方法即可

//先把VueRouter原型对象的push和replace备份一份
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;//重写push | replace
//第一个参数:告诉原来push方法,你往哪里跳转(传递哪些参数)
//第二个参数:成功回调,第三个参数:失败回调
VueRouter.prototype.push = function (location, resolve, reject) {if (resolve && reject) {//call || applay 区别://相同点:都可以调用函数一次,都可以篡改函数的上下文一次//不同点:call与apply传递参数:call传递参数用逗号隔开,apply方法执行,传递数据originPush.call(this, location, resolve, reject);} else {originPush.call(this, location, ()=>{}, ()=>{})}
}
VueRouter.prototype.replace = function(location, resolve, reject) {if (resolve && reject) {//call || applay 区别://相同点:都可以调用函数一次,都可以篡改函数的上下文一次//不同点:call与apply传递参数:call传递参数用逗号隔开,apply方法执行,传递数据originReplace.call(this, location, resolve, reject);} else {originReplace.call(this, location, ()=>{}, ()=>{})}
}

5. 接口统一管理

项目小:可以在组件的生命周期函数中发请求
项目大:axios.get('xxx')
文件:index.js

5.1 二次封装Axios

axios中文文档

可以查看之前 Vue全家桶(二):Vue中的axios异步通信

src/api/request.js文件

import axios from "axios";
//对axios二次封装
const requests = axios.create({//基础路径,requests发出的请求在端口号后面会跟改baseURlbaseURL:'/api',timeout: 5000,
})//配置请求拦截器
requests.interceptors.request.use(config => {//config内主要是对请求头Header配置//比如添加tokenreturn config;
})//配置相应拦截器
requests.interceptors.response.use((res) => {//成功的回调函数return  res.data;
},(error) => {//失败的回调函数console.log("响应失败"+error)return Promise.reject(new Error('fail'))
})//对外暴露
export default requests;

5.2 前端通过代理解决跨域问题

扩展学习:
前端跨域解决方案
前端跨域

跨域: 协议、域名、端口号不同请求
http://localhost:8080/#/home 前端本地服务器
http://39.98.123.211 后台服务器地址
解决跨域问题: JSONP、CORS、代理

在根目录下的vue.config.js中配置,proxy为通过代理解决跨域问题。
我们在封装axios的时候已经设置了baseURL为api,所以所有的请求都会携带/api,这里我们就将/api进行了转换。如果你的项目没有封装axios,或者没有配置baseURL,建议进行配置。要保证baseURL和这里的代理映射相同,此处都为’/api’。

vue.config.js

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({transpileDependencies: true,lintOnSave: false,//代理跨域devServer: {proxy: {// 匹配所有以 '/api1'开头的请求路径'api': {target: 'http://gmall-h5-api.atguigu.cn'     // 将请求代理到目标服务器上}}}
})

5.2 请求接口统一封装

在src/api/文件中创建index.js文件,用于封装所有请求
将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,这样当我们的接口比较多时,如果需要修改只需要修改该文件即可。

src/api/index.js

// 在当前这个模块:API进行统一管理
import requests from "./request";
//三级联动接口
export const reqCategoryList = ()=>{//发请求,request模块已经配置了/api,可以去掉/api,return requests({url: '/product/getBaseCategoryList',method: 'get'})
}
//简化
export const reqCategoryList = ()=>requests.get('/product/getBaseCategoryList')

当组件想要使用相关请求时,只需要导入相关函数即可,以上图的reqCateGoryList 为例:

import {reqCateGoryList} from '@/api'
//发起请求
reqCateGoryList();

5.3 async和await

我们将一个axios请求封装为了函数,我们在下面代码中调用了该函数:
比如在Vuex模块化的src/store/home.js中请求数据

import { reqCategoryList } from "@/api"
//home模块的Vuex模块
const state = {//state中数据默认初始值别瞎写,根据接口的返回值进行初始化cateGoryList: []
}
const mutations = {CATEGORYLIST(state, categoryList){state.cateGoryList = categoryList}
}
const actions = {//通过API里得接口函数调用,向服务器请求,获得服务器参数cateGoryList(context){let ressult = reqCategoryList()        //返回的是Promise实例对象console.log(ressult);}
}
const getters = {}
export default{state,getters,mutations,actions
}

在这里插入图片描述
返回了一个promise,证明这是一个promise请求,但是我们想要的是图片中的data数据。
没有将函数封装前我们都会通过then()回调函数拿到服务器返回的数据,现在我们将其封装了,依然可以使用then获取数据,代码如下

import { reqCategoryList } from "@/api"
//home模块的Vuex模块
const state = {//state中数据默认初始值别瞎写,根据接口的返回值进行初始化cateGoryList: []
}
const mutations = {CATEGORYLIST(state, categoryList){state.cateGoryList = categoryList}
}
actions:{categoryList(){let result = reqCateGoryList().then(res=>{console.log("res:",res)return res})console.log("result:",result)if(result.code == 200){console.log(result.code);context.commit('CATEGORYLIST',result.data)}}}

在这里插入图片描述

由于Promis是异步请求,我们发现请求需要花费时间,但是它是异步的,所有后面的console.log(“result”);console.log(result)会先执行,等我们的请求得到响应后,才执行console.log(“res”);console.log(res),这也符合异步的原则,但是我们如果在请求下面啊执行的是将那个请求的结果赋值给某个变量,这样就会导致被赋值的变量先执行,并且赋值为undefine,因为此时Promise还没有完成。

(具体的关于Promise的扩展学习看 ES6: Promise)

在这里插入图片描述

所以引入了asyncawait,async写在函数名前,await卸载api函数前面。await含义是async标识的函数体内的并且在await标识代码后面的代码先等待await标识的异步请求执行完,再执行。这也使得只有reqCateGoryList执行完,result 得到返回值后,才会执行后面的输出操作。

const actions = {//通过API里得接口函数调用,向服务器请求,获得服务器参数async cateGoryList(context){//返回的是Promise实例对象let result  = await reqCategoryList()console.log("result",result) if(result.code == 200){context.commit('CATEGORYLIST',result.data)}}
}

在这里插入图片描述

5.4 nprogress请求加载进度条

场景:打开一个页面时,往往会伴随一些请求,并且会在页面上方出现进度条。它的原理时,在我们发起请求的时候开启进度条,在请求成功后关闭进度条,所以只需要在request.js中进行配置。
如下图所示,我们页面加载时发起了一个请求,此时页面上方出现蓝色进度条
在这里插入图片描述

nprogress安装:npm i --save nprogress
使用nprogress:src/api/request.js

src/api/request.js

import axios from "axios";                 //对axios二次封装
import nprogress from "nprogress";         //引入进度条
import "nprogress/nprogress.css"           //引入进度条样式
const requests = axios.create({//基础路径,requests发出的请求在端口号后面会跟改baseURlbaseURL:'/api',timeout: 5000,
})
//配置请求拦截器
requests.interceptors.request.use(config => {//config内主要是对请求头Header配置//比如添加token//开启进度条nprogress.start()return config;
})//配置响应拦截器
requests.interceptors.response.use((res) => {//成功的回调函数//响应成功,关闭进度条nprogress.done()return  res.data;
},(error) => {//失败的回调函数console.log("响应失败"+error)return Promise.reject(new Error('fail'))
})
//对外暴露
export default requests;

6. 事件委派

事件委派 : 也叫事件代理,简单理解就是:原事件的委派指将事件统一绑定给元素的共同的祖先元素,这样当后代元素上的事件触发时,会一直冒泡到祖先元素,从而通过祖先元素的响应函数来处理事件。比如:原本是给li绑定点击事件,现在交给它父级ul绑定,利用冒泡原理,点击li的时候会触发ul的事件;

**问题:**在三级列表中,鼠标在“全部商品分类”时,没有对应的选中样式,如图1所示,正常的情况应该是鼠标在离开一级菜单,进入“全部商品分类”时,会有对应的选中样式,存储的data数据应为对应选中的一级分类的索引值,如图2所示。

图1:
在这里插入图片描述
图2:
在这里插入图片描述
解决方法:
给一级分类和全部商品分类包裹一个父元素<div>,在这个<div>绑定一个鼠标移开事件leaveIndex

<!-- 事件委派 -->
<div @mouseleave="leaveIndex">                <h2 class="all">全部商品分类</h2><!-- 三级联动菜单 --><div class="sort"><div class="all-sort-list2"><divclass="item"v-for="(c1, index) in categoryList":key="c1.categoryId":class="{ cur: index == currentIndex }"    <!-- 选中高亮 -->><h3 @mouseenter="changeIndex(index)" >      <!-- 鼠标进入事件 --><!-- …… -->
</div>
<script>
....
data() {return {//存储用户鼠标移上哪一个一级分类currentIndex: -1,};},
methods: {//鼠标进入一级分类修改响应式数据currentIndexchangeIndex(index) {this.currentIndex = index;},//鼠标移出一级分类的事件回调leaveIndex() {this.currentIndex = -1;},},
....
</script>

7. 卡顿现象

场景:
在上面讲到的事件委派中我们为三级联动菜单,为每个一级分类添加了鼠标进入事件,在用户使用过程中会出现以下两种情况。
正常情况 (用户慢慢的操作) :鼠标进入, 每一个一级分类h3,都会触发鼠标进入事件changeIndex
非正常情况 (用户操作很快):本身全部的一级分类都应该触发鼠标进入事件,但是经过测试,只有部分h3触发了,这种情况就是由于用户行为过快,导致浏览器反应不过来,如果当前回调函数中有一些大量业务,有可能出现卡顿现象

**解决方案:**防抖和节流
函数防抖和节流

7.1 函数的防抖和节流

防抖(debounce): 前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发只会执行一次
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

例子: 输入框搜索 输入完内容之后 一秒后才发送一次请求
解决: ladash插件,封装函数的防抖与节流业务(闭包+延迟器)

节流(throttle): 在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发。
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
例子: 计数器限定一秒内不管用户点击按钮多少次,数值只能加一、轮播图左右按钮切换时,只能在1s内切换一张图。
解决: _throttle()
引入:

import throttle from 'lodash/throttle'

默认暴露 不需要花括号
回调函数不要用箭头函数,可能出现上下文this

防抖和节流的区别:
防抖:用户操作很频繁,但是只是执行一次
节流:用户操作很频繁,但是把频繁的操作变为少量操作[可以给浏览器有充裕的时间解析代码]

7.2 三级联动表单的节流

下面代码就是将changeIndex设置了节流,如果操作很频繁,限制50ms执行一次。这里函数定义采用的键值对形式。throttle的返回值就是一个函数,所以直接键值对赋值就可以,函数的参数在function中传入即可。

import throttle from 'lodash/throttle'
//……methods: {//鼠标进入一级分类修改响应式数据currentIndex//采用键值对形式创建函数,将changeIndex定义为节流函数,该函数触发很频繁时,设置50ms才会执行一次changeIndex: throttle(function(index) {this.currentIndex = index;},50),//鼠标移出一级分类的事件回调leaveIndex() {this.currentIndex = -1;},},

7.3 三级联动菜单路由跳转

在这里插入图片描述
如上图所示,三级标签列表有很多,每一个标签都是一个页面链接,我们要实现通过点击表现进行路由跳转。
路由跳转有两种方法:导航式路由,编程式路由。

  1. 导航式路由,我们有多少个a标签就会生成多少个router-link标签,这样当我们频繁操作时会出现卡顿现象。
    卡顿原因:
    router-link是一个组件,当服务器的数据返回之后,循环出很多的router-link组件【创建组件实例–>虚拟DOM】,如果有1000+个router-link,在创建组件实例时,是非常耗内存的,因此会出现卡顿现象
  2. 编程式路由,我们是通过触发点击事件实现路由跳转。同理有多少个a标签就会有多少个触发函数。虽然不会出现卡顿,但是也会影响性能。

上面两种方法无论采用哪一种,都会影响性能。我们提出一种:编程式导航+事件委派 的方式实现路由跳转。事件委派即把子节点的触发事件都委托给父节点。这样只需要一个回调函数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属性获取节点的属性信息。

 <div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex"><div class="item"  v-for="(c1,index) in categoryList" v-show="index!==16" :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,index) 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,index) in c2.categoryChild"  :key="c3.categoryId"><a :data-categoryName="c2.categoryName" :data-category3Id="c3.categoryId">{{c3.categoryName}}</a></em>
</dd></dl></div></div></div></div>

注意:event是系统属性,所以我们只需要在函数定义的时候作为参数传入,在函数使用的时候不需要传入该参数。

//函数使用
<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex">
//函数定义
methods:{//三级联动菜单的路由跳转_编程式导航+事件委派goSearch(event) {//获取触发这个事件的节点,需要带有data-categoryName这样的节点let element = event.target;//节点有一个属性dataset属性,可以获取节点的自定义属性与属性值// console.log(element.dataset);let {categoryname, category1id, category2id, category3id} = element.dataset//如果标签拥有categoryname一定是a标签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)}
}

在这里插入图片描述

8. Vue路由切换的请求优化

问题:组件切换过程多次向服务器发送请求
解决:APP的mounted只会执行一次
问题:在切换路由时会重复发送 商品分类列表数据请求
原因:Vue在路由切换的时候会销毁旧路由。我们在三级列表全局组件TypeNav中的mounted进行了请求一次商品分类列表数据。由于Vue在路由切换的时候会销毁旧路由,当我们再次使用三级列表全局组件时还会发一次请求。
TypeNav/index.vue

//当组件挂载完毕:可以向服务器请求数据mounted() {this.$store.dispatch("cateGoryList");//判断当前的路由是search时,则进行隐藏,Home的话是显示if(this.$route.path !== '/home') {this.show = false}},

如下图所示:当我们在包含三级列表全局组件的不同组件之间进行切换时,都会进行一次信息请求。
在这里插入图片描述
由于信息都是一样的,出于性能的考虑我们希望该数据只请求一次,所以我们把这次请求放在App.vue的mounted中。(根组件App.vue的mounted只会执行一次)
注意:虽然main.js也是只执行一次,但是不可以放在main.js中。因为只有组件的身上才会有$store属性。

9. Mock插件

9.1 Mock使用

Mockjs:用来拦截前端ajax请求,返回我们自定义的数据用于测试前端接口。
参考文档:
http://mockjs.com/
https://github.com/nuysoft/Mock

安装:npm i --save mockjs
使用步骤: mockjs使用步骤

  1. 在项目当中src文件夹中创建mock文件夹
  2. 第二步准备JSON数据(mock 文件夹中创建相应的JSON文件) ----格式化一下,别留有空格(跑不起来的)
  3. mock数据需要的图片放置到public文件夹中(public文件夹在打包的时候,会把相应的资源原封不动打包到dist文件夹)
  4. 创建mock/mockServer.js 通过mockjs插件实现模拟数据

在这里插入图片描述
banners、floors分别为轮播图和页面底部的假数据的JSON文件。

mock/mockServer.js

import Mock from 'mockjs'
//webpack默认对外暴露:json、图片
import banners from './banners.json'
import floors from './floors.json'//mock数据:第一个参数请求地址,第二个参数:请求数据
// 提供广告位轮播数据的接口
Mock.mock("/mock/banners", {code: 200, data: banners})
// 提供所有楼层数据的接口
Mock.mock("/mock/floors", {code: 200, data: floors})//记得要在main.js中引入一下(至少需要执行一次,才能模拟数据)
//import '@/mock/mockServer'

9.2 利用mockjs提供模拟数据

api/mockAjax.js
这个文件跟前面二次封装axios里的api/request.js 文件内容一样,只不过是区分真实接口和虚拟接口

//专门请求mock接口的axios封装
import axios from "axios";const mockAjax  = axios.create({baseURL:'/mock',     //路径前缀timeout: 5000,       //请求超时
})//配置请求拦截器
mockAjax.interceptors.request.use(config => {//config内主要是对请求头Header配置//比如添加token//开启进度条return config;
})//配置响应拦截器
mockAjax.interceptors.response.use((res) => {//成功的回调函数return  res.data;
},(error) => {//失败的回调函数console.log("响应失败"+error)return Promise.reject(new Error('fail'))
})//对外暴露
export default mockAjax;

api/index.js

// 在当前这个模块:API进行统一管理
import requests from "./request";
import mockAjax from './mockAjax'//三级联动接口
// export const reqCategoryList = ()=>{
//     //发请求,request模块已经配置了/api,可以去掉/api,
//     return requests({url: '/product/getBaseCategoryList',method: 'get'})
// }
//简化写法
export const reqCategoryList = ()=> requests.get('/product/getBaseCategoryList')//获取广告轮播列表
export const reqBannersList = ()=> mockAjax.get('/banners')//获取首页楼层列表
export const reqFloors = ()=> mockAjax.get('/floors')

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

 mounted() {this.$store.dispatch("getBannerList")},
  1. 请求实际是在store中的actions中完成的
    store/home/index.js
import { reqCategoryList, reqBannersList } from "@/api"
//home模块的Vuex模块
const state = {//state中数据默认初始值别瞎写,根据接口的返回值进行初始化categoryList: [],bannerList: []
}
const mutations = {CATEGORYLIST(state, categoryList) {state.categoryList = categoryList},BANNERLIST(state, bannerList) {state.bannerList = bannerList}
}
const actions = {//通过API里得接口函数调用,向服务器请求,获得服务器参数async cateGoryList(context){//返回的是Promise实例对象let result  = await reqCategoryList()// console.log("result",result) if(result.code == 200){context.commit('CATEGORYLIST',result.data)}},//获取首页轮播图数据async getBannerList(context) {let result = await reqBannersList()if(result.code == 200){context.commit('BANNERLIST',result.data)}}
}
const getters = {}
export default{state,getters,mutations,actions
}
  1. 轮播图组件ListContainer.vue组件在store中获取轮播图数据。由于在这个数据是通过异步请求获得的,所以我们要通过计算属性computed获取轮播图数据。
    ListContainer.vue
import {mapState} from 'vuex'
export default {name: "ListContainer",mounted() {this.$store.dispatch('getBannerList')},computed: {...mapState({bannerList: (state) => state.home.bannerList,})}
};

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

10. Swiper轮播图

10.1 Swiper的基本使用

Swiper插件
官网中给出了代码实例:
做一个简要总结:

  1. 安装:npm i swiper@5
  2. 在需要使用轮播图的组件内导入swpier和它的css样式
import Swiper from 'swiper'
//引入swiper的样式(如果用到的轮播图的地方多,可以在main.js引入样式)
import 'swiper/css/swiper.css'
  1. 在组件中创建swiper需要的dom标签(html代码,参考官网代码)
    ListContainer/index.vue
<!--banner轮播-->
<div class="swiper-container" id="mySwiper"><div class="swiper-wrapper"><div class="swiper-slide" v-for="(carouse, index) in bannerList" :key="carouse.id"><img :src="carouse.imgUrl" /></div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div>
</div>
<script>
import { mapState } from "vuex";
import Swiper from "swiper";
export default {name: "ListContainer",mounted() {//请求数据this.$store.dispatch("getBannerList");//创建swiper实例let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{// 如果需要分页器pagination:{el: '.swiper-pagination',clickable: true,},// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev',},// 如果需要滚动条scrollbar: {el: '.swiper-scrollbar',},})},computed: {...mapState({bannerList: (state) => state.home.bannerList,}),},
}
</script>
  1. 创建swiper实例
    **问题:**接下来要考虑的是什么时候去加载这个swiper,我们第一时间想到的是在mounted中创建这个实例。如上代码,但是会出现无法加载轮播图片的问题。

原因: 在new Swpier实例之前,页面中结构必须的有(现在把new Swiper实例放在mounte这里发现不行),而我们在mounted中先去dispatch异步请求了轮播图数据,然后又创建的swiper实例。由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。

解决方法:
方法一: update能解决,但若有别的数据更新,同时触发了响应内容,冗余

update() {//创建swiper实例let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{//......})},

方法二: setTimeout定时器解决,但过时效才能显示分页器效果
等我们的数据请求完毕后再创建swiper实例。只需要加一个1000ms时间延迟再创建swiper实例.。将上面代码改为:

mounted() {//请求数据this.$store.dispatch("getBannerList")//创建swiper实例setTimeout(()=>{let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{//......})},1000)},

这个方法肯定不是最好的,但是我们开发的第一要义就是实现功能,之后可以再完善。
原理:为什么setTimeout有效,这个涉及到异步和同步,具体可以看 ES6 事件循环

方法三:最完美的方案——watch+nectTick

  • watch 监听bannerList数据的变化——空数组变为数组里有元素,但是watch中的handler只能保证数据已经存在,不能保证html结构是否完整,就是不能保证html中v-for遍历bannerList数据是否执行完。假如watch先监听到bannerList数据变化,执行回调函数创建了swiper对象,之后v-for才执行,这样也是无法渲染轮播图图片(因为swiper对象生效的前提是html即dom结构已经渲染好了)。
  • nectTickthis.$nextTick它会将回调延迟到下次 DOM 更新循环之后执行
    官方介绍:服务器数据已返回,循环结束之后v-for执行结束,html结构已完整)执行延迟回调。
    修改数据之后(服务器数据回来)立即使用这个方法,获取更新后的DOM。
    **个人理解:**无非是等我们页面中的结构都有了再去执行回调函数
    应用:$nextTick可以保证页面结构存在,常与插件一起使用(一般插件都需要DOM存在)
watch: {// 监听bannerList数据的属性值的变化bannerList: {handler(newValue, oldValue) {this.$nextTick(() => {//创建swiper实例let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{loop: true, // 循环模式选项pagination: {el: ".swiper-pagination",clickable: true,},// 如果需要前进后退按钮navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},// 如果需要滚动条scrollbar: {el: ".swiper-scrollbar",},});});},},},

**注意1:**之前我们在学习watch时,一般都是监听的定义在data中的属性,但是我们这里是监听的computed中的属性,这样也是完全可以的,并且如果你的业务数据也是从store中通过computed动态获取的,也需要watch监听数据变化执行相应回调函数,完全可以模仿上面的写法。

注意2: 在创建swiper对象时,我们会传递一个参数用于获取展示轮播图的DOM元素,官网直接通过class(而且这个class不能修改,是swiper的css文件自带的)获取。但是这样有缺点:当页面中有多个轮播图时,因为它们使用了相同的class修饰的DOM,就会出现所有的swiper使用同样的数据,这肯定不是我们希望看到的。
**解决方法:**在轮播图最外层的DOM中添加ref属性
ref : 为某个元素注册一个唯一标识, vue对象通过$refs属性访问这个元素

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

通过ref属性值获取DOM

let mySwiper = new Swiper(this.$refs.mySwiper,{...})

ListContainer/index.vue完整代码:

<!--banner轮播-->
<div class="swiper-container" id="mySwiper" ref="mySwiper"><div class="swiper-wrapper"><div class="swiper-slide" v-for="(carouse, index) in bannerList" :key="carouse.id"><img :src="carouse.imgUrl" /></div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div>
</div>
<script>
import { mapState } from "vuex";
import Swiper from "swiper";
export default {name: "ListContainer",mounted() {//请求数据this.$store.dispatch("getBannerList");},computed: {...mapState({bannerList: (state) => state.home.bannerList,}),},watch: {// 监听bannerList数据的属性值的变化bannerList: {handler(newValue, oldValue) {this.$nextTick(() => {//执行这个回调后,数据已存在,html结构已加载//创建swiper实例let mySwiper = 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",},});});},},},}
</script>

10.2 父传子数据+轮播图Swiper的使用

假设一个场景:在父组件home请求了一个数据,子组件用props接受该数据floor,该数据有轮播图的数据。

父组件:home/index.vue

<template>
<div>
//...
<!--  父组件通过自定义属性list给子组件传递数据--><Floor v-for="floor in floorList"  :key="floor.id" :list="floor"/>
</div>
</template>

子组件:home/floor/index.vue

<template><!--楼层--><div class="floor"><div class="swiper-container" id="floor1Swiper" ref="floor1Swiper"><div class="swiper-wrapper"><div class="swiper-slide" v-for="carouse in floor.carouselList" :key="carouse.id"><img :src="carouse.imgUrl" /></div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div></div></div>
</template>
<script>
export default {name: "floor",
//子组件通过props属性接受父组件传递的数据props:['list'],mounted() {//创建swiper实例,这里是在mounted里,因为这里的数据是来源父组件home,通过props传递的,数据、html结构都已经存在//所以可以在mounted中创建swiper实例let mySwiper = new Swiper(this.$refs.floor1Swiper, {loop: true, // 循环模式选项pagination: {el: ".swiper-pagination",clickable: true,},// 如果需要前进后退按钮navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},// 如果需要滚动条scrollbar: {el: ".swiper-scrollbar",},});},
}
</script>

这时候子组件里的创建swiper实例可以放在mounted下,而不像是 10.1中的 ListContainer/idnex.vue里的只能放在watch下,这是由于该子组件的轮播图数据是父组件异步请求,并通过props传递给子组件的,其数据、html结构都已经存在,所以可以放在mounted中创建swiper实例。

问:创建swiper实例的代码除了可以放在mounted中,也可以放在watch下吗?
可以,但是需要添加immediate: true属性,在watch没有加immdediate属性的话是无法监听到floor的,因为这个数据从来没有发生过变化(数据是父组件传递的,父组件传递的时候就是一个对象,对象里面该有的数据都是存在的),而immediate: true表示,初始化时让handler调用一下,即初始化就监听一次,不管数据是否有变化,代码如下:

watch: {// 监听bannerList数据的属性值的变化floor: {immediate: true,    //初始化就监听一次(不管数据是否有变化)handler(newValue, oldValue) {//handler只能监听到数据已经有了,但是v-for动态渲染结构还没办法确定,需要用到nextTickthis.$nextTick(() => {//执行这个回调后,数据已存在,html结构已加载//创建swiper实例let mySwiper = new Swiper(this.$refs.floor1Swiper,{loop: true, // 循环模式选项pagination: {el: ".swiper-pagination",clickable: true,},// 如果需要前进后退按钮navigation: {nextEl: ".swiper-button-next",prevEl: ".swiper-button-prev",},// 如果需要滚动条scrollbar: {el: ".swiper-scrollbar",},});});},},},

10.3 将轮播图Swiper模块提取为公共组件

通过前两节,可以发现,ListContainer组件和Floor组件都是Home父组件的子组件,都用到了Swiper轮播图,且代码结构都相似,那么就可以将其Swiper轮播图单独拆出成一个全局组件Carousel

<template><div class="swiper-container" ref="swiper" id="floor1Swiper"><div class="swiper-wrapper"><div class="swiper-slide" v-for="(carouse,index) in carouselList" :key="carouse.id"><img :src="carouse.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";
import 'swiper/css/swiper.css'
export default {name: "Carousel",props:["carouselList"],watch: {carouselList: {//这里监听,无论数据有没有变化,上来立即监听一次immediate: true,//监听后执行的函数handler(){//第一次ListContainer中的轮播图Swiper定义是采用watch+ this.$nextTick()实现this.$nextTick(() => {let mySwiper = new Swiper(this.$refs.swiper,{loop: true, // 循环模式选项// 如果需要分页器pagination: {el: '.swiper-pagination',// clickable: true},// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev',},// 如果需要滚动条scrollbar: {el: '.swiper-scrollbar',},})})}}}
}
</script>
<style scoped></style>

main.js注册全局组件

//轮播图组件——全局组件
import Carousel from '@/components/Carousel'
//全局注册,第一个参数:组件名字,第二参数:是哪个组件
Vue.component(Carousel.name, Carousel)

Floor组件引用Carousel组件

<!-- 轮播图 -->
<Carousel :carouselList="floor.carouselList" />

我们还记得在首页上方我们的ListContainer组件也使用了轮播图,同样我们替换为我们的公共组件。
ListContainer组件引用Carousel组件

<Carousel :carouselList="bannerList"/>

注意:
(1)将该组件在main.js中引入,并定义为全局组件。其实也可以在使用到该组件的地方引入并声明
(2)引用组件时要在components中声明引入的组件。
(3)我们将轮播图组件已经提取为公共组件Carouse,所以我们只需要在Carouse中引入swiper和相应css样式。

11. vuex中getters的使用

官方getters使用
getters是vuex store中的计算属性。
如果不使用getters属性,我们在组件获取state中的数据表达式为:this.$store.state.子模块.属性
如果有多个组件需要用到此属性,我们要么复制这个表达式,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

// 计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {// 当前形参state,是当前仓库中的stategoodsList(state){return state.searchList.goodsList},trademarkList(state) {return state.searchList.trademarkList},attrsList(state) {return state.searchList.attrsList},
}

仓库中的getters是全局属性,是不分模块的。即store中所有模块的getter内的函数都可以通过$store.getters.函数名获取
在这里插入图片描述
我们在Search模块中获取商品列表数据就是通过getters实现,需要注意的是当网络出现故障时应该将返回值设置为空,如果不设置返回值就变成了undefined。

// 计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {// 当前形参state,是当前仓库中的stategoodsList(state){//网络出现故障时应该将返回值设置为空return state.searchList.goodsList || []},
}

在Search组件中使用getters获取仓库数据

//只展示了使用getters的代码
<script>//引入mapGettersimport {mapGetters} from 'vuex'export default {name: 'Search',computed:{//使用mapGetters,参数是一个数组,数组的元素对应getters中的函数名...mapGetters(['goodsList'])}}
</script>

12. Object.asign实现对象拷贝

ES6参考

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign(target, ...sources)    【target:目标对象】,【souce:源对象(可多个)】
举个栗子:
const object1 = {a: 1,b: 2,c: 3
};const object2 = Object.assign({c: 4, d: 5}, object1);console.log(object2.c, object2.d);
console.log(object1)  // { a: 1, b: 2, c: 3 }
console.log(object2)  // { c: 3, d: 5, a: 1, b: 2 }注意:
1.如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性
2.Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标
对象的[[Set]],所以它会调用相关 getter 和 setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如
果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到
原型,应使用Object.getOwnPropertyDescriptor()和Object.defineProperty()

对象深拷贝

针对深拷贝,需要使用其他办法,因为 Object.assign()拷贝的是属性值。假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。
let obj1 = { a: 0 , b: { c: 0}}; 
let obj2 = Object.assign({}, obj1); 
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}} obj1.a = 1; 
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}} 
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}} obj2.a = 2; 
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}} 
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 0}}obj2.b.c = 3; 
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 3}} 
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 3}} 
最后一次赋值的时候,b是值是对象的引用,只要修改任意一个,其他的也会受影响// Deep Clone (深拷贝)
obj1 = { a: 0 , b: { c: 0}}; 
let obj3 = JSON.parse(JSON.stringify(obj1)); 
obj1.a = 4; 
obj1.b.c = 4; 
console.log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}

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

最初想法:在每个三级列表和搜索按钮加一个点击触发事件,只要点击了就执行搜索函数。
这是一个很蠢的想法,如果这样就会生成很多回调函数,很耗性能。
最佳方法: 我们每次进行新的搜索时,我们的query和params参数中的部分内容肯定会改变,而且这两个参数是路由的属性。我们可以通过监听路由信息的变化来动态发起搜索请求。

如下图所示,$route是组件的属性,所以watch是可以监听的(watch可以监听组件data中所有的属性)
在这里插入图片描述

search组件的watch部分代码

//在组件挂载完毕之前,整理发送请求要携带的参数
beforeMount() {//ES6语法 对象拷贝Object.assign(this.searchParams, this.$route.query, this.$route.params)
},  
// mounted只会执行一次
mounted() {//组件挂载请求数据this.getData()
},
methods: {//向服务器发请求获取Search模块数据,根据参数不同返回不同的数据进行展示//把请求封装成一个函数,需要调用时再调用getData() {this.$store.dispatch('getSearchList',this.searchParams)}
},
watch: {//监听路由的信息是否发送变化,如果发送变化,再次发起请求$route(newValue, oldValue) {//再次整理发给服务器的数据Object.assign(this.searchParams, this.$route.query, this.$route.params)//再发请求this.getData()//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数//所以每次请求结束后将相应参数制空this.searchParams.category1Id = '';this.searchParams.category2Id = '';this.searchParams.category3Id = '';}
}

14. 面包屑相关操作

在这里插入图片描述
本次项目的面包屑操作主要就是两个删除逻辑。
分为:
当分类属性(query)删除时删除面包屑同时修改路由信息。
当搜索关键字(params)删除时删除面包屑、修改路由信息、同时删除输入框内的关键字。

14.1 面包屑添加/删除分类属性(query)

在这里插入图片描述

因为此部分在面包屑中是通过categoryName展示的,所所以删除时应将该属性值制空或undefined。
可以通过路由再次跳转修改路由信息和url链接

 <!--面包屑-->
<ul class="fl sui-tag"><li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCateGoryName">×</i></li>
</ul><script>
methods: {//删除面包屑——分类名removeCateGoryName() {// 带给服务器的参数是可选的,如果属性值设置为空字符串,还是会把相应的字段带给服务器,//如果属性值设置undefined,不会发送字段发给服务器,减少参数量this.searchParams.categoryName = undefinedthis.searchParams.category1Id = undefinedthis.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefined//再发一次请求this.getData()//地址栏也要修改,如果有params参数,要保留params参数,这里仅删除query参数if(this.$route.params) {this.$router.push({name: 'search', params:this.$route.params})}}
}
</script>

14.2 面包屑添加/删除搜索关键字(params)

在这里插入图片描述

和query删除的唯一不同点是此部分会多一步操作:删除输入框内的关键字(因为params参数是从输入框内获取的)
输入框实在Header组件中的
在这里插入图片描述

header和search组件是兄弟组件,要实现该操作就要通过兄弟组件之间进行通信完成。
在这里插入图片描述
详细的组件通信

这里通过$bus实现header和search组件的通信。
$bus使用
(1)在main.js中注册

new Vue({//全局事件总线$bus配置beforeCreate() {//此处的this就是这个new Vue()对象//网络有很多bus通信总结,原理相同,换汤不换药Vue.prototype.$bus = this},render: h => h(App),//router2、注册路由,此时组件中都会拥有$router $route属性router,//注册store,此时组件中都会拥有$storestore
}).$mount('#app')

(2)search组件使用$bus通信,第一个参数可以理解为为通信的暗号,还可以有第二个参数(用于传递数据),我们这里只是用于通知header组件进行相应操作,所以没有设置第二个参数。

 <!--面包屑-->
<ul class="fl sui-tag"><!-- 分类名的面包屑 --><li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCateGoryName">×</i></li><!-- 关键字的面包屑 --><li class="with-x" v-if="searchParams.keyWord">{{searchParams.keyWord}}<i @click="removeKeyWord">×</i></li>
</ul><script>
methods: {//删除面包屑——分类名removeCateGoryName() {// 带给服务器的参数是可选的,如果属性值设置为空字符串,还是会把相应的字段带给服务器,//如果属性值设置undefined,不会发送字段发给服务器,减少参数量this.searchParams.categoryName = undefinedthis.searchParams.category1Id = undefinedthis.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefined//再发一次请求this.getData()//地址栏也要修改,如果有params参数,要保留params参数,这里仅删除query参数if(this.$route.params) {this.$router.push({name: 'search', params:this.$route.params})}},//删除面包屑——关键字removeKeyWord() {this.searchParams.keyWord = undefined//再发一次请求this.getData()//通知兄弟组件header删除输入框的keyword关键字this.$bus.$emit('clearKeyWord')this.$router.push({name:'search',query:this.$route.query})}
}
</script>

(3)header组件接受$bus通信
注意:组件挂载时就监听clearKeyWord事件

mounted() {//  组件挂载时就监听clear事件,clear事件在search模块中定义//  当删除关键字面包屑时,触发该事件,同时header的输入框绑定的keyword要删除this.$bus.$on("clearKeyWord",()=>{this.keyWord = ''})}

**问题:**在删除面包屑时,会发送两次请求
**原因:**我们前面写到了用watch监听路由变化的代码,在删除面包屑时,除了要删除面包屑,还要修改路由信息,所以watch监听到了路由信息,所以,只需要将removeKeyWordremoveCateGoryName方法里的this.getData()删除即可

Search/index.vue的部分代码

import SearchSelector from './SearchSelector/SearchSelector'import {mapGetters} from 'vuex'export default {name: 'Search',components: {SearchSelector},data() {return {//带给服务器的参数searchParams: {category1Id: "",  //一级分类category2Id: "",category3Id: "",categoryName: "", //分类名keyword: "",     //搜索关键字order: "",       //排序pageNo: 1,       //默认值为1,分页器,页数pageSize: 10,    //每一页展示条数props: [],       //平台售卖属性trademark: ""    //品牌}}},//在组件挂载完毕之前,整理发送请求要携带的参数beforeMount() {//复杂的写法// this.searchParams.category1Id = this.$route.query.category1Id// this.searchParams.category2Id = this.$route.query.category2Id// this.searchParams.category3Id = this.$route.query.category3Id// this.searchParams.categoryName = this.$route.query.categoryName// this.searchParams.keyword = this.$route.params.keyword//ES6语法 对象拷贝Object.assign(this.searchParams, this.$route.query, this.$route.params)},  // mounted只会执行一次mounted() {//组件挂载请求数据this.getData()},computed: {...mapGetters(['goodsList'])},methods: {//向服务器发请求获取Search模块数据,根据参数不同返回不同的数据进行展示//把请求封装成一个函数,需要调用时再调用getData() {this.$store.dispatch('getSearchList',this.searchParams)},//删除面包屑——分类名removeCateGoryName() {// 带给服务器的参数是可选的,如果属性值设置为空字符串,还是会把相应的字段带给服务器,//如果属性值设置undefined,不会发送字段发给服务器,减少参数量this.searchParams.categoryName = undefinedthis.searchParams.category1Id = undefinedthis.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefined//再发一次请求// this.getData()//地址栏也要修改,如果有params参数,要保留params参数,这里仅删除query参数if(this.$route.params) {this.$router.push({name: 'search', params:this.$route.params})}},//删除面包屑——关键字removeKeyWord() {this.searchParams.keyWord = undefined//再发一次请求// this.getData()this.$bus.$emit('clearKeyWord')if(this.$route.query) {this.$router.push({name:'search',query:this.$route.query})}}},watch: {//监听路由的信息是否发送变化,如果发送变化,再次发起请求$route(newValue, oldValue) {//console.log(newValue, oldValue);//再次整理发给服务器的数据Object.assign(this.searchParams, this.$route.query, this.$route.params)//再发请求this.getData()//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数//所以每次请求结束后将相应参数制空this.searchParams.category1Id = undefined;this.searchParams.category2Id = undefined;this.searchParams.category3Id = undefined;}}}

14.3 SearchSelector子组件传参关联面包屑

在前两小节中描述了通过query、params参数生成面包屑,以及面包屑的删除操作对应地址栏url的修改。
SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性。如下图所示
在这里插入图片描述
此处生成面包屑时会涉及到子组件向父组件传递信息操作,之后的操作和前面两个小节讲的面包屑操作原理相同。唯一的区别是,这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化回影响路由地址变化。

使用自定义事件,让子组件给父组件传递数据
Search/SearchSelector/index.vue

<ul class="logo-list"><li v-for="trademark in trademarkList" :key="trademark.tmId" @click="trademarkHandler(trademark)">{{trademark.tmName}}</li></ul>
<!-- ... -->
<ul class="type-list"><li v-for="(attrValue,index) in attr.attrValueList" :key="index" @click="attrsHandler(attr.attrId, attr.attrName, attrValue)"><a href="#">{{attrValue}}</a></li>
</ul><script>
import {mapGetters} from 'vuex'export default {name: 'SearchSelector',computed: {...mapGetters(['trademarkList', 'attrsList'])},methods: {trademarkHandler(trademark) {//将子组件中的点击的品牌信息传递给父组件Search,自定义事件this.$emit('trademarkInfoEvent', trademark)},attrsHandler(attrId, attrName, attrValue) {let str = `${attrId}:${attrValue}:${attrName}`this.$emit('attrInfoEvent', str)}}}
</script>

Search/index.vue

<ul class="fl sui-tag"><!-- 分类的面包屑 --><li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCateGoryName">×</i></li><!-- 关键字的面包屑 --><li class="with-x" v-if="searchParams.keyWord">{{searchParams.keyWord}}<i @click="removeKeyWord">×</i></li><!-- 品牌的面包屑 --><li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(':')[1]}}<i @click="removeTrademark">×</i></li>
<!-- 售卖属性面包屑 --><li class="with-x" v-for="(attr,index) in searchParams.props" :key="index">{{attr.split(':')[1]}}<i @click="removeAttr(index)">×</i></li>
</ul>
</div><!--selector-->
<SearchSelector @trademarkInfoEvent="getTrademark" /><script>
methods: {//.....//删除面包屑——关键字removeKeyWord() {this.searchParams.keyWord = undefined//再发一次请求, watch已经监听路由变化发请求了,这里不需要再请求// this.getData()this.$bus.$emit('clearKeyWord')if(this.$route.query) {this.$router.push({name:'search',query:this.$route.query})}},//删除面包屑——品牌信息removeTrademark() {this.searchParams.trademark = undefined//由于路由没有变化,需要发送请求this.getData()            },//删除面包屑——属性信息removeAttr(index){this.searchParams.props.splice(index,1)this.getData()},//获取子组件传递的品牌信息(自定义事件)getTrademark(trademark) {//整理品牌字段参数: "ID:品牌名称"this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`//再发请求this.getData()},//获取子组件传递的属性信息(自定义事件)getAttr(attr) {//整理品牌字段参数: ["属性ID:属性值:属性名"]//数组去重if(this.searchParams.props.indexOf(attr) === -1) {this.searchParams.props.push(attr)//再发请求this.getData()}}
}
</script>

在这里插入图片描述

15. 商品排序

排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。
order属性值为字符串,例如‘1:asc’、‘2:desc’。1代表综合,2代表价格,asc代表升序,desc代表降序。

我们的升降序是通过箭头图标来辨别的,如图所示:
在这里插入图片描述

<ul class="sui-nav"><li :class="{ active: isActive1 }" @click="changeOrder('1')"><a href="#"><span>综合</span><em class="fs-down"><i class="arrow"></i></em></a></li><!-- ..... --><li :class="{ active: isActive2 }" @click="changeOrder('2')"><a href="#"><span>价格</span><em :class="getClass()"><i class="arrow-top"></i><i class="arrow-bottom"></i></em></a></li>
</ul><script>
computed: {...mapGetters(["goodsList"]),isActive1() {return this.searchParams.order.indexOf("1") !== -1;},isActive2() {return this.searchParams.order.indexOf("2") !== -1;},},
methods: {//类名设置getClass() {return {'fs-down': this.searchParams.order.indexOf('desc')!==-1,'fs-up': this.searchParams.order.indexOf('asc')!==-1}},//flag区分综合、价格,1:综合,2:价格changeOrder(flag) {let newSearchOrder = this.searchParams.order//将order拆为两个字段orderFlag(1:2)、order(asc:desc)let orderFlag = this.searchParams.order.split(':')[0]let order = this.searchParams.order.split(':')[1]//由综合到价格,由价格到综合if(orderFlag !==  flag) {//点击的不是同一个按钮newSearchOrder = `${flag}:desc`} else {//多次点击的是不是同一个按钮newSearchOrder = `${flag}:${order === 'desc' ? 'asc': 'desc'}`}//需要给order重新赋值this.searchParams.order = newSearchOrder//发送请求this.getData()}
}
</script>
<style>
.fs-up {.arrow-bottom {filter: alpha(opacity=50);-moz-opacity: 0.5;opacity: 0.5;}}.fs-down {.arrow-top {filter: alpha(opacity=50);-moz-opacity: 0.5;opacity: 0.5;}}
</style>

16. 手写分页器

实际开发中是不会手写的,一般都会用一些开源库封装好的分页,比如element ui。但是这个知识还是值得学习一下的。
核心属性:
pageNo(当前页码)、pageSize(每一页展示多少条数据)、total(共有多少条数据)、continues(连续展示的页码)
核心逻辑是获取连续页码的起始页码和末尾页码,通过计算属性获得。(计算属性如果想返回多个数值,可以通过对象形式返回)
分页器注册为一个全局组件,核心属性是通过父组件传递给分页器组件,计算连续页码和总页数
src/components/Pagination/index.vue

<template><div class="fr page"><div class="sui-pagination clearfix"><ul class="p-num"><li :class="{prev:true ,disabled: pageNo == 1}" @click="goToPageHandler(pageNo-1)"><a href="#"><i> &lt; </i><em>上一页</em></a></li><li v-if="startNumAndEnd.start > 1" @click="goToPageHandler(1)"><a href="#">1</a></li><li class="dotted" v-if="startNumAndEnd.start > 2"><span>...</span></li><li:class="{active: pageNo == page}"v-for="(page, index) in startNumAndEnd.end" :key="index" v-if="page >= startNumAndEnd.start" @click="goToPageHandler(page)" ><a href="#">{{ page }}</a></li><li class="dotted" v-if="startNumAndEnd.end < totalPage-1"><span>...</span></li><li v-if="startNumAndEnd.end < totalPage" @click="goToPageHandler(totalPage)"><a href="#">{{totalPage}}</a></li><li :class="{next:true ,disabled: pageNo == totalPage}" @click="goToPageHandler(pageNo+1)"><a href="#"><em>下一页</em><i>&gt;</i></a></li></ul><div class="p-skip"><em><b>{{ totalPage }}</b>&nbsp;&nbsp;到第</em><input type="text" value="1" class="input-txt" v-model="toPage" /><em></em><a href="" class="btn" @click="goToPageHandler(toPage)">确定</a></div></div></div>
</template><script>
export default {name: "Pagination",components: {},props: ["pageNo", "total", "pageSize", "continues"],data() {return {toPage: 1};},computed: {//共有多少页totalPage() {//Math.ceil向上取整return Math.ceil(this.total / this.pageSize);},//连续页码得其实页码、末尾页码startNumAndEnd() {let {  continues, pageNo, totalPage } = this;continues = parseInt(continues)pageNo = parseInt(pageNo)let start = 0,end = 0;//规定连续页码数字5(totalPage至少5页)//不正常现象if (continues > totalPage) {start = 1;end = totalPage;} else {start = pageNo - Math.floor(continues / 2);end = pageNo + Math.floor(continues / 2);if (start < 1) {start = 1;end = continues;}if (end > totalPage) {end = totalPage;start = totalPage - continues + 1;}}return { start, end };}},methods: {//给父组件传递页码goToPageHandler(pageNo) {this.$emit("getPageNoEvent", pageNo)}}
};
</script>
<style lang="less" scoped>
//....
li {//...&.disabled {pointer-events:none;   //该样式会阻止默认事件,但是鼠标样式会变成箭头的样子。a {color: #999;background-color: #fff;cursor: not-allowed;      //在此属性中,光标指示将不会执行所请求的动作。&:hover {color: #999 !important;} }}
}
//...
</style>

当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。

父组组件Search/index.vue

<template>
<!-- 分页器 -->
<Pagination :pageNo="searchParams.pageNo" :total="total" :pageSize="searchParams.pageSize" :continues="5"  @getPageNoEvent="getPageNo"/>
<!-- ... -->
</template>
<script>
import { mapGetters, mapState } from "vuex";
export default {name: "Search",data() {return {//带给服务器的参数searchParams: {category1Id: "", //一级分类category2Id: "",category3Id: "",categoryName: "", //分类名keyword: "", //搜索关键字order: "1:desc", //排序pageNo: 1, //默认值为1,分页器,页数pageSize: 10, //每一页展示条数props: [], //平台售卖属性trademark: "", //品牌},};},computed: {//获取数据总条数...mapState({total: state=>state.search.searchList.total}),},methods: {//向服务器发请求获取Search模块数据,根据参数不同返回不同的数据进行展示//把请求封装成一个函数,需要调用时再调用getData() {this.$store.dispatch("getSearchList", this.searchParams);},// 自定义回调事件,获取子组件传递的页码getPageNo(pageNo) {this.searchParams.pageNo = pageNo//发请求this.getData()}},
}
</script>

效果
在这里插入图片描述
其中,鼠标禁用样式cursor: not-allowed;和鼠标禁用事件pointer-events:none;看这个blog

17. 路由滚动行为

使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。

const router = createRouter({history: createWebHashHistory(),routes: [...],scrollBehavior (to, from, savedPosition) {// return 期望滚动到哪个的位置//滚动到顶部return { y: 0}}
})

18. undefined细节

访问undefined的属性值会引起红色警告,可以不处理,但是要明白警告的原因。
以获取商品categoryView信息为例,categoryView是一个对象。
对应的getters代码

const getters =  {categoryView(state){return state.goodInfo.categoryView}
}

对应的computed代码

 computed:{...mapGetters(['categoryView'])}

html代码

<div class="conPoin"><span v-show="categoryView.category1Name" >{{categoryView.category1Name}}</span><span v-show="categoryView.category2Name" >{{categoryView.category2Name}}</span><span v-show="categoryView.category3Name" >{{categoryView.category3Name}}</span>
</div>

下细节在于getters的返回值。如果getters按上面代码写为return state.goodInfo.categoryView,页面可以正常运行,但是会出现红色警告。
在这里插入图片描述
原因: 假设我们网络故障,导致goodInfo的数据没有请求到,即goodInfo是一个空的对象,当我们去调用getters中的return state.goodInfo.categoryView时,因为goodInfo为空,所以也不存在categoryView,即我们getters得到的categoryView为undefined。所以我们在html使用该变量时就会出现没有该属性的报错。
即:网络正常时不会出错,一旦无网络或者网络问题就会报错。
总结: 所以我们在写getters的时候要养成一个习惯在返回值后面加一个||条件。即当属性值undefined时,会返回||后面的数据,这样就不会报错。
如果返回值为对象加|| {},数组:|| [ ]
此处categoryView为对象,所以将getters代码改为

const getters =  {categoryView(state){return state.goodInfo.categoryView || {}}
}

同样的,我们假设网络故障,没有数据回来的情况下,父组件给子组件传递数据,也有这样的问题
detail父组件

<Zoom :skuImageList="skuInfo.skuImageList" /><script>
computed: {...mapGetters(['categoryView','skuInfo']),}
</script>

zoom子组件

<template><div class="spec-preview"><img :src="skuImageList[0].imgUrl" /><div class="event"></div><div class="big"><img :src="skuImageList[0].imgUrl" /></div><div class="mask"></div></div>
</template><script>export default {name: "Zoom",props: ["skuImageList"],mounted(){console.log(this.skuImageList);}}
</script>

在这里插入图片描述

原因如上所诉,只要在detail父组件的计算属性那里计算skuImageList,设置|| [ ] 就可以解决问题

<Zoom :skuImageList="skuImageList" /><script>
computed: {...mapGetters(['categoryView','skuInfo']),//给子组件的数据skuImageList() {//如果服务器的数据没有回来,skuInfo这个是空对象return this.skuInfo.skuImageList || []}}
</script>

解决上述的问题,又出现一个问题,输出一个空数组,空数组没有imgUrl这个属性,所以报错(假设网络故障的情况)
在这里插入图片描述
数组的第0项至少是个对象,不能是undefined ,因此在zoom子组件里,同样要设置|| {}
zoom子组件

<template><div class="spec-preview"><img :src="imgObj.imgUrl" /><div class="event"></div><div class="big"><img :src="imgObj.imgUrl" /></div><div class="mask"></div></div>
</template><script>export default {name: "Zoom",props: ["skuImageList"],mounted(){console.log(this.skuImageList);},computed: {imgObj() {return this.skuImageList[0] || {}}}}
</script>

问题解决,以上的这些问题不会影响功能实现,但需要搞清楚问题的缘由

19. 商品放大镜

<template><div class="spec-preview"><img :src="imgObj.imgUrl" /><div class="event" @mousemove="handler"></div><div class="big"><img :src="imgObj.imgUrl" ref="big" /></div><div class="mask" ref="mask"></div></div>
</template><script>export default {name: "Zoom",props: ["skuImageList"],data() {return {currentIndex: 0}},mounted(){// console.log(this.skuImageList);this.$bus.$on("getIndex",(index)=>{this.currentIndex = index})},computed: {imgObj() {return this.skuImageList[this.currentIndex] || {}}},methods: {handler(event) {let mask = this.$refs.masklet big = this.$refs.big//鼠标此时的可视区域的横坐标和纵坐标//主要是设置鼠标在遮挡层的中间显示let left = event.offsetX - mask.offsetWidth/2let top = event.offsetY - mask.offsetHeight/2//约束mask的范围left = left < 0 ? 0 : leftleft = left >= mask.offsetWidth ? mask.offsetWidth : lefttop = top < 0 ? 0 : toptop = top >= mask.offsetHeight ? mask.offsetHeight : top//修改元素的left|top属性mask.style.left = left+'px'mask.style.top = top +'px'big.style.left = -2 * left + 'px'big.style.top = -2 * top + 'px'}}}
</script><style lang="less">.spec-preview {position: relative;width: 400px;height: 400px;border: 1px solid #ccc;margin-bottom: 20px;img {width: 100%;height: 100%;}.event {width: 100%;height: 100%;position: absolute;top: 0;left: 0;z-index: 998;}.mask {width: 50%;height: 50%;background-color: rgba(0, 255, 0, 0.3);position: absolute;left: 0;top: 0;display: none;}.big {width: 100%;height: 100%;position: absolute;top: -1px;left: 100%;border: 1px solid #aaa;overflow: hidden;z-index: 998;display: none;background: white;img {width: 200%;max-width: 200%;height: 200%;position: absolute;left: 0;top: 0;}}.event:hover~.mask,.event:hover~.big {display: block;}}
</style>

在这里插入图片描述

20. 加入购物车成功路由

点击加入购物车时,会向后端发送API请求,但是该请求的返回值中data为null,所以我们只需要根据状态码code判断是否跳转到‘加入购物车成功页面’。
detail组件‘加入购物车’请求函数:

//加入购物车事件async addShopCart() {//1.发请求(发数据给服务器) try {//等待这个请求结束再做路由跳转await this.$store.dispatch("addOrUpdateShopCart", {skuId: this.$route.params.goodId,skuNum: this.skuNum})//一些简单的数据,比如skuNum通过query传过去//复杂的数据通过session存储,//sessionStorage、localStorage只能存储字符串        //sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo))//2. 服务器存储成功——进行路由传递参数,this.$router.push({name: 'addcartsuccess', query: {'skuNum': this.skuNum}})} catch (error) {//判断成功还是失败alert(error.message)}}

store/detail/index.js

const actions = {//将产品添加到购物车中async addOrUpdateShopCart({commit}, {skuId, skuNum}) {let result  = await reqAddOrUpdateShopCart(skuId,skuNum)//加入购物车以后,服务器写入数据成功,并未返回数据,只返回code=200,代表此次操作成功if(result.code == 200){return 'ok'} else {return Promise.reject(new Error('fail'))}}
}

其实这里当不满足result.code === 200条件时,也可以返回字符串‘faile’,自己在addShopCar中判断一下返回值,如果为‘ok’则跳转,如果为‘faile’(或者不为‘ok’)直接提示错误。当然这里出错时返回一个Promise.reject更加符合程序的逻辑。

当我们想要实现两个毫无关系的组件传递数据时,首先想到的就是路由的query传递参数,但是query适合传递单个数值的简单参数,所以如果想要传递对象之类的复杂信息,就可以通过Web Storage实现。

sessionStoragelocalStorage概念:
sessionStorage:为每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
localStorage:同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。
注意:无论是session还是local存储的值都是字符串形式。如果我们想要存储对象,需要在存储前JSON.stringify()将对象转为字符串,在取数据后通过JSON.parse()将字符串转为对象。

 //加入购物车事件async addShopCart() {//1.发请求(发数据给服务器) try {//等待这个请求结束再做路由跳转await this.$store.dispatch("addOrUpdateShopCart", {skuId: this.$route.params.goodId,skuNum: this.skuNum})//一些简单的数据,比如skuNum通过query传过去//复杂的数据通过session存储,//sessionStorage、localStorage只能存储字符串        //sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo))//2. 服务器存储成功——进行路由传递参数,//一些简单的数据skuNum,通过query形式给路由组件传递过去//产品信息数据(对象数据),通过会话存储(持久)this.$router.push({name: 'addcartsuccess', query: {'skuNum': this.skuNum}})sessionStorage.setItem('skuInfo', JSON.stringify(this.skuInfo))} catch (error) {//判断成功还是失败alert(error.message)}}},

AddCartSuccess/index.vue 加入购物车成功组件

computed: {skuInfo() {//获取本地存储数据return JSON.parse(sessionStorage.getItem('skuInfo'))}}

21. 购物车组件开发

21.1 临时游客的uuid

根据api接口文档封装请求函数

export const reqGetCartList = () => {
return requests({url:'/cart/cartList',method:'GET'
})}

但是如果想要获取详细信息,还需要一个用户的uuidToken,用来验证用户身份。但是该请求函数没有参数,所以我们只能把uuidToken加在请求头中。

创建utils工具包文件夹,创建生成uuid的js文件,对外暴露为函数(记得导入uuid => npm install uuid)。
生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储

src/utils/uuid_token.js

import {v4 as uuidv4} from 'uuid'//生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储
export const getUUID = ()=> {//1. 判断本地存储是否有uuidlet uuid_token = localStorage.getItem('UUIDTOKEN')//2.本地存储没有uuidif(!uuid_token) {//生成uuiduuid_token = uuidv4()localStorage.setItem("UUIDTOKEN", uuid_token)}//当用户有uuid时不会再生成return uuid_token
}

用户的uuid_token定义在store中的detail模块

import { getUUID } from "@/utils/uuid_token"
const state =  {goodInfo:{},//游客身份uuid_token: getUUID()
}

api/request.js里设置请求头

import store from '@/store';
requests.interceptors.request.use(config => {//config内主要是对请求头Header配置//通过请求头给服务器带临时身份给服务器//1、先判断uuid_token是否为空if(store.state.detail.uuid_token){//2、userTempId字段和后端统一config.headers['userTempId'] = store.state.detail.uuid_token}//比如添加token//开启进度条nprogress.start();return config;
})

注意this.$store只能在组件中使用,不能再js文件中使用。如果要在js中使用,需要引入import store from '@/store';
在这里插入图片描述
商品详情detail添加购物车时发送请求就会带userTempId属性了
在这里插入图片描述

21.2 购物车商品数量更改

  1. every函数使用
    every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
    例如判断底部勾选框是否全部勾选代码部分
<div class="select-all"><input class="chooseAll" type="checkbox" :checked="isAllCheck" /><span>全选</span></div><script>computed: {
//判断底部勾选框是否全部勾选isAllCheck() {//every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回falsereturn this.cartInfoList.every(item => item.isChecked === 1)}
}
</script>
  1. 购物车商品数量更改
    在这里插入图片描述

添加到购物车和对已有物品进行数量改动使用的同一个api,可以查看api文档。(skuNum可以是一个增量减量或者一个整体的数量,添加到购物车组件用到整体量,购物车组件的商品个数修改用到增减量)
在这里插入图片描述

使用@click@change触发changeSkuNum事件修改商品数量,都用到了同一个函数,但是携带的参数个数不同
changeSkuNum函数有三个参数,type区分操作,disNum用于表示数量变化(正负),cart商品的信息

 <li class="cart-list-con5"><a href="javascript:void(0)" class="mins" @click="changeSkuNum('minus',-1,cartInfo)">-</a><input autocomplete="off" type="text" :value="cartInfo.skuNum" @change="changeSkuNum('change',$event.target.value,cartInfo)" minnum="1" class="itxt"><a href="javascript:void(0)" class="plus" @click="changeSkuNum('add',1,cartInfo)">+</a></li><script>
methods: {getData() {this.$store.dispatch("getCartList");},//修改某个产品的个数,加入节流操作changeSkuNum: throttle(async function (type, disNum, cart) {// type区分操作,disNum用于表示数量变化(正负),cart商品的信息switch (type) {case "add":disNum = 1;break;case "minus"://产品的个数大于1,才可传递给服务器-1, 如果产品个数的小于1,设置disNum=0表示不增不减(原封不动)disNum = cart.skuNum > 1 ? -1 : 0;break;case "change"://如果用户输入的文本非法if (isNaN(disNum) || disNum < 1) {disNum = 0; //disNum=0表示不增不减} else {//正常大于1 ,不能出现小数//用户输入的值 - 产品原本个数disNum = parseInt(disNum) - cart.skuNum;}break;}//派发actionstry {await this.$store.dispatch("addOrUpdateShopCart", {skuId: cart.skuId,skuNum: disNum,});//再一次获取服务器最新数据进行展示this.getData();} catch (error) {//判断成功还是失败alert(error.message);}}, 100),
}
</script>

21.3 购物车单个商品状态修改和删除

该部分较为简单,不过多赘述,但唯一需要注意的是当store的action种的函数返回值data为null时,应该采用下面的写法**(if-else)**

action部分:

//删除购物车某个产品
async deleteCart({commit}, skuId) {let result  = await reqDeleteCartById(skuId)if(result.code == 200){return 'ok'} else {return Promise.reject(new Error('fail'))}},//切换某个商品选中状态
async updateChecked({commit}, {skuId, isChecked}) {let result  = await reqUpdateCheckedById(skuId, isChecked)if(result.code == 200){return 'ok'} else {return Promise.reject(new Error('fail'))}},

method部分:(重点是try、catch)

methods: {//删除产品async deleteCartById(skuId) {try {await this.$store.dispatch("deleteCart", skuId);//删除成功再次发请求this.getData();} catch (error) {alert(error.message);}},//切换商品状态async updateChecked(skuId, event) {let isChecked = event.target.checked ? "1" : "0"try {await this.$store.dispatch("updateChecked", {skuId, isChecked});//修改成功,刷新数据,发请求this.getData()} catch (error) {alert(error.message);}},
}

21.4 购物车删除选中的全部商品和全选商品

  1. 删除选中的全部商品
    由于后台只提供了删除单个商品的接口,所以要删除多个商品时,只能多次调用actions中的函数。
    我们可能最简单的方法是在method的方法中多次执行dispatch删除函数,当然这种做法也可行,但是为了深入了解actions,我们还是要将批量删除封装为actions函数。
    actions扩展
deleteAllCheckedById(context) {console.log(context)}

context的内容
在这里插入图片描述
context中是包含dispatch、getters、state的,即我们可以在actions函数中通过dispatch调用其他的actions函数,可以通过getters获取仓库的数据。
这样我们的批量删除就简单了,对应的actions函数代码让如下

//删除选中的所有商品
deleteAllCheckedById({dispatch,getters}) {getters.cartList.cartInfoList.forEach(item =>  {let result = [];//将每一次返回值添加到数组中result.push(item.isChecked === 1?dispatch('deleteCartById',item.skuId):'')})return Promise.all(result)
},

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

购物车组件method批量删除函数

//删除多个选中的商品
async deleteAllCheckedCart() {try {await this.$store.dispatch("deleteAllCheckedCart")//删除成功,刷新数据this.getData()} catch (error) {alert(error)}},
  1. 全选商品
    修改商品的全部状态和批量删除的原理相同
    actions
//修改购物车全选状态
updateAllChecked({getters, dispatch}, checked) {let result = []getters.cartList.cartInfoList.forEach(item=>{result.push(dispatch('updateChecked',{skuId:item.skuId, isChecked:checked}))})//result里需要全部都为成功,只要有一个失败就返回失败return Promise.all(result)}

购物车组件method切换全选状态

//修改全选状态
async updateAllChecked(event) {let checked = event.target.checked ? "1" : "0"try {await this.$store.dispatch("updateAllChecked", checked);//修改成功,刷新数据,发请求this.getData()} catch (error) {alert(error.message);}}

22 CSS样式中使用@符号

在CSS中也可以使用@符号(src别名),但需要在前面加上~

background-image: url(~@/assets/images/icons.png);

关于注册和登录页面、打包上线的笔记总结在 仿京东 项目笔记2(注册登录)

相关文章:

仿京东 项目笔记1

目录 项目代码1. 项目配置2. 前端Vue核心3. 组件的显示与隐藏用v-if和v-show4. 路由传参4.1 路由跳转有几种方式&#xff1f;4.2 路由传参&#xff0c;参数有几种写法&#xff1f;4.3 路由传参相关面试题4.3.1 路由传递参数&#xff08;对象写法&#xff09;path是否可以结合pa…...

huggingface transformers库中LlamaForCausalLM

新手入门笔记。 LlamaForCausalLM 的使用示例&#xff0c;这应该是一段推理代码。 from transformers import AutoTokenizer, LlamaForCausalLMmodel LlamaForCausalLM.from_pretrained(PATH_TO_CONVERTED_WEIGHTS) tokenizer AutoTokenizer.from_pretrained(PATH_TO_CONVE…...

04-过滤器和拦截器有什么区别?【Java面试题总结】

过滤器和拦截器有什么区别&#xff1f; 运行顺序不同&#xff1a;过滤器是在 Servlet 容器接收到请求之后&#xff0c;但在 Servlet被调用之前运行的&#xff1b;而拦截器则是在Servlet 被调用之后&#xff0c;但在响应被发送到客户端之前运行的。 过滤器Filter 依赖于 Servle…...

如何用selenium或pyppeteer来启动多个AdsPower窗口

前言 本文是该专栏的第57篇,后面会持续分享python爬虫干货知识,记得关注。 关于selenium或pyppeteer来启动打开adspower浏览器的方法,笔者在本专栏前面有详细介绍过,感兴趣的同学可往前翻阅《如何用selenium或pyppeteer来链接并打开指纹浏览器AdsPower》,文章内容包含完整…...

京东店铺所有商品API接口数据

​​京东平台店铺所有商品数据接口是开放平台提供的一种API接口&#xff0c;通过调用API接口&#xff0c;开发者可以获取京东整店的商品的标题、价格、库存、月销量、总销量、库存、详情描述、图片、价格信息等详细信息 。 获取店铺所有商品接口API是一种用于获取电商平台上商…...

stm32之27.iic协议oled显示

屏幕如果无法点亮&#xff0c;需要用GPIO_OType_PP推挽输出&#xff0c;加并上拉电阻 1.显示字符串代码 2.显示图片代码&#xff08;unsigned强制转换&#xff08;char*&#xff09;&#xff09; 汉字显示...

paddle 1-高级

目录 为什么要精通深度学习的高级内容 高级内容包含哪些武器 1. 模型资源 2. 设计思想与二次研发 3. 工业部署 4. 飞桨全流程研发工具 5. 行业应用与项目案例 飞桨开源组件使用场景概览 框架和全流程工具 1. 模型训练组件 2. 模型部署组件 3. 其他全研发流程的辅助…...

ChatGPT帮助高职院校学生实现个性化自适应学习与对话式学习

一、学习层面&#xff1a;ChatGPT帮助高职院校学生实现个性化自适应学习与对话式学习 1.帮助高职院校学生实现个性化自适应学习 数字技术的飞速发展引起了教育界和学术界对高职院校学生个性化自适应学习的更多关注和支持&#xff0c;其运作机制依赖于人工智能等技术&#xff0…...

如何通过python写接口自动化脚本对一个需要调用第三方支付的报名流程进行测试?

对于需要调用第三方支付的报名流程进行接口自动化测试&#xff0c;可以通过以下步骤来编写Python代码&#xff1a; 1. 确认API需求 首先&#xff0c;需要确认报名流程的API需求和预期功能。这涉及到对业务需求的理解和API设计的分析。 2. 安装依赖库 在Python程序中&#x…...

将OSGB格式数据转换为3d tiles的格式

现有需求需要将已有的一些OSGB数据加载到CesiumJS中展示,但是CesiumJS本身不支持osbg格式的数据渲染所以我们需要将其转换一下,有两种格式可以转换一种是glTF格式,另一种是我们今天要介绍的3D Tiles格式 下载开源工具 在github上其实有好多这种工具,每个工具的用法大同小异,这…...

【易售小程序项目】小程序首页完善(滑到底部数据翻页、回到顶端、基于回溯算法的两列数据高宽比平衡)【后端基于若依管理系统开发】

文章目录 说明细节一&#xff1a;首页滑动到底部&#xff0c;需要查询下一页的商品界面预览页面实现 细节二&#xff1a;当页面滑动到下方&#xff0c;出现一个回到顶端的悬浮按钮细节三&#xff1a;商品分列说明优化前后效果对比使用回溯算法实现ControllerService回溯算法 优…...

素数求原根

1 模m原根的定义 1.1符号说明: Z m ∗ Z_m^* Zm∗​:代表满足 1 < i < m − 1 , ( i , m ) 1 1<i<m-1,(i,m)1 1<i<m−1,(i,m)1的数字 i i i组成的集合 o r d m ( a ) ord_m(a) ordm​(a):代表 a ( m o d m ) a(mod m) a(modm)在 Z m ∗ Z_m^* Zm∗​中的…...

【Apollo学习笔记】——规划模块TASK之PATH_ASSESSMENT_DECIDER

文章目录 前言PATH_ASSESSMENT_DECIDER功能简介PATH_ASSESSMENT_DECIDER相关信息PATH_ASSESSMENT_DECIDER总体流程1. 去除无效路径2. 分析并加入重要信息给speed决策SetPathInfoSetPathPointType 3. 排序选择最优的路径4. 更新必要的信息 前言 在Apollo星火计划学习笔记——Ap…...

09 mysql fetchSize 所影响的服务器和客户端的交互

前言 这是一个 之前使用 spark 的时候 记一次 spark 读取大数据表 OOM OutOfMemoryError: GC overhead limit exceeded 因为一个 OOM 的问题, 当时使用了 fetchSize 的参数 应用服务 hang 住, 导致服务 503 Service Unavailable 在这个问题的地方, 出现了一个查询 32w 的数据…...

DevEco Studio 配置

首先,打开deveco studio 进入首页 …我知道你们想说什么,我也想说 汉化配置 没办法,老样子,先汉化吧,毕竟母语看起来舒服 首先,点击软件左下角的configure,在配置菜单里选择plugins 进入到插件页面, 输入chinese,找到汉化插件,(有一说一写到这我心里真是很不舒服) 然后点击o…...

Nginx自动探活后端服务状态自动转发,nginx_upstream_check_module的使用

一、三种方案 nginx对后端节点健康检查的方式主要有3种 1. gx_http_proxy_module 模块和ngx_http_upstream_module模块(自带) 官网地址:http://nginx.org/cn/docs/http/ng … proxy_next_upstream 严格来说,nginx自带是没有针对负载均衡后端节点的健康检查的,但是可以通…...

CSS 一个好玩的卡片“开卡效果”

文章目录 一、用到的一些CSS技术二、实现效果三、代码 一、用到的一些CSS技术 渐变 conic-gradientbox-shadowclip-path变换、过渡 transform、transition动画 animation keyframes伪类、伪元素 :hover、::before、::after …绝对布局。。。 clip-path 生成网站 https://techb…...

lintcode 667 · 最长的回文序列【中等 递归到动态规划】

题目 https://www.lintcode.com/problem/667/ 给一字符串 s, 找出在 s 中的最长回文子序列的长度. 你可以假设 s 的最大长度为 1000.样例 样例1输入&#xff1a; "bbbab" 输出&#xff1a; 4 解释&#xff1a; 一个可能的最长回文序列为 "bbbb" 样例2输入…...

oracle sql语言模糊查询

在Where子句中&#xff0c;可以对datetime、char、varchar字段类型的列用Like子句配合通配符选取那些“很像...”的数据记录&#xff0c;以下是可使用的通配符&#xff1a; % 零或者多个字符 _ 单一任何字符&#xff08;下划线&#xff09; / 特殊字符 [] 在某一范…...

【Ubuntu】解决ubuntu虚拟机和物理机之间复制粘贴问题(无需桌面工具)

解决Ubuntu虚拟机和物理机之间复制粘贴问题 第一步 先删除原来的vmware tools&#xff08;如果有的话&#xff09; sudo apt-get autoremove open-vm-tools第二步 安装软件包&#xff0c;一般都是用的desktop版本&#xff08;如果是server换一下&#xff09; sudo apt-get …...

解决ubuntu文件系统变成只读的方法

所欲文件变成只读&#xff0c;这种情况一般是程序执行发生错误&#xff0c;磁盘的一种保护措施 使用fsck修复 方法一&#xff1a; # 切换root sudo su # 修复磁盘错误 fsck -t ext4 -v /dev/sdb6 方法二&#xff1a; fsck.ext4 -y /dev/sdb6 重新用读写挂载 上面两种方法&…...

高数刷题笔记

常见等价无穷小 注意讨论 第二个等价无穷小 夹逼定理&#xff01;&#xff01;&#xff01; 递归数列可以尝试用关系式法 通常用到夹逼定理的时候都会用到放缩构造出一大一小两个函数&#xff0c;将原函数夹在中间&#xff0c;然后使得两端函数极限相同则可推出原函数的极限&am…...

c++入门一

参考&#xff1a;https://www.learncpp.com/cpp-tutorial/ When you finish, you will not only know how to program in C, you will know how NOT to program in C, which is arguably as important. Tired or unhappy programmers make mistakes, and debugging code tends…...

2023年项目进度管理平台排行榜

项目进度管理是项目管理学科中的一门重要课程&#xff0c;通过合理的项目计划&#xff0c;有效控制项目进度&#xff0c;保障项目能够按时交付。 不过&#xff0c;项目进度管理并不是一件简单的工作&#xff0c;不仅需要面对项目过程中各种突发情况&#xff0c;还需要做好团队协…...

【设计模式】面向对象设计八大原则

&#xff08;1&#xff09;依赖倒置原则&#xff08;DIP&#xff09; 高层模块&#xff08;稳定&#xff09;不应该依赖于低层模块&#xff08;变化&#xff09;&#xff0c;二者都应该依赖于抽象&#xff08;稳定&#xff09;。抽象&#xff08;稳定&#xff09;不应该依赖于…...

python数分实战探索霍尔特法之销售预测python代码实现以及预测图绘制

探索霍尔特法:时间序列预测中的线性趋势捕捉 时间序列分析是统计学和数据分析中的一个核心领域。无论是预测股票市场的走势,还是预测未来的销售量,一个精确和可靠的预测模型都是至关重要的。在众多的时间序列预测方法中,霍尔特法(Holts method)脱颖而出,特别是当我们面…...

java线程状态

图形说明: Thread.State源码注释: public enum State {/*** 新生状态&#xff1a;线程对象创建&#xff0c;但是还未start()*/NEW,/*** 线程处于可运行状态&#xff0c;但是这个可运行状态并不代表线程一定在虚拟机中执行。* 需要等待从操作系统获取到资源(比如处理器时间片…...

编译问题:error: ‘printf’ was not declared in this scope

这个错误提示意味着编译器在当前作用域内无法找到 printf 函数的声明。这通常是因为没有包含 <stdio.h> 头文件导致的。 解决方法是在程序中添加 #include <stdio.h> 这一行代码。这个头文件中包含了 printf 函数的声明&#xff0c;告诉编译器如何处理该函数。...

改变C++中私有变量成员的值

1、没有引用的情况&#xff1a; #include <iostream> #include <queue> using namespace std; class Person { public:queue<int>que; public:queue<int> getQueue(){return que;}void push(int a){que.push(a);}void pop(){que.pop();} };int main()…...

线程唯一的单例

经典设计模式的单例模式是指进程唯一的对象实例&#xff0c;实现code如下&#xff1a; package cun.zheng.weng.design.sinstnce;import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExec…...