前端Vue核心

开发一个前端模块可以概括为以下几个步骤:

(1)写 静态页面、拆分为静态组件;

(2)发送请求(API)

(3)vuex (actions、mutations、state三连操作)

(4)组件获取仓库数据,动态展示

1.pges文件夹

创建pages文件夹,并创建路由组件

1.1 在src下创建router文件夹,并创建index.js进行路由配置,最终在main.js中引入注册

1.2 总结

路由组件和非路由组件的区别 :

  • 非路由组件放在components中(全局组件),路由组件放在pages或views文件夹中
  • 非路由组件通过标签的形式使用,传参,收参都在标签里写,而路由组件应该通过路由跳转使用。
  • 在main.js中注册路由元组件,所有的路由和非路由组件身上都会拥有$router、$route属性,记得npm install vue-router@+版本号,具体看使用的是vue的哪个版本。
  • $router:一般进行编程式导航进行路由跳转,$route:一般获取路由信息(name path params等)

1.3 路由跳转的两种方式

  • 声明式导航router-link标签,可以把router-link理解为一个a标签,它可以加class修饰
  • 编程式导航:声明式导航能做的编程式都能做,而且还可以处理一些其他业务

2.footer组件显示与隐藏

  • footer在登录注册页面是不存在的,所以要隐藏,v-if或者v-show
  • 这里使用v-if渲染条件会频繁的操作dom元素消耗性能,使用v-show更好,因为v-show只是通过样式将元素显示或隐藏
  • 配置路由的时候,可以给路由元信息meta
  • 在路由的元信息中定义show属性,用来给footer组件标签里的v-show赋值,来判断footer组件的显示与隐藏,在APP根组件中
  • 还有header组件,个人认为在登陆预注册页面的时候也不能显示,因为在没有登录与注册账号的时候不应该显示搜索框

3.路由传参

  3.1 query、params参数

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

  3.2 几道面试题:

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

答:路由跳转传参的时候,对象的写法可以是name、path形式,但是需要注意的是,path这种写法不能与params参数一起使用

2:如何指定params参数可传可不传?

答:如果配置路由的时候,我们配置并且占位了(params参数),但是路由跳转的时候没有传递params参数,这个时候路径会出现问题

正常的路径应该为:http://localhost:8080/#/search?k=QWE

出现问题后的路径变为:http://localhost:8080/#/?k=QWE

如何指定params参数可以传递、或者不传递,在配置路由的时候,在占位的后面加上一个问号?【params可以传递或者不传递】

3:params参数可以传递也可以不传递参数,但是如果传递是空串,如何解决

答:传递空串,路径也会出现问题,使用undefined解决:params参数可以传递、不传递(空的字符串)

this.$router.push({name:'search',params:{keyword:''||undefined,query:{k:this.keyword.toUpperCase()}}})

4:路由组件能不能传递props数据?

答:可以传递,三种写法:

第一种布尔值写法,但是只能传递params数据。props:true,

第二种写法对象写法,额外的给路由组件传递一些props数据。props:{a:1,b:2}

第三种函数写法:可以传递params参数,query参数,通过props传递给路由组件

props:($route)=>({keyword:$route.params.keyword,k:$route.query.k}),相应地在接受数据的组件中写上props:['keyword','k']

   3.3 路由传参方法

//第一种方法:字符串形式
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: {keyword: this.keyword},query: {k: this.keyword.toUpperCase()}})

4.多次执行相同的push问题

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

--路由跳转有两种形式:声明式导航、编程式导航

--声明式导航 没有这类问题,因为vue-router底层已经处理好了

4.1为什么编程式导航进行路由怕频繁跳转的时候,就会有这种警告错误呢?

vue-router@3,引入了promise,编程式导航push|repalce函数返回的结果是一个promise对象,所以我们可以通过给push方法的对象参数后面再传递两个参数,成功的回调、失败的回调,可以解决这个问题

4.2通过底层的代码,可以实现解决问题

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

但这种写法,治标不治本,将来在别的组件当中push|replace,编程式导航还是有类似错误。

push方法,是VueRouter实例原型上的方法

VueRouter.prototype.push=function(){}

所以解决这个问题的根本方法 ,就是重写原型上的push|replace方法

this.$router属性,当前这个属性,属性值VueRouter类的一个实例对象,当再入口文件注册路由的时候,给组件实例添加$router|$route属性。

重写push和repalce方法,第一个参数:告诉push方法,你往哪里跳转(传递哪些参数)

call||apply区别

相同点:都可以调用函数一次,都可以修改函数的this指向,即函数的上下文

不同点:call与 apply传递参数:call传递参数用逗号隔开,apply方法执行,传递数组。

5.定义全局组件

我们的三级联动组件时全局组件,全局的配置都需要在main.js中配置

//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);

在Home组件中使用该全局组件

<template>
<div>
<!--  三级联动全局组件已经注册为全局组件,因此不需要引入--><TypeNav/>
</div>
</template>

全局组件可以在任一页面中直接使用,不需要导入声明
下面全部商品分类就是三级联动组件

6.代码改变时实现页面自动刷新

根目录下vue.config.js文件设置

module.exports = {//关闭eslintlintOnSave: false,devServer: {// true 则热更新,false 则手动刷新,默认值为 trueinline: true,// development server port 8000port: 8001,}
}

注意:修改完该配置文件后,要重启一下项目

7.Home首页其他组件

home文件夹下的index.vue

<template>
<div>
<!--  三级联动全局组件已经注册为全局组件,因此不需要引入--><TypeNav/>
<!--  轮播图列表--><ListContainer/>
<!--  今日推荐--><Recommend/>
<!--  商品排行--><Rank/>
<!--  猜你喜欢--><Like/>
<!-- 楼层 --><Floor/><Floor/>
<!--  商标--><Brand/>
</div>
</template><script>
import ListContainer from './ListContainer'
import Recommend from './Recommend'
import Rank from './Rank'
import Like from './Like'
import Floor from './Floor'
import Brand from './Brand'
export default {name: "index",components: {ListContainer,Recommend,Rank,Like,Floor,Brand,}
}
</script><style scoped></style>

