D1
01 脚手架目录结构
- node_modules:放置项目依赖的地方
- public:一般放置一些共用的静态资源,打包上线的时候,public文件夹里面资源原封不动打包到dist文件夹里面
- src:程序员源代码文件夹
- -----assets文件夹:经常放置一些静态资源(图片),assets文件夹里面资源webpack会进行打包为一个模块(js文件夹里面)
- -----components文件夹:一般放置非路由组件(或者项目共用的组件)
- App.vue 唯一的根组件
- main.js 入口文件【程序最先执行的文件】
- babel.config.js:babel配置文件
- package.json:看到项目描述、项目依赖、项目运行指令
- README.md:项目说明文件
02 创建非路由组件(2个:Header、Footer)
非路由组件使用分为几步:
第一步:创建
第二步:引入
import Header from './components/Header/index.vue'
第三步:注册
components: {
Header,
Footer
}
第四步:使用
<template>
<div id="app">
<Header></Header>
<Footer></Footer>
</div>
</template>
项目采用的less样式,浏览器不识别less语法,需要一些loader进行处理,把less语法转换为CSS语法
1:安装less less-loader@5
切记less-loader安装5版本的,不要安装在最新版本,安装最新版本less-loader会报错,–> setOption函数未定义
2:需要在style标签的身上加上lang=“less”,不添加样式不生效
03 创建路由组件
vue-router 前端路由: k,v键值对, key: URL(地址栏中的路径), value: 相应的路由组件
项目路由组件:Home、Search、Login、Register, 创建的路由组件一般放在pages文件夹中, 配置路由一般在, router文件夹下
// 配置路由
import Vue from 'vue';
import VueRouter from 'vue-router'
// 使用插件
Vue.use(VueRouter)
// 引入路由组件
import Home from '@/pages/Home'
import Search from '@/pages/Search'
import Register from '@/pages/Register'
import Login from '@/pages/Login'
// 暴露路由VueRouter实例
export default new VueRouter({
// 配置路由
routes: [
{
path: "/home",
component: Home
},
{
path: "/search",
component: Search
},
{
path: "/register",
component: Register
},
{
path: "/login",
component: Login
},
// 重定向,使项目一上来就打开 home 首页
{
path: '*', //全部路径
redirect: "/Home"
}
]
})
路由组件与非路由组件的区别?
路由组件一般放置在pages文件夹下, 非路由组件一般放置在components文件夹下(共用全局组件)
路由组件在使用时, 使用
<router-view></router-view>
, 非路由组件在使用时, 使用<Header></Header>
注册完路由, 非路由组件, 与路由组件, 身上都会有
$route, $router
属性$route: 一般获取路由信息(路径, query, params参数等)
- params参数:路由需要占位,程序就崩了,属于URL当中一部分
- query参数:路由不需要占位,写法类似于ajax当中query参数
$router: 一般进行编程式导航进行路由跳转(push, replace参数等)
- push参数: 页面跳转后存在历史记录,
- replace参数: 页面跳转后不存在历史记录
路由的跳转
- 路由的跳转就两种形式:声明式导航(router-link:务必要有to属性)编程式导航push, replace
声明式导航能做的, 编程式导航都能做, 且可以书写自己的业务逻辑(比如一些点击事件, 显示与隐藏等方法)
04 Footer组件的显示与隐藏
-
功能实现: Footer组件, 在 Home, Search 显示, 在Login、Register隐藏
<Footer v-show="$route.meta.show"></Footer>
可以给路由添加路由元信息【meta】,
meta: {show:true}
v-show与v-if区别?
v-show:通过样式display控制
v-if:通过元素上树与下树进行操作
05 路由传参
methods: {
goSearch(){
// 路由传参 params + query
// 第一种: 字符串形式
// 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()}})
}
}
{
path: "/search/:keyword", //params 参数需要进行占位
component: Search,
meta: {show:true},
name: 'search' //使用对象方式时, 要添加一个name属性
},
路由传递参数相关面试题
1:路由传递参数(对象写法)path是否可以结合params参数一起使用?this.$router.push({path:'/search', params:{keyword:this.keyword}, query:{k:this.keyword.toUpperCase()}})
不可以:对象的写法可以是name, path形式, 但是path这种写法不能与params参数一起使用, 可以与query参数一起使用
2:如何指定params参数可传可不传?
在配置路由时, 再添加params占位时, 在占位后面加上问号(则表示, params参数可传或不不传)
path: "/search/:keyword?"
3:params参数可以传递也可以不传递,但是如果传递是空串,如何解决?
使用undefined解决, params参数可以不传递参,但传了一个空串
this.$router.push({name:'search', params:{keyword:'' || undefined}, query:{k:this.keyword.toUpperCase()}})
4: 路由组件能不能传递props数据?
第一种(布尔值形式): 在配置路由时, 添加
props:true
, 这种布尔值写法只能传递params参数第二种(对象形式): 配置路由时, 额外给路由组件传递一些props
props:{a:1,b:2}
第三种(函数形式):
props:($route) => { return {keyword:$route.params, k:$route.query.k}; }
路由组件接收:
props:[keyword, k]
D2
01 错误处理
编程式导航路由跳转到当前路由(参数不变), 多次执行会抛出NavigationDuplicated的警告错误?
注意:编程式导航(push|replace)才会有这种情况的异常,声明式导航是没有这种问题,因为声明式导航内部已经解决这种问题。
为什么会出现这种现象:
由于vue-router最新版本3.5.2,引入了promise,当传递参数多次且重复,会抛出异常,
第一种解决方案:是给push函数,传入相应的成功的回调与失败的回调,但是以后再用push|replace还是会出现类似现象,因此我们需要从‘根’治病;this.$router.push({name:'search', params:{keyword:this.keyword}, query:{k:this.keyword.toUpperCase()}}, ()=>{}, ()=>{})
第二种解决方案: 在配置路由时重写push方法
// 把VueRouter 原型对象的push, replace方法, 保存一份 let originPush = VueRouter.prototype.push; let originReplace = VueRouter.prototype.replace; // 重写push, replace // 第一个参数: 告诉原来的push方法, 要往哪里跳(传递那些参数) // 第二个参数: 成功时的回调 // 第三个参数: 失败时的回调 // call || apply的区别 // 相同点: 都可以调用函数一次, 都可以修改this指向一次 // 不同点: call传递参数用逗号隔开, apply则使用数组 VueRouter.prototype.push = function(location, resolve, reject){ if(resolve && reject) { // originPush() 不能直接调用, 此时this会指向window, 要使用 .call进行调用 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, ()=>{}, ()=>{}) } }
02 三级联动组件
由于三级联动组件要在 Home, Search, Detail 中使用, 所以注册三级联动组件为全局组件, 使其可以在任何地方使用
// 三级联动组件 --- 全局组件
import TypeNav from '@/pages/Home/TypeNav';
// 第一个参数: 全局组件的名字 第二个参数: 哪一个组件
Vue.component(TypeNav.name, TypeNav);
在Home中使用组件
<!-- 三级联动全局组件, 已经注册为了全局组件, 因此不需要再引入, 可以直接使用-->
<TypeNav></TypeNav>
03 其他静态组件的拆分
这块没什么难度, 要注意图片的路径, 和设置 <style lang="less">
<template>
<div>
<!-- 三级联动全局组件, 已经注册为了全局组件, 因此不需要再引入, 可以直接使用-->
<TypeNav></TypeNav>
<!-- 使用其他组件 -->
<ListContainer></ListContainer>
<Recommend></Recommend>
<Rank></Rank>
<Like></Like>
<Floor></Floor>
<Brand></Brand>
</div>
</template>
<script>
// 引入其余组件
import ListContainer from '@/pages/Home/ListContainer';
import Recommend from '@/pages/Home/Recommend';
import Rank from '@/pages/Home/Rank';
import Like from '@/pages/Home/Like';
import Floor from '@/pages/Home/Floor';
import Brand from '@/pages/Home/Brand';
export default {
name: "",
components: {
ListContainer,
Recommend,
Rank,
Like,
Floor,
Brand
}
}
</script>
04 axios的二次封装
AJAX:客户端可以’敲敲的’向服务器端发请求,在页面没有刷新的情况下,实现页面的局部更新。
XMLHttpRequest、jq、fetch、axios
工作的时候src目录下的API文件夹,一般关于axios二次封装的文件
// 对于 axios 进行二次封装
import axios from 'axios'
// 利用axios对象的方法 create, 去创建一个axios实例
const requests = axios.create({
// 配置对象, 基础路径, 发送请求的时候, 路径中会出现api
baseURL: '/api',
// 设置响应超时 时间
timeout: 5000,
});
// 请求拦截器: 在发送请求之前. 请求拦截器可以检测到, 在请求发出去之前做一些事情
requests.interceptors.request.use((config) => {
// config: 配置对象, 对象里面有 headers 请求头 这个重要属性
return config;
});
// 响应拦截器
requests.interceptors.response.use((res) => {
// 成功的回调函数: 服务器响应数据回来以后, 响应拦截器可以检测到, 可以做一些事情
return res.data;
},(error) => {
// 响应失败的回调函数
return Promise.reject(new error('faile'));
});
05解决请求跨域
跨域:如果多次请求协议、域名、端口号有不同的地方,称之为跨域
-
cors
: 服务器响应返回数据时会携带特殊的响应头, 从真正意义上解决跨域, 但这种方法需要后端做一些配置, 且响应头不能随便配置(可能会造成任何人都会找这台服务器请求数据) -
jsonp
: 借助script
标签里的src
属性, 在引入外部资源的时候不受, 同源策略限制这一特点(这种方式很巧妙, 面试的时候会问)解决跨域, 但是只能解决get请求, 不能解决post, PUT, DELETE, 且需要前后端相互配合 -
配置代理服务器: 开发中使用最多的一种方法,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wPDtsu56-1660641621514)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1%E5%99%A8.png)]
开启代理服务器: 1, 使用 nginx 2, 借助vue-cli(比较简单)
// 借助vue-cli开启代理跨域 devServer: { proxy: { '/api': { //请求前缀, 只有含有请求前缀的请求, 才会走代理服务器, 比较灵活 target: 'http://gmall-h5-api.atguigu.cn/', //请求地址 // pathRewrite: { '^/api': '' }, //重写路径把符合第一个参数(类似于正则匹配)转成第二个参数 用于请求服务器时路径不匹配 }, }, },
06引入进度条
// start: 进度条开始 done: 进度条结束
import nprogress from 'nprogress';
// 引入进度条样式
import "nprogress/nprogress.css";
修改进度条样式, 如 进度条颜色, 宽度, 高度等, 可以在nprogress下的nprogress/nprogress.css 文件中修改
#nprogress .bar {
/* background: #29d; */
background: rgb(11, 226, 144);
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
07 Vuex
vuex:Vue官方提供的一个插件,状态管理库, 集中式管理项目中组件共用的数据
vuex:书写任何项目都需要vuex?–> 项目大的时候,需要有一个地方‘统一管理数据’即为仓库store
Vuex基本使用:
// home模块小仓库
// state: 仓库存储数据的地方
const state = {};
// mutations: 修改state的唯一手段
const mutations = {};
// actions: 处理cation, 可以书写自己的业务逻辑, 也可以处理异步
const actions = {};
// getters: 类似于计算属性, 用于简化仓库数据, 让组件获取仓库的数据更加方便
const getters = {};
export default {
state,
mutations,
actions,
getters,
}
当项目过大, 组件过多, 接口多, 数据多时, 要使用vuex的模块式开发: 一个大仓库, 分为多个小仓库, 统一暴露, 集中引入
import Vue from 'vue';
import Vuex from 'vuex';
// 安装插件
Vue.use(Vuex)
// 引入小仓库
import home from './home'
import search from './search'
// 对外暴露Store类的一个实例
export default new Vuex.Store({
// 实现Vuex仓库模块式开发存储数据
modules: { //用于引入子模块
home,
search,
}
});
D3
01 商品分类三级联动(TypeNav组件)展示动态数据
向服务器请求数据
<script>
import {mapState} from 'vuex';
export default {
name: 'TypeNav',
// 组件挂载完毕: 可以向服务器发送请求
mounted() {
// 通知Vuex发送请求, 获取数据, 存储在仓库中
this.$store.dispatch('categoryList')
//mounted|created:都可以mounted:模板已经变为真是DOM【只不过没有数据,显示空白】,因为ajax是异步,需要时间的。
},
computed: {
...mapState({
categoryList:(state) => { //当使用这个计算属性时, 函数会调用一次, 并传入一个state参数,即为大仓库中的数据,大仓库包含home和search
return state.home.categoryList; // 拿到state下的home下的categoryList
}
})
}
}
</script>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bShm8G6S-1660641621515)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E5%A4%A7%E4%BB%93%E5%BA%93.png)]
服务器发送数据
// home模块小仓库
// state: 仓库存储数据的地方
const state = {
// 服务器返回数组, 对象 (是根据接口返回值初始化的)
categoryList: [],
};
// mutations: 修改state的唯一手段
const mutations = {
CATEGORYLIST(state, categoryList) {
state.categoryList = categoryList
}
};
// actions: 处理cation, 可以书写自己的业务逻辑, 也可以处理异步
const actions = {
// 通过api里面的接口函数调用, 向服务器发请求, 获取数据
async categoryList({commit}) {
let result = await reqCategoryList();
// console.log(result);
if(result.code == 200) {
commit('CATEGORYLIST', result.data)
}
}
};
在页面中展示
<div class="item" v-for="(c1, index) in categoryList" :key="c1.categoryId">
//遍历categoryList数组, 获取id, categoryName的数据, 并用插值语法在页面中展示
<h3>
<a href="">{{c1.categoryName}}</a>
</h3>
<div class="item-list clearfix">
<div class="subitem" v-for="(c2, index) in c1.categoryChild" :key="c2.categoryId">
<dl class="fore">
<dt>
<a href="">{{c2.categoryName}}</a>
</dt>
<dd>
<em v-for="(c3, index) in c2.categoryChild" :key="c3.categoryId">
<a href="">{{c3.categoryName}}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
02完成一级分类的背景效果
页面结构:
<div @mouseleave="leaveIndex">
<!-- 事件委派: 把子元素的事件交给父元素管理(@mouseleave="leaveIndex") -->
<h2 class="all">全部商品分类</h2>
<div class="sort">
<div class="all-sort-list2">
<div class="item" v-for="(c1, index) in categoryList" :key="c1.categoryId"
:class="{ cur: currentIndex == index }">
//动态绑定class样式当满足currentIndex == index时cur才会触发
<h3 @mouseenter="changeIndex(index)">
<a href="">{{ c1.categoryName }}</a>
</h3>
<div class="item-list clearfix">
<div class="subitem" v-for="(c2, index) in c1.categoryChild" :key="c2.categoryId">
<dl class="fore">
<dt>
<a href="">{{ c2.categoryName }}</a>
</dt>
<dd>
<em v-for="(c3, index) in c2.categoryChild" :key="c3.categoryId">
<a href="">{{ c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
</div>
对应方法:
methods: {
// 一级分类鼠标移入修改响应式currentIndex属性
changeIndex(index) {
this.currentIndex = index;
},
// 一级分类鼠标移出修改响应式currentIndex属性
leaveIndex() {
this.currentIndex = -1
}
}
03 解决卡顿问题
正常:事件触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很可能出现浏览器卡顿)
防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行最后一次
节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发
// 按需引入(引入lodash下的throttle) 一般node依赖包里面会自带, 不用在安装
import throttle from 'lodash/throttle'
// 设置节流
changeIndex: throttle(function (index) {
this.currentIndex = index;
// console.log('---');
}, 100), //每个100ms请求一次
04 三级联动组件(TypeNav组件)的路由跳转与传参
-
三级联动, 用户可以点击的: 一级分类, 二级分类, 三级分类, 当用户点击的时候Home模块会跳转到search模块, 一级分类会把用户选中的产品(产品名字, 产品id), 在路由跳转时, 进行传递
-
路由的跳转:
-
第一种声明式导航: 为什么使用
router-link
组件的时候,页面会出现卡顿?router-link
是一个组件:相当VueComponent类的实例对象,一瞬间new VueComponent很多实例(1000+),很消耗内存,因此导致卡顿。 -
第二种编程式导航: push|replace三级分类由于使用router-link的时候,会出现卡顿现象,因此采用编程式导航:
@click="goSearch"
。路由跳转的时候【home->search】:需要进行路由传递参数【分类的名字、一、二、三级分类的id】, 但过多的回调也会导致性能不高 -
最好的方案是采用: 编程式导航 + 事件委派
使用事件委派存在的一些问题:
-
如何确定点击的是a标签?
把子节点中的a标签, 动态绑定自定义的属性
:data-categoryName="c1.categoryName"
, 带有这个属性的就是a标签- 在a标签中定义自定义属性
:data-categoryName="c1.categoryName"
:
<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"自定义属性来区分 其他标签 <a :data-categoryName="c1.categoryName">{{ c1.categoryName }}</a> </h3> <!-- 二, 三级分类 --> <!-- 鼠标事件:显示或显示三级分类 --> <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">{{ c2.categoryName }}</a> </dt> <dd> <em v-for="(c3, index) in c2.categoryChild" :key="c3.categoryId"> <a :data-categoryName="c3.categoryName">{{ c3.categoryName }}</a> </em> </dd> </dl> </div> </div> </div> </div>
- 拿到a标签:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6RSTvCU-1660641621516)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E8%8E%B7%E5%8F%96a%E6%A0%87%E7%AD%BE.png)]
goSearch(event) { // this.$router.push('/search') //获取点击目标元素 let element = event.target; // 节点中有一个属性dataset, 可以获取到自定义节点的属性与属性值 // console.log(element.dataset); // 注意: 自定义属性都被浏览器转为了小写 let {categoryname} = element.dataset; if(categoryname){ alert('拿到了') } }
- 在a标签中定义自定义属性
-
如何区分那个是 1, 2, 3 级分类的名字, id?
把子节点中的a标签, 动态绑定自定义的属性
:data-category1Id="c1.categoryId"
, 利用不同的id来区分- 在a标签中定义自定义属性
:data-category1Id="c1.categoryId"
:
<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-category1Id="c1.categoryId"自定义属性来区分不同级别的分类 <a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId">{{ c1.categoryName }}</a> </h3> <!-- 二, 三级分类 --> <!-- 鼠标事件:显示或显示三级分类 --> <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>
- 实现点击a标签进行路由跳转
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2RHUETzd-1660641621516)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E5%9C%B0%E5%9D%80%E6%A0%8Flocation+query%E5%8F%82%E6%95%B0.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oEDhfspb-1660641621517)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E8%B7%AF%E7%94%B1%E8%B7%B3%E8%BD%AC.png)]
goSearch(event) { // this.$router.push('/search') //获取点击目标元素 let element = event.target; // 节点中有一个属性dataset, 可以获取到自定义节点的属性与属性值 console.log(element.dataset); // 自定义属性都为小写 let { categoryname, category1id, category2id, category3id } = element.dataset; // 判断是否为a标签 if (categoryname) { // 整理路由参数 let location = {name: 'search'}; let query = {categoryName: categoryname}; //定义地址路径的初始值categoryName:categoryname // 判断1, 2, 2级分类 if (category1id) { //时自定义属性获取到的id赋给query参数下的Id query.category1Id = category1id; }else if(category2id){ query.category2Id = category2id; }else { query.category3Id = category3id; } // 整理完参数, 使location与query合并 location.query = query; // 路由跳转 this.$router.push(location); } }
- 在a标签中定义自定义属性
-
-
D4
00 复习一下
三级联动业务:
1.前面基础课程当中v-for很少使用index,以后在写项目的时候,index索引值切记加上
2.防抖与节流【面试经常出现】
3.vuex可以模块式开发
vuex经常用的套路是state、mutations、actions、getters、modules
4.组件name属性的作用?
1开发者工具中可以看见组件的名字
2注册全局组件的时候,可以通过组件实例获取相应组件的名字
01控制(TypeNav组件)中三级分类在search模块的显示与隐藏和过渡效果
1, 三级联动在search一会显示、一会隐藏 —解决方案:通过一个响应式属性(v-show="show"
)控制三级联动显示与隐藏
mounted() {
// 通知Vuex发送请求, 获取数据, 存储在仓库中
this.$store.dispatch('categoryList')
// 当组件挂载完毕, 让show的值变为false
// 如果不是Home组件则将typeNav隐藏
if (this.$route.path != '/home') {
this.show = false
}
},
methods: {
// 一级分类鼠标移入修改响应式currentIndex属性
// changeIndex(index) {
// this.currentIndex = index;
// },
// 设置节流, 每隔100ms触发一次
changeIndex: throttle(function (index) {
this.currentIndex = index;
// console.log('---');
}, 100),
// 一级分类鼠标移出修改响应式currentIndex属性
leaveIndex() {
this.currentIndex = -1;
// 鼠标移出, 隐藏search组件的一级分类商品列表显示
if (this.$route.path != '/home') { //判断是否在home模块的路径中
this.show = false
}
},
// 鼠标移入 显示search组件的一级分类商品列表显示
enterShow() {
this.show = true
},
}
2, 过渡效果
通过Vue的内置组件transition完成
注意: 你可以给 (某一个节点)|(某一个组件)添加过渡动画效果, 但是需要注意,节点|组件务必出现v-if|v-show指令才可以使用。
<!-- 三级联动: -->
<transition name='sort'> // 在三件联动的外部套一个 transition 就可以使用Vue的内置组件transition了
<div class="sort" v-show="show">
//1, 2, 3,级分类...
</div>
</transition>
相关样式:
// 一级分类鼠标移入过渡动画
// 过渡动画开始
.sort-enter {
height: 0px;
opacity: 0.4;
}
// 过渡动画结束
.sort-enter-to {
height: 461px;
opacity: 1;
}
// 定义动画的过渡时间, 速率
.sort-enter-active {
transition: all 1s linear;
overflow: hidden; //使用overflow: hidden解决, 过渡时文字先显示的问题
}
//移出就是.sort-leave
02TypeNav组件三级联动性能优化
项目:home切换到search或者search切换到home,你会发现一件事情,组件在频繁的向服务器发请求,
获取三级联动的数据进行展示。项目中如果频繁的向服务器发请求,很耗费性能,因此咱们需要进行优化。
为什么会频繁的向服务器发请求获取三级联动的数据那?
因为路由跳转的时候,组件会进行销毁的【因此频繁的获取三级联动的数据】只需要发一次请求,获取到三级联动的数据即可,不需要多次。
解决方案: 在App中发送请求:
mounted() {
// 发送一个action || 获取商品分类的三级列表数据(因为App组件最先执行, 在其他组件请求数据时, App里早已准备就绪, 所以请求只会发送一次)
//this.$store.dispatch('getCategoryList') //出错
this.$store.dispatch('categoryList')
}
出现了错误: 未知的操作类型(unknown action type: getCategoryList)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5ARTC1GM-1660641621517)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/%E9%94%99%E8%AF%AF%E6%97%A5%E5%BF%97img/%E6%9C%AA%E7%9F%A5%E7%9A%84%E6%93%8D%E4%BD%9C%E7%B1%BB%E5%9E%8B.png)]
在网上找了半天, 有说是命名空间的问题, 有说是action 少加了 s, 有说是在仓库(store)的js文件中创建store类实例时没加 modules,
但都解决不了, 最后发现时, 老师改了函数名, 😅麻了
store文件下home模块的index.js文件(配置接口请求)
// actions: 处理cation, 可以书写自己的业务逻辑, 也可以处理异步
const actions = {
// 通过api里面的接口函数调用, 向服务器发请求, 获取数据
async categoryList({commit}) {
//这个地方的categoryList 要与调用时的名字一样this.$store.dispatch('categoryList')
let result = await reqCategoryList();
// console.log(result);
if(result.code == 200) {
commit('CATEGORYLIST', result.data)
}
}
};
03query与params参数合并
header文件夹下的index.vue: 实现功能: 先在搜索框里输入关键字, 再点击分类
<button class="sui-btn btn-xlarge btn-danger" type="button" @click="goSearch">搜索</button> //搜索按钮
---------分割线----------
goSearch() {
//判断路由跳转时是否含有query参数
let location = {
name: 'search',
//动态给location配置对象添加params参数
params: { keyword: this.keyword || undefined },
}
if (this.$route.query) {
// 动态给location配置对象添加query参数
location.query = this.$route.query;
// 路由跳转
this.$router.push(location)
}
}
}
TapNav公共模块index.vue : 实现功能: 先点击分类, 再在搜索框里输入关键字
// 路由跳转的方法
goSearch(event) {
//判断路由跳转时是否含有params参数
if (this.$route.params) {
//动态给location配置对象添加query参数
location.query = query;
//动态给location配置对象添加params参数
location.params = this.$route.params;
// 路由跳转
this.$router.push(location);
}
}
},
注意这里控制台会警告:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6I3SSLGh-1660641621518)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/%E9%94%99%E8%AF%AF%E6%97%A5%E5%BF%97img/parmas%E5%8F%82%E6%95%B0%E6%9C%AA%E4%BC%A0%E9%80%92.png)]
这里警告意思是 params参数没有传递, 因为params传递参数都需要配置占位符 。如果 /search后面的占位符没有值的时候, 也就是没有传递params参数时, 是无法跳转到 search组件的,
解决方式:
routes: {
{
//在 :keyword占位符后面加上一个问号 ? 表示 params参数 可传可不传
path: "/search/:keyword?",
//params 参数需要进行占位, 注意路径里不要有空格, 否则也会报错
component: Search,
meta: {show:true},
name: 'search'
}
}
地址栏显示: http://192.168.1.3:8080/#/search/华为?categoryName=手机&category3Id=61
?前面为params, ?后面为query参数
04 首页轮播图
04.1mock模拟数据
-
开发listContainer|Floor组件业务
场景:开发项目,产品经理画出原型,前端与后端人员需要介入(开发项目),leader(老大)刚开完会,前端与后端负责哪些模块,后端人员(…开发服务器),
前端人员【项目起步、开发静态页面、查分静态组件】,回首一看后台‘哥哥’,接口没有写好,这种情况,前端人员可以mock一些数据【前端程序员自己模拟的一些假的接口】,当中工作中项目上线,需要把mock数据变为后台哥哥给的接口数据替换。 -
mock数据。
注意:因为后台老师没有给我们写好其他接口【老师们特意的:因为就是想练习mock数据】但是项目中mock数据,你就把他当中真实接口数据用就行。
注意:mock(模拟数据)数据需要使用到mockjs模块,可以帮助我们模拟数据, mockjs【并非mock.js mock-js】
官方地址: http://mockjs.com/
mock官网一句话:生成随机数据,拦截 Ajax 请求
拦截ajax请求:请求发布出去【浏览器会进行拦截】,只是项目当中本地自己玩耍数据。
步骤:
-
安装依赖包mockjs : npm install --sace mockjs 有镜像的话用 cnpm
-
在src文件夹下创建一个文件夹,文件夹mock文件夹。
-
准备模拟的数据
把mock数据需要的图片放置于public文件夹中!
scr/mock 文件夹下的 banner.json: (模拟的数据), JSON:没有空格,最好使用格式化插件进行格式化[ { "id": "1", "imgUrl": "/images/banner1.jpg" }, { "id": "2", "imgUrl": "/images/banner2.jpg" }, { "id": "3", "imgUrl": "/images/banner3.jpg" }, { "id": "4", "imgUrl": "/images/banner4.jpg" } ]
第四步:在mock文件夹中创建一个mockserver.js文件
注意:在mockserver.js文件当中对于banner.json||floor.json的数据没有暴露,但是可以在server模块中使用。
对于webpack当中一些模块:图片、json,不需要对外暴露,因为默认就是对外暴露。
第六步:回到入口文件(根目录src下的main.js),引入mockserver.js
import Vue from 'vue'
import App from './App.vue'
// 三级联动组件 --- 全局组件
import TypeNav from '@/components/TypeNav';
// 第一个参数: 全局组件的名字 第二个参数: 哪一个组件
Vue.component(TypeNav.name, TypeNav);
// 引入路由
import router from '@/router'
// 引入仓库
import store from '@/store'
// 引入mockServer.js -- mock数据
import '@/mock/mockServer';
第七步:在API文件夹中创建mockRequest【axios实例:baseURL:‘/mock’】
专门获取模拟数据用的axios实例。
api文件下的mockAjax.js (配置模拟请求):
const requests = axios.create({
// 配置对象
// baseURL: '/api', //真实的接口 /api/xxxx
baseURL:'/mock', //模拟的数据/mock/xxxx
// 设置响应超时 时间
timeout: 5000,
});
api文件下的index.js (对api进行统一管理, 获取接口):
// 获取banner(home首页轮播图接口)
export const reqGetBannerList = () => {
return mockAjax.get('/banner');
}
在开发项目的时候:切记,单元测试,某一个功能完毕,一定要测试是否OK
04.2在home仓库(store)中引入
store/home 文件夹下的 index.js :
// 引入首页三级联动列表接口, 引入首页轮播图的接口(模拟)
import { reqCategoryList, reqGetBannerList } from "@/api";
// home模块小仓库
// state: 仓库存储数据的地方
const state = {
// 服务器返回数组, 对象 (是根据接口返回值初始化的)
categoryList: [],
//轮播图数据
bannerList: [],
};
// mutations: 修改state的唯一手段
const mutations = {
CATEGORYLIST(state, categoryList) {
state.categoryList = categoryList
},
GETBANNERLIST(state, bannerList) {
state.bannerList = bannerList //把请求到的bannerList里的数据存到state里的bannerList,
}
};
// actions: 处理cation, 可以书写自己的业务逻辑, 也可以处理异步
// 获取首页的三级联动列表数据, 首页轮播图的数据(模拟)
const actions = {
// 通过api里面的接口函数调用, 向服务器发请求, 获取数据
async categoryList({commit}) {
let result = await reqCategoryList();
// console.log(result);
if(result.code == 200) {
commit('CATEGORYLIST', result.data)
}
},
async getBannerList() {
let result = await reqGetBannerList(); //await 等待异步(async)请求响应结果
// console.log(result);
if(result.code == 200) {
//commit: 把GETBANNERLIST提交到 result.data, 但actions里不能修改数据, 所以要到mutation里进行修改
commit('GETBANNERLIST', result.data);
}
}
};
// getters: 类似于计算属性, 用于简化仓库数据, 让组件获取仓库的数据更加方便
const getters = {};
export default {
state,
mutations,
actions,
getters,
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NJz2Btbs-1660641621519)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/home%E4%BB%93%E5%BA%93.png)]
04.3swiper的基本使用
- swiper基本的使用?
- swiper可以在移动端使用?还是PC端使用?
答:swiper移动端可以使用,pc端也可以使用。 - swiper常用于哪些场景?
常用的场景即为轮播图----【carousel:轮播图】
swiper最新版本为7版本的,项目当中使用的是5版本
https://www.swiper.com.cn/ 官网地址
- Swiper使用步骤:
第一步:引入依赖包【swiper.js|swiper.css】
第二步:静态页面中结构必须完整【container、wrap、slider】,类名不能瞎写
第三步:初始化swiper实例
D5
01 完成首页轮播图
- 第一种解决方案,
同学的想法:都不是完美的【错误的想法】
created里面:created执行与mounted执行,时间差可能2ms,白扯
updated里面:如果组件有很多响应式(data),只要有一个属性值发生变化updated还会再次执行,再次初始化实例。
总结:第一种解决方案可以通过延迟器(异步)去解决问题,
但是这种解决方案存在风险(无法确定用户请求到底需要多长时间),因此没办法确定
延迟器时间。
-
第二种解决方案: wacth + $nextTick
watch:监听属性,watch可以检测到属性值的变化,当属性值发生变化的时候,可以执行一次。
nextTick官网解释:
在下次DOM更新, 循环结束之后,执行延迟回调。在 修改数据之后 立即使用这个方法,获取更新后的DOM。
注意:组件实例的$nextTick方法,在工作当中经常, 结合第三方插件使用,获取更新后的DOM节点
watch: {
// 在new swiper实例之前, 页面中结构必须已经存在
bannerList(newValue, oldValue) {
this.$nextTick(() => {
// 当执行这个回调的时候: 保证服务器的数据一定回来了(这时候v-for已经执行完毕了)
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',
},
})
})
}
}
02 开发Floor组件
与前面的三级联动组件, 轮播图 流程一样:
- 在api获取模拟数据接口
// 获取banner(home首页轮播图接口)
export const reqGetBannerList = () => {
return mockRequests.get('/banner');
}
// 获取floor数据
export const reqGetFloorList = () => {
return mockRequests.get('/floor');
}
- 在仓库(store发送请求)
import { reqCategoryList, reqGetBannerList, reqGetFloorList } from "@/api";
// state: 仓库存储数据的地方
const state = {
// 服务器返回数组, 对象 (是根据接口返回值初始化的)
categoryList: [],
//轮播图数据
bannerList: [],
// floor数据
floorList: []
};
// mutations: 修改state的唯一手段
const mutations = {
CATEGORYLIST(state, categoryList) {
state.categoryList = categoryList
},
GETBANNERLIST(state, bannerList) {
state.bannerList = bannerList //把请求到的bannerList里的数据存到state里的bannerList,
},
GETFLOORLIST(state, floorList) {
state.floorList = floorList
}
};
const actions = {
// 获取首页的三级联动列表数据
async categoryList({commit}) {
let result = await reqCategoryList();
// console.log(result);
if(result.code == 200) {
commit('CATEGORYLIST', result.data)
}
},
// 获取首页轮播图的数据(模拟)
async getBannerList({commit}) {
let result = await reqGetBannerList(); //await 等待异步(async)请求响应结果
// console.log(result);
if(result.code == 200) {
commit('GETBANNERLIST', result.data);
}
},
// 获取Floor数据
async getFloorList({commit}) {
let result = await reqGetFloorList();
if(result.code == 200) {
commit('GETFLOORLIST', result.data)
}
}
};
- 在组件内派发(触发)action, (不过这里不是在floor组件内, 而是在父组件home组件内, 因为floor要进行复用, 在floor组件内进行
v-for
遍历, 无法遍历出两个不同的对象, 因此要在父组件内遍历, 在传给子组件floor)
//遍历从仓库获取到的数据
<Floor v-for="(floor, index) in floorList" :key="floor.id" :lsit="floor"></Floor>
------分割线--------
import { mapState } from "vuex";
export default {
name: "Home",
mounted() {
// 派发action, 通知vuex发送请求
this.$store.dispatch('getFloorList');
},
//获取仓库里存储的数据
computed: {
...mapState({
floorList: (state) => {
return state.home.floorList;
}
})
}
}
v-for|v-show|v-if|这些指令可以在自定义标签(组件)的身上使用
03组件之间通信
组件间通信面试必问的东西
props:父传子(最常用)
插槽:父传子
自定义事件($on, $emit
):子传父
全局事件总线$bus
:万能
pubsub:万能(但在vue中几乎不用)
Vuex:万能
$ref:父子通信
在home组件:floorList='floor'
把数据转给子组件Floor
子组件Floor接受数据, 并进行操作
<script>
import Swiper from 'swiper';
export default {
name: 'Floor',
props: ['floorList'], //接受父组件传过来的数据
mounted() {
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>
为什么在Floor组件的mounted中初始化SWiper实例轮播图可以使用. 因为父组件的mounted发请求获取Floor组件,当父组件的mounted执行的时候。Floor组件结构可能没有完整,但是服务器的数据回来以后Floor组件结构就一定是完成的了,因此v-for在遍历来自于服务器的数据,如果服务器的数据有了,Floor结构一定的完整的。
否则,你都看不见Floor组件
04 carousel全局组件
如果项目当中出现类似的功能,且重复利用,封装为全局组件----【不封装也可以】,但要有封装的思想
为了封装全局的轮播图组件:让Floor与listContainer组件中的代码一样,如果一样完全可以独立出来封装为一个全局组件。
<template>
<div class="swiper-container" ref="mySwiper">
<div class="swiper-wrapper">
//遍历 一个数组list, 这个list数组里的数据取决于 父元素传过来的数据, 其他没太大改动,还是swiper结构
<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'; // 引入 swiper
export default {
name: 'Carousel',
props: ['list'], //接受父组件传来的数据
//这个watch对象会对, Floor组件与首页轮播图组件都能生效, 也因此才能封装全局组价
watch: {
list: {
immediate: true,
// 在new swiper实例之前, 页面中结构必须已经存在
handler() {
this.$nextTick(() => {
// 当执行这个回调的时候: 保证服务器的数据一定回来了(这时候v-for已经执行完毕了)
var mySwiper = new Swiper(this.$refs.mySwiper, {
loop: true, // 循环模式选项
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
clickable: true
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
})
})
} } } }
</script>
D6
01 获取search模块的数据
vue模块化开发的步骤:
- 书写静态页面【布局、样式】
- 拆分组件
- 获取服务器数据展示数据
- 玩业务
流程还是老样子:
-
在api获取数据接口 (也包括了之前的轮播图,三级联动等等, 全部复制过来看的清除一点)
// 对api 进行统一管理 // 引入二次封装的axios(请求, 响应拦截器) import requests from "./request"; // 引入mock模拟数据请求 import mockRequests from "./mockAjax" // 三级联动接口: /api/product/getBaseCategoryList get请求, 无参数 export const reqCategoryList = ()=> { // 发送请求: axios 发请求返回结果是一个Promise对象 return requests({url: '/product/getBaseCategoryList', method:'get'}) } // 获取banner(home首页轮播图接口) export const reqGetBannerList = () => { return mockRequests.get('/banner'); } // 获取floor数据 export const reqGetFloorList = () => { return mockRequests.get('/floor'); } // 获取search数据接口: 地址:/api/list, 请求方式:post, 需要参数 // 这个接口在获取search模块的数据时, 要给服务器传递一个默认参数(至少是一个空对象) export const reqGetSearchInfo = (params) => { return requests({url: '/list', method:'post', data:params}); }
-
在仓库(store)发送请求, 不过之前都是在home仓库中进行, 这里要在search里写
// search模块小仓库 import { reqGetSearchInfo } from "@/api"; // state: 仓库存储数据的地方 const state = { searchList: {}, // 初始值为一个对象 }; // mutations: 修改state的唯一手段 const mutations = { GETSEARCHLIST(state, searchList) { state.searchList = searchList; } }; // actions: 处理cation, 可以书写自己的业务逻辑, 也可以处理异步 const actions = { // 获取search模块数据 async getSearchList({commit}, params={}) { // 当前这个reqSearchInfo这个函数在调用获取服务器数据的时候, 至少要传递一个参数(至少是空对象) // params形参是在: 用户派发action的时候, 作为第二个参数传进来的, 至少是一个空对象 let result = await reqGetSearchInfo(params); if(result.code == 200) { commit('GETSEARCHLIST', result.data); } } }; // getters: 类似于计算属性, 用于简化仓库数据, 让组件获取仓库的数据更加方便 const getters = {}; export default { state, mutations, actions, getters, }
-
在组件内(search)派发action, 获取仓库数据
mounted() { // 派发action this.$store.dispatch('getSearchList', {}); }, computed: { // mapGetters里面的写法: 传递的数组, 因为Getters计算是没有划分模块的(没有分home, search模块) ...mapGetters(['goodsList']) },
02 search模块的根据不同参数显示不同数据
data() {
return {
searchParams: {
// 1, 2, 3, 级分类的id, 从home跳转到search时传入
category1Id: "",
category2Id: "",
category3Id: "",
// 分类名字, 关键字
categoryName: "",
keyword: "",
// 排序
order: "",
// 分页器, 代表当前是第几页
pageNo: 1,
// 代表每一个展示数据的个数
pageSize: 3,
// 平台售卖属性操作带的参数
props: [],
// 品牌
trademark: "",
}
}
},
beforeMount() { //在挂载之前合并参数
// 复杂的写法
// this.searchParams.category1Id = this.$route.query.category1Id;
// this.searchParams.category2Id = this.$route.query.category2Id;
// this.searchParams.category3Id = this.$route.query.category3Id;
// this.searchParams.categoryName = this.$route.query.categoryName;
// this.searchParams.keyword = this.$route.query.keyword;
Object.assign(this.searchParams, this.$route.query, this.$route.params);
console.log(this.searchParams, this.$route.query, this.$route.params);
},
mounted() { //由于只在挂载的时候,调用了getData()(请求search数据), 所以只能搜索一次商品
// // 派发action
// this.$store.dispatch('getSearchList', {});
this.getData()
},
computed: {
// mapGetters里面的写法: 传递的数组, 因为Getters计算是没有划分模块的(没有分home, search模块)
...mapGetters(['goodsList'])
},
methods: {
// 向服务器发送请求获取search模块数据,(根据参数不同,返回不同的数据)
// 把这次请求封装为一个函数, 当需要调用时,调用即可
getData() {
this.$store.dispatch('getSearchList', this.searchParams);
}
}
03解决search只能搜索一次
// 数据监听: 监听组件实例身上的属性的属性值变化
watch: {
// 监听路由的信息是否发生变化, 如果发生变化, 则再次发起请求
$route(newValue, oldValue) {
// 第一次请求完毕, 把相应的1, 2, 3级分类的id清空, 让他接受下一次的三级分类
// this.searchParams.category1Id = '';
// this.searchParams.category2Id = '';
// this.searchParams.category3Id = '';
// 再次发请求之前整理带给服务器参数
Object.assign(this.searchParams, this.$route.query, this.$route.params);
// 在次发送 ajax请求
this.getData()
console.log(this.searchParams, this.$route.query, this.$route.params);
},
}
这里我把清空相应的1, 2, 3级分类的id, 放在了第二次与第一次之间, 才生效 (老师放在最后面好像有bug😂)
04 面包屑展示业务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3kPTtMfh-1660641621519)(C:/Users/w3112/Desktop/vue%E7%AC%94%E8%AE%B0/vue%E9%A1%B9%E7%9B%AEimg/%E9%9D%A2%E5%8C%85%E5%B1%91.png)]
<li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}
<i @click="removeCategoryName">×</i></li>
// 当有searchParams.categoryName属性时才会显示面包屑
methods: {
// 向服务器发送请求获取search模块数据,(根据参数不同,返回不同的数据)
// 把这次请求封装为一个函数, 当需要调用时,调用即可
getData() {
this.$store.dispatch('getSearchList', this.searchParams);
},
// 删除面包屑名字
removeCategoryName() {
// 把带给服务器的参数(name, id)清空, 还需要再次向服务器发请求
// 清空时传空字符串, 还会把相应的字段带给服务器, 如果设为undefined则不会把相应的字段带给服务器(节省宽带), 但前提是这些字段不是必要的
this.searchParams.categoryName = undefined;
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
this.getData();
// 地址栏也需要修改, 进行路由跳转(当跳转时如果带有params参数则, 则携带params参数跳转到search, 就是只把query参数干掉)
if(this.$route.params) {
this.$router.push({name:'search', params: this.$route.params});
}
}
},
D7
01面包屑处理关键字
<!-- 分类面包屑 -->
<li class="with-x" v-if="searchParams.categoryName">{{searchParams.categoryName }}<i @click="removeCategoryName">×</i></li>
<!-- 关键字的面包屑 -->
<li class="with-x" v-if="searchParams.keyword">{{ searchParams.keyword }}<i @click="removeKeyword">×</i></li>//当有searchParams.keyword属性时才会显示面包屑
----------分割线---------
// 删除面包屑的关键字
removeKeyword() {
// 把带给服务器的参数(keyword)清空, 还需要再次向服务器发请求
this.searchParams.keyword = undefined;
this.getData();
// 发出清除关键字的请求
this.$bus.$emit('clear');
// 清除params参数
if(this.$route.query) {
// 携带query参数进行路由跳转
this.$router.push({name: 'search', query: this.$route.query})
}
}
- 使用去全局组件通信进行兄弟组件间的通讯
//main入口文件
beforeCreate() {
// 把vue实例(vm)赋给, Vue原型对象上的$bus属性
Vue.prototype.$bus = this
}
---------------------
//Header
mounted() {
// 在挂载时, 回应兄弟组件search的清除关键字请求
this.$bus.$on('clear', () => {
this.keyword = '';
})
}
02 完成品牌 与 平台属性的业务
我们还是需要收集用户选择的数据,把用户选择的数据信息,给服务器传递获取,获取相应的数据进行展示
组件通信-----(工作使用频率非常高、面试的时候经常出现)
父子:props、插槽、ref
子父:自定义事件 (如: @xx)
万能:vuex、$bus、pubsub
<!-- 分类面包屑 -->
<li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}<i @click="removeCategoryName">×</i></li>
<!-- 关键字的面包屑 -->
<li class="with-x" v-if="searchParams.keyword">{{ searchParams.keyword }}<i @click="removeKeyword">×</i></li>
<!-- 品牌的面包屑 -->
<li class="with-x" v-if="searchParams.trademark">{{
//split('xx')以xx分割字符串为数组 searchParams.trademark.split(":")[1] }}<i @click="removeTrademark">×</i></li>
<!-- 平台售卖属性的面包屑 -->
<li class="with-x" v-for="(attrValue, index) in searchParams.props" :key="index">{{ attrValue.split(":")[1] }}
<i @click="removeAttr(index)">×</i> </li>
-----------------分割-----------------------------------
// 删除面包屑名字
removeCategoryName() {
// 把带给服务器的参数(name, id)清空, 还需要再次向服务器发请求
// 清空时传空字符串, 还会把相应的字段带给服务器, 如果设为undefined则不会把相应的字段带给服 务器(节省宽带)
this.searchParams.categoryName = undefined;
this.searchParams.category1Id = undefined;
this.searchParams.category2Id = undefined;
this.searchParams.category3Id = undefined;
this.getData();
// 地址栏也需要修改, 进行路由跳转(当跳转时如果带有params参数则, 则携带params参数跳转到 search, 就是只把query参数干掉)
if (this.$route.params) {
this.$router.push({ name: 'search', params: this.$route.params });
}
},
// 删除面包屑的关键字
removeKeyword() {
// 把带给服务器的参数(keyword)清空, 还需要再次向服务器发请求
this.searchParams.keyword = undefined;
this.getData();
// 发出清除关键字的请求
this.$bus.$emit('clear');
// 清除params参数
if (this.$route.query) {
// 携带query参数进行路由跳转
this.$router.push({ name: 'search', query: this.$route.query })
}},
// 获取子组件发送的参数: trademark
trademarkInfo(trademark) {
// 整理品牌字段参数, (id:品牌名称)
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`
// 再次发送请求
this.getData();
console.log('我是父组件', trademark);},
// 删除品牌面包屑
removeTrademark() {
// 把带给服务器的参数(tmId, tmName)清空, 还需要再次向服务器发请求
this.searchParams.trademark = undefined;
this.getData();}
// 获取子组件发送的参数: attr, attrValue
attrInfo(attr, attrValue) {
console.log(attr, attrValue);
// 整理参数 属性id: 属性值: 属性名 (注意模板字符串里不能有空格)
let props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
// 将整理的参数添加到 data 里的props数组
// 数组去重(当indexOf检测到数组下标为-1时, 即数组里没有数据时, 才进行添加)
if (this.searchParams.props.indexOf(props) == -1) {
this.searchParams.props.push(props);}
// 再次发送请求
this.getData();},
// 删除平台售卖属性面包屑
removeAttr(index) {
console.log(index);
// 整理参数 (从点击获取到的索引值开始删除, 每次删除一个)
this.searchParams.props.splice(index,1);
// 再次发送请求
this.getData();}
经典面试题:数组去重[1,2,2,3,4,2];
03完成排序业务
- :综合与价格按钮,点击谁,谁的背景颜色变为红色。(类名:active)
谁有类这件事情,区分开综合与价格 - 将来点击综合||价格,还是需要给服务器发请求
【价格升序:把这个信息给服务器传递过去,服务器接收到信息,数据库自动把排序这件事情做了,把排序做好的数据返回给你,你展示即可】
order:服务器需要字段,代表的是排序方式
order这个字段需要的是字符串(可以传递也可以不传递)
1:代表综合
2:代表价格
3:asc代表升序
4:desc代表降序
告诉服务器排序方式有几种情况?
“1:asc” “1:desc” “2:asc” “2:desc”
- 综合与价格箭头
- 箭头【可以选用阿里图标库】 https://www.iconfont.cn/
- 对于综合|价格旁边的箭头【动态显示:时而又,时而没有】,带有类名active,拥有箭头
- 根据1、2区分谁有类名(背景)、谁有箭头
根据asc|desc区分它用哪一个箭头【上、下】
<li :class="{ active: isOne }" @click="changeOrder('1')">
<a>综合<span v-show="isOne" class="iconfont":class="{ 'icon-upload': isAsc, 'icon-download': isDesc }"></span></a>
</li>
<li :class="{ active: isTwo }" @click="changeOrder('2')">
<a>价格<span v-show="isTwo" class="iconfont":class="{ 'icon-upload': isAsc, 'icon- download': isDesc }"></span></a>
</li>
--------分割线----------------------
isOne() {
return this.searchParams.order.indexOf('1') != -1;
},
isTwo() {
return this.searchParams.order.indexOf('2') != -1;
},
isAsc() {
return this.searchParams.order.indexOf('asc') != -1;
},
isDesc() {
return this.searchParams.order.indexOf('desc') != -1;
},
// 排序操作
// flag形参: 他是一个标记, 代表用户点的是综合(1) 或者是价格(2) [会在用户点击时传入]
changeOrder(flag) {
// 把order的出初始值(data里的order)赋给originOrder
let originOrder = this.searchParams.order;
// 获取第一次点击状态 (点击的是综合(1)或者是价格(2))
// let originFlag = originOrder.split(':')[0];
let originFlag = flag; // 这里可以用flag 代替originOrder.split(':')[0]
// 获取第一次点击状态 (点击的是desc 或者是 asc)
let originSort = originOrder.split(':')[1];
// 准备一个空数组 用于存放点击的数据(容器)
let newOrder = '';
// 判断点击的是同一个按钮 (要么是综合1(第一次默认值), 要么是价格2)
if(flag == originFlag) {
// 如果点击的同一个按钮, 则判断他的originSort
newOrder = `${originFlag}:${originSort == 'desc'? 'asc': 'desc'}`
}else {
newOrder = `${flag}:${'desc'}`
}
this.searchParams.order = newOrder;
//注意这个this.searchParams.order是再出的order(data里面的order)
this.getData();
}
04分页功能。
分页功能: 防止页面一次渲染大量的数据, 造成卡顿, 面试的时候可能会问呢(自己封装过分页业务吗?)
1:分页器组件需要知道我一共展示多少条数据 ----total【100条数据】
2:每一个需要展示几条数据------pageSize【每一页3条数据】
3:需要知道当前在第几页-------pageNo[当前在第几页]
4:需要知道连续页码数【起始数字结束数字:连续页码数市场当中一般5、7、9】奇数,对称好看----[continues]
01静态分页器封装
分页器结构:
<div class="sui-pagination clearfix">
<ul>
<button :disabled="pageNo == 1" @click="$emit('getPageNo', pageNo - 1)">«上一页</button>
<!-- 注意下面配置的都是起始页与结合页码, 我们动态绑定的时当前页码 -->
<!-- 起始页码大于1时显示 -->
<li><a v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)">1</a></li>
<!-- 起始页码大于2时显示 -->
<li><span v-if="startNumAndEndNum.start > 2">...</span></li>
<!-- 中间部分 从1开始遍历到结束页码-->
<li><a v-for="(page, index) in startNumAndEndNum.end" :key="index" v-if="page >= startNumAndEndNum.start"
@click="$emit('getPageNo', page)" :class='{ active: pageNo == page }'>
{{ page }}</a></li>
<!-- 结束页页码小于总页数减1时显示 -->
<li class="dotted"><span v-if="startNumAndEndNum.end < totalPage - 1">...</span></li>
<!-- 结束页页码小于总页数时显示 -->
<li><a v-if="startNumAndEndNum.end < totalPage" @click="$emit('getPageNo', totalPage)">{{ totalPage }}</a></li>
<button :disabled="pageNo == totalPage">下一页»</button>
<div class="end"><span>共{{ total }}页</span></div>
</ul>
</div>
分页器逻辑:
<script>
export default {
name: 'Pagination',
props: ['pageNo', 'pageSize', 'total', 'continues'],
computed: {
// 总共多少页
totalPage() {
// Math.ceil: 向上取整(总数据 / 分的页数)
return Math.ceil(this.total / this.pageSize);},
//计算出连续的页码的起始页码与结束页码(连续的页码为5页)
startNumAndEndNum() {
// 结构赋值, 不用使用this.continues this.xxx
const { continues, pageNo, totalPage } = this;
// 定义变量用于保存起始页码与结束页码
let start = 0, end = 0;
// 如果总页数没有连续的页码多
if (continues > totalPage) {
start = 1; //起始页码则为1
end = totalPage; //结束页码就是总页数
} else {
// 总页数比连续的页码多(连续页数分为5页的标准: 当前页码前有两个连续页码, 当前页码后有两个连续页码)
start = pageNo - parseInt(continues / 2);//起始页码 = 当前页码 - 连续页数 / 2的取整
end = pageNo + parseInt(continues / 2); //结束页码 = 当前页码 + 连续页码 / 2 的取整
//因为当前页为1的时候star为负数, 因此要判断start(起始页)的最小初始值 -- 最小不能小于1(如果小于则强制使其等于1)
if (start < 1) {
start = 1, //start 必须是1 不能为0 | 负数
end = continues; // 结束页码就等于 连续页码
}
//当结束页码大于总页数时超过了页码的长度, 因此要判断end(结束页码)的最大初始值 -- 最大不能大于总页数(如果大于则强制使其等于总页数)
if (end > totalPage) {
end = totalPage, //结束页码超过最大页数时, 强制等于最大页数
start = totalPage - continues + 1; //结束页码超过最大页数时,强制等于最大页数后,在最大结束页码前有四个连续页码, 第一个即为开始页码,
}}
return { start, end }}}}
</script>
经典面试题:v-for与v-if优先级? v-for优先级更高(vue3中v-if更高)
02动态页面完成
在组件标签里传入参数:
<Pagination :pageNo="searchParams.pageNo" :pageSize="searchParams.pageSize" :total="total" :continues="5" @getPageNo="getPageNo"></Pagination>
接受子组件的参数:
// 自定义事件回调函数, 获取当前第几页
getPageNo(pageNo) {
// console.log(pageNo);
//把子组件传过来的pagNo参数, 赋给this.searchParams.pageNo
this.searchParams.pageNo = pageNo;
//再次发送请求
this.getData();
}
D8
01 滚动条初始位置
// 滚动行为
scrollBehavior(to, from, savedPosition) {
// 返回的y:0代表滚动条在最上方
return { y:0 }
}
02 商品详情页
5)push与replace区别?
编程式导航:push 与 replace
能不能记录历史记录:push(能记住历史记录) replace(不能记住历史记录)
目前项目当中:进行路由跳转(编程式导航)基础都是push
-
开发详情业务
-
熟悉静态页面、书写样式, 拆分组件
-
获取服务器动态展示(发送请求, 接收数据—api)
-
vuex
-
完成动态业务(展示数据)
-
- 完成静态页面, 拆分组价(pages/detail)
- 获取接口, 请求数据(api/index)
// 获取产品详情信息的接口 URL: /api/item/{skuId} 请求方式: get
export const reqGoodsInfo = (skuId) => {
return requests({url:`/item/${skuId}`,method:'get'});
}
- vuex(store/detail)
import { reqGoodsInfo } from "@/api"
const state = {
goodInfo: {}
}
const mutations = {
GETGOODSINFO(state, goodInfo) {
state.goodInfo = goodInfo;
}}
const actions = {
// 获取产品信息的action
async getGoodsInfo({commit}, skuid) {
let result = await reqGoodsInfo(skuid)
if(result.code == 200) {
commit('GETGOODSINFO', result.data)
}}}
const getters = {}
export default{
state,
actions,
mutations,
getters,
}
- 派发请求并,展示请求数据(pages/detail)
<script>
import ImageList from './ImageList/ImageList'
import Zoom from './Zoom/Zoom'
import { mapGetters } from 'vuex'
export default {
name: 'Detail',
components: {
ImageList,
Zoom,},
mounted() {
// 派发action 获取产品详情的信息
this.$store.dispatch('getGoodsInfo', this.$route.params.skuid)},
computed: {
...mapGetters(['categoryView'])
}}
</script>
03 放大镜静态组件
computed: {
// 获取仓库中的数据
...mapGetters(['categoryView', 'skuInfo','spuSaleAttrList']),
// 给子组件的数据, 并在没有数据返回时, 至少返回一个空数组
skuImageList() {
return this.skuInfo.skuImageList || [];
}
}
----------------------------------
props: ['skuImageList'],
computed: {
// 接受到父组件传过来的数据时没有对象数据, 至少返回一个空对象
imgObj() {
return this.skuImageList[0] || {}
//如果至少返回一个空对象则会报假错
}
}
在工作中假报错现象很常见,因为什么导致的,尽可能解决掉-----【不解决掉对于你的程序没有任何影响】
D9
01完成放大镜业务
5.1遮罩层为什么能动。
获取节点(DOM:必须要定位),通过JS动态修改left|top、定位元素才有left、top属性
methods: {
// 鼠标移动事件(获取鼠标坐标)
handler(event) {
// 获取遮罩层
let mask = this.$refs.mask;
let big = this.$refs.big;
// 正常框中心到边框的宽度 减去 遮罩层宽度的一半(中心到边框的宽度) (得到遮罩层边框到正常框边框的距离)
let left = event.offsetX - mask.offsetWidth/2;
let top = event.offsetY - mask.offsetHeight/2;
// 约束范围
if(left <=0) left = 0;
if(left >= mask.offsetWidth) left = mask.offsetWidth
if( top<= 0) top = 0;
if(top >= mask.offsetHeight) top= mask.offsetHeight
// 修改遮罩层元素的left|top属性值 (把减去后的边距 赋给遮罩层)
mask.style.left = left + 'px';
mask.style.top = top + 'px';
// 修改大图框元素的left|top属性值
// 乘以-2 : 大图等于两倍的遮罩框, 且大图的移动方向应该与遮罩框的移动方向相反
big.style.left = -2 *left + 'px';
big.style.top= -2 *top + 'px';
}
}
02购物车
01 判断加入购物车成功
加入购物车的业务? 购物车项目第二个重要地方
购物车:每一个人都有属于自己的购物车,那为什么不同用户登录自己账号,可以看见属于自己产品
一定是用户点击加入购物车,把你的产品信息提交给服务器进行保存,当你下次在进入购物车的时候,
需要向服务器发请求,获取你购物车里面的信息展示
项目:点击加入购物车按钮的时候,以前经常进行路由跳转【调到另外一个路由】,
但是你要注意,点击加入购物车这个按钮的时候,将用户选择产品,提交给服务器进行存储,如果服务器存储成功,之后在进行路由跳转
- 请求接口:
// 获取购物车的数据接口 URL: api/cart/addToCart/{skuId}/{skuName} 方式post
export const reqAddOrUpdateShopCart = (skuId, skuName) => {
return requests({url:`/cart/addToCart/${skuId}/${skuName}`, method:'post'})
}
- 在store里获取action(但没有保存数据在仓库中, 之前都是把数据存在仓库中, 然后组件调用再去捞数据)
// 获取购物车的action
async addOrUpdateShopCart({commit}, {skuId,skuNum}) {
let result = await reqAddOrUpdateShopCart(skuId, skuNum)
// 服务器加入购物车成功
if(result.code == 200 ) {
return 'ok'
}else {
//加入购物车失败
return Promise.reject(new Error('faile'));
}
}
- 派发action
<a @click="addShopCar">加入购物车</a>
-------------------------------------
// 加入购物车的回调函数
async addShopCar() {
//派发action, 通知服务器发送请求, 调用了store(仓库中的addOrUpdateShopCart), 但是要判断加入购物车成功还是失败,
//就要在这个方法上添加async, 使其返回一个Promise对象(Promise对象返回要么成功要么失败)
try {
await this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuid, skuNum:this.skuNum});
// 成功时进行路由跳转
this.$router.push({name: 'addcartsuccess'});
} catch (error) {
alert(error.message);
}
}
02 购物车进行路由传参结合会话存储
// 加入购物车的回调函数
async addShopCar() {
//派发action, 通知服务器发送请求, 调用了store(仓库中的addOrUpdateShopCart), 但是要判断加入购物车成功还是失败,
//就要在这个方法上添加async, 使其返回一个Promise对象(Promise对象返回要么成功要么失败)
try {
await this.$store.dispatch('addOrUpdateShopCart',{skuId:this.$route.params.skuid, skuNum:this.skuNum});
// 成功时进行路由跳转, 传递简单的query参数skuNum(产品个数)
this.$router.push({name: 'addcartsuccess', query:{skuNum:this.skuNum}});
// 会话存储, 一般存储的是字符串, 存储复杂的skuInfo参数(产品数据)
sessionStorage.setItem('SKUINFO', JSON.stringify(this.skuInfo)) //json.stringify转化为字符串,才能存到真正的数据
} catch (error) {
alert(error.message);
}
}
--------------------------
//在AddCartSuccess里获取会话存储数据
computed: {
skuInfo() {
//JSON.parse把会话存储里的字符串 转化对象才能用 .xx 捞到数据
return JSON.parse(sessionStorage.getItem('SKUINFO'))
}
}
面试题:本地存储与会话存储区别?
本地存储: 持久化 —存储容量为 5M
会话存储: 临时性 ---- 会话结束, 数据销毁
面试题:GET与POST
相同点:都是HTTP协议。
不同点:
1:GET请求携带参数是有上限的 post请求携带的参数是没有’上限的’
2:GET请求相对而言不安全,POST安全面试题:H5新增那些特性?
CSS4、本地存储、多媒体、canvas
D10
01购物车详情
<h3><i class="sui-icon icon-pc-right"></i>商品已成功加入购物车!</h3>
<div class="goods">
<!-- 左侧商品信息 -->
<div class="left-good">
<div class="left-pic">
<img :src="skuInfo.skuDefaultImg">
</div>
<!-- 右侧信息 -->
<div class="right-info">
<p class="title">{{skuInfo.skuNum}}</p>
<p class="attr">{{skuInfo.skuDesc}} 数量:{{$route.query.skuNum}}</p>
</div>
</div>
<!-- 右侧购物车 -->
<div class="right-gocart">
<router-link class="sui-btn btn-xlarge" :to="`/detail/${skuInfo.id}`">查看商品详情</router-link> //点击携带商品id 调回详情页面
<router-link to="/shopcart">去购物车结算</router-link>
</div>
</div>
02 购物车结算
01临时游客身份
-
使用临时id身份 —>uuid, 并携带id发送请求
- 创建一个uuid_token.js文件
import {v4 as uuidv4} from 'uuid' export const getUUID = () => { // 从本地获取uuid(查看本地存储里面是否存在) let uuid_token = localStorage.getItem('UUIDTOKEN'); // 如果没有就生成uuid if(!uuid_token) { // 生成uuid uuid_token = uuidv4(); // 在本地 存储一次 localStorage.setItem('UUIDTOKEN', uuid_token); } // 返回uuid return uuid_token; }
- 在store(仓库)中的detail.js里引入:
import {getUUID} from '@/utils/uuid_token'; const state = { goodInfo: {}, // 临时游客身份 uuid_token: getUUID() }
- 在配置api里的request.js(重写axios)中引入store, 配置请求响应头:
// 引入store仓库 import store from '@/store' --------- // 请求拦截器: 在发送请求之前. 请求拦截器可以检测到, 在请求发出去之前做一些事情 requests.interceptors.request.use((config) => { // console.log(store) // config: 配置对象, 对象里面有 headers 请求头 这个重要属性 // 进度条开始 if (store.state.detail.uuid_token) { // 请求头添加一个字段(userTempId) config.headers.userTempId = store.state.detail.uuid_token; } nprogress.start(); return config; });
-
vuex :
- 创建store大仓库里的 shopcart.js小仓库
import { reqCartList } from "@/api";
const state = {
// 初始值
cartList: [],
};
const mutations = {
GETCARTLIST(state, cartList) {
state.cartList = cartList
}
};
const actions = {
// 获取购物车结算列表数据
async getCartList({commit}) {
let result = await reqCartList();
if(result.code = 200) {
commit('GETCARTLIST', result.data)
}
}
};
const getters = {
// 购物车数据的外层对象
cartList(state) {
return state.cartList[0] || {}
},
};
export default {
state,
mutations,
actions,
getters,
}
- 在shopcart组件里进行动态配置
import { mapGetters } from 'vuex';
export default {
name: 'ShopCart',
mounted() {
this.getData();
},
methods: {
getData() {
this.$store.dispatch('getCartList');
}
},
computed: {
...mapGetters(['cartList']),
//数组里的真正数据, 数组
cartInfoList() {
return this.cartList.cartInfoList || []
},
// 计算购买产品的总价
totalPrice() {
let sum = 0
// 遍历
this.cartInfoList.forEach(item => {
sum+=item.skuNum * item.skuPrice
});
return sum;
},
// 判断底部的全选复选框是否勾选
isAllCheck() {
// every 遍历cartInfoList数组, 如果数组中有一个为1 则返回false
return this.cartInfoList.every(item =>item.isChecked == 1)
}
}
}
02处理购物车列表商品数量
复习, 什么时候用router-link、什么时候用编程式导航?
1.1如果一个按钮点击过后,如果只有路由跳转的业务【声明式导航、编程式导航】
1.2如果点击一个按钮除了路由跳转,还有其他的业务,可以选择编程式导航。
<li class="cart-list-con5">
<a href="javascript:void(0)" class="mins" @click="handler('minus', -1, cart)">-</a>
<input autocomplete="off" type="text" minnum="1" class="itxt" :value="cart.skuNum"
@change="handler('change', $event.target.value * 1, cart)">
<a href="javascript:void(0)" class="plus" @click="handler('add', 1, cart)">+</a>
</li>
--------------------------------------------
methods: {
getData() {
this.$store.dispatch('getCartList');
},
// 修改购物车中商品的数量, 并添加函数节流
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则传递给服务器0 (商品数量不变)
disNum = cart.skuNum > 1 ? -1 : 0;
break;
// 文本框输入
case "change":
//如果输入的是非法数字: 带有汉字, 英文, 特殊符号, 负号等 则给服务器传递 0
if (isNaN(disNum) || disNum < 1) {
disNum = 0;
} else {
// 取整再见与原来的数量 (防止输入的是小数)
disNum = parseInt(disNum) - cart.skuNum;
};
break;
}
// 向服务器派发action
try {
//修改成功
await this.$store.dispatch('addOrUpdateShopCart', {
// 那个商品的id
skuId: cart.skuId,
// 商品的数量
skuNum: disNum,
});
// 再次获取服务器最新数据
this.getData();
} catch (error) {
alert(error.message)
}
},1000)
03 购物车商品 的删除 与选中状态
// 删除商品操作
async deleteCartById(cart) {
try {
await this.$store.dispatch('deleteCartListBySkuId', cart.skuId);
this.getData();
} catch (error) {
alert(error.message)
}
},
// 修改商品的选中状态
async updateChecked(cart, event) {
// 修改商品
try {
// 带给服务器的参数isChecked, 不是布尔值, 应该是0|1
let isChecked = event.target.checked ? '1' : '0';
await this.$store.dispatch('updateCheckedById', { skuId: cart.skuId, isChecked });
// 再次发送请求
this.getData()
} catch (error) {
alert(error.message)
}
},
----------------------------------------
// 获取删除购物车商品的接口 URL: api//cart/deleteCart/{skuId} 方式: delete
export const reqDeleteCartById = (skuId) => {
return requests({url:`/cart/deleteCart/${skuId}`, method:'delete'})
}
// 修改购物车商品选中状态 URL:/api/cart/checkCart/{skuId}/{isChecked} 方式:get
export const reqUpdateCheckedById = (skuId, isChecked) => {
return requests({url:`/cart/checkCart/${skuId}/${isChecked}`, method: 'get'})
}
04 删除选中状态的商品与全选
// 删除选中的全部商品
async deleteAllCheckedCart() {
try {
// 派发actions
await this.$store.dispatch('deleteAllCheckedCart');
// 重新发送请求
this.getData();
} catch (error) {
}
},
// 全部商品的全选
async updateAllCartChecked(event) {
try {
// 判断是否点击, 点击为1 否则为0
let isChecked = event.target.checked? "1" : "0"
// 派发action
await this.$store.dispatch('updateAllCartIsChecked', isChecked);
this.getData();
} catch (error) {
alert(error.message)
}}
----------------------------
// 删除选中的全部商品
deleteAllCheckedCart({dispatch, getters}) {
// context: 小仓库, commit(提交mutations修改state) getters(计算属性) dispatch(派发action) state(当前仓库的数据)
// 获取购物车里的全部数据
let PromiseAll = [];
getters.cartList.cartInfoList.forEach(item => {
let promise = item.isChecked == 1? dispatch('deleteCartListBySkuId', item.skuId) : '';
// 将每一次的返回的promise添加到数组中
PromiseAll.push(promise);
});
// 只要全部返回成功, 则都成功, 只要有一个失败,则都失败
return Promise.all(PromiseAll);
},
// 修改全选的状态
updateAllCartIsChecked({dispatch, state}, isChecked) {
// 初始数组状态
let promiseAll = [];
// 遍历购物车商品列表
state.cartList[0].cartInfoList.forEach((item) => {
// 派发updateCheckedById 的 action 携带skuId, isChecked
let promise = dispatch('updateCheckedById', {skuId: item.skuId, isChecked: isChecked,});
promiseAll.push(promise);
});
// 返回最终结果, 只要全部返回成功, 则都成功, 只要有一个失败,则都失败
return Promise.all(promiseAll);
}
D11
01 注册与登录静态组件
- 获取接口:
// 获取验证码 URL: /api/user/passport/sendCode/{phone} 方式: get
export const reqGetCode = (phone) => {
return requests({url:`/user/passport/sendCode/${phone}`, method: 'get'})
}
// 获取注册 URL: /api/user/passport/register 方式: post 参数: phone, code, password
export const reqUserRegister = (data) => {
return requests({url:`/user/passport/register`, data, method:'post'});
}
// 登录 URL: /api/user/passport/login 方式: post 参数: phone password
export const reqUserLogin = (data) => {
return requests({url:`/user/passport/login`, data, method:'post'});
}
- 配置登录(Login)与注册(Register)仓库
//登录与注册仓库
import { reqGetCode, reqUserRegister, reqUserLogin} from "@/api";
// 登录与注册
const state = {
code: '',
};
const mutations = {
GETCODE(state, code) {
state.code = code;
},
USERLOGIN(state, token) {
state.token = token;
}};
const 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'));}},
// 用户注册
async userRegister({commit}, user) {
let result = await reqUserRegister(user);
if(result.code == 200) { //只需要注册返回成功或失败的结果, 不需要返回结果,
return 'ok';
}else {
return Promise.reject(new Error('faile'))}},
// 用户登录[token]
async userLogin({commit}, data) {
let result = await reqUserLogin(data);
// 拿到token, 用户唯一标识符, 通过token来向服务器获取用户信息
console.log(result)
};
const getters = {};
export default {
state,
mutations,
actions,
getters,}
- 派发action
- 注册组件
name: 'Register',
data() {
return {
// 收集表单数据 ---手机号
phone: '',
// 验证码
code: '',
// 密码
password: '',
// 确认密码
password1: '',
// 是否同意协议
agree: true
}},
methods: {
// 获取验证码
async getCode() {
try {
const { phone } = this;
// state 里定义的属性都会放到 vue实例上
// console.log(this);
phone && (await this.$store.dispatch('getCode', phone))
// 将组件中的code属性变为仓库中的验证码
console.log(this.$store);
this.code = this.$store.state.user.code;
} catch (error) {}},
// 完成注册
async userRegister() {
try {
const { phone, code, password, password1 } = this;
(phone && code && password == password1) && (await this.$store.dispatch('userRegister', { phone, code, password }));
// 成功则进行路由跳转
this.$router.push('/login')
} catch (error) {
alert('错误')}}}
-
- 登录组件
name: 'Login',
data() {
return {
phone: '',
password: '',
}},
methods: {
async userLogin() {
// 登录回调
try {
const { phone, password } = this;
//同时存才phon与password才能登录成功
(phone && password) && (await this.$store.dispatch('userLogin', { phone, password }));
// 登录成功进行路由跳转
this.$router.push('/home');
} catch (error) {
alert(error.message);
}}}
02携带token登录
获取接口
// 获取用户信息 URL:/api/user/passport/auth/getUserInfo 方式:get 参数: 请求头配置
export const reqUserInfo = () => {
return requests({url:`/user/passport/auth/getUserInfo`, method:`get`});
}
请求拦截器配置token
// 请求拦截器: 在发送请求之前. 请求拦截器可以检测到, 在请求发出去之前做一些事情
requests.interceptors.request.use((config) => {
// console.log(store)
// config: 配置对象, 对象里面有 headers 请求头 这个重要属性
// uuid的请求头
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;
});
vuex, user仓库
// 用户登录[token]
async userLogin({ commit }, data) {
let result = await reqUserLogin(data);
// 拿到token, 用户唯一标识符, 通过token来向服务器获取用户信息
if (result.code == 200) {
commit("USERLOGIN", result.data.token);
// 持久化存储 token
setToken(result.data.token);
return 'ok'
} else {
return Promise.reject(new Error('faile'));
}
},
// 获取用户信息
async getUserInfo({commit}) {
let result = await reqUserInfo();
if(result.code == 200) {
commit('GETUSERINFO', result.data);
}},
--------------------mutations-------
// 登录
USERLOGIN(state, token) {
state.token = token;
},
// 用户信息
GETUSERINFO(state, userInfo) {
state.userInfo = userInfo
},
--------------------state------------------
const state = {
code: '',
// 如果本地存储的有token则使用本地token
token: getToken(),
userInfo: {},
};
token.js:
// 存储token
export const setToken = (token) =>{
localStorage.setItem("TOKEN", token);
}
// 读取token
export const getToken = () => {
return localStorage.getItem("TOKEN");
}
// 清空token (退出登录)
export const removeToken = () => {
localStorage.removeItem("TOKEN");
}
token面试题:项目当中token过期、失效如何处理?
答:清除本地token(本地存储),让用户回到登录页,获取最新的token
一、用户登录以后获取用户信息进行展示
企业项目:登录成功以后,服务器会返回token【存储于vuex当中】,如果想获取用户信息
还需要再发请求【用户信息】,携带token给服务器。
api/user/passport/auth/getUserInfo 获取用户信息的接口
1.1:为什么刷新页面,用户信息就消失
用户刷新页面,用户信息消失没了获取不到,因为token没有携带给服务器。
Vuex存储数据是否持持久化的? 并非持久化, 当浏览器刷新时数据会丢失
1.2:本地存储持久化存储token ----> localStorage.setItem(result.data.token)
1.2为什么去别的模块【非home模块】获取用户信息失败?
因为你去别的模块根本没有发请求获取用户信息,没办法展示用户信了
怎么解决:
每一个组件都在mounted里面发起获取用户信息,进行展示(可以太麻烦)
残留的问题:用户在home模块刷新的时候,用户信息一直在展示(mounted执行的时候在向服务器发请求、获取用户信息展示)
home->search[用户信息刷新数据就没了,因为在search模块当中根本没有发请求获取用户信息]
search-detail[根本没有获取用户信息进行展示]
03退出登陆
二、退出登录
2.1发请求,需要通知服务器,把现在用户身份token【销毁】
2.2清除仓库数据+本地存储数据【都需要清理】
获取接口
// 退出登录 URL: /api/user/passport/logout 方式get
export const reqLogout = () => {
return requests({url:`/user/passport/logout`, method:'get'});
}
vuex, user仓库
------acations-----
// 退出登录
async userLogout({commit}) {
let result = await reqLogout();
if(result.code == 200) {
commit("CLEAR");
return 'ok';
}else {
return Promise.reject(new Error('faile'));
}}
----------mutations------------
// 退出登录
CLEAR(state) {
state.token = '';
state.userInfo = {};
// 清空本地存储
removeToken();
}
派发action
------template------
<a class="register" @click="logout">退出登录</a>
-------methods-------
methods: {
async logout() {
// 退出登录, 清空 userInfo, token
try {
await this.$store.dispatch('userLogout');
// 退出成功则返回首页home
this.$router.push('/home')
} catch (error) {
}},
三、演示一些操作,你看一下是否正常?
用户已经登录了,用户想从home路由跳转到login路由,不应该这么操作了。
现在用户登录以后,home路由不应该跳转到login路由当中【因为登陆了】,
D12
01 导航守卫
三大守卫:
- 全局守卫:
项目当中任何路由变化都可以检测到,通过条件判断可不可以进行路由跳转。 - 前置守卫:路由跳转之前可以做一些事情。
- 后置守卫:路由跳转已经完成在执行。
使用前置守卫判断:
// 全局守卫: 前置守卫(路由跳转之前进行判断(守卫))
router.beforeEach(async (to, form, next) => {
// to: 获取到要跳转到那个路由的信息 console.log(to)
// from: 获取到你从那个路由跳过来的信息
// next: (放行,下一步)函数 : next(), next(path):放行到指定路由的路径 next(false): url路径发生变化, 退回到from点
// 用户登陆了才会有token
let token = store.state.user.token;
// 用户信息(用户名)
// let userInfo = store.state.user.userInfo; 不能直接判断userInfo, (因为在未登录的状态下, userInfo为空对象, 但是空对象的Boolean值永远为true)
let name = store.state.user.userInfo.name;
// 如果用户已经登陆, 则不能跳转login
if(token) {
// 如果已经登录则, 把去login的路径, 重定向为 home
if(to.path == '/login') {
next('/home')
}else {
// 已登录, 但不是跳转login
// 如果存在用户名
if(name) {
next();
}else {
// 没有用户信息, 则派发action让仓库存储用户信息在进行跳转
try {
//解决了在不同组件, 刷新浏览器, 用户信息(不是登录状态)不存在的问题
await store.dispatch('getUserInfo');
} catch (error) {
// token 过期, 则清空token, 重定向为login再次登录
// 清空token
await store.dispatch('userLogout');
// 重定向路径为login
next('/login');
}}}
}else {
// 未登录
next();
}});
D13
01交易业务
项目十三天:
发现:基础知识点–>回首复习:, 分页, 购物车, 登录、注册
用户地址信息与交易页面
- 获取接口
// 获取用户地址信息 URL: /api/user/userAddress/auth/findUserAddressList 方式: get
export const reqAddressInfo = () => {
return requests({url:`/user/userAddress/auth/findUserAddressList`, method: 'get'});
}
// 获取订单交易页信息 URL: /api/order/auth/trade 方式:get
export const reqOrderInfo = () => {
return requests({url:`/order/auth/trade`, method:'get'});
}
- vuex, trade.js仓库
------------state--------
const state = {
address: [],
orderInfo: {},
};
------------mutations--------
const mutations = {
GETUSERADDRESS(state, address) {
state.address = address;
},
GETORDERINFO(state, orderInfo) {
state.orderInfo = orderInfo;
}
};
-----------actions--------------
const actions = {
// 获取用户地址信息
async getUserAddress({commit}) {
let result = await reqAddressInfo();
if(result.code == 200) {
commit('GETUSERADDRESS', result.data);
}
},
// 获取商品交易页
async getOrderInfo({commit}) {
let result = await reqOrderInfo();
if(result.code == 200) {
commit('GETORDERINFO', result.data);
} }};
- 派发请求action
mounted() {
this.$store.dispatch('getUserAddress');
this.$store.dispatch('getOrderInfo');
},
computed: {
...mapState({
addressInfo: (state) => {
return state.trade.address;
},
orderInfo: state => state.trade.orderInfo
}),
// 提交订单, 最终地址
userDefaultAddress() {
// 查找数组中点击的那个地址(isDefault == 1), 且至少有一个空对象
return this.addressInfo.find(item => item.isDefault == 1) || {};
}
},
02 提交订单业务
当用户点击提交订单按钮的时候,需要发请求的
提交订单的请求地址:/api/order/auth/submitOrder?tradeNo={tradeNo}
- 获取接口
// 获取提交订单接口 URL: /api/order/auth/submitOrder?tradeNo={tradeNo} 方式: post
export const reqSubmitOrder = (tradeNo, data) => {
return requests({url:`/order/auth/submitOrder?tradeNo=${tradeNo}`, data, method: 'post'});
}
- 不使用vuex , 在main.js入口文件进行配置
// 统一接口api文件夹里面的全部请求函数, 统一引入
import * as API from '@/api';
new Vue({
render: h => h(App),
// 注册路由(k,v 省略写法)
router,
// 注册仓库: 组件实例身上会多了一个$store 的属性
store,
// 配置全局总线, 进行组件通信
beforeCreate() {
// 把vue实例(vm)赋给, Vue原型对象上的$bus属性
Vue.prototype.$bus = this
// 把所有的api请求, 挂载到Vue原型对象上的$API属性, 所有组件都能使用
Vue.prototype.$API = API;
}
}).$mount('#app')
- 在Trade组件发送请求
methods: {
// 提交订单
async submitOrder() {
// 交易编码
let {tradeNo} = this.orderInfo;
let data = {
consignee: this.userDefaultAddress.consignee, //收件人名字
consigneeTel: this.userDefaultAddress.phoneNum, //收件人手机号
deliveryAddress: this.userDefaultAddress.fullAddress, //收件人地址
paymentWay: "ONLINE", //支付方式 : 在线支付
orderComment:this.msg, //买家留言
orderDetailList: this.orderInfo.detailArrayList, //商品订单
}
let result = await this.$API.reqSubmitOrder(tradeNo, data);
console.log(result);
}
}
03 微信支付
前台:需要告诉服务器:谁买的、买的啥、买几个、 支付多少钱、留言信息…
后台:订单号,这笔交易的一个标识符【支付的】
- 获取支付信息与支付状态
// 获取支付信息 URL: /api/payment/weixin/createNative/{orderId} 方式: get
export const reqPayInfo = (orderId) => requests({url:`/payment/weixin/createNative/${orderId}`, method: 'get'});
// 获取支付订单状态 URL: /api/payment/weixin/queryPayStatus/{orderId} 方式: get
export const reqPayStatus = (orderId) => requests({url:`/payment/weixin/queryPayStatus/${orderId}`, method: 'get'})
- 在pay.vue组件内发送请求
mounted() {
// 不要在生命周期函数中写 async|await
this.getPayInfo();
},
mrthods: {
// 获取支付信息
async getPayInfo() {
let result = await this.$API.reqPayInfo(this.orderId);
// 如果成功, 组件当中存储支付信息
if (result.code == 200) {
this.payInfo = result.data;
}
}},
- 在pay.vue组价里, 使用element-ui创建弹出框, 使用qrcode生成二维码
import QRCode from 'qrcode'
--------------data-------------------
data() {
return {
payInfo: {},
timer: null,
// 支付状态码
code: '',
}},
---------------methods-------------------
// element 弹出框
async open() {
// 生成二维码(地址)
let url = await QRCode.toDataURL(this.payInfo.codeUrl); //字符串转图片
// 弹出框样式
this.$alert(`<img src=${url} />`, '请你微信支付', {
dangerouslyUseHTMLString: true,
// 中间布局
center: true,
// 是否显示取消按钮
showCancelButton: true,
// 取消按钮的文本样式
cancelButtonText: '取消支付',
// 确认按钮文本样式
confirmButtonText: '支付成功',
// 显示右上角的'X'
showClose: false,
});
// 支付成功,路由跳转, 支付失败,提示信息
// 如果没有定时器则, 创建一个定时器
if (!this.timer) {
this.timer = setInterval(async () => {
// 发送请求,获取用户支付状态
let result = await this.$API.reqPayStatus(this.orderId);
// 支付成功
if (result.code == 200) {
// 清除定时器
clearInterval(this.timer);
this.timer = null;
// 保存支付成功并返回支付码(code)
this.code = result.code;
// 关闭弹出框
this.$msgbox.close();
// 跳转路由
this.$router.push('/paysuccess');
// 关闭弹出框的配置值
beforeClose: (type, instance, done) => {
// type: 区分取消|确定按钮
// instance: 当前组件实例
// done: 关闭弹出框的方法
if (type == 'cancel') {
alert('请联系管理人员');
// 清除定时器
clearInterval(this.timer);
this.timer = null;
// 关闭弹出框
done();
} else {
// 判断是否真的支付成功了
if(type == 200){
// 清除定时器
clearInterval(this.timer);
this.timer = null;
// 关闭弹出框
done();
}}}}}, 1000) }}
element-ui官方UI组件库(插件)?
react框架:
UI组件库antd【蚂蚁金服旗下PC端UI组件库】
antd-mobile【蚂蚁金服旗下的移动端UI组件库】
Vue框架:
element-UI【饿了吗旗下的UI组件库,官方承认的PC组件库插件】
vant【Vue官方提供移动端UI组件库】
官网地址:https://element.eleme.cn/#/zh-CN
官网地址:https://youzan.github.io/vant/#/zh-CN/
第一步:项目中安装element-ui组件库 [2.15.6版本:Vue2]
第二步:在入口文件引入elementUI组件库
第一种:全部引入【不采用:因为项目中只是用到一个组件,没必要全都引入进来】
第二种:按需引入【按照开发需求引入相应的组件,并非全部组件引入】
第三步:按需引入,安装相应的插件
cnpm install babel-plugin-component -D
文档中说的.babelrc文件,即为babel.config.js文件
修改完babel.config.js配置文件以后,项目重启
第四部:按照需求引入相应的组件即可
Vue.component();
Vue.prototype.$xxx = xxx;
element-ui
使用messageBox显示弹框
6.3展示二维码----qrcode插件
通过qrCode.toDataUrl方法,将字符串转换为加密的在线二维码链接,通过图片进行展示。
moment.js, swiper.js, nprogress.js, qrcode.jsGET|POST:短轮询,请求发一次,服务器响应一次,完事。
第一种做法:前端开启定时器,一直找服务器要用户支付信息【定时器】
第二种做法:项目务必要上线 + 和后台紧密配合
当用户支付成功以后,需要后台重定向到项目某一个路由中,将支付情况通过URL参数形式传给前端,
前端获取到服务器返回的参数,就可以判断了。
D14
01 个人中心路由搭建
1.1当年学习路由的时候:一级路由、二级路由、三级路由 【二级路由搭建】
1.2完成个人中心数据的展示【分页】
2)未登录全局守卫的判断
在前面课程当中:导航守卫【导航:路由发生变化,守卫可以检测到,通过判断,确定这次路由跳转】
前置守卫:在路由跳转之前,进行判断
后置守卫:路由都已经跳转完毕才执行。
未登录的情况:
全局守卫:只要的项目当中任何某一个路由发生变化,就会出发。
项目守卫使用:一般有用前置全局守卫
用户登录:
用户未登录:点击购物车的结算按钮->交易页面【没有登录:去不了】
未登录不能调到支付页面
未登录不能调到支付成功页面
未登录不能去个人中心【都不知道你是谁:展示谁的个人中心啊】
3)路由独享守卫
路由独享守卫:需要在配置路由的地方使用
导航守卫:全局守卫->项目当中有任何路由变化【a->b,b->d】触发。
路由独享守卫:专门负责某一个路由
用户登陆了:
去交易页面:从购物车才能跳转到交易页面。
next():你本来想去哪里,我就放行,你就去完事了。
next(‘/login’):执行守卫放行到执行的路由。
next(false):路由跳转的时候,从哪里来回那里去。
4)组件内守卫---->一般很少用【全局 + 路由独享守卫】
组件内守卫:也是专门负责某一个路由【并非负责全部路由】,写法和路由独享守卫有区别?
组件内守卫需要书写在组件内部
beforeRouteEnter
beforeRouteUpdate (2.2 新增)
beforeRouteLeave
6)路由懒加载
面试【高频的面试】:项目的性能优化手段有哪些?
v-if|v-show:尽可能采用v-show
按需引入【lodash、elementUI】
防抖与节流
路由懒加载:当用户访问的时候,加载对应组件进行展示。
7)图片懒加载
vue-lazyload:图片懒加载
图片:比用用户网络不好,服务器的数据没有回来,
总不可能让用户看白色,至少有一个默认图片在展示。
8)表单验证【后台管理系统:大量使用elementUI】
以后工作的时候经常会进行表单验证【element-ui】进行表单验证,so 简单。
项目当中表单验证功能比较常见的。
8.1vee-validate插件:Vue官方提供的一个表单验证的插件【老师接下来的操作能大概看懂即可】
这个插件很难用:如果你翻看它的文档(看一个月:不保证能看懂),依赖文件很多(文档书写的很难理解)
花大量时间学习,很难搞懂。
8.2哪怕将来工作了,真的使用vee-valadiate【老师项目搞出来:改老师代码即可】
使用步骤:
1:安装vee-valadite,别安装最新版本@2
2:在plugins文件夹中创建一个validate.js[专门注册vee-valadite]
3:注册插件
4:注册插件的时候,用中文,以及需要验证的字段【用中文显示提示形式】
5:在入口文件需要引入执行一次
6:使用vee-valadiate插件
8)vee-validate 基本使用
第一步:插件安装与引入
cnpm i vee-validate@2 --save 安装的插件安装2版本的
import VeeValidate from ‘vee-validate’
import zh_CN from ‘vee-validate/dist/locale/zh_CN’ // 引入中文 message
Vue.use(VeeValidate)
第二步:提示信息
VeeValidate.Validator.localize(‘zh_CN’, {
messages: {
…zh_CN.messages,
is: (field) => ${field}必须与密码相同
// 修改内置规则的 message,让确认密码和密码相同
},
attributes: { // 给校验的 field 属性名映射中文名称
phone: ‘手机号’,
code: ‘验证码’,
password:‘密码’,
password1:‘确认密码’,
isCheck:‘协议’
}
})
第三步:基本使用
{{ errors.first(“phone”) }}
const success = await this.$validator.validateAll(); //全部表单验证
//自定义校验规则
//定义协议必须打勾同意
VeeValidate.Validator.extend(‘agree’, {
validate: value => {
return value
},
getMessage: field => field + ‘必须同意’
})
第一步:项目中安装element-ui组件库 [2.15.6版本:Vue2]
第二步:在入口文件引入elementUI组件库
第一种:全部引入【不采用:因为项目中只是用到一个组件,没必要全都引入进来】
第二种:按需引入【按照开发需求引入相应的组件,并非全部组件引入】
第三步:按需引入,安装相应的插件
cnpm install babel-plugin-component -D
文档中说的.babelrc文件,即为babel.config.js文件
修改完babel.config.js配置文件以后,项目重启
第四部:按照需求引入相应的组件即可
Vue.component();
Vue.prototype.$xxx = xxx;
element-ui
使用messageBox显示弹框
6.3展示二维码----qrcode插件
通过qrCode.toDataUrl方法,将字符串转换为加密的在线二维码链接,通过图片进行展示。
moment.js, swiper.js, nprogress.js, qrcode.jsGET|POST:短轮询,请求发一次,服务器响应一次,完事。
第一种做法:前端开启定时器,一直找服务器要用户支付信息【定时器】
第二种做法:项目务必要上线 + 和后台紧密配合
当用户支付成功以后,需要后台重定向到项目某一个路由中,将支付情况通过URL参数形式传给前端,
前端获取到服务器返回的参数,就可以判断了。
D14
01 个人中心路由搭建
1.1当年学习路由的时候:一级路由、二级路由、三级路由 【二级路由搭建】
1.2完成个人中心数据的展示【分页】
2)未登录全局守卫的判断
在前面课程当中:导航守卫【导航:路由发生变化,守卫可以检测到,通过判断,确定这次路由跳转】
前置守卫:在路由跳转之前,进行判断
后置守卫:路由都已经跳转完毕才执行。
未登录的情况:
全局守卫:只要的项目当中任何某一个路由发生变化,就会出发。
项目守卫使用:一般有用前置全局守卫
用户登录:
用户未登录:点击购物车的结算按钮->交易页面【没有登录:去不了】
未登录不能调到支付页面
未登录不能调到支付成功页面
未登录不能去个人中心【都不知道你是谁:展示谁的个人中心啊】
3)路由独享守卫
路由独享守卫:需要在配置路由的地方使用
导航守卫:全局守卫->项目当中有任何路由变化【a->b,b->d】触发。
路由独享守卫:专门负责某一个路由
用户登陆了:
去交易页面:从购物车才能跳转到交易页面。
next():你本来想去哪里,我就放行,你就去完事了。
next(‘/login’):执行守卫放行到执行的路由。
next(false):路由跳转的时候,从哪里来回那里去。
4)组件内守卫---->一般很少用【全局 + 路由独享守卫】
组件内守卫:也是专门负责某一个路由【并非负责全部路由】,写法和路由独享守卫有区别?
组件内守卫需要书写在组件内部
beforeRouteEnter
beforeRouteUpdate (2.2 新增)
beforeRouteLeave
6)路由懒加载
面试【高频的面试】:项目的性能优化手段有哪些?
v-if|v-show:尽可能采用v-show
按需引入【lodash、elementUI】
防抖与节流
路由懒加载:当用户访问的时候,加载对应组件进行展示。
7)图片懒加载
vue-lazyload:图片懒加载
图片:比用用户网络不好,服务器的数据没有回来,
总不可能让用户看白色,至少有一个默认图片在展示。
8)表单验证【后台管理系统:大量使用elementUI】
以后工作的时候经常会进行表单验证【element-ui】进行表单验证,so 简单。
项目当中表单验证功能比较常见的。
8.1vee-validate插件:Vue官方提供的一个表单验证的插件【老师接下来的操作能大概看懂即可】
这个插件很难用:如果你翻看它的文档(看一个月:不保证能看懂),依赖文件很多(文档书写的很难理解)
花大量时间学习,很难搞懂。
8.2哪怕将来工作了,真的使用vee-valadiate【老师项目搞出来:改老师代码即可】
使用步骤:
1:安装vee-valadite,别安装最新版本@2
2:在plugins文件夹中创建一个validate.js[专门注册vee-valadite]
3:注册插件
4:注册插件的时候,用中文,以及需要验证的字段【用中文显示提示形式】
5:在入口文件需要引入执行一次
6:使用vee-valadiate插件
8)vee-validate 基本使用
第一步:插件安装与引入
cnpm i vee-validate@2 --save 安装的插件安装2版本的
import VeeValidate from ‘vee-validate’
import zh_CN from ‘vee-validate/dist/locale/zh_CN’ // 引入中文 message
Vue.use(VeeValidate)
第二步:提示信息
VeeValidate.Validator.localize(‘zh_CN’, {
messages: {
…zh_CN.messages,
is: (field) => ${field}必须与密码相同
// 修改内置规则的 message,让确认密码和密码相同
},
attributes: { // 给校验的 field 属性名映射中文名称
phone: ‘手机号’,
code: ‘验证码’,
password:‘密码’,
password1:‘确认密码’,
isCheck:‘协议’
}
})
第三步:基本使用
{{ errors.first(“phone”) }}
const success = await this.$validator.validateAll(); //全部表单验证
//自定义校验规则
//定义协议必须打勾同意
VeeValidate.Validator.extend(‘agree’, {
validate: value => {
return value
},
getMessage: field => field + ‘必须同意’
})