怎么做简单的网站,传媒公司排行榜,海南新闻在线新闻中心,广州预约小程序开发目录 项目代码1. 项目配置2. 前端Vue核心3. 组件的显示与隐藏用v-if和v-show4. 路由传参4.1 路由跳转有几种方式#xff1f;4.2 路由传参#xff0c;参数有几种写法#xff1f;4.3 路由传参相关面试题4.3.1 路由传递参数#xff08;对象写法#xff09;path是否可以结合pa… 目录 项目代码1. 项目配置2. 前端Vue核心3. 组件的显示与隐藏用v-if和v-show4. 路由传参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 二次封装Axios5.2 前端通过代理解决跨域问题5.2 请求接口统一封装5.3 async和await5.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 面包屑添加/删除分类属性query14.2 面包屑添加/删除搜索关键字params14.3 SearchSelector子组件传参关联面包屑 15. 商品排序16. 手写分页器17. 路由滚动行为18. undefined细节19. 商品放大镜20. 加入购物车成功路由21. 购物车组件开发21.1 临时游客的uuid21.2 购物车商品数量更改21.3 购物车单个商品状态修改和删除21.4 购物车删除选中的全部商品和全选商品 22 CSS样式中使用符号 项目代码
项目代码1
1. 项目配置
项目运行起来时让浏览器自动打开 在package.json文件中设置 --open
scripts: {serve: vue-cli-service serve --open,build: vue-cli-service build,lint: vue-cli-service lint},关闭eslint校验工具 vue.config.js
const { defineConfig } require(vue/cli-service)
module.exports defineConfig({transpileDependencies: true,lintOnSave: false
})
src文件夹简写方法配置别名 因为项目大的时候src源代码文件夹里面目录会很多找文件不方便设置src文件夹的别名的好处找文件会方便一些 在文件根目录下跟vue.config.js同级目录下新建jsconfig.json文件[代表的是src文件夹]
{compilerOptions: {baseUrl: ./,paths: {/*: [src/*]}},exclude: [node_modules,dist]
}创建的vue2项目默认是有jsconfig.js文件文件配置选项说明可参考这个blog 路由分析 Vue-Router 前端路由: K即为URL网络资源定位符 V即为相应的路由组件 确定项目结构顺序:上中下 -----只有中间部分的V在发生变化中间部分应该使用的是路由组件 项目路由分析2个非路由组件和四个路由组件 两个非路由组件Header 、Footer 路由组件 : Home、Search、Login没有底部的Footer组件带有二维码的、Register没有底部的Footer组件带二维码的 路由组件和非路由组件区别
非路由组件放在components中路由组件放在pages或views中非路由组件通过标签使用路由组件通过路由使用在main.js注册完路由所有的路由和非路由组件身上都会拥有$router $route属性$router一般进行编程式导航进行路由跳转$route 一般获取路由信息name path params等
2. 前端Vue核心
开发一个前端模块可以概括为以下几个步骤 1写静态页面、拆分为静态组件 2发请求API 3vuexactions、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 /Footer4. 路由传参
详细学习 Vue-Router
4.1 路由跳转有几种方式
声明式导航router-link务必要有to属性编程式导航主要利用的是组件实例的$router.push | replace方法可以书写一些自己的业务
4.2 路由传参参数有几种写法
params参数属于路径中的一部分在配置路由的时候需要占位query参数不属于路径的一部分类似于Ajax中的querystring 不需要占位 /home?kvkv 情况说明 当点击搜索按钮之后 将home页面跳转到search页面输入内容后需要将内容传递给search页面 params: 占位(注意冒号)
path: /search/:keyWord,params传参
第一种字符串
this.$router.push(/search/this.keyWord)query传参 (不需要占位)
第一种字符串
this.$router.push(/search/this.keyWord ?kthis.keyWord.toUpperCase())第二种方法模板字符串
this.$router.push(/search${this.keyWord}?k${this.keyWord.toUpperCase()})第三种对象写法常用
this.$router.push({name: search, //使用params 不能使用 path: /Searchparams: {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参数一起使用
不可以以对象方式传参时对象写法可以是name、path形式但需要注意的是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: /Searchparams: {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/?kasd
此时的地址信息少了 /search
正常的地址栏信息: http://localhost:8080/search?kasd解决方法 可以通过改变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/?kSAD解决方法 使用undefined解决params参数可以传递也可不传递空字符串
this.$router.push({name: search, params: {keyWord: || undefined}, query: {k:this.keyWord.toUpperCase()}
})
此时的url路径为http://localhost:8080/search?kSAD4.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是一个promisepromise需要传递成功和失败两个参数我们的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) 所以引入了async 和await,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 使用nprogresssrc/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 mouseleaveleaveIndex h2 classall全部商品分类/h2!-- 三级联动菜单 --div classsortdiv classall-sort-list2divclassitemv-for(c1, index) in categoryList:keyc1.categoryId:class{ cur: index currentIndex } !-- 选中高亮 --h3 mouseenterchangeIndex(index) !-- 鼠标进入事件 --!-- …… --
/div
script
....
data() {return {//存储用户鼠标移上哪一个一级分类currentIndex: -1,};},
methods: {//鼠标进入一级分类修改响应式数据currentIndexchangeIndex(index) {this.currentIndex index;},//鼠标移出一级分类的事件回调leaveIndex() {this.currentIndex -1;},},
....
/script7. 卡顿现象
场景 在上面讲到的事件委派中我们为三级联动菜单为每个一级分类添加了鼠标进入事件在用户使用过程中会出现以下两种情况。 正常情况 (用户慢慢的操作) :鼠标进入 每一个一级分类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 三级联动菜单路由跳转 如上图所示三级标签列表有很多每一个标签都是一个页面链接我们要实现通过点击表现进行路由跳转。 路由跳转有两种方法导航式路由编程式路由。 导航式路由我们有多少个a标签就会生成多少个router-link标签这样当我们频繁操作时会出现卡顿现象。 卡顿原因 router-link是一个组件当服务器的数据返回之后循环出很多的router-link组件【创建组件实例–虚拟DOM】如果有1000个router-link在创建组件实例时是非常耗内存的因此会出现卡顿现象编程式路由我们是通过触发点击事件实现路由跳转。同理有多少个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 classall-sort-list2 clickgoSearch mouseleaveleaveIndexdiv classitem v-for(c1,index) in categoryList v-showindex!16 :keyc1.categoryId :class{cur:currentIndexindex}h3 mouseenterchangeIndex(index) a :data-categoryNamec1.categoryName :data-category1Idc1.categoryId {{c1.categoryName}}/a/h3div classitem-list clearfix :style{display:currentIndexindex?block:none}div classsubitem v-for(c2,index) in c1.categoryChild :keyc2.categoryIddl classforedta :data-categoryNamec2.categoryName :data-category2Idc2.categoryId{{c2.categoryName}}/a/dtddem v-for(c3,index) in c2.categoryChild :keyc3.categoryIda :data-categoryNamec2.categoryName :data-category3Idc3.categoryId{{c3.categoryName}}/a/em
/dd/dl/div/div/div/div注意event是系统属性所以我们只需要在函数定义的时候作为参数传入在函数使用的时候不需要传入该参数。
//函数使用
div classall-sort-list2 clickgoSearch mouseleaveleaveIndex
//函数定义
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使用步骤
在项目当中src文件夹中创建mock文件夹第二步准备JSON数据(mock 文件夹中创建相应的JSON文件) ----格式化一下别留有空格(跑不起来的)mock数据需要的图片放置到public文件夹中public文件夹在打包的时候会把相应的资源原封不动打包到dist文件夹创建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/mockServer9.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)},请求实际是在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
}轮播图组件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插件 官网中给出了代码实例 做一个简要总结
安装npm i swiper5在需要使用轮播图的组件内导入swpier和它的css样式
import Swiper from swiper
//引入swiper的样式如果用到的轮播图的地方多可以在main.js引入样式
import swiper/css/swiper.css在组件中创建swiper需要的dom标签html代码参考官网代码 ListContainer/index.vue
!--banner轮播--
div classswiper-container idmySwiperdiv classswiper-wrapperdiv classswiper-slide v-for(carouse, index) in bannerList :keycarouse.idimg :srccarouse.imgUrl //div/div!-- 如果需要分页器 --div classswiper-pagination/div!-- 如果需要导航按钮 --div classswiper-button-prev/divdiv classswiper-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创建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 事件循环
方法三最完美的方案——watchnectTick
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 classswiper-container idmySwiper refmySwiper通过ref属性值获取DOM
let mySwiper new Swiper(this.$refs.mySwiper,{...})ListContainer/index.vue完整代码
!--banner轮播--
div classswiper-container idmySwiper refmySwiperdiv classswiper-wrapperdiv classswiper-slide v-for(carouse, index) in bannerList :keycarouse.idimg :srccarouse.imgUrl //div/div!-- 如果需要分页器 --div classswiper-pagination/div!-- 如果需要导航按钮 --div classswiper-button-prev/divdiv classswiper-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,},});});},},},}
/script10.2 父传子数据轮播图Swiper的使用
假设一个场景在父组件home请求了一个数据子组件用props接受该数据floor该数据有轮播图的数据。
父组件home/index.vue
template
div
//...
!-- 父组件通过自定义属性list给子组件传递数据--Floor v-forfloor in floorList :keyfloor.id :listfloor/
/div
/template子组件home/floor/index.vue
template!--楼层--div classfloordiv classswiper-container idfloor1Swiper reffloor1Swiperdiv classswiper-wrapperdiv classswiper-slide v-forcarouse in floor.carouselList :keycarouse.idimg :srccarouse.imgUrl //div/div!-- 如果需要分页器 --div classswiper-pagination/div!-- 如果需要导航按钮 --div classswiper-button-prev/divdiv classswiper-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
templatediv classswiper-container refswiper idfloor1Swiperdiv classswiper-wrapperdiv classswiper-slide v-for(carouse,index) in carouselList :keycarouse.idimg :srccarouse.imgUrl/div/div!-- 如果需要分页器 --div classswiper-pagination/div!-- 如果需要导航按钮 --div classswiper-button-prev/divdiv classswiper-button-next/div/div
/templatescript
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/stylemain.js注册全局组件
//轮播图组件——全局组件
import Carousel from /components/Carousel
//全局注册,第一个参数组件名字第二参数是哪个组件
Vue.component(Carousel.name, Carousel)Floor组件引用Carousel组件
!-- 轮播图 --
Carousel :carouselListfloor.carouselList /我们还记得在首页上方我们的ListContainer组件也使用了轮播图同样我们替换为我们的公共组件。 ListContainer组件引用Carousel组件
Carousel :carouselListbannerList/注意 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 vuexexport default {name: Search,computed:{//使用mapGetters参数是一个数组数组的元素对应getters中的函数名...mapGetters([goodsList])}}
/script12. 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 classfl sui-tagli classwith-x v-ifsearchParams.categoryName{{searchParams.categoryName}}i clickremoveCateGoryName×/i/li
/ulscript
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})}}
}
/script14.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)2search组件使用$bus通信第一个参数可以理解为为通信的暗号还可以有第二个参数用于传递数据我们这里只是用于通知header组件进行相应操作所以没有设置第二个参数。 !--面包屑--
ul classfl sui-tag!-- 分类名的面包屑 --li classwith-x v-ifsearchParams.categoryName{{searchParams.categoryName}}i clickremoveCateGoryName×/i/li!-- 关键字的面包屑 --li classwith-x v-ifsearchParams.keyWord{{searchParams.keyWord}}i clickremoveKeyWord×/i/li
/ulscript
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})}
}
/script3header组件接受$bus通信 注意组件挂载时就监听clearKeyWord事件
mounted() {// 组件挂载时就监听clear事件clear事件在search模块中定义// 当删除关键字面包屑时触发该事件同时header的输入框绑定的keyword要删除this.$bus.$on(clearKeyWord,(){this.keyWord })}
**问题**在删除面包屑时会发送两次请求 **原因**我们前面写到了用watch监听路由变化的代码在删除面包屑时除了要删除面包屑还要修改路由信息所以watch监听到了路由信息所以只需要将removeKeyWord和removeCateGoryName方法里的this.getData()删除即可
Search/index.vue的部分代码
import SearchSelector from ./SearchSelector/SearchSelectorimport {mapGetters} from vuexexport 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 classlogo-listli v-fortrademark in trademarkList :keytrademark.tmId clicktrademarkHandler(trademark){{trademark.tmName}}/li/ul
!-- ... --
ul classtype-listli v-for(attrValue,index) in attr.attrValueList :keyindex clickattrsHandler(attr.attrId, attr.attrName, attrValue)a href#{{attrValue}}/a/li
/ulscript
import {mapGetters} from vuexexport 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)}}}
/scriptSearch/index.vue
ul classfl sui-tag!-- 分类的面包屑 --li classwith-x v-ifsearchParams.categoryName{{searchParams.categoryName}}i clickremoveCateGoryName×/i/li!-- 关键字的面包屑 --li classwith-x v-ifsearchParams.keyWord{{searchParams.keyWord}}i clickremoveKeyWord×/i/li!-- 品牌的面包屑 --li classwith-x v-ifsearchParams.trademark{{searchParams.trademark.split(:)[1]}}i clickremoveTrademark×/i/li
!-- 售卖属性面包屑 --li classwith-x v-for(attr,index) in searchParams.props :keyindex{{attr.split(:)[1]}}i clickremoveAttr(index)×/i/li
/ul
/div!--selector--
SearchSelector trademarkInfoEventgetTrademark /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属性值为字符串例如‘1asc’、‘2desc’。1代表综合2代表价格asc代表升序desc代表降序。
我们的升降序是通过箭头图标来辨别的如图所示
ul classsui-navli :class{ active: isActive1 } clickchangeOrder(1)a href#span综合/spanem classfs-downi classarrow/i/em/a/li!-- ..... --li :class{ active: isActive2 } clickchangeOrder(2)a href#span价格/spanem :classgetClass()i classarrow-top/ii classarrow-bottom/i/em/a/li
/ulscript
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(opacity50);-moz-opacity: 0.5;opacity: 0.5;}}.fs-down {.arrow-top {filter: alpha(opacity50);-moz-opacity: 0.5;opacity: 0.5;}}
/style16. 手写分页器
实际开发中是不会手写的一般都会用一些开源库封装好的分页比如element ui。但是这个知识还是值得学习一下的。 核心属性 pageNo当前页码、pageSize(每一页展示多少条数据)、total共有多少条数据、continues连续展示的页码 核心逻辑是获取连续页码的起始页码和末尾页码通过计算属性获得。计算属性如果想返回多个数值可以通过对象形式返回 分页器注册为一个全局组件核心属性是通过父组件传递给分页器组件计算连续页码和总页数 src/components/Pagination/index.vue
templatediv classfr pagediv classsui-pagination clearfixul classp-numli :class{prev:true ,disabled: pageNo 1} clickgoToPageHandler(pageNo-1)a href#i lt; /iem上一页/em/a/lili v-ifstartNumAndEnd.start 1 clickgoToPageHandler(1)a href#1/a/lili classdotted v-ifstartNumAndEnd.start 2span.../span/lili:class{active: pageNo page}v-for(page, index) in startNumAndEnd.end :keyindex v-ifpage startNumAndEnd.start clickgoToPageHandler(page) a href#{{ page }}/a/lili classdotted v-ifstartNumAndEnd.end totalPage-1span.../span/lili v-ifstartNumAndEnd.end totalPage clickgoToPageHandler(totalPage)a href#{{totalPage}}/a/lili :class{next:true ,disabled: pageNo totalPage} clickgoToPageHandler(pageNo1)a href#em下一页/emigt;/i/a/li/uldiv classp-skipem共b{{ totalPage }}/b页nbsp;nbsp;到第/eminput typetext value1 classinput-txt v-modeltoPage /em页/ema href classbtn clickgoToPageHandler(toPage)确定/a/div/div/div
/templatescript
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;//规定连续页码数字5totalPage至少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 langless 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 :pageNosearchParams.pageNo :totaltotal :pageSizesearchParams.pageSize :continues5 getPageNoEventgetPageNo/
!-- ... --
/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: statestate.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 classconPoinspan v-showcategoryView.category1Name {{categoryView.category1Name}}/spanspan v-showcategoryView.category2Name {{categoryView.category2Name}}/spanspan v-showcategoryView.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 :skuImageListskuInfo.skuImageList /script
computed: {...mapGetters([categoryView,skuInfo]),}
/scriptzoom子组件
templatediv classspec-previewimg :srcskuImageList[0].imgUrl /div classevent/divdiv classbigimg :srcskuImageList[0].imgUrl //divdiv classmask/div/div
/templatescriptexport default {name: Zoom,props: [skuImageList],mounted(){console.log(this.skuImageList);}}
/script原因如上所诉只要在detail父组件的计算属性那里计算skuImageList设置|| [ ] 就可以解决问题
Zoom :skuImageListskuImageList /script
computed: {...mapGetters([categoryView,skuInfo]),//给子组件的数据skuImageList() {//如果服务器的数据没有回来skuInfo这个是空对象return this.skuInfo.skuImageList || []}}
/script解决上述的问题又出现一个问题输出一个空数组空数组没有imgUrl这个属性所以报错(假设网络故障的情况) 数组的第0项至少是个对象不能是undefined 因此在zoom子组件里同样要设置|| {} zoom子组件
templatediv classspec-previewimg :srcimgObj.imgUrl /div classevent/divdiv classbigimg :srcimgObj.imgUrl //divdiv classmask/div/div
/templatescriptexport default {name: Zoom,props: [skuImageList],mounted(){console.log(this.skuImageList);},computed: {imgObj() {return this.skuImageList[0] || {}}}}
/script问题解决以上的这些问题不会影响功能实现但需要搞清楚问题的缘由
19. 商品放大镜
templatediv classspec-previewimg :srcimgObj.imgUrl /div classevent mousemovehandler/divdiv classbigimg :srcimgObj.imgUrl refbig //divdiv classmask refmask/div/div
/templatescriptexport 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 leftpxmask.style.top top pxbig.style.left -2 * left pxbig.style.top -2 * top px}}}
/scriptstyle langless.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;}}
/style20. 加入购物车成功路由
点击加入购物车时会向后端发送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)//加入购物车以后服务器写入数据成功并未返回数据只返回code200代表此次操作成功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实现。
sessionStorage、localStorage概念 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 购物车商品数量更改
every函数使用 every遍历某个数组判断数组中的元素是否满足表达式全部为满足返回true否则返回false 例如判断底部勾选框是否全部勾选代码部分
div classselect-allinput classchooseAll typecheckbox :checkedisAllCheck /span全选/span/divscriptcomputed: {
//判断底部勾选框是否全部勾选isAllCheck() {//every遍历某个数组判断数组中的元素是否满足表达式全部为满足返回true否则返回falsereturn this.cartInfoList.every(item item.isChecked 1)}
}
/script购物车商品数量更改
添加到购物车和对已有物品进行数量改动使用的同一个api可以查看api文档。skuNum可以是一个增量减量或者一个整体的数量添加到购物车组件用到整体量购物车组件的商品个数修改用到增减量
使用click和change触发changeSkuNum事件修改商品数量都用到了同一个函数但是携带的参数个数不同 changeSkuNum函数有三个参数type区分操作disNum用于表示数量变化正负,cart商品的信息 li classcart-list-con5a hrefjavascript:void(0) classmins clickchangeSkuNum(minus,-1,cartInfo)-/ainput autocompleteoff typetext :valuecartInfo.skuNum changechangeSkuNum(change,$event.target.value,cartInfo) minnum1 classitxta hrefjavascript:void(0) classplus clickchangeSkuNum(add,1,cartInfo)/a/liscript
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设置disNum0表示不增不减原封不动disNum cart.skuNum 1 ? -1 : 0;break;case change://如果用户输入的文本非法if (isNaN(disNum) || disNum 1) {disNum 0; //disNum0表示不增不减} 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 : 0try {await this.$store.dispatch(updateChecked, {skuId, isChecked});//修改成功刷新数据发请求this.getData()} catch (error) {alert(error.message);}},
}21.4 购物车删除选中的全部商品和全选商品
删除选中的全部商品 由于后台只提供了删除单个商品的接口所以要删除多个商品时只能多次调用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)}},全选商品 修改商品的全部状态和批量删除的原理相同 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 : 0try {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注册登录 文章转载自: http://www.morning.jqzns.cn.gov.cn.jqzns.cn http://www.morning.dfbeer.com.gov.cn.dfbeer.com http://www.morning.lmdfj.cn.gov.cn.lmdfj.cn http://www.morning.mlcnh.cn.gov.cn.mlcnh.cn http://www.morning.qrzwj.cn.gov.cn.qrzwj.cn http://www.morning.nuejun.com.gov.cn.nuejun.com http://www.morning.trjr.cn.gov.cn.trjr.cn http://www.morning.mcbqq.cn.gov.cn.mcbqq.cn http://www.morning.xxhc.cn.gov.cn.xxhc.cn http://www.morning.mmtjk.cn.gov.cn.mmtjk.cn http://www.morning.nmngg.cn.gov.cn.nmngg.cn http://www.morning.gczzm.cn.gov.cn.gczzm.cn http://www.morning.yhywr.cn.gov.cn.yhywr.cn http://www.morning.wfjrl.cn.gov.cn.wfjrl.cn http://www.morning.wmpw.cn.gov.cn.wmpw.cn http://www.morning.ntwxt.cn.gov.cn.ntwxt.cn http://www.morning.ylph.cn.gov.cn.ylph.cn http://www.morning.nydgg.cn.gov.cn.nydgg.cn http://www.morning.xpqsk.cn.gov.cn.xpqsk.cn http://www.morning.pphgl.cn.gov.cn.pphgl.cn http://www.morning.brrxz.cn.gov.cn.brrxz.cn http://www.morning.bttph.cn.gov.cn.bttph.cn http://www.morning.xpfwr.cn.gov.cn.xpfwr.cn http://www.morning.pqqxc.cn.gov.cn.pqqxc.cn http://www.morning.bxnrx.cn.gov.cn.bxnrx.cn http://www.morning.xtrnx.cn.gov.cn.xtrnx.cn http://www.morning.bmyrl.cn.gov.cn.bmyrl.cn http://www.morning.yznsx.cn.gov.cn.yznsx.cn http://www.morning.mhbcy.cn.gov.cn.mhbcy.cn http://www.morning.dmlgq.cn.gov.cn.dmlgq.cn http://www.morning.rdkqt.cn.gov.cn.rdkqt.cn http://www.morning.xlmgq.cn.gov.cn.xlmgq.cn http://www.morning.rxlck.cn.gov.cn.rxlck.cn http://www.morning.mjpgl.cn.gov.cn.mjpgl.cn http://www.morning.27asw.cn.gov.cn.27asw.cn http://www.morning.fznj.cn.gov.cn.fznj.cn http://www.morning.hkshy.cn.gov.cn.hkshy.cn http://www.morning.dtrz.cn.gov.cn.dtrz.cn http://www.morning.tfrlj.cn.gov.cn.tfrlj.cn http://www.morning.fhjnh.cn.gov.cn.fhjnh.cn http://www.morning.mhcft.cn.gov.cn.mhcft.cn http://www.morning.zlfxp.cn.gov.cn.zlfxp.cn http://www.morning.mrckk.cn.gov.cn.mrckk.cn http://www.morning.fqtdz.cn.gov.cn.fqtdz.cn http://www.morning.gcdzp.cn.gov.cn.gcdzp.cn http://www.morning.tytly.cn.gov.cn.tytly.cn http://www.morning.nlwrg.cn.gov.cn.nlwrg.cn http://www.morning.rrwft.cn.gov.cn.rrwft.cn http://www.morning.zpkfb.cn.gov.cn.zpkfb.cn http://www.morning.tbcfj.cn.gov.cn.tbcfj.cn http://www.morning.rrpsw.cn.gov.cn.rrpsw.cn http://www.morning.lpgw.cn.gov.cn.lpgw.cn http://www.morning.txzqf.cn.gov.cn.txzqf.cn http://www.morning.xkyqq.cn.gov.cn.xkyqq.cn http://www.morning.yrnll.cn.gov.cn.yrnll.cn http://www.morning.wrbnh.cn.gov.cn.wrbnh.cn http://www.morning.pzbjy.cn.gov.cn.pzbjy.cn http://www.morning.bpkqd.cn.gov.cn.bpkqd.cn http://www.morning.xxgfl.cn.gov.cn.xxgfl.cn http://www.morning.rdnpg.cn.gov.cn.rdnpg.cn http://www.morning.sdamsm.com.gov.cn.sdamsm.com http://www.morning.xwlmr.cn.gov.cn.xwlmr.cn http://www.morning.lxqkt.cn.gov.cn.lxqkt.cn http://www.morning.rdbj.cn.gov.cn.rdbj.cn http://www.morning.ryysc.cn.gov.cn.ryysc.cn http://www.morning.zxfr.cn.gov.cn.zxfr.cn http://www.morning.rmjxp.cn.gov.cn.rmjxp.cn http://www.morning.glwyn.cn.gov.cn.glwyn.cn http://www.morning.fkmyq.cn.gov.cn.fkmyq.cn http://www.morning.jjxxm.cn.gov.cn.jjxxm.cn http://www.morning.yxplz.cn.gov.cn.yxplz.cn http://www.morning.dyzbt.cn.gov.cn.dyzbt.cn http://www.morning.skksz.cn.gov.cn.skksz.cn http://www.morning.mhcys.cn.gov.cn.mhcys.cn http://www.morning.tmnyj.cn.gov.cn.tmnyj.cn http://www.morning.pkfpl.cn.gov.cn.pkfpl.cn http://www.morning.nthyjf.com.gov.cn.nthyjf.com http://www.morning.wdxr.cn.gov.cn.wdxr.cn http://www.morning.blqgc.cn.gov.cn.blqgc.cn http://www.morning.qsdnt.cn.gov.cn.qsdnt.cn