尚品汇项目学习笔记(待更新...)

原文链接:https://blog.csdn.net/weixin_43424325/article/details/121684101 

前端Vue核心

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

1、vue文件目录分析

vue-cli脚手架初始化项目。

node + webpack + 淘宝镜像

node_modules:项目依赖文件夹

public:静态资源(图片),webpack进行打包的时候,会原封不动打包到dist文件夹中。

pubilc/index.html是一个模板文件,作用是生成项目的入口文件,webpack打包的js,css也会自动注入到该页面中。我们浏览器访问项目的时候就会默认打开生成好的index.html。

src(程序员代码文件夹)

assets: 放置静态资源(多个组件共用的静态资源),放置在assets文件夹里面静态资源,在webpack打包的时候,webpack会把静态资源当做一个模块,打包JS文件里面。
components: 非路由组件(全局组件),其他组件放在views或者pages文件夹中
App.vue: 唯一的根组件
main.js: 程序入口文件,最先执行的文件

babel.config.js:配置文件(babel相关)
package.json: 项目的详细信息记录。认为项目“身份证”,记录项目叫做什么、项目当中有哪些依赖、项目怎么运行。
package-lock.json: 缓存性文件(各种包的来源)

README.md:说明性文件

2、项目配置

2.1 项目运行,浏览器自动打开

package.json
"scripts": {"serve": "vue-cli-service serve --open","build": "vue-cli-service build","lint": "vue-cli-service lint"},

2.2 关闭eslint校验工具(不关闭会有各种规范,不按照规范就会报错)

  • 根目录下创建vue.config.js,进行配置
module.exports = {//关闭eslintlintOnSave: false}

2.3 src文件夹简写方法,配置别名。

创建jsconfig.json配置别名@提示【用@/代替src/,exclude表示不可以使用该别名的文件】

{"compilerOptions": {"baseUrl": "./","paths": {"@/*": ["src/*"]}},"exclude": ["node_modules", "dist"]
}

3、组件页面样式

组件页面的样式使用的是less样式,浏览器不识别该样式,需要下载相关依赖
npm install --save less less-loader@5
如果想让组件识别less样式,则在组件中设置

12、封装axios

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

axios二次封装:XMLHttpRequest、fetch、JQ、axios

12.1 为什么需要进行二次封装axios

请求拦截器、相应拦截器:

        请求拦截器,可以在发请求之前处理一些业务

        相应拦截器,当服务器数据返回以后,可以处理一些事情

12.2  axios如何封装?
在根目录下创建api文件夹,创建request.js文件。
内容如下,当前文件代码还比较少,后续有需求可以增添内容。

// 对axios进行二次封装
import axios from 'axios'// 1.利用axios对象的方法create,去创建一个axios实例
// 2.request就是axios,只不过稍微配置一下
const requests = axios.create({// 配置对象// 基础路径,发请求的时候,路径当中会出现apibaseURL: '/api',// 代表请求超时的时间5Stimeout: 5000
})
// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config) => {// config: 配置对象,对象里面有一个属性很重要,headers请求头//比如添加tokenreturn config
})// 相应拦截器
requests.interceptors.response.use((res) => {// 成功的回调函数:服务器相应数据回来以后,响应拦截器可以检测到,可以做一些事情return res.data
}, (error) => {// 响应失败的回调函数console.log("响应失败" + error)return Promise.reject(new Error('faile'))
})// 对外暴露
export default requests

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

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

module.exports = {//关闭eslintlintOnSave: false,devServer: {// true 则热更新,false 则手动刷新,默认值为 trueinline: false,// development server port 8000port: 8001,//代理服务器解决跨域proxy: {//会把请求路径中的/api换为后面的代理服务器'/api': {//提供数据的服务器地址target: 'http://gmall-h5-api.atguigu.cn',}},}
}

webpack官网相关知识解读
网站中的webpack.config.js就是vue.config.js文件。

14、请求接口统一封装

在文件夹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();

15、nprogress进度条插件

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

在这里插入图片描述

对应的request.js设置 

// 对axios进行二次封装
import axios from 'axios'
// 引入进度条
import nprogress from 'nprogress'
// 引入进度条样式
import "nprogress/nprogress.css"
// start:进度条开始  done:进度条结束// 1.利用axios对象的方法create,去创建一个axios实例
// 2.request就是axios,只不过稍微配置一下
const requests = axios.create({// 配置对象// 基础路径,发请求的时候,路径当中会出现apibaseURL: '/api',// 代表请求超时的时间5Stimeout: 5000
})
// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config) => {// config: 配置对象,对象里面有一个属性很重要,headers请求头//比如添加token// 进度条开始nprogress.start()return config
})// 相应拦截器
requests.interceptors.response.use((res) => {// 成功的回调函数:服务器相应数据回来以后,响应拦截器可以检测到,可以做一些事情// 进度条结束nprogress.done()return res.data
}, (error) => {// 响应失败的回调函数console.log("响应失败" + error)return Promise.reject(new Error('faile'))
})// 对外暴露
export default requests

可以通过修改nprogress.css文件的background来修改进度条颜色。

在这里插入图片描述

