目录
5、pages文件夹
创建pages文件夹,并创建路由组件
5.1创建router文件夹,并创建index.js进行路由配置,最终在main.js中引入注册
5.2 总结
路由组件和非路由组件区别:
- 非路由组件放在components中,路由组件放在pages或views中
- 非路由组件通过标签使用,路由组件通过路由使用
- 在main.js注册玩路由,所有的路由和非路由组件身上都会拥有$router $route属性
- $router:一般进行编程式导航进行路由跳转
- $route: 一般获取路由信息(name path params等)
5.3 路由跳转方式
- 声明式导航router-link标签 ,可以把router-link理解为一个a标签,它 也可以加class修饰,to属性写路径
- 编程式导航 :声明式导航能做的编程式都能做,而且还可以处理一些业务。
@click="goSearch"
methods:{
goSearch(){
this.$router.push/replace('路径');
}
}
6、footer组件显示与隐藏
- footer在登录注册页面是不存在的,所以要隐藏,v-if 或者 v-show
- 这里使用v-show,因为v-if会频繁的操作dom元素消耗性能,v-show只是通过样式将元素显示或隐藏
- 配置路由的时候,可以给路由配置元信息meta,在路由的元信息中定义show属性,用来给v-show赋值,判断是否显示footer组件,第二种方法是通过$route获取当前理由信息,通过路径判断是否显示footer组件
7、路由传参
7.1、query、params
- query、params两个属性可以传递参数
- params意思就是调用的时候,你传过来的所有东西。所有根据每次你调用的时候传过来的东西不一样他就不一样。通常query参数是GET请求时常用的携带参数方式。如果是POST请求也要携带query类型的参数,可以把它拼接到url里。query传值页面刷新数据还在,而params传值页面数据消失??????????
- query参数:不属于路径当中的一部分,类似于get请求,地址栏表现为 /search?k1=v1&k2=v2
- query参数对应的路由信息 path: "/search"
- params参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位 ,地址栏表现为 /search/v1/v2
- params参数对应的路由信息要修改为path: "/search/:keyword" 这里的/:keyword就是一个params参数的占位符
params传参问题
(1)、如何指定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?",?表示该参数可传可不传
参考连接:https://blog.csdn.net/weixin_44867717/article/details/109773945
(2)、由(1)可知params可传可不传,但是如果传递的时空串,如何解决 。
this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''}})
出现的问题和1中的问题相同,地址信息少了/search
解决方法: 加入||undefined,当我们传递的参数为空串时地址栏url也可以保持正常
this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''||undefined}})
(3)路由组件能不能传递props数据?
可以,但是只能传递params参数,具体知识为props属性 。
// 路由组件传递props数据:
// 第一种:布尔值:只能传params参数
// props: true,
// 第二种:对象写法:额外给路由组件传递一些props
// props: { a: 1, b: 2 },
// 第三种:函数写法:可以把params参数,query参数,通过props传递给路由组件(常用
props: ($route) => {
return {
keyword: $route.params.keyword,
k: $route.query.k,
}
}
路由里传递完成后,要在组件里接收:props:['keyword','k']
7.2、传参方法
字符串形式
this.$router.push(“/search/”+this.params传参+“?k=”+this.query传参)
模板字符串
- this.$router.push(`/search/${this.params传参}?k=${this.query传参}`)
- 注意: 上面字符串的传参方法可以看出params参数和’/'结合,query参数和?结合
- http://localhost:8080/#/search/asd?keyword=asd
- 上面url中asd为params的值,keyword=asd为query传递的值。
对象(常用)
- this.$router.push({name:“路由名字”,params:{传参},query:{传参})。
- 以对象方式传参时,如果我们传参中使用了params,只能使用name,不能使用path,如果只是使用query传参,可以使用path 。
7.3报错
- 编程式路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated警告
-
例如:使用this.$router.push({name:‘Search’,params:{keyword:“…”||undefined}})时,如果多次执行相同的push,控制台会出现警告。
let result = this.$router.push({name:"Search",query:{keyword:this.keyword}}) console.log(result)
- 原因:push是一个promise,promise需要传递成功和失败两个参数,我们的push中没有传递。
- 方法:this.$router.push({name:‘Search’,params:{keyword:“…”||undefined}},()=>{},()=>{})后面两项分别代表执行成功和失败的回调函数。这种写法治标不治本,将来在别的组件中push|replace,编程式导航还是会有类似错误
- push是VueRouter.prototype的一个方法,在router中重写该方法即可
//先把VueRouter原型对象的push先保存一份
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;
//重写push|replace
//第一个参数:告诉原来的push方法,你往哪里跳转(传递哪些参数)
//第2个参数:成功回调
//第3个参数:失败回调
//面试:函数apply与call区别?
//相同的地方:都可以篡改函数里面this
//不同的地方:apply传递参数 数组 call传递参数 逗号分割
VueRouter.prototype.push = function (location, resolve, reject) {
if (reject && resolve) {
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, () => { }, () => { });
}
}
8、定义全局组件
- 我们的三级联动组件是全局组件,全局的配置都需要在main.js中配置
//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);
- 在Home组件中使用该全局组件,全局组件可以在任一页面中直接使用,不需要导入声明
<template>
<div>
<!-- 三级联动全局组件已经注册为全局组件,因此不需要引入-->
<TypeNav/>
</div>
</template>
9、代码改变时实现页面自动刷新
- 根目录下vue.config.js文件设置
module.exports = {
//关闭eslint
lintOnSave: false,
devServer: {
// true 则热更新,false 则手动刷新,默认值为 true
inline: true,
// development server port 8000
port: 8001,
}
}
10、二次封装axios:使用请求和响应拦截器
- 向服务器发请求:XMLHttpRequest、fetch、JQ、axios
- axios中文文档,包含详细信息
- 使用说明 · Axios 中文说明 · 看云
- 在根目录下创建api文件夹,创建request.js文件。
//二次封装axios
import axios from 'axios';
//利用axios对象的create,去创建axios实例
let requests = axios.create({
//配置对象
//基础路径,发请求的时候,路径当中会出现api
baseURL: "/api",
//代表请求超时的时间5S
timeout: 5000,
});
//请求拦截器
requests.interceptors.request.use((config) => {
//config内主要是对请求头Header配置
return config;
});
//响应拦截器
requests.interceptors.response.use((res) => {
//成功的回调函数:
return res.data;
}, (error) => {
//响应失败的回调函数
console.log("响应失败" + error)
//终止Promise链
return new Promise();
})
//对外暴露
export default requests;
11、前端通过代理解决跨域问题
- 传统解决方案:JSONP,CROS,代理
- 在根目录下的vue.config.js中配置,proxy为通过代理解决跨域问题。
- 我们在封装axios的时候已经设置了baseURL为api,所以所有的请求都会携带/api,这里我们就将/api进行了转换。如果你的项目没有封装axios,或者没有配置baseURL,建议进行配置。要保证baseURL和这里的代理映射相同,此处都为’/api’。
- webpack官网相关知识解读
网站中的webpack.config.js就是vue.config.js文件。
module.exports = {
//关闭eslint
lintOnSave: false,
devServer: {
// true 则热更新,false 则手动刷新,默认值为 true
inline: false,
// development server port 8000
port: 8001,
//代理服务器解决跨域
proxy: {
//会把请求路径中的/api换为后面的代理服务器
'/api': {
//提供数据的服务器地址
target: 'http://39.98.123.211',
}
},
}
}
12、请求接口统一封装
- 在文件夹api中创建index.js文件,用于封装所有请求
- 将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,这样当我们的接口比较多时,如果需要修改只需要修改该文件即可。
//所有API进行统一管理
import requests from './request';
//添加到购物车
export const reqAddOrUpdateShortCart = (skuId, skuNum) => {
return requests({
url: `/cart/addToCart/${skuId}/${skuNum}`,
method: 'post',
})
}
- 当组件想要使用相关请求时,只需要导入相关函数即可,以上图的reqAddOrUpdateShortCart为例:
import {reqAddOrUpdateShortCart} from './api'
//发起请求
reqAddOrUpdateShortCart();
13、nprogress进度条插件
- 打开一个页面时,往往会伴随一些请求,并且会在页面上方出现进度条。它的原理时,在我们发起请求的时候开启进度条,在请求成功后关闭进度条,所以只需要在request.js中进行配置。
- 可以通过修改nprogress.css文件的background来修改进度条颜色。
//二次封装axios
import axios from 'axios';
//引入进度条
import nProgress from 'nprogress';
//引入进度条样式
import 'nprogress/nprogress.css';
let requests = axios.create({
//配置对象
//基础路径,发请求的时候,路径当中会出现api
baseURL: "/api",
//代表请求超时的时间5S
timeout: 5000,
});
//请求拦截器
requests.interceptors.request.use((config) => {
//需要携带token带给服务器
nProgress.start();
return config;
});
//响应拦截器
requests.interceptors.response.use((res) => {
//成功的回调函数:
nProgress.done();
return res.data;
}, (error) => {
//响应失败的回调函数
console.log("响应失败" + error)
//终止Promise链
return new Promise();
})
//对外暴露
export default requests;
14、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)
}
- 结果
15、手动引入vuex
首先确保安装了vuex,根目录创建store文件夹,文件夹下创建index.js,内容如下:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
//引入小仓库
import home from './home/home.js'
//对外暴露Store类的一个实例
export default new Vuex.Store({
//实现vuex仓库模块式开发存储数据
modules: {
home,
}
})
import { reqCategoryList} from '@/api';
//home模块小仓库
// state:状态管理,用来存放想共享的数据,仓库存储数据的地方
// mutations:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
// actions:处理action,可以书写自己的业务逻辑,支持异步操作,但不能修改state
// getters:类似是Vuex中的计算属性,也具有缓存功能,用于简化仓库数据,让组件获取仓库的数据更方便
let state = {};
let mutations = {
CATEGORYLIST(state, categoryList) {
state.categoryList = categoryList.slice(0, 16);
},
};
let actions = {
//通过API里面的接口函数调用,向服务器发请求,获取服务器数据
async categoryList({ commit }) {
let result = await reqCategoryList();
// console.log(result);
if (result.code == 200) {
commit("CATEGORYLIST", result.data);
}
},
};
let getters = {};
export default {
state,
mutations,
actions,
getters
}
- 再main.js中引入,(1) 引入文件(2) 注册store
import store from './store'
new Vue({
render: h => h(App),
//注册路由,此时组件中都会拥有$router $route属性
router,
//注册仓库:组件实例的身上会多一个$store属性
store
}).$mount('#app')
- state、actions、mutations、getters的辅助函数使用,当多次访问store中的上述属性时,要使用个属性的辅助函数,可以减少代码量。
- 在使用上面的函数时,如果需要传递多个参数,需要把多个参数组合为一个对象传入(vuex是不允许多个参数分开传递的)。
- 注意:使用action时,函数的第一个参数,必须是{commit},即使不涉及到mutations操作,也必须加上该参数,否则会报错。
我们会把公共的数据放在store中,然后使用时再去store中取。以我们的首页轮播图数据为例。
- 1、在轮播图组件ListContainer.vue组件加载完毕后发起轮播图数据请求。
mounted() {
//派发action:通过vuex发送Ajax请求,将数据存储在仓库中
this.$store.dispatch('getBannerList');
},
- 2、请求实际是在store中的actions中完成的
let actions={
//获取首页轮播图数据
async getBannerList({ commit }) {
let result = await reqGetBannerList();
console.log(result);
if (result.code == 200) {
commit("GETBANNERLIST", result.data);
}
},
}
- 3、获取到数据后存入store仓库,在mutations完成
//唯一修改state的部分
let mutations={
GETBANNERLIST(state, bannerList) {
state.bannerList = bannerList;
},
},
- 4、轮播图组件ListContainer.vue组件在store中获取轮播图数据。由于在这个数据是通过异步请求获得的,所以我们要通过计算属性computed获取轮播图数据。
- ListContainer.vue代码
<script>
import { mapState } from 'vuex';
export default {
name: '',
mounted() {
//派发action:通过vuex发送Ajax请求,将数据存储在仓库中
this.$store.dispatch('getBannerList');
},
computed: {
//提取仓库中的数据
...mapState({
bannerList: state => state.home.bannerList,
})
},
}
</script>
- 总结:只要是公共数据都会放在store中,之后的实现步骤就是上面的固定步骤。
16、loadsh插件防抖和节流
- 在进行窗口的resize、scroll,输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时我们可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率,同时又不影响实际效果。
- 安装lodash插件,该插件提供了防抖和节流的函数,我们可以引入js文件,直接调用。当然也可以自己写防抖和节流的函数
- lodash官网
- 防抖函数
- 节流函数
- 防抖:用户操作很频繁,但是只执行一次,减少业务负担。
- 节流:用户操作很频繁,但是把频繁的操作变为少量的操作,使浏览器有充分时间解析代码
- 防抖和节流简述
- 例如:下面代码就是将changeIndex设置了节流,如果操作很频繁,限制50ms执行一次。这里函数定义采用的键值对形式。throttle的返回值就是一个函数,所以直接键值对赋值就可以,函数的参数在function中传入即可。
import throttle from 'lodash/throttle';
methods: {
//鼠标进入修改响应元素的背景颜色:currentIndex属性
//采用键值对形式创建函数,将changeIndex定义为节流函数,该函数触发很频繁时,设置50ms才会执行一次
//throttle回调函数别用箭头函数,可能出现上下文this问题
changeIndex: throttle(function (index){
this.currentIndex = index
},50),
}
17、编程式导航+事件委托实现三级联动路由跳转
- 三级标签列表有很多,每一个标签都是一个页面链接,我们要实现通过点击表现进行路由跳转。
- 路由跳转的两种方法:router-link导航式路由,编程式路由。
- 对于导航式路由,我们有多少个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属性获取节点的属性信息。
<div class="all-sort-list2" @click="goSearch">
<!-- 从后台获取数据 -->
<div class="item" v-for="(c1, index) in categoryList" :key="c1.categoryId"
:class="{ cur: currentIndex == index }">
<h3 @mouseenter="changeIndex(index)">
<a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId">{{
c1.categoryName
}}</a>
</h3>
<!-- 二,3级分类 -->
<div class="item-list clearfix" :style="{ display: currentIndex == index ? 'block ' : 'none' }">
<div class="subitem" v-for="(c2, index) in c1.categoryChild" :key="c2.categoryId">
<dl class="fore">
<dt>
<a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{{
c2.categoryName
}}</a>
</dt>
<dd>
<em v-for="(c3, index) in c2.categoryChild" :key="c3.categoryId">
<a :data-categoryName="c3.categoryName" :data-category3Id="c3.categoryId">{{
c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
注意:event是系统属性,所以我们只需要在函数定义的时候作为参数传入,在函数使用的时候不需要传入该参数。
//进行路由跳转的方法
goSearch(event) {
//第一个问题:div父节点子元素太多【h3、h2、em、dt、dd、dl...a】?你怎么知道你点击的一定是a
// 为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)
let element = event.target;
//节点有一个属性dateset属性,可以获取节点的自定义属性与属性值
let { categoryname, category1id, category2id, category3id } = element.dataset;
//如果标签身上有categoryname 一定是a标签
if (categoryname) {
//整理路由跳转的参数
let location = { name: 'search' };
let query = { categoryName: categoryname };
//第二个问题:要区分一级分类、二级分类、三级分类的a标签【category1Id|category2Id|category2Id】
if (category1id) {
query.category1Id = category1id;
} else if (category2id) {
query.category2Id = category2id;
} else {
query.category3Id = category3id;
}
//如果路由跳转时带有params参数,也要传递过去
if (this.$route.params) {
location.params = this.$route.params;
//整理完参数
location.query = query;
//路由跳转
this.$router.push(location);
}
}
},
优化:Vue路由销毁问题
- Vue在路由切换的时候会销毁旧路由。我们在三级列表全局组件TypeNav中的mounted进行了请求一次商品分类列表数据。由于Vue在路由切换的时候会销毁旧路由,当我们再次使用三级列表全局组件时还会发一次请求。如下图所示:当我们在包含三级列表全局组件的不同组件之间进行切换时,都会进行一次信息请求。
- 由于信息都是一样的,出于性能的考虑我们希望该数据只请求一次,所以我们把这次请求放在App.vue的mounted中。(根组件App.vue的mounted只会执行一次)
mounted() { //App的根组件挂载完毕,发一次请求即可. this.$store.dispatch("categoryList"); },
- 注意:虽然main.js也是只执行一次,但是不可以放在main.js中。因为只有组件的身上才会有$store属性。
18、mock插件使用(模拟数据)
- mock的数据当作真实的服务器返回的数据,mock发的请求会被浏览器拦截。
- 将不同的数据类型封装为不同的json文件,创建mockServer.js文件
- banner、floor分别为轮播图和页面底部的假数据。
- mockServer.js文件
//记得要在main.js中引入一下
//import ''@/mock/mockServer
//引入mockjs插件开始模拟数据
import Mock from 'mockjs';
//引入数据:JSON数据格式数据
//比如:图片资源、JSON资源【里面不能书写export关键字】,这些资源默认对外暴露【默认暴露】
import banner from './banner.json';
import floor from './floor.json';
//接口:相当于nodejs里面中间件
//第一个参数:接口的地址 第二个参数:向这个接口发请求获取到的数据
//Mock插件:中间件默认是GET请求
Mock.mock("/mock/banner", { code: 200, data: banner });
//Mock插件:中间件默认是GET请求
Mock.mock('/mock/floor', { code: 200, data: floor });
19、swiper插件实现轮播图
- 使用方法:
(1)安装swiper
(2)在需要使用轮播图的组件内导入swpier和它的css样式
(3)在组件中创建swiper需要的dom标签(html代码,参考官网代码)
(4)创建swiper实例
- 注意:在创建swiper对象时,我们会传递一个参数用于获取展示轮播图的DOM元素,官网直接通过class(而且这个class不能修改,是swiper的css文件自带的)获取。但是这样有缺点:当页面中有多个轮播图时,因为它们使用了相同的class修饰的DOM,就会出现所有的swiper使用同样的数据,这肯定不是我们希望看到的。
- 解决方法:在轮播图最外层DOM中添加ref属性
<div class="swiper-container" ref="cur">
- 通过ref属性值获取DOM,在new Swiper之前页面中的结构必须要有
let mySwiper = new Swiper(this.$refs.cur,{...})
<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>
- 接下来要考虑的是什么时候去加载这个swiper,我们第一时间想到的是在mounted中创建这个实例。但是会出现无法加载轮播图片的问题。
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',
},
})
},
- 原因:我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。
- 解决方法一(不好):等我们的数据请求完毕后再创建swiper实例。只需要加一个1000ms时间延迟再创建swiper实例.。将上面代码改为
mounted() {
//派发action:通过vuex发送Ajax请求,将数据存储在仓库中
this.$store.dispatch('getBannerList');
//在new Swiper之前,页面中的结构必须有
// 第一种
setTimeout(() => {
var mySwiper = new Swiper(document.querySelector('.swiper-container'), {
loop: true,
//如果需要分页器
pagination: {
el: '.swiper-pagination',
//点击小球时切换图片
clickable: true,
},
//如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
}
})
}, 1000);
},
- 解决方法二:我们可以使用watch监听bannerList轮播图列表属性,因为bannerList初始值为空,当它有数据时,我们就可以创建swiper对象
watch: {
//监听bannerList数据的变化:因为这条数据发生过变化---由空数组变为数组里面有四个元素
bannerList: {
immediate: true,
handler(newValue, oldValue) {
//如果执行handler方法,代表组件实例身上这个属性已经有了【数组:四个元素】
//当前的函数执行只能保证数据已经有了,但是不能保证v-for已经执行结束,v-for执行完毕才有结构
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)
watch: {
list: {
immediate: true,
handler() {
//nextTick:在下次DOM更新(服务器的数据回来后 此时结构已经有了) 循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,获取更新后的DOM
//只能监听到数据已经有了,但是v-for动态渲染结构还是没办法确定,因此还是需要nextTick
this.$nextTick(() => {
//当你执行这个回调时:保证服务器数据回来了,v-for已经执行结束,轮播图的结构一定有了
var mySwiper = new Swiper(this.$refs.cur, {
loop: true,
//如果需要分页器
pagination: {
el: '.swiper-pagination',
//点击小球时切换图片
clickable: true,
},
//如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
}
})
})
}
}
}
- 注意:之前我们在学习watch时,一般都是监听的定义在data中的属性,但是我们这里是监听的computed中的属性,这样也是完全可以的,并且如果你的业务数据也是从store中通过computed动态获取的,也需要watch监听数据变化执行相应回调函数,完全可以模仿上面的写法
<template>
<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>
//引入Swiper
import Swiper from 'swiper';
export default {
name: 'Carousel',
props: ['list'],
watch: {
list: {
immediate: true,
handler() {
//只能监听到数据已经有了,但是v-for动态渲染结构还是没办法确定,因此还是需要nextTick
this.$nextTick(() => {
var mySwiper = new Swiper(this.$refs.cur, {
loop: true,
//如果需要分页器
pagination: {
el: '.swiper-pagination',
//点击小球时切换图片
clickable: true,
},
//如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
}
})
})
}
}
}
}
</script>
<style>
</style>
20、props父子组件通信
- 原理:父组件设置一个属性绑定要传递的数据,子组件props接受该属性值
- 本项目的
- 父组件:home文件下的home.js
<template>
<div>
//...
<!-- props用于父子组件通讯;自定义事件:@on @emit可以实现子给父通信;全局事件总线:$bus 全能 -->
<!-- 父组件通过自定义属性list给子组件传递数据-->
<Floor v-for="(floor, index) in floorList" :key="floor.id" :list="floor"></Floor>
</div>
</template>
- 子组件:Floor下的floor.vue
<template>
<!--楼层-->
<div class="floor">
//...省略
</div>
</template>
<script>
export default {
name: "floor",
//子组件通过props属性接受父组件传递的数据
props:['list']
}
</script>
-
组件通信方式:
- 第一种父子组件通信:
-
$ on、$emit自定义事件实现子组件给父组件传递信息。 props实现父组件给子组件传递数据。
- 第二种全局事件总线 $bus(适用于所有的场景)
- 第三种Vuex
- 第四中插槽(适用于父子组件通信)
- 组件通信方式连接
21、将轮播图模块提取为公共组件
- 需要注意的是我们要把定义swiper对象放在mounted中执行,并且还要设置immediate:true属性,这样可以实现,无论数据有没有变化,上来立即监听一次。
- 上一小节刚刚讲了props实现父组件向子组件传递消息,这里同样也会将轮播图列表传递给子组件,原理相同。
公共组件Carousel代码
<template>
<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>
//引入Swiper
import Swiper from 'swiper';
export default {
name: 'Carousel',
props: ['list'],
watch: {
list: {
immediate: true,
handler() {
//只能监听到数据已经有了,但是v-for动态渲染结构还是没办法确定,因此还是需要nextTick
this.$nextTick(() => {
var mySwiper = new Swiper(this.$refs.cur, {
loop: true,
//如果需要分页器
pagination: {
el: '.swiper-pagination',
//点击小球时切换图片
clickable: true,
},
//如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
}
})
})
}
}
}
}
</script>
<style>
</style>
- Floor组件引用Carousel组件<Carousel :carouselList="list.carouselList"/>
-
ListContainer组件引用Carousel组件 <Carouse :carouselList="bannerList"/>
22、getters使用
- getters是vuex store中的计算属性。如果不使用getters属性,我们在组件获取state中的数据表达式为:
this.$store.state.子模块.属性
- 如果有多个组件需要用到此属性,我们要么复制这个表达式,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
- 个人理解:getters将获取store中的数据封装为函数,代码维护变得更简单(和我们将请求封装为api一样)。而且getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
- 注意:仓库中的getters是全局属性,是不分模块的。即store中所有模块的getter内的函数都可以通过$store.getters.函数名获取
//计算属性,是为了简化仓库数据
const getters = {
//当前形参state,当前仓库中的state,并非大仓库中的那个state
//若没有网 state.searchList.goodsList 返回的是undefined 所以要加[]
goodsList(state) {
return state.searchList.goodsList || [];
},
}
- 在Search组件中使用getters获取仓库数据
computed: {
//mapGetters里面的写法传递的是数组,因为getters计算没有划分模快
...mapGetters(['goodsList']),
}
23、Object.assign
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}}
- 把参数声明成动态数据,在本项目搜索页面发送请求前合并参数,并把发送请求封装成一个函数,在需要发请求时调用即可
data() {
return {
//带给服务器的参数
searchParams: {
category1Id: "", //一级分类的id
category2Id: "", //二级分类的id
category3Id: "", //三级分类的id
categoryName: "", //商品的名字
keyword: "", //用户搜索的关键字
props: [], //商品属性的搜索条件
trademark: "", //品牌的搜索条件
order: "1:desc", //排序的参数 【默认初始值:1:desc】
pageNo: 1, //当前分页器的页码 【默认初始值:1】
pageSize: 3, //代表当前一页显示几条数据 【默认初始值:10】
}
}
},
//组件挂载完毕前执行一次,在mounted之前
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;*/
// Object.assign:ES6新增语法,合并对象
Object.assign(this.searchParams, this.$route.query, this.$route.params);
},
//组件挂载完毕执行一次
mounted() {
// 在发送请求之前带给服务器参数(searchParams参数发生变化有数值带给服务器
this.getData();
},
methods: {
//向服务器发请求获取search模块数据(根据参数不同返回不同的数据进行展示)
//把这次请求封装成一个函数:当你需要在调用的时候调用即可
getData() {
//先测试接口返回的数据格式
this.$store.dispatch('getSearchList', this.searchParams);
},
}
24、利用路由信息变化实现动态搜索
- 最初想法:search页面中在每个三级列表和搜索按钮加一个点击触发事件,只要点击了就执行搜索函数。这是一个很蠢的想法,如果这样就会生成很多回调函数,很耗性能。
- 最佳方法:我们每次进行新的搜索时,我们的query和params参数中的部分内容肯定会改变,而且这两个参数是路由的属性。我们可以通过监听路由信息的变化来动态发起搜索请求。
- 如下图所示,$route是组件的属性,所以watch是可以监听的(watch可以监听组件data中所有的属性)
注意:组件中data的属性包括:自己定义的、系统自带的(如 $route)、父组件向子组件传递的等等。
//数据监听:监听组件实例身上的属性的属性值变化
watch: {
//监听路由信息是否发生变化,如果变化,再次发起请求
$route(newValue, oldValue) {
//再次发请求之前整理带给服务器参数
Object.assign(this.searchParams, this.$route.query, this.$route.params)
//再次发ajax请求
this.getData();
//每次请求完毕,应该把相应得1,2,3级分类的id置空,让他接受下一次的相应的1,2,3级分类的id
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
}
}
25、面包屑相关操作
- 本次项目的面包屑操作主要就是两个删除逻辑。分为:当分类属性(query)删除时删除面包屑同时修改路由信息。当搜索关键字(params)删除时删除面包屑、修改路由信息、同时删除输入框内的关键字。
- 面包屑生成逻辑:判断searchParams相关属性是否存在,存在即显示。
- 1、query删除时:因为此部分在面包屑中是通过categoryName展示的,所所以删除时应将该属性值制空或undefined。可以通过路由再次跳转修改路由信息和url链接
//删除分类名字
removeCategoryName() {
//把带给服务器的参数置空,还需向服务器发送请求
//带给服务器的参数说明可有可无:如果属性值为空的字符串还是会把相应的字段带给服务器,但是把相应字段变为undefined,当前字段不会带给服务器
this.searchParams.categoryName = undefined;
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
this.getData();
//地址栏也需要修改:进行路由跳转(现在的路由跳转只是跳转到自己)
if (this.$route.params) {
this.$router.push({ name: 'search', params: this.$route.params });
}
},
- 2、params删除时:和query删除的唯一不同点是此部分会多一步操作:删除输入框内的关键字(因为params参数是从输入框内获取的)输入框实在Header组件中的 ,header和search组件是兄弟组件,要实现该操作就要通过兄弟组件之间进行通信完成。
- 这里通过$bus实现header和search组件的通信。$bus使用:
- (1)在main.js中注册
new Vue({
render: h => h(App),
//全局事件总线$bus
beforeCreate() {
Vue.prototype.$bus = this;
Vue.prototype.$API = API;
},
//注册路由:地下的写法是KV一致 省略V【router小写】
//注册路由信息,当这里书写router的时候,组件身上都拥有$router(进行编程式导航进行路由跳转 push|replace),$route(获取路由信息:路径,querry,params等)属性
router,
// 注册仓库:组件实例的身上会多一个$store属性
store
}).$mount('#app')
- (2)search组件使用$bus通信,第一个参数可以理解为为通信的暗号,还可以有第二个参数(用于传递数据),我们这里只是用于通知header组件进行相应操作,所以没有设置第二个参数。
//删除关键字
removeKeyword() {
//把给服务器的参数searchParams的keyword置空
this.searchParams.keyword = undefined;
//再次向服务器发送请求
this.getData();
//通知兄弟组件Header清楚关键字
this.$bus.$emit('clear');
//地址栏也需要修改:进行路由跳转(现在的路由跳转只是跳转到自己)
if (this.$route.query) {
this.$router.push({ name: 'search', query: this.$route.query });
}
},
- (3)header组件接受$bus通信,开始$on事件
- 注意:组件挂载时就监听clear事件
mounted() {
//通过全局事件总线清楚关键字
this.$bus.$on('clear', () => {
this.keyword = '';
});
},
26、SearchSelector子组件传参及面包屑操作
- 在25小节中描述了通过query、params参数生成面包屑,以及面包屑的删除操作对应地址栏url的修改。但是SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性。
- 子给父通信使用自定义事件
- (1)在父组件search中:
//在父组件中定义一个函数
<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo" />
//自定义事件回调
trademarkInfo(trademark) {
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;
this.getData();
},
- (2)在子组件SearchSelector中
<!-- 品牌 -->
<ul class="logo-list">
<li v-for="(trademark, index) in trademarkList" :key="trademark.tmId" @click="tradeMarkHandler(trademark)">{{ trademark.tmName}}</li>
</ul>
methods: {
//品牌事件处理函数
tradeMarkHandler(trademark) {
//点击品牌后 整理参数 向服务器发送请求获取相应数据进行展示(在父组件发请求,因为父组件中searchParams参数是带给服务器参数,子组件把你点击的品牌信息,需要给父组件传递过去---自定义事件)
this.$emit('trademarkInfo', trademark);
},
}
- 删除面包屑同25节
- 对于平台售卖属性也是使用自定义事件实现通信然后实现删除更新参数操作。
27、商品排序
- 排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。order属性值为字符串,例如‘1:asc’、‘2:desc’。1代表综合,2代表价格,asc代表升序,desc代表降序。
- 在public文件index引入该css,在search模块使用该图标
<link rel="stylesheet" href="https://at.alicdn.com/t/font_2994457_qqwrvmss9l9.css">
<!-- 排序的结构 -->
<ul class="sui-nav">
<li :class="{ active: isOne }" @click="changeOrder('1')">
<a>综合<span v-show="isOne" class="iconfont" :class="{ 'icon-UP': isAsc, 'icon-DOWN': isDesc }"></span></a></li>
<li :class="{ active: isTwo }" @click="changeOrder('2')">
<a>价格<span v-show="isTwo" class="iconfont" :class="{ 'icon-UP': isAsc, 'icon-DOWN': isDesc }"></span></a>
</li>
</ul>
computed: {
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('des') != -1;
},
},
methods: {
//排序操作
changeOrder(flag) {
//flag代表用户点击的是综合还是价格(用户点击的时候传递进来
let originOrder = this.searchParams.order;
//这里获取的是最开始的状态
let originFlag = this.searchParams.order.split(':')[0];
let originSort = this.searchParams.order.split(':')[1];
//准备一个新的order属性值
let newOrder = '';
//判断的是多次点击的是不是同一个按钮
if (flag == originFlag) {
newOrder = `${originFlag}:${originSort == 'desc' ? 'asc' : 'desc'}`;
} else {
//点击的不是同一个按钮
newOrder = `${flag}:${'desc'}`;
}
//将新的order赋予searchParams
this.searchParams.order = newOrder;
//再次发请求
this.getData();
},
}
28、手写分页器
- 实际开发中是不会手写的,一般都会用一些开源库封装好的分页,比如element ui。
- 核心属性:pageNo(当前页码)、pageSize、total、continues(连续展示的页码).
- 核心逻辑是获取连续页码的起始页码和末尾页码,通过计算属性获得。(计算属性如果想返回多个数值,可以通过对象形式返回)
- 父组件Search把需要的数据传给pagination子组件
<!-- 分页器 -->
<Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="total" :continues="5" @getPageNo="getPageNo"></Pagination>
- pagination子组件使用props接收数据,并处理;当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。
<template>
<div class="pagination">
<button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)">上一页</button>
<button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)"
:class="{ active: pageNo == 1 }">1</button>
<button v-if="startNumAndEndNum.start > 2">...</button>
<!-- 中间部分 -->
<button v-for="(page, index) in startNumAndEndNum.end" :key="index" 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" @click="$emit('getPageNo', pageNo + 1)">下一页</button>
<button style="margin-left: 30px">共{{ total }}条</button>
</div>
</template>
<script>
export default {
name: 'Pagination',
props: ['pageNo', 'pageSize', 'total', 'continues'],
computed: {
//总共多少页
totalPage() {
return Math.ceil(this.total / this.pageSize);
},
//计算出连续的页码的起始数据与结束数据(连续页码数据至少是5)
startNumAndEndNum() {
const { continues, totalPage, pageNo } = this;
//存储起始和结束数据
let start = 0, end = 0;
//连续页码数字5(至少五页),若没有5页
if (continues > totalPage) {
start = 1;
end = totalPage;
} else {
//连续页码数字5(至少五页),有5页或更多
start = pageNo - parseInt(continues / 2);
end = pageNo + parseInt(continues / 2);
//start数字出现0|负数,
if (start < 1) {
start = 1;
end = continues;
}
// end超过总页数
if (end > totalPage) {
end = totalPage;
start = totalPage - continues + 1;
}
}
return { start, end }
}
},
}
</script>
29、滚动条
- 使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
- router滚动行为
30、undefined细节(*****)
- 访问undefined的属性值会引起红色警告,可以不处理,但是要明白警告的原因。
- 以获取商品categoryView信息为例,categoryView是一个对象。对应的getters代码
const getters = {
//路径导航简化的数据
categoryView(state) {
return state.goodInfo.categoryView || {};
},
}
- 对应的detail的computed代码和html代码
computed:{
...mapGetters(['categoryView'])
}
<div class="conPoin">
<span v-show="categoryView.category1Name" >{{categoryView.category1Name}}</span>
<span v-show="categoryView.category2Name" >{{categoryView.category2Name}}</span>
<span v-show="categoryView.category3Name" >{{categoryView.category3Name}}</span>
</div>
- 细节在于getters的返回值。如果getters上面代码写为return state.goodInfo.categoryView,页面可以正常运行,但是会出现红色警告。
- 原因:假设我们网络故障,导致goodInfo的数据没有请求到,即goodInfo是一个空的对象,当我们去调用getters中的return state.goodInfo.categoryView时,因为goodInfo为空,所以也不存在categoryView,即我们getters得到的categoryView为undefined。所以我们在html使用该变量时就会出现没有该属性的报错。即:网络正常时不会出错,一旦无网络或者网络问题就会报错。
- 总结:所以我们在写getters的时候要养成一个习惯在返回值后面加一个||条件。即当属性值undefined时,会返回||后面的数据,这样就不会报错。
- 如果返回值为对象加||{},数组:||[ ]。
- 此处categoryView为对象,所以将getters代码改为return state.goodInfo.categoryView||{}
31、商品详情
- 商品详情唯一难点就是点击轮播图图片时,改变放大镜组件展示的图片。
- 老师的方法很巧妙:在轮播图组件中设置一个currendIndex,用来记录所点击图片的下标,并用currendIndex实现点击图片高亮设置。当符合图片的下标满足
currentIndex===index
时,该图片就会被标记为选中。
<template>
<div class="swiper-container" ref="cur">
<div class="swiper-wrapper">
<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>
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
</template>
- 轮播图组件和放大镜组件是兄弟组件,所以要通过全局总线通信。在轮播图组件中,点击图片触发全局事件changeCurrentIndex,参数为图片所在数组的下标。
changeCurrentIndex(index) {
//修改响应式数组
this.currentIndex = index;
//通知兄弟组件:当前的索引值为几
this.$bus.$emit('getIndex', this.currentIndex);
}
},
- 对应的放大镜组件Zoom,首先在mounted接收ImageList传过来的图片下标
mounted() {
//全局事件总线:获取兄弟组件传递过来的索引值
this.$bus.$on('getIndex', (index) => {
//修改当前响应式数组
this.currentIndex = index;
})
},
- 放大镜组件中也会有一个响应式数据currentIndex,他表示大图中显示的图片的下标(因为放大镜组件只能显示一张图片),全局事件传递的index赋值给currentIndex ,通过computed计算属性改变放大镜组件展示的图片。
computed: {
imgObj() {
return this.skuImageList[this.currentIndex] || {};
}
},
- zoom组件展示图片的html代码
<img :src="imgObj.imgUrl " />
32、失焦事件
- blur与change事件在绝大部分情况下表现都非常相似,输入结束后,离开输入框,会先后触发change与blur,唯有两点例外。
- (1) 没有进行任何输入时,不会触发change。在这种情况下,输入框并不会触发change事件,但一定会触发blur事件。在判断表单修改状态时,这种差异会非常有用,通过change事件能轻易地找到哪些字段发生了变更以及其值的变更轨迹。
- (2)输入后值并没有发生变更。这种情况是指,在没有失焦的情况下,在输入框内进行返回的删除与输入操作,但最终的值与原值一样,这种情况下,keydown、input、keyup、blur都会触发,但change依旧不会触发
33、加入购物车成功路由
- 点击加入购物车时,会向后端发送API请求,但是该请求的返回值中data为null,所以我们只需要根据状态码code判断是否跳转到‘加入购物车成功页面’。
- detail组件‘加入购物车’请求函数:
//加入购物车的回调函数
async addShopCar() {
//1.发请求,将产品加入到数据库(通知服务器),当前这里派发action,也像服务器发送请求,但要判断加入购物车是否成功
//this.$store.dispatch('addOrUpdateShopCart', { skuId: this.$route.params.skuId, skuNum: this.skuNum }):调用仓库中的addOrUpdateShopCart,当前函数如果执行返回Promise(要么成功,要么失败)
try {
await this.$store.dispatch('addOrUpdateShopCart', { skuId: this.$route.params.skuId, skuNum: this.skuNum });
//成功就路由跳转,在路由跳转的时候还需要将产品信息带给下一级的路由组件
//一些简单的数据skuNum,通过query形式给路由组件传递过去,产品信息的数据(skuInfo),通过会话存储(不持久化,会话结束数据在消失)
//本地存储|会话存储,一般存储的是字符串
sessionStorage.setItem("SKUINFO", JSON.stringify(this.skuInfo));
this.$router.push({ path: "/addcartsuccess", query: { skuNum: this.skuNum } });
} catch (error) {
// 失败,给用户进行提示
alert("加入购物车失败");
}
// 2.服务器存储成功--进行路由跳转传递参数
}
- detail store对应代码:
//将产品添加到购物车
async addOrUpdateShopCart({ commit }, { skuId, skuNum }) {
//加入购物车后,前台将参数带给服务器,服务器写入数据成功,并没有返回其他数据,只返回code=200,不反悔其余数据,因此不需要三连环存储数据
let result = await reqAddOrUpdateShopCart(skuId, skuNum);
if (result.code == 200) {
return 'ok'
} else {
return Promise.reject(new Error('fail'));
}
}
- 当我们想要实现两个毫无关系的组件传递数据时,首相想到的就是路由的query传递参数,但是query适合传递单个数值的简单参数,所以如果想要传递对象之类的复杂信息,就可以通过Web Storage实现。
- sessionStorage:为每一个给定的源维持一个独立的存储区域,该区域在页面会话期间可用(即只要浏览器处于打开状态,包括页面重新加载和恢复)。
- localStorage:同样的功能,但是在浏览器关闭,然后重新打开后数据仍然存在。
-
注意:无论是session还是local存储的值都是字符串形式。如果我们想要存储对象,需要在存储前JSON.stringify()将对象转为字符串,在取数据后通过JSON.parse()将字符串转为对象
34、购物车游客身份
- 想要获取详细信息,还需要一个用户的uuidToken,用来验证用户身份。但是该请求函数没有参数,所以我们只能把uuidToken加在请求头中。
- 创建utils工具包文件夹,创建生成uuid的js文件,对外暴露为函数(记得导入uuid => npm install uuid)。
- 生成临时游客的uuid(随机字符串),每个用户的uuid不能发生变化,还要持久存储
import { v4 as uuidv4 } from 'uuid';
//要生成一个随机字符串,且每次执行不能发生变化,游客身份持久存储
export const getUUID = () => {
//先从本地存储获取uuid(看一下本地存储是否有)
let uuid_token = localStorage.getItem('UUIDTOKKEN');
//如果没有则生成
if (!uuid_token) {
uuid_token = uuidv4();
//本地存储一次
localStorage.setItem('UUIDTOKKEN', uuid_token);
}
return uuid_token;
}
- 用户的uuid_token定义在store中的detail模块
const state = {
goodInfo:{},
//游客身份
uuid_token: getUUID()
}
- 在request.js中设置请求头
//请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发送出去之前做一些事情
requests.interceptors.request.use((config) => {
//config:配置对象。对象里面有一个属性很重要,headers请求头
//进度条开始
if (store.state.detail.uuid_token) {
//给请求头添加一个字段userTempId
config.headers.userTempId = store.state.detail.uuid_token;
}
//需要携带token带给服务器
/* if (store.state.user.token) {
config.headers.token = store.state.user.token;
}*/
nprogress.start();
return config;
});
- 注意this.$store只能在组件中使用,不能再js文件中使用。如果要在js中使用,需要引入
import store from '@/store';
35、购物车商品列表
- 判断底部勾选框是否全部勾选代码部分
//判断底部复选框是否勾选(全部产品都勾选,才勾选)
// every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
isAllChecked() {
return this.cartInfoList.every(item => item.isChecked == '1');
}
- 修改商品数量前端代码部分:
- 注意:通过@click、@change触发handler函数改变商品数量。
<li class="cart-list-con5">
<a 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 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 > 2 ? -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) {
}
}, 500),
36、购物车状态修改和商品删除
- 这部分都比较简单,这里不多做赘述,唯一需要注意的是当store的action中的函数返回值data为null时,应该采用下面的写法(重点是if,else部分)
- action部分:以删除购物车某个商品数据为例
//删除购物车某一个产品
async deleteCartListBySkuId({ commit }, skuId) {
let result = await reqDeleteCartById(skuId);
if (result.code == 200) {
return 'ok';
} else {
return Promise.reject(new Error('fail'));
}
},
- method部分:(重点是try、catch)
//删除某一产品
async deleteCartById(cart) {
try {
await this.$store.dispatch('deleteCartListBySkuId', cart.skuId);
this.getData();
} catch (error) {
alert(error.message);
}
},
37、删除多个商品(actions扩展)
- 由于后台只提供了删除单个商品的接口,所以要删除多个商品时,只能多次调用actions中的函数。
- 我们可能最简单的方法是在method的方法中多次执行dispatch删除函数,当然这种做法也可行,但是为了深入了解actions,我们还是要将批量删除封装为actions函数。
- actions扩展,官网的教程,一个标准的actions函数如下所示:
deleteAllCheckedById(context) {
console.log(context)
}
- 我们可以看一下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(element => {
let promise = element.isChecked == 1 ? dispatch('deleteCartListBySkuId', element.skuId) : '';
PromiseAll.push(promise);
});
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
//修改全部产品的状态
updateAllCartIsChecked({ dispatch, state }, isChecked) {
let PromiseAll = [];
state.cartList[0].cartInfoList.forEach(item => {
let promise = dispatch('updateCheckedById', { skuId: item.skuId, isChecked });
PromiseAll.push(promise);
});
return Promise.all(PromiseAll);
},
- method
//修改全部产品的选中状态
async updateAllCartChecked(event) {
try {
let isChecked = event.target.checked ? '1' : '0';
await this.$store.dispatch('updateAllCartIsChecked', isChecked);
this.getData();
} catch (error) {
alert(error.message);
}
},
38、注册登录业务(ES6 const新用法)
1、ES6 const新用法
const {comment,index,deleteComment} = this
- 上面的这句话是一个简写,最终的含义相当于:
const comment = this.comment
const index = this.index
const deleteComment = this.deleteComment
2、用户注册模块
- 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('fail'));
}
},
//用户注册
async userRegister({ commit }, user) {
let result = await reqUserRegister(user);
if (result.code == 200) {
return 'ok';
} else {
return Promise.reject(new Error('fail'));
}
},
- 组件:
data() {
return {
//收集表单数据--手机号
phone: '',
//验证码
code: '',
//密码
password: '',
//确认密码
password1: '',
//是否同意协议
agree: true,
}
},
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)
}
},
//用户注册
async userRegister() {
const success = await this.$validator.validateAll(); //全部表单验证
if (success) {
try {
//如果成功--路由跳转
const { phone, code, password, password1 } = this;
await this.$store.dispatch('userRegister', { phone, code, password });
this.$router.push('/login');
} catch (error) {
alert(error.message)
}
}
},
},
3、用户登录
- actions部分
//用户登录业务(token)
async userLogin({ commit }, data) {
let result = await reqUserLogin(data);
//服务器下发token,将来经常通过带token找服务器要用户信息进行展示
if (result.code == 200) {
//用户已经登陆成功且获取到token
commit('USERLOGIN', result.data.token);
//持久化存储token
setToken(result.data.token);
//localStorage.setItem('TOKEN',result.data.token)
return 'ok';
} else {
return Promise.reject(new Error(result.message));
}
},
- 组件
data() {
return {
phone: '',
password: '',
}
},
methods: {
//登录
async userLogin() {
try {
//登录成功
const { phone, password } = this;
(phone && password) && await this.$store.dispatch('userLogin', { phone, password });
//登录的路由组件:看路由当中是否包含query参数,有:跳到query参数指定路由,没有:跳到home
let toPath = this.$route.query.redirect || 'home';
this.$router.push(toPath);
} catch (error) {
alert(error.message);
}
}
},
4、登陆成功后获取用户信息并展示在首页
- 请求拦截器
//请求拦截器
requests.interceptors.request.use((config) => {
if (store.state.detail.uuid_token) {
config.headers.userTempId = store.state.detail.uuid_token
}
//需要携带token带给服务器
if (store.state.user.token) {
config.headers.token = store.state.user.token;
}
nProgress.start();
return config;
});
- actions部分
// 获取用户信息
async getUserInfo({ commit }) {
let result = await reqUserInfo();
console.log(result);
if (result.code == 200) {
//提交用户信息
commit('GETUSERINFO', result.data);
return 'ok';
} else {
return Promise.reject(new Error(result.message));
}
},
- header组件:
<!-- 没有用户名:未登录 -->
<p v-if="!username">
<span>请</span>
<!-- 声明式导航务必要有to属性 -->
<router-link to="/login">登录</router-link>
<router-link to="/register" class="register">免费注册</router-link>
</p>
<!-- 登陆了 -->
<p v-else>
<a>{{ username }}</a>
<a class="register" @click="logout">退出登录</a>
</p>
computed: {
//用户名信息
username() {
return this.$store.state.user.userInfo.name;
}
},
- home首页
- 刷新后token会消失,因此要在mounted中再次发请求并持久化存储token
mounted() {
//派发action获取floor组件的数据
this.$store.dispatch('getFloorList');
//获取用户信息在首页展示
this.$store.dispatch('getUserInfo');
},
- state部分
const state = {
code: '',
token: getToken(),
// token: localStorage.getItem('TOKEN'),
userInfo: {},
};
5、退出登录
- action部分
//退出登录
async userLogout({ commit }) {
//只是向服务器发起一次请求,通知服务器清除token
let result = await reqLogout();
if (result.code == 200) {
//action里不能操作state,只能提交mutation修改state
commit('CLEAR');
return 'ok';
} else {
return Promise.reject(new Error(result.message));
}
},
//清除本地数据
CLEAR(state) {
//把仓库中相关信息和本地存储清空
state.token = '';
state.userInfo = {};
removeToken();
}
- Header组件
<a class="register" @click="logout">退出登录</a>
//退出登录
async logout() {
//1.发请求,通知服务器退出登录,清除一些数据:token;2.清除项目中的数据(userinfo,token)
try {
//退出成功
await this.$store.dispatch('userLogout');
// 回到首页
this.$router.push('/home');
} catch (error) {
}
},
39、导航守卫
- router index.js全局前置守卫代码
//全局守卫:前置守卫(在路由跳转前进行判断
router.beforeEach(async (to, from, next) => {
//next:放行函数,放行到指定的路由 next();
//用户登录了,才会有token,未登录一定不会有token
let token = store.state.user.token
//用户信息
let name = store.state.user.userInfo.name;
if (token) {
//登陆了,就不能再去login
if (to.path == '/login' || to.path == '/register') {
next("/home");
} else {
//登陆了,去的不是login
//如果用户名已有
if (name) {
next();
} else {
//没有用户信息,派发action让仓库存储用户信息在跳转
try {
//获取用户信息成功
await store.dispatch('getUserInfo');
next();
} catch (error) {
//token失效,获取不到用户信息,需要重新登陆
//1.清除token
await store.dispatch('userLogout');
next('/login')
}
}
}
} else {
//未登录:不能去交易相关,支付相关,不能去个人中心
//未登录去上面的路由---登录,去的不是上面的路由--放行
let toPath = to.path;
if (toPath.indexOf('/trade') != -1 || toPath.indexOf('/pay') != -1 || toPath.indexOf('/center') != -1) {
//把未登录前想去而没有去成的信息,存储到地址栏(路由)
next('/login?redirect=' + toPath);
}
else {
next();
}
}
})
40、交易模块
- (1)封装API(2)vuex三件套(3)dispatch发请求(4)数据渲染
- 注意:(3)中,如果在发请求之后还有一些对返回数据的操作,应考虑到是否需要async await。
//弹出框
async open() {
//生成一个二维码URL
let url = await QRCode.toDataURL(this.payInfo.codeUrl);
//第一个参数:即为内容区域
//第二个参数:标题
//第三个参数:组件的配置项
this.$alert(`<img src=${url} />`, "请你微信扫码支付", {
dangerouslyUseHTMLString: true, //将字符串转换为标签
center: true, //居中
showClose: false, //右上角的关闭按钮不显示
confirmButtonText: "支付成功", //确定按钮的文本
showCancelButton: true, //显示取消按钮
cancelButtonText: "支付遇见问题", //取消按钮的文本
closeOnClickModal: true, //点击遮罩层关闭messagebox
//关闭弹窗设置
beforeClose: (type, instance, done) => { //在消息盒子关闭之前会触发
//type参数:可以区分用户点击的是取消【cancel】、确定【confirm】
//instance参数:当前消息框组件VC
//done参数:是一个函数,函数可以关闭消息盒子
if (type == 'cancel') {
alert('支付遇见问题请联系超管豪哥');
//清除定时器
clearInterval(this.timer);
this.timer = null;
//关闭盒子
done();
} else {
//判断是否真的支付了
// if (this.code == 200) {
//清除定时器
clearInterval(this.timer);
this.timer = null;
//关闭盒子
done();
//路由跳转
this.$router.push('/paysuccess');
// }
}
}
});
//需要知道支付成功||失败,成功则路由跳转,失败则提示信息
if (!this.timer) {
//定时器没有,则开启一个新的定时器
this.timer = setInterval(async () => {
//发请求获取用户支付状态
let result = await this.$API.reqPayStatus(this.orderId)
if (result.code == 200) {
//1.清除定时器
clearInterval(this.timer);
this.timer = null;
//2.保存支付成功返回的code
this.code = result.code;
//3.隐藏弹框
this.$msgbox.close();
//4.跳转到下一路由
this.$router.push('/paysuccess');
}
}, 1000);
}
},
1.Vue图片引入
- 图片引入分两种:js内引入图片,非js内引入图片。
- 非js内引入图片(html):一般都是通过路径引入,例如:<img src="../assets/pay.jpg">。
- js内引入图片: 可分为通过路径引入和不通过路径引入。
- 1、如果想要通过路径方式在vue中的js引入图片,必须require引入。
- 例如:js中引入个人支付二维码可以通过下面方式实现
this.$alert(`<img src=${url} />`, "请你微信扫码支付", {
dangerouslyUseHTMLString: true, //将字符串转换为标签
center: true, //居中
showClose: false, //右上角的关闭按钮不显示
confirmButtonText: "支付成功", //确定按钮的文本
showCancelButton: true, //显示取消按钮
cancelButtonText: "支付遇见问题", //取消按钮的文本
closeOnClickModal: true, //点击遮罩层关闭messagebox
//关闭弹窗设置
beforeClose: (type, instance, done) => { //在消息盒子关闭之前会触发
//type参数:可以区分用户点击的是取消【cancel】、确定【confirm】
//instance参数:当前消息框组件VC
//done参数:是一个函数,函数可以关闭消息盒子
if (type == 'cancel') {
alert('支付遇见问题请联系超管豪哥');
//清除定时器
clearInterval(this.timer);
this.timer = null;
//关闭盒子
done();
} else {
//判断是否真的支付了
// if (this.code == 200) {
//清除定时器
clearInterval(this.timer);
this.timer = null;
//关闭盒子
done();
//路由跳转
this.$router.push('/paysuccess');
// }
}
}
});
- 2、当然也可以不使用require,如果使用require,就不能通过路径方式引入。有两种方式:
- (1)直接引入网页图片链接(前面引入个人收款码就是通过该方法实现)
- (2)在< script >中通过import导入图片,然后在js中使用
<script>
import payImg from '@/assets/pay.jpg'
export default {
methods:{
//点击支付按钮,弹出支付二维码1
async opens(){
this.$alert(`<img height="200px" width="200px" src="${require('@/assets/pay.jpg')}" / >`, '请使用微信扫码', {
dangerouslyUseHTMLString: true,
showCancelButton: true,
center: true
});
}
}
</script>
41、个人中心
-
routers.js 注意: 二级路由要么不写/,要么写全:‘/center/myorder’。
{
path: '/center',
component: Center,
meta: { show: true },
//二级路由
children: [
{
path: 'myorder',
component: MyOrder,
},
{
path: 'grouporder',
component: GroupOrder,
},
{
path: '/center',
redirect: '/center/myorder'
},
]
},
- 当某个路由有子级路由时,父级路由须要一个默认的路由,因此父级路由不能定义name属性.
42、路由独享的守卫
- 全局导航守卫已经帮助我们限制未登录的用户不可以访问相关页面。但是还会有一个问题。例如:用户已经登陆,用户在home页直接通过地址栏访问trade结算页面,发现可以成功进入该页面,正常情况,用户只能通过在shopcart页面点击去结算按钮才可以到达trade页面。我们可以通过路由独享守卫解决该问题
- 路由独享的守卫:只针对一个路由的守卫,所以该守卫会定义在某个路由中。
- 在trade路由信息中加入路由独享守卫
{
path: '/pay',
component: Pay,
meta: { show: true },
//路由独享守卫
beforeEnter: (to, from, next) => {
if (from.path == '/trade') {
next();
} else {
next(false);
}
}
},
{
path: '/trade',
component: Trade,
meta: { show: true },
//路由独享守卫
beforeEnter: (to, from, next) => {
if (from.path == '/shopcart') {
next();
} else {
next(false);
}
}
},
43、图片懒加载
- 懒加载vue-lazyload插件官网插件的使用直接参考官方教程
- vue使用插件的原理:每个插件都会有一个install方法,install后就可以在我们的代码中可以使用该插件。这个install有两类参数,第一个为Vue实例,后面的参数可以自定义。
- vue使用插件的步骤:
- 1、安装
- 2、引入插件 import VueLazyload from "vue-lazyload";
- 3、注册插件Vue.use(VueLazyload),这里的Vue.use()实际上就是调用了插件的install方法。如此之后,我们就可以使用该插件了。
import atm from '@/assets/logo.png'
//引入插件
import VueLazyload from 'vue-lazyload';
//注册插件
Vue.use(VueLazyload, {
loading: atm,
});
- 在Search组件中使用
<!-- 路由跳转时要带参数 -->
<router-link :to="`/detail/${good.id}`">
<img v-lazy="good.defaultImg" />
</router-link>
44、表单验证
- 表单验证推荐使用element ui的from表单验证,element ui from表单验证链接
1、自定义验证插件
- 创建js文件src\plugins\validate.js
//vee-validate插件:表单验证
import Vue from 'vue';
import VeeValidate from 'vee-validate';
// 中文提示信息
import zh_CN from 'vee-validate/dist/locale/zh_CN'
Vue.use(VeeValidate)
//表单验证
VeeValidate.Validator.localize('zh_CN', {
messages: {
...zh_CN.messages,
is: (field) => `${field}必须与密码相同` // 修改内置规则的 message,让确认密码和密码相同
},
attributes: { // 给校验的 field 属性名映射中文名称
phone: '手机号',
code: '验证码',
password: '密码',
password1: '确认密码',
agree: '协议'
}
});
//自定义校验规则
//定义协议必须打勾同意
VeeValidate.Validator.extend('tongyi', {
validate: value => {
return value
},
getMessage: field => field + '必须同意'
})
- 在入口文件引入校验插件
//引入自定义校验插件
import '@/plugins/validate';
- 使用
<div class="content">
<label>手机号:</label>
<input placeholder="请输入你的手机号" v-model="phone" name="phone" v-validate="{ required: true, regex: /^1\d{10}$/ }"
:class="{ invalid: errors.has('phone') }" />
<span class="error-msg">{{ errors.first("phone") }}</span>
</div>
<div class="content">
<label>验证码:</label>
<input placeholder="请输入验证码" v-model="code" name="code" v-validate="{ required: true, regex: /^\d{6}$/ }"
:class="{ invalid: errors.has('code') }" />
<button style="height:38px;width:100px" @click="getCode">获取验证码</button>
<span class="error-msg">{{ errors.first("code") }}</span>
<!-- <img ref="code" src="http://182.92.128.115/api/user/passport/code" alt="code"> -->
</div>
<div class="content">
<label>登录密码:</label>
<input placeholder="请输入你的登录密码" v-model="password" name="password"
v-validate="{ required: true, regex: /^[0-9A-Za-z]{8,20}$/ }" :class="{ invalid: errors.has('password') }" />
<span class="error-msg">{{ errors.first("password") }}</span>
</div>
<div class="content">
<label>确认密码:</label>
<input placeholder="请输入确认密码" v-model="password1" name="password1" v-validate="{ required: true, is: password }"
:class="{ invalid: errors.has('password1') }" />
<span class="error-msg">{{ errors.first("password1") }}</span>
</div>
<div class="controls">
<input type="checkbox" v-model="agree" name="agree" v-validate="{ required: true, 'tongyi': true }"
:class="{ invalid: errors.has('agree') }" />
<span>同意协议并注册《尚品汇用户协议》</span>
<span class="error-msg">{{ errors.first("agree") }}</span>
</div>
45、路由懒加载
- 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
- 路由懒加载链接
- 代码示例:
{
path: '/home',
component: () =>
import('@/pages/Home'),
meta: { show: true }
},
46、打包项目
- 在项目文件夹下执行
npm run build
。会生成dist打包文件。dist就是我们打包好的项目文件,dist文件下的js文件存放我们所有的js文件,并且经过了加密,并且还会生成对应的map文件。 - map文件作用:因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时那里的代码报错。有了map就可以向未加密的代码一样,准确的输出是哪一行那一列有错。当然map文件也可以去除(map文件大小还是比较大的)在vue.config.js配置productionSourceMap: false即可。
- 注意:vue.config.js配置改变,需要重启项目
47、补充
1、$ children $parent使用
- 在56小节中讲到,如果我们父组件想要获取修改子组件信息,可以通过ref实现。但是,当子组件较多时,就会出现多次的$refs操作,会十分的麻烦。所以,引入了children属性。
- children 属性:每个组件都有children属性,可以通过this.$ children操作,该属性会返回当前组件的所有子组件信息,接下来就可以实现子组件信息的获取和修改。
- parent属性:了解了children属性,那么parent也比较好理解。子组件可以通过parent属性获取父组件的全部信息,同样也可以修改父组件的信息。
例题:想要通过点击子组件,使得父组件的money变量减100。
不使用parents:子组件通过之前常用的自定事件来通知父组件修改money,然后父组件money执行减操作。
使用parents:子组件直接通过this.$parent属性获取父组件,然后在子组件内部对money执行减操作。
2、插槽使用
- 插槽也是可以用来传数据的
- 子组件HintButton
<template>
<div>
<slot :item1="{'a':1,'b':2}" item2="asd1">e了吗</slot>
</div>
</template>
- 父组件
<template>
<div>
<HintButton title="提示" icon="el-icon-delete" type="danger" @click="handler">
<template v-slot:default="slopProps" >
<p>{{slopProps}}</p>
<p>{{slopProps.item1}}</p>
<p v-for="(item,index) in slopProps.item1">{{index}}----{{item}}</p>
</template>
</HintButton>
</div>
</template>
- 插槽的原理就是在子组件(HintButton)内定义一个slot(插槽),父组件可以向该插槽内插入数据。父组件向子组件传递信息还是通过props传递。子组件想父组件传递信息时可以通过插槽传递。
- (1)在子组件HintButton的slot内绑定要传递的数据。
- (2) 父组件通过v-slot:default="slotProps"可以接收到全部的信息。
- 箭头所指内容就是子组件通过插槽传递给父组件的信息。接受的数据是键值对的形式。
- 插槽官方链接