8.封装axios(axios的二次封装)

axios中文文档,包含详细信息(使用说明 · Axios 中文说明 · 看云)

在根目录src下创建api文件夹,创建request.js文件。

内容如下,当前文件代码还比较少,后续有需求可以增添内容。(要用npm下载axios)

import axios from 'axios'//对axios二次封装
const requests = axios.create({baseUrl:'/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'))
})
//4、对外暴露
export default requests;

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

在根目录下的vue.config.js中配置poxy代理服务器来解决跨域问题,浏览器之间存在跨域问题,服务器之间不存在跨域问题,我们在封装axios的时候,已经设置了baseURL为api,所以所有请求都会携带api,所以 我们在vue.config.js中配置代理服务器 如下:

module.exports = {//关闭eslintlintOnSave: false,devServer: {// true 则热更新,false 则手动刷新,默认值为 trueinline: false,// development server port 8000port: 8001,//代理服务器解决跨域proxy: {'/api': {//提供数据的服务器地址target: 'http://39.98.123.211',//服务器地址在前,api+后面的接口地址在后}},}
}

10.请求接口统一封装

在文件夹api中创建index.js文件,用域封装所有请求

将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,这样当我们接口比较多时 ,如果需要修改只需要修改该文件即可。

//当前模块,API进行统一管理,即对请求接口统一管理
import requests from "@/api/request";//首页三级分类接口
export const reqCateGoryList = () => {return  requests({url: '/product/getBaseCategoryList',method: 'GET'})
}

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

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

11.nprogress进度条插件

打开一个页面是,王王会伴随着一些请求,并且会在页面上方出现进度条 ,它的原理是在我们 发起请求的时候开启进度条,在请求成功后关闭进度条,所以只需要在request.js中进行配置 。

如下图所示,我们页面加载时发起了一个请求,此时页面上方出现蓝色进度条

对应的request设置,在request.js中的请求拦截器和相应拦截器中添加相应配置:

请求拦截器里,nprogress.start();响应拦截器里,nprogress.end();注意要在文件的开始引入

import  nprogress from 'nprogress';//引入进度条

import "nprogress/nprogress.css";//引入进度条样式

12.手动引入vuex,并且建立仓库文件夹

首先确保安装了vuex,根目录创建store文件夹,文件夹下创建index.js,内容如下:

import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex)//对外暴露store的一个实例
export default new Vuex.Store({state:{},mutations:{},actions:{},})

如果想要使用vuex,还要再main.js中引入
main.js:
(1) 引入文件
(2) 注册store
但凡是在main.js中的Vue实例中注册的实体,在所有的组件中都会有(this.$.实体名)属性;

import store from './store'
new Vue({render: h => h(App),//注册路由,此时组件中都会拥有$router $route属性router,//注册store,此时组件中都会拥有$storestore
}).$mount('#app')

13.async、await使用

如果我们没有封装api请求,而是直接调用axios,就不需要使用async、await.

案例:我们将一个axios请求封装为了函数,我们在下面代码中调用了该函数:

import {reqCateGoryList} from '@/api'
export default {actions:{categoryList(){let result =  reqCateGoryList()console.log(result)}}
}

浏览器结果:会返回一个promise对象:

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

actions:{categoryList(){let result =  reqCateGoryList().then(res=>{console.log("res")console.log(res)return res})console.log("result")console.log(result)}}

结果:

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

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

   async categoryList(){let result = await reqCateGoryList()console.log("result")console.log(result)}

结果:

14.vuex(辅助函数的使用)

state、actions、mutations、getters的辅助函数使用,当多次访问store中的上述属性时,要使用个属性的辅助函数,可以减少代码量。
在使用上面的函数时,如果需要传递多个参数,需要把多个参数组合为一个对象传入(vuex是不允许多个参数分开传递的)

