node_modules: 文件夹:项目依赖文件夹
public文件夹: 一般防置一些静态资源(图片),需要注意的,放在public文件夹中的静态资源,webpack进行打包的时候,会原封不动的打包到dist文件夹中.
src文件夹(程序员源代码文件夹):
assets文件夹:一般也是防置静态资源(一般防置多个组件共用的静态资源),需要注意,防置assets文件夹里面静态资源,在webpack打包的时候,webpack会把静态资源当做一个模块,打包JS文件里面。
components文件夹: 一般放置的是非路由组件(全局组件)
APP.vue:唯一的根组件,Vue当中的组件(.vue)
main.js:程序的入口文件 也是最先执行的文件
babel.config.js: 配置文件(babel相关)
package.json: 认为是项目的身份证,记录项目叫什么,项目当中有哪些依赖、项目怎么运行。
package-lock.json: 缓存性文件
README.md: 说明性文件
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
不关闭会有各种规范,不按照规范就会报错
module.exports = {
//关闭eslint
lintOnSave: false
}
jsconfig.json别名@提示【@代表的src文件夹,这样将来文件过多,找的时候方便很多】
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
组件页面的样式使用的是less样式、浏览器不识别该样式,需要下载相关依赖
npm install --save less less-loader@5
如果想让组件识别less样式,则在组件中设置
修改public下的index.html文件
"stylesheet" href="./reset.css">
创建pages文件夹,并创建路由组件
创建index.js进行路由配置,最终在main.js中引入注册
1:路由组件一般放置在pages|views文件夹,非路由组件一般放置components文件夹中
2:路由组件一般需要router文件夹中进行注册(使用的即为组件的名字),非路由组件在使用的时候,一般都是以标签的形式使用
3:注册完路由,不管路由组件、还是非路由组件身上都有$route
、$router
属性
$route:一般获取路由信息【路径、query、params等等】
$router:一般进行编程式导航进行路由跳转【push|replace】
(1)声明式导航router-link,可以进行路由的跳转;
(2)编程式导航push|replace,可以进行路由跳转;
编程式导航:声明式导航能做的,编程式都能做;
但是编程式导航除了可以进行路由跳转,还可以做一些其他的业务逻辑。
// 路由传参:
//第一种:字符串形式
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()}})
答:路由跳转传参的时候,对象的写法可以是name、path形式,但是需要注意的是,path这种写法不能与params参数一起使用。
如果路由要求传递params参数,但是你就不传递params参数,会导致URL出现问题;
可在配置路由的时候,在占位的后面加上一个问号【params可以传递或者不传递】;
使用undefined解决:params参数可以传递、不传递(空的字符串)
this.$router.push({name:"search",params:{keyword:''||undefined},query:{k:this.keyword.toUpperCase()}})
(1)布尔值写法:params
props:true,
(2)对象写法:额外的给路由组件传递一些props
props:{a:1,b:2},
(3)函数写法:可以params参数、query参数,通过props传递给路由组件
props:($route)=>({keyword:$route.params.keyword,k:$route.query.k})
注意:在路由配置router内写
“vue-router”:“^3.5.3”:最新的vue-router引入promise
this.$router.push({name:"search",params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()},()=>{},()=>{}});
这种写法:治标不治本,将来在别的组件当中push|replace,编程式导航还是有类似的错误。
终极解决方案:重写push
//重写push|replace
//第一个参数:原来push方法,你在哪里跳转(传递哪些参数)
VueRouter.prototype.push = function(location,resolve,reject){
if(resolve && reject){
originPush.call(this,location,resolve,reject);
}else{
originPush.call(this,location,()=>{},()=>{});
}
}
VueRouter.prototype.replace = function(location,resolve,reject){
if(resolve && reject){
originReplace.call(this,location,resolve,reject);
}else{
originReplace.call(this,location,()=>{},()=>{});
}
}
相同点:都可以调用函数一次,都可以篡改函数的上下文
不同点:call与apply传递参数:call传递参数用逗号隔开,apply方法执行,传递数组
(1)query参数:不属于路径当中的一部分,类似于get请求,地址表现为/search?k1=v1&k2=v2
(2)query参数对应的路由信息path:“/search”
(1)params参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位,地址栏表现为/search/v1/v2
(2)params参数对应的路由信息要修改为path:“/search/:keyword”
这里的/:keyword
就是一个params参数的占位符。
如果路由path要求传递params参数,但是没有传递,会发现地址栏URL有问题,详情如下:
Search路由项的path已经指定要传一个keyword的params参数,如下所示:
path: "/search/:keyword",
执行下面进行路由跳转的代码:
this.$router.push({name:"Search",query:{keyword:this.keyword}})
当前跳转代码没有传递params参数
地址栏信息:http://localhost:8080/#/?keyword=asd
此时的地址信息少了/search
正常的地址栏信息: http://localhost:8080/#/search?keyword=asd
解决方法:可以通过改变path来指定params参数可传可不传
path: "/search/:keyword?",?表示该参数可传可不传
//第一种:字符串形式
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()}})
//params参数可以传递也可以不传递,但是如果传递是空串,如何解决?
this.$router.push({name:"search",params:{keyword:this.keyword||undefined},query:{k:this.keyword.toUpperCase()}})
1、全局组件的配置都需要在main.js中配置
2、全局组件可以在任一页面中直接使用,不需要导入声明
//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);
module.exports = {
//关闭eslint
lintOnSave: false,
devServer: {
// true 则热更新,false 则手动刷新,默认值为 true
inline: true,
// development server port 8000
port: 8001,
}
}
注意:修改完该配置文件后,要重启一下项目
在根目录下创建api文件夹,创建request.js文件
//对于axios进行二次封装
import axios from 'axios'
import nprogress from 'nprogress';
//引入进度条样式 start:进度条开始 done:进度条结束
import "nprogress/nprogress.css"
//1、利用axios对象的方法create,去创建一个axios实例
//2、request就是axios,只不过稍微配置一下
const requests = axios.create({
//配置对象
//基础路径,发请求的时候,路径当中会出现api
baseURL:'/api',
//代表请求超的时间5s
timeout:5000,
});
//请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config)=>{
//config:配置对象,对象里面有一个属性很重要,headers请求头
//进度条开始动
nprogress.start();
return config;
})
//响应拦截器
requests.interceptors.response.use((res)=>{
//成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,可以做一些事情
//进度条结束
nprogress.done();
return res.data;
},(error)=>{
//失败的回调函数
console.log("响应失败"+error)
return Promise.reject(new Error('fails'));
})
//对外暴露
export default requests;
在根目录下的vue.config.js中配置,proxy为通过代理解决跨域问题。
module.exports = {
//关闭eslint
lintOnSave:false,
//代理跨域
devServer:{
// true 则热更新,false 则手动刷新,默认值为 true
inline: false,
// development server port 8000
port: 8001,
//代理服务器解决跨域
proxy:{
'/api':{
target:'http://39.98.123.211'
}
}
}
}
我们在封装axios的时候已经设置了baseURL为api,所以所有的请求都会携带/api,这里我们就将/api进行了转换。如果你的项目没有封装axios,或者没有配置baseURL,建议进行配置。要保证baseURL和这里的代理映射相同,此处都为’/api’。
1、在文件夹api中创建index.js文件,用于封装所有请求
2、将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,这样当我们的接口比较多时,如果需要修改只需要修改该文件即可。
//当前这个模块:API进行统一管理
import request from '@/api/request';
//三级联动接口 api/produce/getBaseCategoryList get 无参数
export const getBaseCategoryList = () =>{
return request({
url:'/product/getBaseCategoryList',
method:'get'
})
}
原理是:在我们发起请求的时候开启进度条,在请求成功后关闭进度条,所以只需要在request.js中进行配置。如下图的nprogress引入及使用。
//对于axios进行二次封装
import axios from 'axios'
import nprogress from 'nprogress';
//引入进度条样式 start:进度条开始 done:进度条结束
import "nprogress/nprogress.css"
//1、利用axios对象的方法create,去创建一个axios实例
//2、request就是axios,只不过稍微配置一下
const requests = axios.create({
//配置对象
//基础路径,发请求的时候,路径当中会出现api
baseURL:'/api',
//代表请求超的时间5s
timeout:5000,
});
//请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config)=>{
//config:配置对象,对象里面有一个属性很重要,headers请求头
//进度条开始动
nprogress.start();
return config;
})
//响应拦截器
requests.interceptors.response.use((res)=>{
//成功的回调函数:服务器响应数据回来以后,响应拦截器可以检测到,可以做一些事情
//进度条结束
nprogress.done();
return res.data;
},(error)=>{
//失败的回调函数
console.log("响应失败"+error)
return Promise.reject(new Error('fails'));
})
//对外暴露
export default requests;
1、首先保证已经安装了vuex,然后根目录创建store文件夹,文件夹下创建index.js,内容如下:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
//对外暴露store的一个实例
export default new Vuex.Store({
state:{},
mutations:{},
actions:{},
})
2、使用vuex,还要在main.js中引入文件,并注册store
注意:凡是在main.js中的Vue实例中注册的实体,在所有的组件中都会有(this.$.实体名)属性
import store from './store'
new Vue({
render: h => h(App),
//注册路由后,组件中都会拥有$router $route属性
router,
//注册store,此时组件中都会拥有$store
store
}).$mount('#app')
await含义是async标识的函数体内的,先等待await标识的异步请求执行完,再执行其他。这也使得只有reqCateGoryList执行完,result得到返回值后,才会执行后面的输出操作。
//通过API里面的接口函数调用,向服务器发请求,获取服务器的数据
async categoryList({commit}){
let result = await getBaseCategoryList();
console.log(result);
if(result.code == 200){
commit("CATEGORYLIST",result.data);
}
}
1、state、actions、mutations、getters的辅助函数使用,当多次访问store中的上述属性时,要使用个属性的辅助函数,可以减少代码量。
2、在使用上面的函数时,如果需要传递多个参数,需要把多个参数组合为一个对象传入(vuex是不允许多个参数分开传递的)。
注意:使用action时,函数的第一个参数,必须是{commit},即使不涉及到mutations操作,也必须加上该参数,否则会报错
。
loadsh官网、 防抖节流概述
防抖(debounce):用户操作很频繁,但是只执行一次,减少业务负担。
节流(throttle):用户操作很频繁,但是把频繁的操作变为少量的操作,使浏览器有充分时间解析代码
如果事件处理函数调用的频率很高,会加重浏览器的负担,导致用户体验很差劲,因此我们采用了防抖和节流的方式来减少调用频率,且不会影响效果。
下面代码就是将changeIndex设置了节流,如果操作很频繁,限制50ms执行一次。这里函数定义采用的键值对形式。throttle的返回值就是一个函数,所以直接键值对赋值就可以,函数的参数在function中传入即可。
//最好的引入方式是 按需嘉爱
import throttle from 'lodash/throttle';
methods:{
//鼠标进入修改响应式数据currentIndex属性
// changeIndex(index){
// //index:鼠标移入某一个一级分类的元素的索引值
// console.log(index);
// this.currentIndex = index;
// },
changeIndex:throttle(function(index){
//index:鼠标移入某一个一级分类的元素的索引值
this.currentIndex = index;
},50),
}
下面的三级联动需要进行路由跳转。路由跳转的方法有两种【导航式路由、编程式路由】
方法一(使用导航式路由router-link):当我们a标签循环,就会有很多router-link,这样当我们频繁操作时候会出现卡顿现象。
方法二(使用编程式导航):我们是使用点击事件触发路由跳转,同样会有多少a标签就有多少个触发函数,虽不卡顿但影响性能。
终极方案:编程式导航+事件委派 的方式实现路由跳转
事件委派:把子节点的触发事件都委托给父节点。这样只需要一个回调函数goSearch就可以解决。
使用事件委派的问题:
(1)如何确定我们点击的一定是a标签呢?如何保证我们只能通过点击a标签才跳转呢?
(2)如何获取子节点标签的商品名称和商品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){
//利用事件委派存在一些问题:1:点击一定是a标签 2、如何获取参数【1、2、3分类的产品的名字】
let element = event.target;
//获取到当前触发这个事件的节点 节点有一个属性dataset属性 可以获取节点的自定义属性与属性值
console.log(element.dataset);
let {categoryname,category1id,category2id,category3id} = element.dataset;
if(categoryname){
//整理路由跳转的参数
let location = {name:"search"};
let query = {categoryName:categoryname};
// 如何判断是 一级分类 二级分类 三级分类
if(category1id){
query.category1Id = category1id;
}else if(category2id){
query.category2Id = category2id;
}else if(category3id){
query.category3id = category3id;
}
//整理完参数
location.query = query;
//路由跳转
this.$router.push(location);
}
}
问题缘由:Vue在路由切换的时候会销毁旧路由;
如下图所示,当我们频繁切换不用组件的时候,都会进行一次请求。
出于性能的考虑我们希望该数据[信息一样]只请求一次,所以我们把这次请求放在App.vue的mounted中。(根组件App.vue的mounted只会执行一次)
mock用来拦截前端ajax请求,返回我么们自定义的数据用于测试前端接口。
将不同的数据类型封装为不同的json文件,创建mockServer.js文件
在main.js引入
1、store中存放公共的数据然后使用
this.$store.dispatch()
取;
2、公共资料均可放在store中,然后使用下面固定操作即可;
1、安装swiper 2、在需要使用轮播图的组件内倒入swiper和它的css样式
3、在组件中创建swiper需要的dom标签(html代码等) 4、创建swiper实例
问题:我们在mounted中创建这个实例,但是出现无法加载轮播图片的问题
原因:由于mounted先异步请求轮播图数据,然后在创建swiper实例,由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建swiper实例。但是此刻我们轮播图数据没有得到,就导致无法展示图片的问题。
mounted() {
//派发action:通过Vuex发起ajax请求,讲数据仓库
this.$store.dispatch('getBannerList');
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实例。只需要加一个200ms时间延迟再创建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',
},
})
},200)
}
方案一肯定不是最好的,但是我们开发的首要是先实现功能,然后再去完善。
方案二:我们可以使用watch监听bannerList轮播图列表属性,因为bannerList初始值为空,当它有数据时,我们就可以创建swiper对象。
watch:{
bannerList(newValue,oldValue){
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',
},
})
}
}
这样也无法实现轮播图,导致的原因是:轮播图的html中有v-for的循环,我们是通过v-for遍历bannerList中的图片数据,然后展示。我们的watch只能保证在bannerList变化时创建swiper对象,但是并不能保证此时v-for已经执行完了。
假如watch先监听到bannerList数据变化,执行回调函数创建了swiper对象,之后v-for才执行,这样也是无法渲染轮播图图片(因为swiper对象生效的前提是html即dom结构已经渲染好了)。
终极完美解决方案: 使用watch+this.$nextTick()
1、
this. $nextTick
它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for),也就是等我们页面中结构都有了再去执行回调函数。
2、$nextTick
:在下次DOM更新 循环结束之后 执行延迟回调 在修改数据之后 立即使用这个方法 获取更新后的DOM。
3、$nextTick
:可以保证 页面中的解构一定是有的,经常和很多插件一起使用【都需要DOM存在了】
注意:之前学习watch时,一般都是监听的定义在data中的属性,但是我们这里是监听的computed中的属性,这也是可以的.
props官网
原理:父组件设置一个属性绑定要传递的数据,子组件使用props接收该属性值
1、父组件调用子组件方法:在组件上定义ref然后使用
this.$refs.定义ref的名称.要调用的方法
2、子组件调用传值父组件:使用$emit("名称","传的值")
,在父组件的子组件上定义@名称=“执行的方法”
注意:定义的swiper对象放在mounted中执行,还需要设置immediate:true属性,这样可以实现,无论数据有没有变化,上来立即监听一次。
<template>
<!--banner轮播-->
<div class="swiper-container" ref="cur">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(carousel,index) in list" :key="carousel.id">
<img :src="carousel.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</template>
<script>
import Swiper from 'swiper';
export default{
name:'Carousel',
props:{
list:{
type:Object,
default:()=>{}
}
},
watch:{
list:{
//立即监听:不管数据是否变化 进来就立即监听一次
immediate:true,
handler(){
this.$nextTick(()=>{
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',
},
})
})
}
}
}
}
</script>
<style scoped>
</style>
getters是vuex store中的计算属性,目的是为了解化数据而生;
1、如果不使用getters属性,我们在获取state中的数据时表达式为:this.$store.state.子模块.属性
,如果多个组件使用,将会很不理想;
2、Vuex允许我们在store中国定义getters计算属性,getters的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
注意:getters是全局属性,不分模块。即store中所有模块的getter内的函数都可以通过
$store.getters.函数名
获取到store内容
注意:当网络出现故障时应该将返回值设置为空,如果不设置返回值就变成了undefined。
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() 。
思考:让我们点击搜索、三级列表点击时,如何调用回调函数?
方案一:给点击按钮都加上触发事件,但是如此就会反复调用回调函数,特耗性能。
最佳方案:当我们每次搜索时,我们的query和params参数的部分内容会发生改变,因此我们可以使用watch进行监听它们的变化并发起请求。
//数据监听:监听组件实例身上的属性的属性值的表换
watch:{
//监听路由的信息是否发生变化,如果发生变化,再次发起请求
$route(newValue,oldValue){
//再次发请求之前整理带给服务器参数
Object.assign(this.searchParams,this.$route.query,this.$route.params);
//再次发起ajax请求
this.getData();
//每一次请求完毕 应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应的1、2、3id
this.searchParams.category1Id = '';
this.searchParams.category2Id = '';
this.searchParams.category3Id = '';
}
}
1、props:父子通信
2、自定义事件:子父通信【v-on/$emit
】
3、vuex:万能
4、插槽:父子
5、pubsub-js:万能
6、$bus
:全局事件总线
removeCategoryName(){
//把带给服务器的参数置空了,还需要相关服务区发起请求
//带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器
//但是你把相应的字段变为undefined 当前这个字段不会带给服务器
this.searchParams.categoryName = undefined;
//再次发起ajax请求
this.getData();
//地址栏也需要改:进行路由跳转
if(this.$route.params){
this.$router.push({name:"search",params:this.$route.params});
}
},
此操作需要删除输入框内容,因为params参数是从输入框内获取的,但输入框在Header组件中,而且header和search组件是兄弟组件,因此要实现兄弟组件之间的通信。
我们选择使用$bus
实现兄弟组件通信
(1)第一步:在main.js中注册
(2)search组件使用$bus通信,第一个参数可以理解为为通信的暗号,还可以有第二个参数(用于传递数据),我们这里只是用于通知header组件进行相应操作,所以没有设置第二个参数。
(3)组件接受$bus
通信
注意:组件挂载时就监听clear事件
mounted() {
//通过全局事件总线清楚关键字
this.$bus.$on("clear",()=>{
this.keyword = "";
});
}
SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性。如下图所示
//自定义事件回调【获取子组件传递的品牌信息(自定义事件)】
trademarkInfo(trademark){
console.log("我是父组件",trademark)
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`
this.getData();
},
//删除品牌的信息
removetrademark(){
//将品牌信息置空
this.searchParams.trademark = undefined;
//再次发起请求
this.getData();
},
//收集平台属性 回调函数(自定义事件)
attrInfo(attr,attrValue){
let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
//数组去重
if(this.searchParams.props.indexOf(props) == -1){
this.searchParams.props.push(props);
}
//再次发起请求
this.getData();
},
//删除售卖的
removeAttr(index){
this.searchParams.props.splice(index,1);
this.getData();
}
let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
this.searchParams.props.indexOf(props) == -1
逻辑分析:排序只需要改变参数order即可;属性值为字符串【1:asc、2:desc】(1代表综合,2代表价格,asc代表升序,desc代表降序)
data(){
return{
//带给服务器的参数【其他属性暂不显示】
searchParams:{
order: "1:desc",//排序:初始的状态 应该是综合|降序
}
}
},
1、iconfont中选择并添加图标,如下图:
2、在public文件index引入该css
3、在search中使用
<!-- 这里isOne、isTwo、isAsc、isDesc是计算属性,如果不使用计算属性要在页面中写很长的代码-->
<li :class="{active:inOne}" @click="changeOrder('1')">
<a href="#">综合<span v-show="inOne" class="iconfont" :class="{'icon-up':isAsc,'icon-todown':isDesc}"></span></a>
</li>
<li :class="{active:inTwo}" @click="changeOrder('2')">
<a href="#">价格<span v-show="inTwo" class="iconfont" :class="{'icon-up':isAsc,'icon-todown':isDesc}"></span></a>
</li>
//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.getData()
}else{
//多次点击的是不是同一个按钮
newSearchOrder = `${flag}:${order==='desc'?'asc':'desc'}`
}
//需要给order重新赋值
this.searchParams.order = newSearchOrder;
//再次发请求
this.getData();
}
computed:{
inOne(){
return this.searchParams.order.indexOf('1') !== -1;
},
inTwo(){
return this.searchParams.order.indexOf('2') !== -1;
},
isDesc(){
return this.searchParams.order.indexOf('desc') !== -1;
},
isAsc(){
return this.searchParams.order.indexOf('asc') !== -1;
},
},
核心属性:pageNo(当前页码)、pageSize、total、continues(连续展示的页码)
核心逻辑是获取连续页码的起始页码和末尾页码,通过计算属性获得(计算属性如果想返回多个数值,可以通过对象形式返回)
<div class="pagination">
<button :disabled="pageNo==1" @click="$emit('getPageNo',pageNo-1)">上一页</button>
<button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo',1)">1</button>
<button v-if="startNumAndEndNum.start > 2">···</button>
<button v-for="(page,index) in startNumAndEndNum.end" v-if="page>=startNumAndEndNum.start"
@click="$emit('getPageNo',page)"
:class="{active:pageNo==page}">{{page}}</button>
<button v-if="startNumAndEndNum.end < totalPage-1">···</button>
<button v-if="startNumAndEndNum.end < totalPage" @click="$emit('getPageNo',totalPage)"
:class="{active:pageNo==totalPage}">{{totalPage}}</button>
<button :disabled="pageNo==totalPage">下一页</button>
<button style="margin-left: 30px">共 {{total}} 条</button>
</div>
computed:{
//总共多少页
totalPage(){
//向上取整
return Math.ceil(this.total/this.pageSize);
},
//计算出连续的页码的起始数字与结束数字
startNumAndEndNum(){
const {continues,pageNo,totalPage} = this;
//先定义两个变量存储起始数字与结束数字
let start = 0 , end = 0;
//连续页码数字5[就是至少五页],如果出现不正常的现象[就不够五页]
//不正常现象[总页数没有连续页码多]
if(continues > totalPage){
start = 1;
end = totalPage;
}else{
//正常现象[连续页码5,但是你的总页数一定是大于5的]
//起始数字
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}
}
},
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 使用声明式导航vue-router。
老师的方法很巧妙:在轮播图组件中设置一个currendIndex,用来记录所点击图片的下标,并用currendIndex实现点击图片高亮设置。当符合图片的下标满足currentIndex===index时,该图片就会被标记为选中。
轮播图组件和放大镜组件是兄弟组件,所以要通过全局总线通信。
blur与change事件在绝大部分情况下表现都非常相似,输入结束后,离开输入框,会先后触发change与blur,唯有两点例外。
(1) 没有进行任何输入时,不会触发change。
在这种情况下,输入框并不会触发change事件,但一定会触发blur事件。在判断表单修改状态时,这种差异会非常有用,通过change事件能轻易地找到哪些字段发生了变更以及其值的变更轨迹。(2)输入后值并没有发生变更。
这种情况是指,在没有失焦的情况下,在输入框内进行返回的删除与输入操作,但最终的值与原值一样,这种情况下,keydown、input、keyup、blur都会触发,但change依旧不会触发。
功能解析:点击加入购物车时,会向后端发送API请求,但是该请求的返回值data为null,因此我们只需要根据code来判断是否成功,就不需要在vuex三连环了。
//加入购物车的回调函数
async addShopCar() {
//派发action
try {
//1、发请求 ---将产品加入到数据库 通知服务器
await this.$store.dispatch('addOrUpdateShopCart', {
skuId: this.$route.params.id,
skuNum: this.skuNum
});
//2、服务器存储成功 ----进行路由跳转传递参数
this.$router.push({name:'addcartsuccess',query:{skuInfo:this.skuInfo,skuNum:this.skuNum}})
} catch (error) {
//3、失败:给用户进行提示
alert(error.message)
}
}
async会返回一个Promise,因此可以将结果返回回去
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实现
浏览器存储功能:HTML5中新增的 本地存储和会话存储
会话存储sessionStorage:并非持久化—会话结束就消失
本地存储localStorage:持久化----5M
1、sessionStorage:为每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
2、localStorage:同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。
//加入购物车的回调函数
async addShopCar() {
//派发action
try {
//1、发请求 ---将产品加入到数据库 通知服务器
await this.$store.dispatch('addOrUpdateShopCart', {
skuId: this.$route.params.id,
skuNum: this.skuNum
});
//2、服务器存储成功 ----进行路由跳转传递参数
//下面这种手段路由跳转一级传递参数是可以的
// this.$router.push({name:'addcartsuccess',query:{skuInfo:this.skuInfo,skuNum:this.skuNum}})
//本地存储|会话存储,一般存储的是字符串 通过会话存储(不持久化,会话结束数据在消失)
sessionStorage.setItem("SKUINFO",JSON.stringify(this.skuInfo))
this.$router.push({name:'addcartsuccess'})
} catch (error) {
//3、失败:给用户进行提示
alert(error.message)
}
}
注意:无论是session还是local存储的值都是字符串形式。如果我们想要存储对象,需要在存储前JSON.stringify()将对象转为字符串,在取数据后通过JSON.parse()将字符串转为对象。
根据api接口文档封装请求函数
export const reqGetCartList = () => {
return requests({
url:'/cart/cartList',
method:'GET'
})}
思考:
当我们需要获取详细信息时,用户还需要一个uuidToken来验证身份。无奈接口无参数,只能添加到请求头中。
创建utils工具包文件夹,创建生成uuid的js文件,对外暴露为函数(记得导入uuid => npm install uuid)。
生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储
import {v4 as uuidv4} from "uuid";
//要生成一个随机字符串,且每次执行不能发生变化,游客身份持久存储
export const getUUID = ()=>{
//1、先从本地存储获取UUID(看一下本地存储里面是否有)
let uuid_token = localStorage.getItem('UUIDTOKEN');
//2、若没有
if(!uuid_token){
//2.1生成游客临时身份
uuid_token = uuidv4();
//2.2本地存储
localStorage.setItem('UUIDTOKEN',uuid_token);
}
return uuid_token;
}
用户的uuid_token定义在store中的detail模块
const state = {
goodInfo:{},
//游客身份
uuid_token: getUUID()
}
注意:
this.$store只能在组件中使用,不能再js文件中使用。如果要在js中使用,需要引入import store from ‘@/store’;
相同点: every和some都有三个参数,即item当前项,index当前项的索引值,array数组本身;都可循环遍历
数组
不同点: every相当于逻辑关系中的且,只有所有参数都满足条件时,才返回true,一旦有一个不满足,则逻辑中断,返回false,some相当于逻辑关系中的或,只要有一个参数满足条件,就中断遍历,返回true,若遍历完所有参数,没有符合的项,返回false。
// 一真即真,满足一个条件都返回true
let arr = [ 1, 2, 3, 4, 5, 6 ];
console.log( arr.some((item,index)=>{
return item > 5
}); // true
// 一假即假,一个条件不满足都返回false
let arr = [ 1, 2, 3, 4, 5, 6 ];
console.log( arr.every((item,index)=>{
return item > 5
}); // false
例如判断底部勾选框是否全部勾选代码部分
//判断底部复选框是否勾选[全部产品都玄宗 才勾选]
isAllCheck() {
//遍历数组里面元素,只要全部元素isChecked属性都为1===>真 true
//只要有一个不是1===> 假false
return this.cartInfoList.every(item => item.isChecked == 1)
}
修改商品数量前端代码部分:
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins" @click="handler('minus',-1,cart)">-</a>
<input autocomplete="off" type="text" value="1" 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>
handler函数,修改商品数量时,加入节流操作。
添加到购物车和对已有物品进行数量改动使用的同一个api,可以查看api文档。
handler函数有三个参数,type区分操作,disNum用于表示数量变化(正负),cart商品的信息
//修改某一个产品的个数[节流]
handler:throttle(async function(type, disNum, cart){
//type:为了区分这三个元素
//disNum:形参: +变化量(1) -变化量(-1) input最终的个数(并不是变化量)
//cart:哪一个产品[身上有id]
//向服务器发请求 修改数量
switch (type) {
case "add":
disNum = 1;
break;
case 'minus':
//判断产品的个数大于1,才可以传递给服务器-1
disNum = cart.skuNum > 1 ? -1 : 0
break;
case 'change':
//用户输入进来的最终量,非法的(带有汉字)带给服务器数字
if(isNaN(disNum) || disNum<1){
disNum = 0;
}else{
disNum = parseInt(disNum) - cart.skuNum;
}
break;
}
//派发action
try {
//代表的是修改成功
await this.$store.dispatch('addOrUpdateShopCart', {
skuId: cart.skuId,
skuNum: disNum
});
this.getData();
} catch (error) {
alert(error.message)
}
},500),
删除某一个产品的操作
//删除某一个产品的操作
deleteCartById:throttle(async function(cart){
try{
//如果成功再次发请求获取新的数据进行展示
await this.$store.dispatch('deleteCartListBySkuId',cart.skuId);
this.getData();
}catch(error){
alert(error.message)
}
},1000),
修改某一个产品的勾选状态 (重点是try、catch)
//修改某一个产品的勾选状态
async updateChecked(cart,event){
try{
//带给服务器的参数isChecked,不是布尔值,应该是0|1
let checked = event.target.checked ? "1":"0";
await this.$store.dispatch('updateCheckedById',{skuId:cart.skuId,isChecked:checked})
this.getData();
}catch(error){
alert(error.message)
}
}
1、最简单的方法->调用多次dispatch删除 ;
2、将批量删除封装成action函数。
官网的教程,一个标准的actions函数如下所示:
deleteAllCheckedById(context) {
console.log(context)
}
context中是包含dispatch、getters、state的,即我们可以在actions函数中通过dispatch调用其它的actions函数,可以通过getters获取仓库的数据。
actions函数代码让如下:
//删除全部勾选的产品
deleteAllCheckedCart({dispatch,getters}){
//context:小仓库 commit[提交mutations修改state] getters[计算属性] dispatch[派发action] state[当前仓库数据]
let PromiseAll = [];
getters.cartList.cartInfoList.forEach(item=>{
//将每一次返回的Promise添加到数组当中
PromiseAll.push(item.isChecked === 1?dispatch('deleteCartListBySkuId',item.skuId):'');
});
//只要全部的p1|p2|p3...都成功,返回结果即为成功 若有一个失败则失败
return Promise.all(PromiseAll);
},
Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
购物车组件method批量删除函数
//删除全部选中的产品
async deleteAllCheckedCart(){
try{
//派发一个action
await this.$store.dispatch("deleteAllCheckedCart");
//再请求获取购物车列表
this.getData();
}catch(error){
alert(error.message);
}
},
actions:
//修改全部产品的状态
updateAllCartChecked({dispatch,state},isChecked){
let PromiseAll = [];
state.cartList[0].cartInfoList.forEach(item=>{
let promise = dispatch("updateCheckedById",{skuId:item.skuId,isChecked:isChecked});
PromiseAll.push(promise)
});
//最终返回结果
return Promise.all(PromiseAll);
}
methods:
//修改全部产品 选中的状态
updateAllCartChecked:throttle(async function(event){
console.log(event)
try{
let isChecked = event.target.checked?"1":"0";
//派发action
await this.$store.dispatch("updateAllCartChecked",isChecked);
this.getData();
}catch(error){
alert(error.message);
}
},500),
const {comment,index,context} = this
上面的这句话是一个简写,最终的含义相当于:
const comment = this.comment
const index = this.index
const context= this.context
methods:
async getCode(){
//简单判断用一下---至少用数据
try{
const {phone} = this;
phone && (await this.$store.dispatch("getCode",phone))
//将组件中的code属性变为仓库中验证码[验证码自己填写]
this.code = this.$store.state.user.code;
}catch(error){
alert(error.message);
}
},
actions:
async getCode({commit},phone){
//获取验证码的接口 把验证码返回 正常情况下是发手机上
let result = await reqGetCode(phone);
if(result.code == 200){
commit("GETCODE",result.data)
return 'ok'
}else{
return Promise.reject(new Error('faile'));
}
},
methods:
async userRegister(){
try{
const {phone,code,password,password1} = this;
(phone && code && password == password1) && this.$store.dispatch('userRegister',{phone,code,password});
//注册成功跳转到登陆页面,并且携带用户账号
await this.$router.push({path:'/login',query:{name:this.phone}})
}catch(error){
alert(error.message);
}
},
actions:
async userRegister({commit},user){
let result = await reqUserRegister(user);
if(result.code == 200){
return 'ok'
}else{
return Promise.reject(new Error('faile'));
}
},
methods:
async goLogin(){
try{
//登录成功
const {phone,password} = this;
(phone && password) && (await this.$store.dispatch("userLogin",{phone,password}))
//跳转到home首页
this.$router.push("/home");
}catch(error){
alert(error.message)
}
}
actions:
async userLogin({commit},data){
let result = await reqUserLogin(data);
if(result.code == 200){
commit("SETUSERTOKEN",result.data.token)
//持久化存储token
setToken(result.data.token);
return 'ok'
}else{
return Promise.reject(new Error('faile'));
}
},
mutations设置用户token
GETUSERINFO(state,userInfo){
state.userInfo = userInfo;
},
computed:
computed:{
//用户名信息
userName(){
return this.$store.state.user.userInfo.name;
}
}
actions:
async getUserInfo({commit}){
let result = await reqUserInfo();
if(result.code == 200){
commit("GETUSERINFO",result.data);
return 'ok';
}else{
return Promise.reject(new Error('faile'));
}
},
//对外暴露一个函数
export const setToken = ()=>{
localStorage.setItem('TOKEN',token);
};
//获取token
export const getToken = () =>{
return localStorage.getItem("TOKEN");
};
//清除token
export const removeToken = () =>{
return localStorage.clear("TOKEN")
}
methods:
//退出登录
async logout(){
try{
//退出 并清空
await this.$store.dispatch("userLogout");
this.$router.push("home")
}catch(error){
alert(error.message)
}
}
actions:
async userLogout({commit}){
//向服务器发请求 清除token
let result = await reqLogout();
//action里面不能操作state,提交mutation修改state
if(result.code == 200){
commit("CLEAR");
return 'ok';
}else{
return Promise.reject(new Error('faile'));
}
}
mutations:
CLEAR(state){
state.token = '';
state.userInfo = {};
removeToken();
}
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。
//全局守卫:前置守卫(在路由跳转之间进行跳转)
router.beforeEach(async (to,from,next)=>{
//to:可以获取到你要跳转到哪个路由信息
//from:可以获取到你从哪个路由而来的信息
//next:放行函数 next()放行 next(path)放行到指令路由 next(false);
//用户登录了 才会有token 否则不会有token
let token = store.state.user.token;
console.log(token);
//用户信息
let name = store.state.user.userInfo.name;
if(token){
if(to.path=='/login'){
next('/home')
}else{
if(name){
next();
}else{
try{
//没有用户信息 派发action让仓库存储用户信息在跳转
await store.dispatch("getUserInfo");
next();
}catch(error){
//token失效
await store.dispatch("userLogout");
next();
}
}
}
}else{
//2、未登录 首页或者登录页可以正常访问
if(to.path === '/login' || to.path === '/home' || to.path === 'register'){
next();
}else{
alert('请先登录')
next('login');
}
}
});
路由独享的守卫: 只针对一个路由的守卫,所以该守卫会定义在某个路由中。
//交易组件
{
name: 'Trade',
path: '/trade',
meta: {show:true},
component: () => import('@/pages/Trade'),
//路由独享首位
beforeEnter: (to, from, next) => {
if(from.path === '/shopcart' ){
next()
}else{
next(false)
}
}
},
上面的代码已经实现了trade路由只能从shopcart路由跳转。next(false)指回到from路由。
新问题: 如果用户通过地址栏去访问trade时还是会成功。
解决方法:
在shopcart路由信息meta中加一个flag,初始值为false。当点击去结算按钮后,将flag置为true。在trade的独享路由守卫中判断一下flag是否为true,当flag为true时,代表是通过点击去结算按钮跳转的,所以就放行。
shopcart路由信息
//购物车
{
path: "/shopcart",
name: 'ShopCart',
component: ()=> import('../pages/ShopCart'),
meta:{show: true,flag: false},
},
shopcart组件去结算按钮触发事件
toTrade(){
this.$route.meta.flag = true
this.$router.push('/trade')
}
trade路由信息
//交易组件
{
name: 'Trade',
path: '/trade',
meta: {show:true},
component: () => import('@/pages/Trade'),
//路由独享首位
beforeEnter: (to, from, next) => {
if(from.path === '/shopcart' && from.meta.flag === true){
from.meta.flag = false
next()
}else{
next(false)
}
}
},
注意,判断通过后,在跳转之前一定要将flag置为false。
懒加载vue-lazyload插件官网
vue使用插件的原理: 每个插件都会有一个install方法,install后就可以在我们的代码中可以使用该插件。这个install有两类参数,第一个为Vue实例,后面的参数可以自定义。
步骤:
1、引入插件 import VueLazyload from "vue-lazyload";
2、注册插件Vue.use(VueLazyload)
这里的Vue.use()实际上就是调用了插件的install方法。如此之后,我们就可以使用该插件了。
我们使用的import()就是路由懒加载
当打包构建应用时,JavaScript
包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
代码示例:
//详情页面组件
{
//需要params传参(产品id)
path: "/detail/:skuId",
name: 'Detail',
component: ()=> import('../pages/Detail'),
meta:{show: true},
},
//添加购物车成功
{
path: "/addcartsuccess",
name: 'AddCartSuccess',
component: ()=> import('../pages/AddCartSuccess'),
meta:{show: true},
},
执行npm run build
。会生成dist
打包文件。
dist就是我们打包好的项目文件
dist文件下的js文件存放我们所有的js文件,并且经过了加密,并且还会生成对应的map文件。
map文件作用: 因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时那里的代码报错。有了map就可以向未加密的代码一样,准确的输出是哪一行那一列有错。
当然map文件也可以去除(map文件大小还是比较大的) 在vue.config.js配置
productionSourceMap: false
即可。
注意:vue.config.js配置改变,需要重启项目
注意事项:
如果父组件给子组件传递数据【函数】:本质其实就是子组件给父组件传递数据;
如果父组件给子组件传递的数据【非函数】:本质就是父组件给子组件传递数据;
书写方式:3种
【'todos'】,{type:Array},{type:Array,default:[]}
$on
与$emit
$bus
[万能]Vue.prototype.$bus = this;
默认插槽、具名插槽、作用域插槽
事件:系统事件、自定义事件、click、双击、鼠标系列等等。
事件源、事件类型、事件回调
1、原生DOM:button可以绑定系统事件—click单击事件等等;
2、组件标签:event可以绑定系统事件(不起作用,因为属于自定义事件)----.native(可以把自定义事件变为原生DOM事件)
v-model它是Vue框架中指令,它主要结合表单元素一起使用(文本框、复选、单选等等)它主要的作用是收集表单数据。
v-model实现原理:value与input事件实现的,而且和需要注意可以通过v-model实现父子组件数据同步。
作用:可以实现父子组件数据同步
:money.sync,代表父组件给子组件传递props【money】 给当前子组件绑定一个自定义事件(update:money)
$listeners
与$attrs
【组件通信方式之一】他们两者是组件实例的属性,可以获取到父组件给子组件传递props与自定义事件。
$children
与$parent
【组件通信方式之一】ref可以获取到某一个组件,子组件;
$children是组实例的属性,可以获取到当前组件的全部子组件;
$parent组件实例的属性,可以获取到当前子组件的父组件,进而可以操作父组件的书与方法。
如果项目当中出现很多结构类似功能,想到组件复用。
如果项目当中很多的组件JS业务逻辑相似。想到mixin。【可以把多个组件JS部分重复、相似地方】
作用:可以实现父子组件通信(通信的结构)
默认插槽、具名插槽
作用域插槽:子组件的数据来源于父组件,子组件是决定不了自身结构与外观