16、手动引入vuex

vuex状态管理库

16.1 vuex是什么?

vuex是官方提供的一个插件,状态管理库,集中式管理项目中组件共用的数据。

切记,并不是全部项目都需要vuex,如果项目很小,完全不需要vuex,如果项目很大,组件很多、数据很多,数据维护很费劲,vuex

state  mutations  actions getters modules

16.2 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')

17、async await使用

如果我们没有封装请求api,而是直接调用axios,就不需要使用async await。
案例:我们将一个axios请求封装为了函数,我们在下面代码中调用了该函数:

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

浏览器结果

在这里插入图片描述

返回了一个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)}}

结果

在这里插入图片描述

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

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

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

结果

在这里插入图片描述

18、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){}

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

19、lodash插件防抖和节流

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

lodash官网
防抖函数
节流函数
防抖(debounce):【回城,被打断就要重新来】用户操作很频繁,但是只执行一次,减少业务负担。

节流(throttle):【技能CD,CD没好,你用不了技能】用户操作很频繁,但是把频繁的操作变为少量的操作,使浏览器有充分时间解析代码
防抖和节流简述

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

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

函数防抖与节流*******面试题

正常:事件触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很可能出现浏览器卡顿)

防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行最后一次

节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发

今晚需要把防抖与节流的原理,通过JS实现【闭包 + 延迟器】

20、编程式导航+事件委托实现路由跳转

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

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

 

{{c1.categoryName}}

{{c2.categoryName}}
{{c3.categoryName}}

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

//函数使用

//函数定义
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)}},

21、Vue路由销毁问题

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

在这里插入图片描述

 由于信息都是一样的,出于性能的考虑我们希望该数据只请求一次,所以我们把这次请求放在App.vue的mounted中。(根组件App.vue的mounted只会执行一次)
注意:虽然main.js也是只执行一次,但是不可以放在main.js中。因为只有组件的身上才会有$store属性。

22、mock插件使用 mock.js

mock用来拦截前端ajax请求,返回我么们自定义的数据用于测试前端接口。
将不同的数据类型封装为不同的json文件,创建mockServe.js文件

banner、floor分别为轮播图和页面底部的假数据。
mockServe.js文件

// 先引入mockjs模块
import Mock from 'mockjs'
// 把JSON数据格式引入进来
//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中引入一下
// 引入mockServe.js   ---- mock数据
import '@/mock/mockServe'

23、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("GETBANNERLIST",result.data)}}}

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

state: {bannerList:[] // 轮播图的数据
}
//唯一修改state的部分
mutations: {GETBANNERLIST(state,bannerList){state.bannerList = bannerList}
}

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


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

24、swiper插件实现轮播图

swiper官网
官网中给出了代码实例:
做一个简要总结:(代码可以直接复制本小节最后面的代码)

(1)安装swiper  【注意:安装swiper@5版本】
(2)在需要使用轮播图的组件内导入swpier和它的css样式
(3)在组件中创建swiper需要的dom标签(html代码,参考官网代码)
(4)创建swiper实例

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


通过ref属性值获取DOM
let mySwiper = new Swiper(this.$refs.cur,{...})

 



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

原因:

我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。

因为dispatch当中涉及到异步语句,导致v-for遍历的时候结构还没有完全,因此不行

由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建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',}})},

解决方法一:等我们的数据请求完毕后再创建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',}})},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',}})}}

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

完美解决方案使用watch+this.$nextTick()
官方介绍:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

this. $nextTick它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for)。

$nextTick:可以保证页面中的结构一定是有的。经常和很多插件一起使用【都xu'yao】

完整代码



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

25、props父子组件通信

prop官方讲解
原理:父组件设置一个属性绑定要传递的数据
子组件props接受该属性值
本项目的
父组件:home文件下的index.js

子组件:Floor下的index.vue


上面两代码一看,发现父子组件竟然都是Floor组件,这使得我们对父子组件的概念难以理解。
个人理解1:
我们Floor文件夹下的index.vue创建了Floor组件,我们把它认为子组件。
我们在home文件夹下引用了该组件并使用了它,具体表现为,此处使用的Floor标签,我们将其称为父组件。
个人理解2:
Floor是子组件,我们在home组件中调用了Floor,我们把home组件认为父组件,我们在home组件中实现了由home组件向Floor组件传递信息的操作,即父组件向子组件传递信息。

如下图所示:
第一张图是home组件的信息,我们的目的上将floorList中的数据分发给Floor组件。
在这里插入图片描述

通过前面描述的代码我们实现了父子通信,即将floorList分发给Floor组件。下图为Floor组件信息

在这里插入图片描述

对于父子组件的理解,我更偏向于个人理解二,因为它可以通过上面图片得到很好的解释。但是个人理解一对于新手理解起来更容易。

26、将轮播图模块提取为公共组件

需要注意的是我们要把定义swiper对象放在mounted中执行,并且还要设置immediate:true属性,这样可以实现,无论数据有没有变化,上来立即监听一次。

上一小节刚刚讲了props实现父组件向子组件传递消息,这里同样也会将轮播图列表传递给子组件,原理相同。
公共组件Carousel代码