async addOrUpdateShopCart({commit},{skuId,skuNum}){let result = await reqAddOrUpdateShopCart(skuId,skuNum)console.log(result)if(result.data ===  200){}

注意:使用actions时,函数的第一个参数,必须是{commit},即使不涉及到mutations操作,也必须加上该参数,否则会报错。

15.lodash插件防抖和节流(手写函数的防抖与节流见 面试题1.md)

在进行窗口的resize、scroll,输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时我们可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率,同时又不影响实际效果。
安装lodash插件,该插件提供了防抖和节流的函数,我们可以引入js文件,直接调用。当然也可以自己写防抖和节流的函数

防抖:用户操作很频繁,但是只执行一次,减少业务负担。
节流:用户操作很频繁,但是把频繁的操作变为少量的操作,使浏览器有充分时间解析代码
防抖和节流简述

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

import {throttle} from 'lodash'methods: {//鼠标进入修改响应元素的背景颜色//采用键值对形式创建函数,将changeIndex定义为节流函数,该函数触发很频繁时,设置50ms才会执行一次changeIndex: throttle(function (index){this.currentIndex = index},50),//鼠标移除触发时间leaveIndex(){this.currentIndex = -1}}

16.编程式导航+事件委派实现路由跳转

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

对于导航式路由,我们有多少个a标签就会生成多少个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属性获取节点的属性信息。

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

//函数使用
<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex">
//函数定义
goSearch(event){console.log(event.target)}

 对应的goSearrch函数:

goSearch(event){let element = event.target//html中会把大写转为小写//获取目前鼠标点击标签的categoryname,category1id,category2id,category3id,// 通过四个属性是否存在来判断是否为a标签,以及属于哪一个等级的a标签let {categoryname,category1id,category2id,category3id} = element.dataset//categoryname存在,表示为a标签if(categoryname){//category1id一级a标签//整理路由跳转的参数let location = {name:'Search'}//跳转路由namelet query = {categoryName:categoryname}//路由参数if(category1id){query.category1Id = category1id}else if(category2id){//category2id二级a标签query.category2Id = category2id}else if(category3id){//category3id三级a标签query.category3Id = category3id}//整理完参数location.query = query//路由跳转this.$router.push(location)}},

17.Vue路由销毁问题:

Vue在路由切换的时候会销毁旧路由。

我们在三级列表全局组件TypeNav中的mounted进行了请求一次商品分类列表数据。

由于Vue在路由切换的时候会销毁路由,当我们再次使用三级列表全局组件的时候还会发送一次请求。

如下图所示:当我们在包含三级列别全局组件的不同组件之间相互切换时,都会进行换一次信息请求。

由于信息都是一样的,出于性能的考虑我们希望该数据只请求一次,所以我们把这次请求放在App.vue的mounted中。(根组件App.vue的mounted只会执行一次)

注意:虽然main.js也是只执行一次,但是不可以放在main.js中,因为只有组件的身上才会有$store的属性。

18.mock插件使用

mock用来拦截前端ajax请求,返回我们自定义的数据用于测试前端接口。

将不同的数据类型封装为不同的json文件,创建mockServer.js文件

banner、floor分别为轮播图和页面底部的假数据。

mockServer.js文件

import Mock  from 'mockjs'
//webpack默认对外暴露:json、图片
import banner from './banner.json'
import floor from './floor.json'//mock数据:第一个参数请求地址、第二个参:请求数据
Mock.mock("/mock/banner",{code:200,data:banner})
Mock.mock("/mock/floor",{code:200,data:floor})
//记得要在main.js中引入一下
//import ''@/mock/mockServer

19.vuex数据存储与使用

我们会把公共的数据放在store里,然后使用时去store仓库里取。

以我们首页的轮播图数据为例。

1.在轮播图组件ListContainer.vue组件加载完毕后发起轮播图数据请求。

 mounted() {this.$store.dispatch("getBannerList")},

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

actions:{//获取首页轮播图数据async getBannerList({commit}){let result = await reqGetBannerList()if(result.code ===  200){commit("BANNERLIST",result.data)}}}

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

/唯一修改state的部分mutations:{BANNERLIST(state,bannerList){state.bannerList = bannerList}},

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

 <script>
import {mapState} from "vuex";
export default {name: "index",//主键挂载完毕,请求轮播图图片mounted() {this.$store.dispatch("getBannerList")},computed:{...mapState({bannerList: (state => state.home.bannerList)})}
}
</script>

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

20.swiper插件实现轮播图(手写轮播图实现见面试题2)

首先做一个简要的总结:

  • 安装swiper
  • 在需要使用轮播图的组件内导入swiper和它的css样式
  • 在组件中创建swiper需要的dom标签(html代码,参考官网代码)
  • 创建swiper实例

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

通过ref属性值获取DOM

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

 <!--banner轮播--><div class="swiper-container" id="mySwiper" ref="cur"><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>
<script>
//引入Swiper
import Swiper from 'swiper'
//引入Swiper样式
import 'swiper/css/swiper.css'
</script>

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

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

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',},})},

解决方法一:等我们的数据请求完毕后再创建swiper实例。只需要加一个1000ms时间延迟再创建swiper实例.。将上面代码改为:

mounted() {this.$store.dispatch("getBannerList")setTimeout(()=>{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',},})},1000)},

方法一肯定不是最好的,但是我们开发的第一要义就是实现功能,之后再完善。

解决方法二:我们可以使用watch监听bannerList轮播图列表属性,因为bannerList初始值为空,当它有数据时,我们就可以创建swiper对象

watch:{bannerList(newValue,oldValue){let mySwiper = new Swiper(this.$refs.cur,{pagination:{el: '.swiper-pagination',clickable: true,},// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev',},// 如果需要滚动条scrollbar: {el: '.swiper-scrollbar',},})}}

即使这样也还是无法实现轮播图,原因是,我们轮播图的html中有v-for的循环,我们是通过v-for遍历bannerList中的图片数据,然后展示。我们的watch只能保证在bannerList变化时创建swiper对象,但是并不能保证此时v-for已经执行完了。假如watch先监听到bannerList数据变化,执行回调函数创建了swiper对象,之后v-for才执行,这样也是无法渲染轮播图图片(因为swiper对象生效的前提是html即dom结构已经渲染好了)。

完美解决方案:使用watch+this.$nextTick()
官方介绍:this. $nextTick它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for)。
个人理解:无非是等我们页面中的结构都有了再去执行回调函数

完整代码:

<template><!--列表--><div class="list-container"><div class="sortList clearfix"><div class="center"><!--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></div></div></div>
</template>
<script>
//引入Swiper
import Swiper from 'swiper'
//引入Swiper样式
import 'swiper/css/swiper.css'import {mapState} from "vuex";export default {name: "index",//主键挂载完毕,ajax请求轮播图图片mounted() {this.$store.dispatch("getBannerList")},computed:{...mapState({//从仓库中获取轮播图数据bannerList: (state) => {return state.home.bannerList}})},watch:{bannerList(newValue,oldValue){//this.$nextTick()使用this.$nextTick(()=>{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',},})})}}
}
</script>

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

21.将轮播图模块提取为公共组件

在components文件夹下新建一个Carousel文件夹,并通过watch属性,写swiper轮播图那一套,既然是全局组件,就需要在main.js中注册并引入,引入swiper和swiper样式

Vue.component(Carousel.name,Carousel);

......

import "swiper/css/swiper.css"

22.getters使用 :

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

23.Object.assign实现对象拷贝(对象的深浅拷贝参考面试题1.md)

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() 。

改进后的办法,利用JSON.stringfy和JSON.parse方法进行拷贝,但是这种方法也有一定的缺陷,当数据类型为function或者数据值为undefined情况下无法复制。

针对深拷贝,需要使用其他办法,因为 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}}

学习了上述的拷贝方法,所以要利用起去实现路由信息变化时实现动态搜索。

因为当每次进行新的搜索时,我们的query和params参数中的部分内容肯定会发生改变 ,而且 这两个参数是路由的属性。我们可以通过监听路由信息的变化来动态发起搜索请求。

search组件中的watch部分的代码:

watch:{$route(newValue,oldValue){Object.assign(this.searchParams,this.$route.query,this.$route.params)this.searchInfo()//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数//所以每次请求结束后将相应参数制空this.searchParams.category1Id = '';this.searchParams.category2Id = '';this.searchParams.category3Id = '';this.$route.params.keyword = '';}},

24.面包屑相关操作:

本次项目的面包屑操作主要就是两个删除逻辑。

分为:

当分类属性(query)删除时删除面包屑同时修改路由信息

当搜索关键字(params)删除时删除面包屑、修改路由信息、同时删除输入框的关键字。

1.query删除时

因为此部分在面包屑中是通过categoryName展示的,所以删除时候应该将属性值置空或者undefined(undefined从页面效果上来说效果更佳),因为当把这些参数置空之后,要带着这是个参数重新向服务器发送请求,如果只是置空,空的字符串也会重新带给服务器一次,而undefined就不会携带无用的字符串给服务器。(前提是参数说明中是可有可无的)

可以通过路由再次跳转修改路由信息和url链接

//删除分类removeBread(){this.searchParams.categoryName = undefinedthis.$router.push({name:'Search',params:this.$route.params})},

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组件进行相应操作,所以没有设置第二个参数。

//删除搜索关键字removeBreadParams(){this.searchParams.keyword = undefined//通知兄弟组件header删除输入框的keyword关键字this.$bus.$emit("clear")this.$router.push({name:'Search',query:this.$route.query})},

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

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

25.SearchSelector子组件传参及面包屑操作(*重点子组件向父组件传参通信)

子组件SearchSelector中,首先在仓库中用mapGetters函数拿来trademarkList数据,然后找到相应的HTML结构中,首先v-for遍历,再绑定点击事件品牌处理函数(trademarkHandler(trademark))

<div class="value logos"><!-- 品牌的地方 --><ul class="logo-list"><li v-for="(trademark,index) in trademarkList" :key="trademark.tmId" @click="trademarkHandler(trademark)">{{trademark.tmName}}</li></ul>
</div>

传入trademark参数后,其父组件search下的index.vue需要子组件里的trademark参数来更新自己的trademark参数向服务器发送新的请求,所以需要子组件给父组件传递参数。

 methods: {//品牌的事件处理函数trademarkHandler(trademark) {this.$emit('trademarkInfo', trademark)},//平台售卖属性值的点击事件attrInfo(attr, attrvalue) {this.$emit('attrInfo', attr, attrvalue)}}

再子组件中this.$emit触发,带相应的参数,在父组件中,应该在子组件标签中绑定自定义事件,向子组件传递一个函数,这个函数就是子组件方法中触发的同名函数trademarkInfo

<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo"/>

相应的在父组件中的methods里写上tradeInfo方法,应该是将子组件传递来的trademark参数整理到searchParams参数中,然后再次向服务器发送请求。

//自定义事件回调trademarkInfo(trademark) {//整理品牌字段的参数this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`//再次发送请求获取search模块信息进行展示this.getdata();
},

注意这里整理的trademark应该和接口文档里要求的参数格式一样,才可以向服务器发送请求。

然后进行删除面包屑操作,和之前一样,也是在search组件中,添加一个面包屑结构

 <ul class="fl sui-tag"><!-- 分类的面包屑 --><li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName}}<i @click="removeCategoryName">x</i></li><!-- 关键字面包屑  --><li class="with-x" v-if="searchParams.keyword">{{searchParams.keyword}}<i @click="removeKeyword">x</i></li><!-- 品牌面包屑 --><li class="with-x" v-if="searchParams.trademark">{{searchParams.trademark.split(":")[1]}}<i @click="removeTrademark">x</i></li><!-- 品牌售卖的属性值展示 --><li class="with-x" v-for="(attrvalue,index) in searchParams.props" :key="index"> {{attrvalue.split(":")[1]}}<i @click="removeProp(index)">x</i></li></ul>

绑定removeTrademark事件,在methods里写上相关操作。

这里应该是,将searchParams中的trademark置为undefined,并重新发送请求

// 删除品牌信息removeTrademark() {this.searchParams.trademark = undefinedthis.getdata()},

因为这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化回影响路由地址变化。所以watch监听不到路由的改变所以需要手动向服务器发送一次请求。

26.商品排序操作

在html结构中,加上综合与价格两个li标签,里面分别添加升序与降序两种操作选择,并用v-show来控制显示与隐藏。

 data() {return {searchParams: {category1id: "",category2id: "",category3id: "",categoryName: "",keyword: "",//初始状态默认综合且 降序order: "1:desc",pageNo: 1,pageSize: 10,props: [],trademark: ""}};},

在data里定义如下参数,order参数里面控制综合or价格还是升序降序,初始默认值伟综合且降序

order:"1:desc"

然后再computed属性中分别计算出isOne、isTwo、isAsc、isDesc四个属性,和html中的v-show一起使用。

computed: {...mapGetters('search', ['goodsList']),isOne() {return this.searchParams.order.indexOf('1') != -1},isTwo() {return this.searchParams.order.indexOf('2') != -1},isAsc() {return this.searchParams.order.indexOf('asc') != -1},isDesc() {return this.searchParams.order.indexOf('desc') != -1},// 获取search模块展示产品一共多少数据...mapState({total: state => state.search.searchList.total})},

在两个li结构上绑定点击事件changeOrder函数

// 排序操作changeOrder(flag) {let originOrder = this.searchParams.orderlet originFlag = this.searchParams.order.split(":")[0]let originSort = this.searchParams.order.split(":")[1]let newOrder = ''
// 确定点击的一定是综合if (flag === originFlag) {newOrder = `${originFlag}:${originSort==="desc"?"asc":"desc"}`} else {newOrder = `${flag}:${"desc"}`}this.searchParams.order = newOrderthis.getdata()},

27.手写分页器(pagination)

实际开发中是不会手写的,一般都会用一些开源库封装好的分页,比如element-UI。

分页器核心属性:pageNo(当前页码)、pageSize、total、continues(连续展示的页码)

核心逻辑是获取连续页码的起始页和末尾页码,通过计算属性获得。(计算属性如果想返回多个数值,可以通过对象形式进行返回)

<template><div class="pagination"><button :disabled="pageNo===1" @click="$emit('getPageNo',pageNo-1)">上一页</button><button v-if="startNumEndNum.start>1" @click="$emit('getPageNo',1)" >1</button><button  v-if="startNumEndNum.start>2">···</button><button v-for="(page,index) in startNumEndNum.end" :key="index" v-if="page>=startNumEndNum.start" @click="$emit('getPageNo',page)":class="{active:pageNo===page}">{{page}}</button><button v-if="startNumEndNum.end<totalPage-1">···</button><button v-if="startNumEndNum.end<totalPage" @click="$emit('getPageNo',totalPage)">{{totalPage}}</button><button :disabled="pageNo===totalPage" @click="$emit('getPageNo',pageNo+1)">下一页</button><button style="margin-left: 30px">共{{total}}条</button><!-- <h1>{{startNumEndNum}}------{{pageNo}}</h1> --></div>
</template><script>export default {name: "Pagination",props: ['pageNo', 'pageSize', 'total', 'continues'],computed: {totalPage() {return Math.ceil(this.total / this.pageSize)},//计算出连续的页码与结束的数字[连续页面数字最少是五],至少五页startNumEndNum() {//   解构一下变量const {continues,pageNo,totalPage} = this//先定义两个变量let start = 0,end = 0;if (continues > totalPage) {//非正常现象start = 1;end = totalPage} else {//正常现象start = pageNo - parseInt(continues / 2)end = pageNo + parseInt(continues / 2)if (start < 1) {start = 1;end = continues;}if (end > totalPage) {end = totalPage,start = totalPage - continues + 1}}return {start,end}}}}
</script><style lang="less" scoped>.pagination {text-align: center;button {margin: 0 5px;background-color: #f4f4f5;color: #606266;outline: none;border-radius: 2px;padding: 0 4px;vertical-align: top;display: inline-block;font-size: 13px;min-width: 35.5px;height: 28px;line-height: 28px;cursor: pointer;box-sizing: border-box;text-align: center;border: 0;&[disabled] {color: #c0c4cc;cursor: not-allowed;}&.active {cursor: not-allowed;background-color: #409eff;color: #fff;}}}
</style>

本组件还涉及到子组件给父组件传递参数,用上一节复习到的内容自定义事件,这里直接在html结构里写$emit触发父组件给子组件传递过来的函数,并将当前的页码传入父组件search中,search根据子组件传来的当前页码数,放入searchParams参数中,重新向服务器发送请求得到新的商品列表信息。

28.滚动条行为

let router = new VueRouter({//配置路由routes,scrollBehavior(to, from, savePosition) {//代表路由跳转的滚动条在最上方return { y: 0 }}
})

在router路由文件夹下创建两个文件,一个用来配置路由信息,一个用来写各个路由组件的配置,在index.js文件中暴露一个router实例,在实例中配置路由跳转的滚动条的位置。

29.undefined细节

有时会出现如图所示的报错:

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

30.产品售卖属性值排他操作-裁剪

先搞清楚数据类型 ,然后进行排他操作:

changeActive(saleAttrvalue,arr){arr.forEach(item=>{item.isChecked =  '0';});saleAttrvalue.isChecked='1';}

31.放大镜操作(再学最后一遍,牢记步骤与方法)

放大镜在detail组件中 ,detail组件中包含两个子组件:zoom组件和imageList组件。

在imageList组件中再次只用轮播图,轮播图应该搭配watch监听属性,且有一个很重要的点,就是watch搭配nextTick钩子一起使用,可以保证数据一定ok,保证v-for解构已经完成,可以搭配 $nextTick一起使用。

先引入swiper,并且在main.js引入swiper的样式,然后复制过来swiper轮播图的html结构与实例。

watch: {// 监听数据,可以保证数据一定ok,要保证v-for解构已完成,可以搭配$nextTick钩子一起使用skuImageList: {immediate: true,handler(newValue, oldValue) {this.$nextTick(() => {new Swiper(this.$refs.swiperZoom, {// direction: 'vertical', // 垂直切换选项,删掉之后默认就是水平// loop: true, // 循环模式选项// 如果需要分页器// pagination: {//     el: '.swiper-pagination',//     clickable: true// },// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev',},// 显示几个图片设置slidesPerView: 3,slidesPerGroup: 2// 如果需要滚动条// scrollbar: {//     el: '.swiper-scrollbar',// },})});}}}

不用css来展示点谁谁高亮,用js来操作,所以给轮播图的图片添加一个动态类名,并且绑定一个点击事件changeCurrentIndex

<div class="swiper-slide" v-for="(slide,index) in skuImageList" :key="slide.id"><img :src="slide.imgUrl" :class="{active:currentIndex==index}" @click="changeCurrentIndex(index)">
</div>......methods: {changeCurrentIndex(index) {this.currentIndex = index//通知兄弟组件Zoom当前的索引值为几this.$bus.$emit('getIndex', this.currentIndex)}},

当点击轮播图的小图时,小图上方的大图也应该换成对应的指定的图片,所以我们需要将轮播图小图的索引值index告诉兄弟组件zoom下的index.vue进行操作,这叫涉及到了兄弟组件通信,所以用全局事件总线$bus,传过去一个currentIndex参数,并且在zoom组件下进行$on接收

 mounted() {// 全局事件总线获取兄弟组件ImageList传递过来的索引值this.$bus.$on('getIndex', (index) => {this.curIndex = index})},

接收了index之后,展示图片:

<template><div class="spec-preview"><img :src="skuImageList[curIndex].imgUrl" /><div class="event" @mousemove="handler"></div><div class="big" ><img :src="skuImageList[curIndex].imgUrl" ref="big"/></div><!-- 遮罩层 --><div class="mask" ref="mask"></div></div>
</template>

接下来就是放大镜的操作(**边界判断):

methods: {handler(event) {let mask = this.$refs.masklet big = this.$refs.biglet left = event.offsetX - mask.offsetWidth / 2let top = event.offsetY - mask.offsetHeight / 2// 约束范围if (left <= 0) left = 0if (left >= mask.offsetWidth) left = mask.offsetWidthif (top <= 0) top = 0if (top > mask.offsetHeight) top = mask.offsetHeight// 修改元素的left|top属性值mask.style.left = left + 'px'mask.style.top = top + 'px'big.style.left = -2 * left + 'px'big.style.top = -2 * top + 'px'}}

32.失焦事件

blur与change事件在绝大部分情况下表现都非常相似,输入结束后,离开输入框,会先后触发change与blur,唯有两点例外。
(1) 没有进行任何输入时,不会触发change。
在这种情况下,输入框并不会触发change事件,但一定会触发blur事件。在判断表单修改状态时,这种差异会非常有用,通过change事件能轻易地找到哪些字段发生了变更以及其值的变更轨迹。

(2)输入后值并没有发生变更。
这种情况是指,在没有失焦的情况下,在输入框内进行返回的删除与输入操作,但最终的值与原值一样,这种情况下,keydown、input、keyup、blur都会触发,但change依旧不会触发。

33.加入购物车成功路由

点击加入购物车之后,会向后端发送API请求,但是该请求的返回值中的data为null,所以我们只需要根据状态码code判断是否跳转到“加入购物车成功页面”

detail组件‘加入购物车’请求函数:

async addShopCar() {try{await  this.$store.dispatch("addOrUpdateShopCart", {skuId: this.$route.params.skuId,skuNum: this.skuNum});//一些简单的数据,比如skuNum通过query传过去//复杂的数据通过session存储,//sessionStorage、localStorage只能存储字符串        sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo))this.$router.push({name:'AddCartSuccess',query:{'skuNum':this.skuNum}})}catch (error){alert(error.message)}}

detail store对应代码

//将产品添加到购物车中async addOrUpdateShopCart({commit},{skuId,skuNum}){let result = await reqAddOrUpdateShopCart(skuId,skuNum)if(result.code === 200){return 'ok'}else{return Promise.reject(new Error('faile'))}}

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

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

async addShopCart() {//发请求--将产品加入到数据库(通知服务器)//服务器存储成功------进行路由跳转 利用 try catch(成功干什么......失败干什么.....)//失败:给用户进行提示try {await this.$store.dispatch('detail/addOrUpdateShopCart', {skuId: this.$route.params.skuId,skuNum: this.skuNum});//成功了所以进行路由跳转//一些简单的数据skuNum,通过query形式给路由组件传递过去//产品信息的数据【比较复杂:skuInfo】,通过会话存储(不持久化,会话结束数据就消失)essionStorage.setItem('GETINFO', JSON.stringify(this.skuInfo))this.$router.push({name: "addCartSuccess",uery: {skuNum: this.skuNum}})} catch (error) {alert(error.message)}// 当前如果想拿到请求成功还是失败的结果,需要用promise,此处的result就是promise包装的一个对象//此时成功result会打印ok,失败会打印fail}

34.购物车组件开发

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

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

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

创建utils工具包文件夹,创建生成uuid的js文件,对外暴露为函数(记得导入uuid => npm install uuid)。

生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储

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

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

const state = { goodInfo:{}, //游客身份 uuid_token: getUUID() }

在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';

35.购物车组件详细开发(重点,涉及到多种请求,以及页面渲染,还有各种数组上的方法 来进行增删改查操作)

import { reqCartList, reqDeleteCartById, reqUpdateCheckedById } from '@/api'
export default {namespaced: true,actions: {//获取购物车列表数据async getCartList({ commit }) {let result = await reqCartList()if (result.code === 200) {commit('GETCARTLIST', result.data)}},//删除单个购物车中的商品async deleteCartListBySkuId({ commit }, skuId) {let result = await reqDeleteCartById(skuId)if (result.code === 200) {return 'ok'} else {return Promise.reject(new Error('fail'))}},//修改商品有没有被选中的状态async UpdateCheckedById({ commit }, { skuId, isChecked }) {let result = await reqUpdateCheckedById(skuId, isChecked)if (result.code === 200) {return 'ok'} else {return Promise.reject(new Error('fail'))}},//删除选中的所有商品,这时候要解构出dispatch而不是机械的解构出commit了,因为,我们需要调用上面的删除单个购物车商品的actions函数deleteAllCheckedCart({ dispatch, getters }) {let PromiseAll = []getters.cartShopList.cartInfoList.forEach(item => {let Promise = item.isChecked == 1 ? dispatch('deleteCartListBySkuId', item.skuId) : ''PromiseAll.push(Promise)});return Promise.all(PromiseAll)},//同理,这个是全选和全不选的函数,也要解构出dispatch,同时向修改商品状态的actions函数发送dispatch请求。同时也用了Promise.all来接受Promise返回的成功or失败的结果。updateAllCartIsChecked({ dispatch, state }, isChecked) {let PromiseAll = []state.cartList[0].cartInfoList.forEach((item) => {let Promise = dispatch('UpdateCheckedById', { skuId: item.skuId, isChecked })//PromiseAll方法,Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。PromiseAll.push(Promise)})return Promise.all(PromiseAll)}},mutations: {GETCARTLIST(state, cartList) {state.cartList = cartList}},state: {cartList: []},getters: {cartShopList(state) {return state.cartList[0] || {}},}
}

记下来,封装好了这些actions里的函数,我们就可以回到shopCart组件下写具体的js逻辑以及html结构的渲染。(涉及到了switch语句,try...catch来处理接收到promise成功和失败相对应的回调函数、节流函数、数组的reduce方法、c数组的every方法)

先看html部分的代码,商品列表部分

 <div class="cart-body"> <ul class="cart-list" v-for="(cart,index) in cartInfoList" :key="cart.id"><li class="cart-list-con1"><input type="checkbox" name="chk_list" :checked="cart.isChecked==1" @change="reqUpdateCheckedById(cart,$event)"></li><li class="cart-list-con2"><img :src="cart.imgUrl"><div class="item-msg">{{cart.skuName}}</div></li><li class="cart-list-con4"><span class="price">{{cart.skuPrice}}</span></li><li class="cart-list-con5"><a href="javascript:void(0)" class="mins" @click="handler('minus',-1,cart)">-</a><input autocomplete="off" type="text" minnum="1" class="itxt" :value="cart.skuNum" @change="handler('change',$event.target.value*1,cart)"><a href="javascript:void(0)" class="plus" @click="handler('add',1,cart)">+</a></li><li class="cart-list-con6"><span class="sum">{{cart.skuNum*cart.skuPrice}}</span></li><li class="cart-list-con7"><a href="#none" class="sindelet" @click="deleteCartById(cart)">删除</a><br><a href="#none">移到收藏</a></li></ul></div>

基本的部分按照列表来根据从getters捞过来的数据进行展示,利用computed属性。

 computed: {...mapGetters('shopcart', ['cartShopList']),cartInfoList() {return this.cartShopList.cartInfoList || []},// 计算购买产品的总价totalPrice() {return this.cartInfoList.reduce((pre, cart) => pre + (cart.isChecked === 1 ? (cart.skuNum * cart.skuPrice) : 0), 0)// let sum = 0;// this.cartInfoList.forEach(item => {//     sum += item.skuNum * item.skuPrice// });// return sum},isAllChecked() {return this.cartInfoList.every(item => item.isChecked === 1)}}

第一个重点是在+ 输入框 - 三个操作上

对应处理函数,要进行类型type的判断

接着为每一个商品后面的删除字段绑定点击事件:

对应的处理函数很简单:

好,商品列表的一行里面的操作已经完成,那么接着顺着html结构找到下面的要写的逻辑结构:

为全选前面的checkbox绑定一个 全选的绑定事件:

这里的checked的值需要有两个条件来进行限制(计算属性isAllChekced以及购物车列表不能为空数组)

绑定的事件方法:

这个方法里,关键就是要把全选按钮的 checked值带给actions里的函数,具体的操作就要回到store下的shopcart里来操作了,这里只负责通知并且传递checked值。

紧接着是删除选中的商品操作:

对应的方法也很简单:

到了最后,就是到了结算按钮来进行路由跳转了,由于逻辑简单 ,所以可以直接把a标签换成router-link声明式路由导航进行页面跳转。

注意,还有一个计算属性为totalPrice用来计算购物车中选中的商品用于结算的所有商品价格之和:利用数组的reduce方法进行条件遍历:

36.注册登陆业务

注意ES6 新增的新语法,解构

const {comment,index,deleteComment} = this

注册业务(涉及到的接口:一个是获取手机验证码、一个是用户注册成功与否跳转路由 )很简单,唯一比较重要的难点是关于表单验证,这一部分放在项目结尾来讲,所以无非就是向服务器发送请求,获取成功和失败的回调,然后跳转到登录路由界面:

两个函数派发的actions都在store里的user文件夹下

接下来搞登陆业务,在登录页面找到登录按钮的位置绑定click点击事件

因为是 在form表单里,所以会有默认跳转事件 ,所以要@click.prenvent阻止默认事件,

登陆成功的时候,后台为了区分这个用户是谁,服务器会下发token【令牌:唯一标识】,

一般情况下用户信息不会返回给你,之会返回一个token,用户拿着token再去向服务器发请求获取用户信息,这里前台要持久化存储token,带着token找服务器用户信息进行展示(*vuex存储数据不是持久化的)

登录之后进行路由跳转道home之后,要展示用户信息,所以跳转以后还需要拿着token向服务器要用户信息

为了让token持久化存储,要利用localStorage:

网页一刷新,store里的token就没了,所以也要接触localStorage的getItem来获取token;

第一种想法:问题解决了,但是每次进行路由跳转的时候都要在mounted组件挂载完毕后去向服务器发送请求获取用户信息太为繁琐,所以不可取

第二种想法:挂载在APP根组件中,你登陆之后第一次是获取不到,因为登陆首页加载完毕之后,APP组件也已经加载完毕,此时没有token,所以获取不到userInfo,但是你刷新网页可以获取到用户信息,因为一刷新APP组件相当于重新挂载了一次

还存在一些不能进行相应路由跳转限制的需求,加上上述这个获取用户信息的问题,所以我们需要借助导航守卫来进行解决问题。

最后一个需求:退出登录,事件应该绑定在Header组件里

对应的methods:

仓库下的actions


commit到mutations来进行逻辑操作:清除token,清除用户信息,清除本地存储:

37.导航守卫

如果你前面的只是都能真正的掌握,那么后面的模块开发都会比较简单,无非就是开发四步骤:

(1)封装API

(2)vuex三件套

(3)dispatch发请求(如果在发送请求之后 对返回的数据要有一定的逻辑操作,那么要想一想要不要需要async、await)

(4)数据渲染

基于vue2模块开发的商品汇项目到此 告一段落,后面复习发现新的知识点,或者掌握不熟练的知识点,会继续更新

SGG前台项目复习笔记相关推荐

  1. 【Vue项目复习笔记】详情页的展示

    一.跳转详情页并携带iid 1.点击商品跳转到详情页 当我们点击GoodListItem中的每一个item,就跳转到对应的页面.我们首先要做的就是监听GoodsListItem的点击 在GoodsLi ...

  2. 【XJTUSE软件项目管理复习笔记】 第二章 软件项目整体管理

    仅供学习参考,禁止商用与转载 文章目录 软件项目管理复习笔记 第二章 软件项目整体管理 什么是项目整体管理 战略计划和项目选择 项目选择 项目的财务分析 净现值分析(重点) 投资收益率(ROI)分析法 ...

  3. 【XJTUSE项目管理复习笔记】第五章 软件项目成本管理

    仅供学习参考,禁止商用与转载 项目管理复习笔记 第五章 软件项目成本管理 项目成本管理的主要过程 项目成本管理:包括用来确保在批准的预算范围内完成项目的必要过程 项目成本管理的主要过程:计划成本管理➡ ...

  4. 2018.8.14-C#复习笔记总

    2018.8.14-C#复习笔记总 using System; using System.Collections.Generic; //using System.Linq; using System. ...

  5. Java基础复习笔记系列 九 网络编程

    Java基础复习笔记系列之 网络编程 学习资料参考: 1.http://www.icoolxue.com/ 2. 1.网络编程的基础概念. TCP/IP协议:Socket编程:IP地址. 中国和美国之 ...

  6. USB摄像头视频监控项目学习笔记

    一个摄像头监控应用程序的系统调用如下所示: /* open  * VIDIOC_QUERYCAP 确定它是否视频捕捉设备,支持哪种接口(streaming/read,write)  * VIDIOC_ ...

  7. 【前端】HTML标签基础复习笔记

    不够完美又何妨?万物皆有裂隙,那是光进来的地方. 文章目录 HTML复习笔记 JavaWeb相关概述 HTML概述 HTML语法 基本标签 图片标签 链接 列表标签 块级标签 表格标签 表单标签 HT ...

  8. 【期末复习笔记】知识产权法——著作权、专利法、商标权

    [期末复习笔记]知识产权法 著作权 著作权法不予以保护的客体 著作权的归属 著作权的内容 著作人身权 著作财产权 著作权的取得方式:自动取得 著作权的保护期限: 邻接权 表演者权 表演者义务 表演者权 ...

  9. 考研《软件工程--面向对象和传统的方法》复习笔记

    <软件工程--面向对象和传统的方法>复习笔记 第一章 软件工程的范畴 引言 可以结合 软件工程导论第六版 --张海藩 1.软件工程概念: 软件工程是一门学科,目的是生产出没有错误的软件,按 ...

  10. 【北航软院+保研复习】计算机网络复习笔记

    基于王道计网 1.0版本 北航软院大三上计网课程复习笔记 2.0版本 保研复习笔记 文章目录 第一章 1.1 计算机网络概述 1.1.1 计算机网络的概念 1.1.2 计算机网络的组成 1.1.4 计 ...

最新文章

  1. 微信小程序把玩(四十)animation API
  2. php xxtea加密,PHP实现的XXTEA加密解密算法示例
  3. python程序只能使用源代码进行运行吗-谈谈 Python 程序的运行原理
  4. 【2006-4】【木偶玩具】
  5. SAP UI5:how to connect mock data
  6. 使用Java的@Deprecated前瞻
  7. 洛谷 P3102 [USACO14FEB]秘密代码Secret Code
  8. C# json解析字符串总是多出双引号_一篇长文带你在python里玩转Json数据
  9. PC-hosts 的使用 [可使电脑无法正常上网]
  10. 利用octave求逆矩阵
  11. Boom 2 for Mac(音频助推器及均衡器) v1.6.7中文激活版
  12. python符号怎么打_「符号怎么打出来」```这个符号怎么打出来 - seo实验室
  13. 接收前端传回的JSON字符串,并存入数据库
  14. 彻底解决联想手机数据连接不能上网问题(无需恢复出厂设置) 本文来自移动叔叔论坛 ,详细出处请参考:http://bbs.ydss.cn/thread-201115-1-1.html
  15. yapi 权限_YAPI安装方法
  16. 后缀表达式的求值(c语言)
  17. 360°全景影像建库流程
  18. Mac版eclipse安装Memory Analyzer (MAT)堆转储分析工具
  19. Kafka中steamAPi操作
  20. PowerBI矩阵分析模型

热门文章

  1. IdentityServer4揭秘---Consent(同意页面)
  2. 做APM领域德国队,笃信技术和极简体验的听云打造历程
  3. work profile关闭时桌面图标变灰
  4. Flurry、友盟、TalkingData,Google analytic移动应用统计分析对比
  5. mysql secure_file_priv 属性相关的文件读写权限问题
  6. Windows11亮度调节滑块不见了怎么办
  7. python excel 单元格换行_数据标准化 使用Python脚本处理excel单元格换行符
  8. PS中的文字叠加纹理
  9. 在线App开发平台——应用之星傻瓜式开发平台
  10. python写微信小程序商城,oejia_weshop