在main.js中把Carousel组件设置为全局组件

import Carousel from '@/components/Carousel'
Vue.component(Carousel.name, Carousel)

Floor组件引用Carousel组件
ListContainer组件引用Carousel组件

27、getters使用

getters是vuex store中的计算属性。
getters使用

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

在这里插入图片描述

我们在Search模块中获取商品列表数据就是通过getters实现,需要注意的是当网络出现故障时应该将返回值设置为空,如果不设置返回值就变成了undefined。

store中search模块代码

import {reqGetSearchInfo} from '@/api';
const state = {searchList:{},
}
const mutations = {SEARCHLIST(state,searchList){state.searchList = searchList}
}
const actions = {//第二个参数data默认是一个空对象async getSearchListr({commit},data={}){let result = await reqGetSearchInfo(data)if(result.code === 200){commit("SEARCHLIST",result.data)}}
}
const getters = {goodsList(state){//网络出现故障时应该将返回值设置为空return state.searchList.goodsList||[]}
}
export default {state,mutations,actions,getters,
}

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

//只展示了使用getters的代码

 后续数据的动态渲染就和之前模块相同,没有什么难度。

28、Object.asign实现对象拷贝

参考链接

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

29、对象深拷贝

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

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

最初想法:在每个三级列表和收缩按钮加一个点击触发事件,只要点击了就执行搜索函数。

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

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

在这里插入图片描述

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 = '';}},

31、面包屑相关操作

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

分为:
当分类属性(query)删除时删除面包屑同时修改路由信息。
当搜索关键字(params)删除时删除面包屑、修改路由信息、同时删除输入框内的关键字。
1、query删除时
因为此部分在面包屑中是通过categoryName展示的,所以删除时应将该属性值制空或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")if (this.$route.query) {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 = ''})}

32、组件通信方式

第一种父子组件通信:

$ on、$emit自定义事件实现子组件给父组件传递信息。 props实现父组件给子组件传递数据。

第二种全局事件总线 $bus(适用于所有的场景)
第三种Vuex
第四中插槽(适用于父子组件通信)

组件通信方式连接

33、SearchSelector子组件传参及面包屑操作

31小节中描述了通过query、params参数生成面包屑,以及面包屑的删除操作对应地址栏url的修改。
SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性。如下图所示

在这里插入图片描述

此处生成面包屑时会涉及到子组件向父组件传递信息操作(在32小节有相关知识点),之后的操作和前面31小姐讲的面包屑操作原理相同。唯一的区别是,这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化回影响路由地址变化。
在具体的操作内还会涉及一些小的知识点,例如
字符串拼接 ·${}·,使用方法如下

var a = 1;
console.log(`a的值是:${a}`); //a的值是:1

至此面包屑部分内容结束。
总结:面包屑由四个属性影响:parads、query、品牌、手机属性
面包屑生成逻辑
判断searchParams相关属性是否存在,存在即显示。
面包屑删除逻辑
Search.vue js代码()


34、商品排序

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

我们的升降序是通过箭头图标来辨别的,如图所示:

在这里插入图片描述

图标是iconfont网站的图标,通过引入在线css的方式引入图标

在这里插入图片描述

在public文件index引入该css

在search模块使用该图标


isOne、isTwo、isAsc、isDesc计算属性代码

computed:{...mapGetters(['goodsList']),isOne(){return this.searchParams.order.indexOf('1')!==-1},isTwo(){return this.searchParams.order.indexOf('2')!==-1},isDesc(){return this.searchParams.order.indexOf('desc')!==-1},isAsc(){return this.searchParams.order.indexOf('asc')!==-1},},

点击‘综合’或‘价格’的触发函数changeOrder

//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`this.searchInfo()}else{//多次点击的是不是同一个按钮newSearchOrder = `${flag}:${order==='desc'?'asc':'desc'}`}//需要给order重新赋值this.searchParams.order = newSearchOrder;//再次发请求this.searchInfo();}

35、手写分页器

实际开发中是不会手写的,一般都会用一些开源库封装好的分页,比如element ui。但是这个知识还是值得学习一下的。
核心属性:

pageNo(当前页码)、pageSize、total、continues(连续展示的页码)
核心逻辑是获取连续页码的起始页码和末尾页码,通过计算属性获得。(计算属性如果想返回多个数值,可以通过对象形式返回)

  //连续页码的起始页码、末尾页码startNumAndEnd(){let start = 0 , end = 0;//规定连续页码数字5(totalPage至少5页)//不正常现象if(this.continues > this.totalPage){start = 1end = this.totalPage}else{//正常现象      Math.floor:想下取整start = this.pageNo - Math.floor(this.continues/2)end = this.pageNo + Math.floor(this.continues/2)//start出现不正常现象纠正if(start < 1){start = 1end = this.continues}//end出现不正常现象纠正if(end > this.totalPage){end = this.totalPagestart = this.totalPage - this.continues + 1}}return {start,end}}

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

36、字符串拼接

如果你想在你的字符串内加入某个变量的值,就需要字符串拼接使用 ``(飘符号),由于 在markdown是单行代码标记所以下面我们用··代替。


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

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部