Ⅰ
node_modules文件夹:项目依赖
public文件夹:放置静态资源(图片)
注意:放在public文件夹中的静态资源,webpack进行打包时候
会原封不动打包到dist文件夹中
src文件夹(程序员文件夹):
assets:放置静态资源(一般放置多个组件共用的静态资源)
注意:放置在assets里面的静态资源在webpack打包的时候
会把静态资源当作一个模块,打包到JS文件里面
components文件夹:一般放置的是非路由组件(全局组件)
比如:三级联动组件(TypeNav)、轮播图(Carsousel)、分页器(Pagination)
头部导航栏(Header)、页尾(Footer)
App.vue:唯一的根组件,vue当中组件(.vue)
main.js:程序入口文件,也是整个程序最先执行的文件
babel.config.js:配置文件(babel相关)eg.可以把ES6语法翻译成ES5语法
package.json文件:认为是项目“身份证”,记录项目叫做什么,有哪些依赖,项目怎么运行
package-lock.json文件:缓存性文件
README.md:说明性文件
2.1项目运行起来浏览器自动打开
(1)找到package.json文件
在scripts中"serve":"vue-cli-serviceserve--open"
(2)eslint校验工具关闭,它可以检测
在根目录下,创建vue.config.js(已存在就直接添加)
lintOnSave:false
因为:声明变量变量未使用eslint校验工具就报错,因此关闭它
(3)src文件夹简写方式,配置别名,@
一般已配置完毕[@代表的是src文件夹,方便查找]
jsconfig.json配置别名@提示,要不然路径查找输写困难
{
"compilerOptions":{
"baseUrl":'./',
"paths":{
"@/*":["src/*"]
}
},
"exclude":["node_modules","dist"]
}
3.项目路由的分析
vue-router
前端所谓路由:kv键值对,
key:URL(地址栏中的路径)
value:相应路由组件
该项目是上中下结构
路由组件:
home首页路由组件,search路由组件,login登录路由,register注册路由
所以可以把头部和尾部定为非路由组件
非路由组件:(共用header和footer)
Header【在首页,搜索页】
Footer【在首页,搜索页】但是在登录页|注册页没有
1.书写静态页面(HTML+CSS)
2.拆分组件
3.获取服务器的数据动态展示
4.完成相应的动态业务逻辑
注意1:组件结构+组件样式+图片资源
注意2:项目采用的less样式,浏览器不识别less样式,需要通过less、less-loader进行处理
把less样式变为css样式,浏览器才能够识别
注意3:如果让组件识别less样式,需要在style标签的身上加上lang:less属性
使用组件的步骤(非路由组件)
创建与定义
引入
注册
使用
5.路由组件的搭建
共有路由组件四个——vue-router
:Home\Search\Login\Register
-components文件夹:放置的是非路由组件(共用全局组件)
-pages|views文件夹:放置路由组件
5.1配置路由
项目当中配置的路由一般放置在router文件夹中
5.2总结
路由组件与非路由组件的区别?
1.路由组件一般放置在pages|views文件夹中,非路由组件放置在components文件夹中
2.路由组件一般要在router文件夹中进行注册(使用的即为路由的名字),非路由组件一般是以标签的形式调用<header><footer>
$route:一般获取路由信息[路径、query、params等等]
$router:一般进行编程式导航进行路由跳转[push|replace]
3.注册完路由,不管是路由组件,还是非路由组件身上都有$router和$route属性
但是在访问根目录的时候一般要去到首页,所以需要重定向
path:'/',
redirect:需要跳转到的路径
5.3路由的跳转
有两种形式:声明式和编程式
声明式导航router-link,可以进行路由跳转
编程式导航push|replace,可以进行路由导航
编程式导航:声明式导航能做的,编程式导航都能做
编程式导航除了路由跳转也可以实现其他业务逻辑
补充小知识:
push和replace的区别?
router.push(location)会向histroy栈添加一条新的记录,当用户点击浏览器后退按钮时,回到之前的URL
<router-link:to=''>相当于router.push(...字符串路径|描述地址的对象)
eg.
router.push('home')//字符串
this.$router.push({path:'/login?url='+this.$router.path})//对象
router.push({name:'user',params:{userId:123})//命名路由
router.replace(location)
导航是不会留下histroy,替换掉当前的histroy
<router-link:to="..."replace></router-link>
//编程式
router.replace(...)
this.$router.push({
path:'/home',replace:true
})
params:/router1/:id,/router1/123,/router1/789,这里的id叫做params
query:/router1?id=123,/router1?id=456,这里的id叫做query。
6.Footer组件显示与隐藏
显示与隐藏组件使用:v-if(频繁操作DOM)|v-show
Footer组件:在Home,Select显示,在登录、注册时候隐藏
6.1我们可以直接使用v-show='$route.path="url"'来获取当前路由身上的信息,通过路由的路径判断Footer,但是不太方便因为如果一旦网页数量过多那么代码量也会很多
6.2使用路由元信息在router的route里加上meta属性它的返回值是boolean所以可以直接写meta:{show:xxx(boolean)}
路由要的是配置信息,不能随便写参数属性
v-show:$route.meta.show
7路由传参
7.1路由跳转有几种方式
声明式导航:router-linkto属性,可以实现路由的跳转
编程式导航:利用的组件实例$router.push|replace方法,可以实现路由的跳转
8.2路由传参,参数有几种写法
params参数:属于路径当中的一部分,注意,在配置路由的时候需要占位
query参数:不属于路径中的一部分,类似于ajax的queryString
/home?=k=v&kv=,配置路由的时候不需要占位
补充
路由传递参数
- 字符串形式---直接拼接
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()}})
//1.题目:路由传参(对象写法)可以path与params结合使用吗
//不可以,只能结合query
//this.$router.push({path:'/search',params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}})
//2.题目:如何指定params参数可传可不传
//如何指定params参数可以传递或者不传递,就是在配置路由的时候
//在占位后面加上问号(?),表示可传可不传
//this.$router.push({name:'search',query:{k:this.keyword.toUpperCase()}})
eg.path:'/search/:keyword?'
//3.题目:params可传也不传,但是传过来是空值,如何解决
/因为如果传了空串还是会有路径问题
//使用undefined解决params参数可传可不传的问题(包括空字符串)
//this.$router.push({
//name:"search",
//params:{keyword:""||undefined},
//query:{k:this.keyword.toUpperCase()},
//});
//4.题目:路由组件能不能传递props参数
//可以,有三种写法:
//,query:{k:this.keyword.toUpperCase()}
//this.$router.push({name:'search',params:{keyword:this.keyword||undefined}});
//如果有query参数也不能忽略
props:true,布尔值类型,只用params,可以把params属性作为路由组件中的属性
props:{a:1,b:2}对象类型,给路由多传递一些参数props
props:($route)=>{
return{keyword:$route.params.keyword,k:this.$route.k}
}
函数类型,可以params参数和query参数,通过props传递给路由组件
//路由组件是可以传递props
//props:['keyword','k']
Ⅱ
1.编程式路由跳转到当前路由(参数不变),多次执行会抛出NavigationDuplicated的警告错误
路由跳转有两种形式:
声明式导航和编程式导航
--对于声明式导航没有报错,因为在vue-router已经处理好了
1.1为什么编程式导航就有这个错误
'vue-router':'^3.5.3':最新的vue-router引入promise
functionpush(){
returnnewPromise((resolve,reject)=>{
})
}
回答:通过push方法传递相应的成功、失败的回调函数,可以捕获到当前错误,可以解决
通过底部代码,可以解决问题
this.$router.push({name:'search',params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}},()=>{},(error)=>{
console.log(error);
})
但是这种写法治标不治本,其他别的组件中,就是编程式导航中还是会有这个问题,都要使用回调函数获取异常会导致代码高耦合
说明:
this:是当前组件实例(search),
this.$router属性:
属性值@VueRouter类的一个实例,当在入口文件注册路由的时候,已经给组件实例添加$router和$route
push:VueRouter类上的一个实例
functionVueRouter(){}
//这个是原型对象的方法
VueRouter.prototype.push=function(){
//这个函数的上下文为VueRouter类的一个实例
}
let$router=newVueRouter();
$router是VueRouter的一个实例,可以借用原型对象身上的push方法
$router.push(xxx)
治本重写VueRouter身上的push
把VueRouter.push()重写
1.先保存因为我们要继承
2.重写,实例上下文要继承VueRouter
如果有参数就覆盖(resolve,reject),没有就使用默认的添加(这样才不会抛出异常报错,异常都被截取了)
意思就是它会返回promise->会有resolve和reject两种返回表示该路由跳转是否成功或失败
但是我们在实际应用中不管这条数据就会报错
所以在路由配置中重写了push和replace两个方法
首先,保存原本的原型方法
letoriginPush=VueRouter.prototype.push;
letoriginReplace=VueRouter.prototype.replace;
再次,重写push|replace方法
VueRouter.prototype.push=function(location,resolve,reject)
{
//不能直接调用originPush();因为它的上下文是window
//而是要改变它的指向
if(resolve&&reject){
originPush.call(this,location,resolve,reject)
}else{
originPush.call(this,location,()=>{},()=>{}) } }
VueRouter.prototype.replace同上
2.Home模块组件拆分
--先把静态页面完成
--拆分出静态组件
--获取服务器的数据进行展示
--动态业务
3.三级联动组件完成
---由于三级联动,在Home、Search、Detail,把三级联动注册为全局组件。
好处:只需要注册一次,就可以在项目任意地方使用
importTypeNavfrom‘@/components/TypeNav’
Vue.component(TypeNav.name,TypeNav)
4.完成其余静态组件
HTML+CSS+图片资源------信息[结构、样式、图片资源]
5.一般要先测试接口是否能够使用
POSTMAN测试接口
--如果服务器返回code为200表示服务器返回数据成功
--整个项目,接口前缀都有/api的字样
6.axios二次封装
XMLHttpRequest,fetch,JQ,axios
为什么需要二次封装axios
理由:请求拦截器,响应拦截器:请求拦截器可以在发送请求之前可以处理有些业务,响应拦截器当服务器返回之后做一些业务
1.首先要引入inportaxiosfrom‘axios’
- request就是axios,只是可以进行配置
constrequests=axios.create({
//配置对象,像是基础路径baseURL或请求超时的时间设置timeout
})
- 配置请求拦截器
requests.interceptors.request.use((config)=>{
returnconfig;
})
requests.interceptors.response.use((res)=>{
returnres.data;
}),
(error)=>{
//响应失败的回调函数
returnPromise.reject(newError(‘faile’))
}
6.2在项目中出现的API文件夹【axios】
接口当中路径都带有/api
baseURL:'/api'(就是基础路径)
7.接口统一管理
如果项目很小:可以在组件的生命周期函数中发请求
如果项目很大:axios.get('xxx');
7.1跨域问题
跨域:协议、域名、端口号相同
如有一个不同就 违背同源策略
http://localhost:8080/#/home---前端项目本地服务器
http://39.98.123.211---后台服务器
解决跨域的几个方法:
JSONP\CORS\代理 补充:document.domain 只能跨子域,需要主域相同才能使用
location.hash+iframe
window.name+iframe
postMessage
nginx代理跨域
CORS分为简单请求和非简单请求
简单请求,浏览器自动添加一个Origin字段
同时后端要给响应头 允许跨域
response.setHeader(‘Access-Control-Allow-Origin);
服务端响应头想要被浏览器允许接收生效
reponse.setHeader(‘Access-Control-Exponse-Headers)
Access-Control-Allow-Credentials 是否可传cookie
要是想传cookie,在前端需要设置xhr.withCredentials=true,后端设置Access-Control-Allow-Credentials
———————————————————————————————————————
在webpack中DevServer.proxy---webpack.config.js(即vue.config.js)
devServer:{
proxy:{
“/api”:{
target:’服务器url’,
changeOrigin:true,
pathRewrite:{ "^/api": ""}}}}
8.nprogress进度条的使用
发请求进度条执行,响应之后取消进度条
安装nprogress插件
cnpminstall--savenprogress
要在api/request.js中添加,因为是在发请求之前和响应之前做的事情,所以在拦截器中使用
1.首先引入 import nProgress from “nprogress” start:进度条开始 done:进度条结束
2.添加样式,即nprogress/nprogress.css中可以直接修改
3.在拦截器中添加方法
start:进度条开始
done:进度条结束
9.vuex状态管理库
9.1vuex是官方提供的一个插件,状态管理库,集中式管理项目中组件共用的数据:
不是所有项目都需要vuex,如果项目很大,组件很多,数据维护很费劲,vuex
state,mutations,getters,actions,modules
9.2vuex基本使用
9.3vuex实现模块式开发
当项目过大,组件过多,接口也很多,数据很多
可以让vuex实现模块式开发
模块式开发,从小仓库中引入数据,每个小仓库负责对应的数据
export default new Vuex.Store({
modules:{
home,
search,
}
})
10.动态展示三级联动
全局组件应该放到components中,并修改在main.js中的路径
知识点1:查找数据需要服务器+接口
所以在配置基础路径的时候baseURL,需要
http://gmall-h5-api.atguigu.cn(这是服务器)|
/api/product/getBaseCategoryList(这是接口)
接口放在服务器后面
最终拼接路径是http://gmall-h5-api.atguigu.cn/api/product/getBaseCategoryList
所以在baseURL:http://gmall-h5-api.atguigu.cn/api(这是服务器+/api)
建vuex库去发ajax请求
- 在store文件夹下,新建search文件夹中index,js文件
- 初始化仓库,就是state,mucations,actions,gettters,再对外暴露
- 拉接口,三连环,在actions发请求,因为会返回promise所以使用async和await接
大致代码:
//通过API里的接口函数调用,向服务器发请求,获取服务器的数据
const state={
//注意服务器返回的是一个数组,所以数据默认初始化不能随便写,返回是对象就初始对象,返回是数组就初始数组
categoryList:[],},
const mutations={
CATEGORYLIST(state,categoryList)
{
CATEGORYLIST(state,categoryList){
state.categoryList=categoryList;
}
}},
const actions={
async categoryList({commit}){
let result=await reqCategoryList();
if(result.code==200)
{
commit(‘CATEGORYLIST’,result.data);}}
}
当然也是需要挂载在钩子上,但是为了避免重复挂载所以在源组件中
this.$route.dispatch(“categoryList”)
知识点2:在v-for循环中第一个是值,第二是键,第三个索引
所以可以选择性循环,当然不能丢了:key=值的属性,
选择性用v-if='index<=多少',就输出多少内容
一级分类中有二级分类,二级分类有三级分类
[
{
id:1,
name:'电子书',
child:[
{
id:2,
name:'很多书',
child:[{},{}]
},{
id:3,
name:'非常多的书'
child:[{},{}]
}
]
},{},{}
]
Ⅲ
如何实现三级列表显示?
1.完成一级分类动态添加背景颜色
第一种解决方案:采用样式完成
第二种解决方案:
(1)因为v-for(c1,index) in categpryList,c1是值,index是键值,
(2)所以我们可以监听mouseenter获取index后(@mouseenter='changeIndex(index)'),去添加方法进行处理
methods:{
//鼠标进入从修改响应式数据currentIndex数据
//changeIndex(index){
//知识点:
正常情况:鼠标缓慢进入一级分类中都会触发
但是会出现另一种不正常情况:经过测试,只有部分触发了样式,是由于用户行为过快,导致浏览器反应捕获,回调函数中一大堆事件,浏览器根本反应不过来,就会让浏览器卡顿
所以不能直接赋值
//this.currentIndex=index;
_.throttle(是全部引入),因为他是默认暴露就不需要_.
throttle回调函数别用箭头函数,可能出现上下文this
changeIndex:throttle(function(index){
this.currentIndex=index;
},50),
}
}
,值就是放在
data(){
return {
currentIndex:-1,
}
}
(3)当index==currentIndex(判断)时,添加样式
使用:class={cur:currentIndex==index},在css添加样式cur
(4)把移除事件挂载到父盒子上去监听,就是事件委派|事件委托(真正监听的是这个委托监听的子元素)
leaveShow(){
this.currentIndex=-1;
}
2.通过JS控制二三级商品分类的显示与隐藏
最开始的时候,是通过CSS样式display:block|none显示与隐藏二三级商品分类
所以当currentIndex==index时候显示分类页面
用:style=''控制显示
:style="{display:currentIndex==index?'block':'none'}"
3.显示卡顿现象
正常:事件触发非常频繁,每次触发都要执行回调函数(如果时间很短,且回调函数内部有计算,就可能出现浏览器卡顿)
节流:在规定的间隔时间范围内不会重复触发回调,只有大于时间间隔才会触发,把频繁触发变为少量触发(回血量,技能点)
防抖:前面的所有触发都被取消,最后一次执行在规定时间之后才会触发,如果连续快速触发,只会执行一次(回城)
(向服务器去发送请求)
下载lodash插件:里面封装的防抖与节流的业务(闭包+延迟器)
//1.lodash函数库对外暴露_函数
下划线函数
let result=_.debounce(function(){
console.log('我在一秒之后执行一次')
},1000)
result;
eg.是回调函数,前面所有的触发都会被取消(因为会被覆盖),当结束输入后1s才执行事件
input.οninput=_.debounce(function(){
},1000);
会返回一个函数
节流,计数器
eg.
button.οnclick=_.throttle(function(){
count++;
span.innerHTML=count;
},1000)
//防抖:用户操作很频繁,但是只能执行一次
//节流:用户操作很频繁,但是把频繁操作变成少量操作,给浏览器给予充裕的事件进行解析代码
完成三级联动节流的操作
且vuex本身就有lodash插件
4.三级联动组件的路由跳转与传递参数
三级联动用户可以点击的:一级分类、二级分类、三级分类,当点击时Home模块跳转到Search模块,一级会把用户选中的产品(产品的名字,产品ID),路由跳转的时候,进行传递。
路由跳转:
声明式导航:router-link
编程式导航:push|replace
三级联动,如果使用声明式导航router-link,可以实现路由跳转传递路由参数,但是会造成卡顿的现象。
router-link:当作一个组件使用,当服务器数值返回,循环很多router-link组件,就会创建很多实例1000+,一瞬间创建很耗内存,会出现卡顿现象。
使用@click监听绑定事件也不是最优解,因为每一个a标签都要绑定一个回调
最优解是使用:
编程式导航+事件委派
但是怎么查找到点击的是a标签
如何获取参数
5.完成三级联动路由跳转与传递参数
【1】首先如何解决事件委派时到底是哪个元素进行的点击,即点击a标签时,怎么确定是哪个目标a
【2】其次,当能够确定点击标签,到底是哪一层级标签进行了点击
解决1:
因为是根据点击a标签进行跳转,所以给每一个a标签一个自定义属性,就能够确定点击的是否是a标签
实际解决方案:把子节点当中的a标签都加上自定义属性data-categoryName,其余子节点没有
解决2:
使用event.target得到点击对象,并在每个a标签上再自定义一个属性来确定到底是哪层级别(自定义属性名)不一致
实际解决方案:使用event.target可以获取到当前触发事件的节点
然后用{}来解构,{属性名1,属性名2,属性名3,属性名4}=element.dataset;dataset可以得到自定义属性名
if(属性名1)
{
if(属性名1){}else if(属性名2){}...
}
最后是跳转路径怎么写
this.$router.push({name:"search",query:{categoryName:'xxx',2id:'xxx'}})
实际解决总结:
- 在ul上添加监听事件,因为事件委派的原理可以向下委派
- 再获取当前点击的触发事件节点
let element=event.target;
解构点击节点身上的自定义属性
注意浏览器会把所有大写的子图改成小写所以解构中的变量也要是小写
dataset是取出标签自定义属性
let { categoryname, category1id, category2id, category3id } =element.dataset;
- 如果存在当前categoryname属性判断是哪一级别的
if(categoryname)
{
//无论是1,2,3级都要整理路由跳转的参数,所以开始整理获取的数据
let location={name:”search”}//跳转路径
let query={categoryName:categoryname}
if (category1id) {
query.category1Id = category1id;
} else if (category2id) {
query.category2Id = category2id;
} else if (category3id) {
query.category3Id = category3id;
}
//判断跳转的时候是否带有params参数,携带了也传递进行
if(this.$route.params)
{
location.params=this.$route.params;
location.query=query;
this.$router.push(location);
}
}
Ⅳ
复习:
1.商品分类三级列表静态变为动态形式【获取服务器数据:解决跨域问题】 jsonp\CORS\插件 package中直接配置vue.config.js devServer proxy
2.函数防抖与节流[重要]
3.路由跳转:声明式导航、编程式导航
使用编程式导航,为什么
由于声明式导航是个组件router-link,一旦使用要生成很多组件很耗内存,卡顿
编程式导航:采用事件委派
自定义属性 dataset event.target
4.开发search模块中Typenav商品分类菜单(过渡动画效果)
过渡动画:前提组件|元素务必有v-if|v-show指令才可以进行过渡动画——transition标签
[1]首先把全局组件Typenav添加到search上,因为已经在main.js中定义为全局组件,所以直接使用
[2]如何让三级列表在首页显示,而在search不经过就隐藏呢?
第一,先在三级联动中添加v-show功能,并在data中存储初始状态为true
因为标题是显示的所以挂载dd标签上
第二,mounted挂载钩子中,判断所在路径,如果是首页显示不是就隐藏
this.$route.path这表示的是一开始的隐藏或显示
第三,添加经过和离开三级联动显示隐藏的方法,mouseenter还是mouseleave用show
因为事件委派的原理,所以把监听事件添加在父盒子上
@mouseleave-->leaveShow @mouseenter-->enterShow
leaveShow(){
//判断如果是search路由组件的时候才会执行
if(this.$route.path!==”/home”)
{this.show=false;}}
//当鼠标移入的时候让商品列表显示
enterShow(){
if(this.$route.psth!==”/home”){
this.show=true;
}}
第四,添加过渡动画。transition包裹三级联动列表,并以name定义name='sort'
有了name就不用v-来定义
.sort-enter{} .sort-enter-to{}
.sort-leave{} .sort-leave-to{}
定义动画的时间
.sort-enter-active{transition:all .3s linear !important;}注意层级问题
三级列表优化?
主要是减少发送请求的次数,把挂载在三级列表中发送请求的方法挂载到App.vue中,因为只会渲染一次App根组件,所以其实也只要发送其次请求获取参数就可以了
5.合并参数
为了满足两种情况
【1】点击完三级联动后又在查找框中查找具体内容
【2】先查找内容后点击三级联动
要满足params和query都能获取
所以在三级联动和搜索框跳转时先去判断是否有params和query然后把已有的添加进去再进行跳转
6.开发Home首页中ListContainer组件与Floor组件
但是没有提供
使用mock数据(模拟)
需要用到一个插件mockjs
(它的作用:提供假数据,拦截ajax)
mockjs使用步骤
1.在src中添加一个文件夹mock
2.准备json数据(在mock文件夹中创建相应的JSON文件)
使用的数据一定要格式化后,不能有空格
3.把mock数据需要的图片放置到public文件夹中
public文件夹打包时,会把相应的资源原封不动打包到dist文件夹中
4.开始mock虚拟数据,通过mockjs实现
创建mockServe.js通过mockjs插件实现模拟数据(Mock.mock(url请求地址,参数请求数据))
var Mock=require(‘mockjs’);
//JSON数据需要被引入,因为JSON未对外暴露,但是可以引入
//webpack默认对外暴露的有:图片、JSON数据格式
import banner from ‘./banner.json’;
//mock数据,第一季是参数请求地址 第二个参数是请求数据
Mock.mock(“/mock/banner”,{code:200,data:banner});//这是首页轮播图的数据
5.mockServe.js文件在入口文件中引入(至少需要执行一次,才能模拟数据)
import mock from '@/mock/mockServe'
把所需的资源获取遍历
【1】拷贝一份改变请求基础路径配置是/mock,并配置发送请求的基本信息
【2】在轮播图页面发送请求向仓库获取资源,仓库去请求虚拟数据并备份
export default reqGetBannerList=()=>mockRequests.get(‘/banner’);
【3】轮播图页面引入仓库mapState得到相应的资源state.home.bannerList
7.swiper基本使用(适用于轮播图)PC端移动端都适用
【1】引包(相应JS|CSS)
【2】页面中结构务必要有
【3】在以上前提下:new Swiper实例,轮播图添加动态效果
8.安装ListContainer组件开发重点?
安装swiper插件:最新版本6,安装的是swiper5
npm install --save swiper@5
如果直接在mounted实例化swiper实例,由于当前仓库还未进行修改轮播图数据所以不能实现,因为DOM中根本还没渲染完成,而new Swiper的实例前提是页面结构完整,就是要考虑页面异步渲染
!!!注意:swiper5以上版本vue2不兼容(用不了),所以下载cnpm来下载swiper5,因为npm也下载不了swiper5及以下版本
1.引包,在入口文件中引入CSS样式
//轮播图样式插件,样式不需要a from b,直接引入即可
import 'swiper/css/swiper.css'
- 在需要使用的组件中引入JS文件
import Swiper from “swiper”
- 引入相关的结构
<div class="swiper-container col_329" ref="floorSwiper">
//轮播图的骨架
<a href="#" class="swiper-wrapper">
//每一张轮播图片
<div
class="swiper-slide"
v-for="(carousel, index) in list"
:key="carousel.id">
<img :src="carousel.imgUrl" data-loading="lazy" />
</div>
</a>
//小点
<div class="swiper-pagination"></div>
//左按钮
<div class="swiper-button-prev"></div>
//右按钮
<div class="swiper-button-next"></div>
</div>
4.实例化 使用watch,因为new Swiper实例的前提就是DOM渲染完成,而this.$nextTick就是可以确认每次更新DOM完毕之后调用
并且要注意watch和computed的区别是watch不会初始执行一次,因为它的本质是监听数据的变化,如果数据没有发生变化就不会调用,而computed不一样,它不仅是有缓存的机制,而且依赖于数据属性,初始就会进行计算
所以想要watch初始化就进行变化要修改immediate属性
具体过程如下:
watch:{
immediate:true,
handler(newVal,oldVal)
{
this.$nextTick(()=>{
var mySwiper=new Swiper(this.$refs.floorSwiper,{
loop: true, // 循环模式选项
speed: 300,
autoplay: {
delay: 5000,
},
// 如果需要分页器
pagination: {
el: ".swiper-pagination",
},
// 如果需要前进后退按钮
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
// 如果需要滚动条
scrollbar: {
el: ".swiper-scrollbar",
},
})
}) }}
Ⅴ
复习:
[1]完成商品分类的三级列表路由跳转一级路由传参(合并传参)
[2]完成search模式中对TypeNav使用(过渡动画)
[3]对TypeNav请求参数也进行优化
[4]swiper 轮播图插件
swiper 经常制作轮播图(移动端|PC端也可以使用)
第一步:引入相应的依赖包(swiper.js|swiper.css)
第二步:页面中的结构必须要有
第三步:初始化swiper实例,给轮播图添加动态效果
[5]mock数据 同mockjs模块实现的
不需要对外暴露,因为像图片、视频、json数据都是默认对外暴露
1)最完美的轮播图解决方案:
watch+nextTick:数据监听:监听已有数据的变化
nextTick:
在下次DOM更新循环结束之后执行延迟回调,在修改数据之后立即使用这个方法,获取更新后的DOM
$nextTick:/下次DOM时更新,执行延迟回调
修改执行完毕,立即使用这个方法,获取更新后的DOM
$nextTick:可以证明页面中的结构一定是会有的,经常和很多使用(DOM存在)
bannerList是监听的数据+handler(newValue,oldValue)+this.$nextTick+this.$refs.id的名称
一般vue用ref来代替获取DOM
2)开发floor组件
切记:仓库当中state的数据格式,不要乱写,数据格式取决于服务器返回的数据
2.1getFloorList这个action应该在哪里触发,需要在Home路由组件当中发的,不能在Floor组件内部发action
因为我们需要v-for遍历floor组件
2.2v-for也可以在自定义标签当中使用
2.3组件通信的方式有哪些?面试频率高
props:用于父子组件通信
自定义事件:@on @emit 可以实现子给父通信
全局事件总线:$bus 全能
pubsub-js:vue当中几乎不用 全能
插槽slot-scope
vuex actions-->dispatch mucations-->commit
其他补充:$parent与$children $attr与$listener reject与provide v-model与sync
$ref与refs
3)把首页轮播图拆分为一个共用全局的组件
js写法一样,就进行拆分
切记:当某个组件经常使用,就把它全局组件,这样只注册一次,可以在任意地方使用公用的组件,非路由组件(公用组件)放到components文件夹中
4)search模块开发
1.写静态页面+拆分静态组件出来
2.发请求
3.vuex(三连环)
4.组件获取仓库数据,动态展示数据
获取搜索商品使用POST请求 带参数data
当前这个接口(获取搜索模块的数据),给服务器传递一个默认参数(至少是一个空对象)
其次在store中search中发送请求三连环,一定要注意会带参数进行查找
而携带的参数比较多所以改成对象形式
有关于query中的参数1、2、3级id和用户选中分类的名字 keyword关键字
排序order:”1:desc”表示初始是综合|降序 pageNo当前分页 pageSize每一页展示数据
平台售卖属性的参数props trademark品牌
所以在委派请求的时候带上params参数
并且在查找页面进行的mounted中dispatch发请求不仅要有请求方法还有有{}空对象避免无参数发送,
而且为了减少获取数据冗余需要在仓库中使用getters计算属性,其目的就是为了简化数据,输写的函数带形参state是当前的仓库唯一数据源,而不是总仓库的数据,且在返回时使用[]保证页面加载速度慢或服务器连接失败也要返回一个空数组,避免页面出错
例如
const getters={
goodsList(state)
{
//为了避免当服务器断网的时候就找不到goodsList数组,会返回undefined
//会报错
return state.searchList.goodsList||[];
}
}
而在search组件中钩子获取数据
computed:{//mapGetters里面的写法传递的是数组,没有划分模块所以直接调用
...mapGetters([“goodsList”])
}
Ⅵ
复习:
1.search模块需要服务器数据,已经请求到了,而且储存vuex仓库当中,而且有一些数据通过getter进行简化
2.getters是以简化数据而生
商品列表、平台售卖属性已经动态数据(来自服务器数据)
首先,在发送请求数据之前(beforeMount),把接口需要的参数进行整理,Object.assign,服务器就会返回查询的数据
Object.assign(this.searchParams,this.$route.params,this.$route.query)
1.注意:如何解决多次发送请求(查找功能)?
监听路由是否发生变化,当路由参数变化时再发送请求(watch)
【1】监听的是$route参数是否变化
【2】整理参数再发送请求
【3】删除之前残留的三级列表query参数(清空)便于查找,而params的keyword不需要清空,它在每次输入的时候会被覆盖
//每一次请求完毕,就应该把相应的1、2、3级分类的id留空
this.searchParams.category1Id=undefined;
......
2.如何完成面包屑的功能
【1】首先要让从路由中得到的参数展示在面包列表当中,那就在列表中使用v-if把整理的数据显示searchParams
【2】只在三级列表中,完成点击就清空searchParams的用户选择分类名字功能,方法使得categoryName、category1Id、category2Id、category3Id值为undefined,因为值为undefined不需要向服务器带这个字段,并且要把三级列表字段清空
eg.在三级列表中选择的商品名展示与销毁
<li class=”fl tag”v-if=”searchParams.categoryName”>
<a><span>{{searchParams.categoryName}}</span>
<i @click=”removeCategoryName”>×</i>
</a>
</li>
//移除商品名
removeCategoryName(){
//如果把相应的字段变为undefined,这个字段就不会带给服务器了
this.searchParams.categoryName=underdined;category1Id 2 3同样
//地址栏也需要修改,进行路由跳转
if(this.$route.params){
//因为点击只删除query参数,而搜索框中的params值不能被删掉
//需要严谨,只删除query参数,有params在路由跳转中需要携带
this.$router.push({name:’search’,params:this.$route.params})
}}
//移除关键字
removveKeyword(){
this.searchParams.keyword=undefined;
//通知兄弟组件把关键字在Header中清除
this.$bus.$emit(‘clear’);//这里是另一个知识点,就是当要清除关键字的时候会把用户在搜索框中的一并清除,使用的是全局事件总线
if(this.$route.query)
{this.$router.push({name:”search”,query:this.$route.query})}}
//移除品牌removeTrademark,同上
//移除平台售卖信息
已经通过自定义事件获取到子组件的数据,就是点击平台售卖信息
要在父组件发请求韩式子组件发请求?
因为父组件中的searchParams参数带给服务器参数,所以子组件要把点击的参数整理到searchParams,主要数据在父组件中,使用自定义事件 子传夫
this.$emit(“trademarkInfo”,trademark);
//自定义事件的回调
trademarkInfo(trademark)
{
//当传递过来的信息
this.searchParams.trademark=`${trademark.tmId}:${trademark.tmName}`;
//还要再发请求
this.getData();
}//收集平台属性值的回调函数/自定义事件 props = `${attr.attrId}:${attrValue}:${attr.attrName}`;
如果数组中没有indexOf(props)==-1就推入push
【3】路由跳转到自身,注意要判断是否携带params参数,如果携带不能直接删除所有参数的跳转路由形式
3.面包屑处理关键字
【1】编程式导航路由跳转[自己跳转],判断是否有query
【2】当面包屑中的关键字以后,需要让兄弟组件Header组件中的关键字清除
设计组件通信:
props:父子
自定义事件:子父
vuex:仓库数据
插槽:父子
pubsub-js:
$bus:全局事件总线 $emit/$on
最后使用全局事件总线,步骤
1.在入口函数注册
beforeCreate(){
Vue.prototype.$bus=this;
}
2.在父组件的组件方法中通知兄弟组件把关键字在头部搜索框中移除,this.$bus.$emit("clear");
3.在搜索框所在的挂载钩子中添加
this.$bus.$on('clear',()=>{
this.keyword='';
})
4.对SearchSelector信息搜查
使用自定义事件$emit与@属性名传递数据
【1】在子组件SearchSelector中使用事件监听click与导入所需参数 this.$emit('父组件事件名',参数1,参数2...)
【2】在父级的子组件模板中@事件名=值
在自定义事件的函数中
整理品牌传来的参数,再次发送请求
如果是平台属性值因为不能重复显示就需要用v-for与数组去重 this.searchParams.props.indexOf(props) ==-1 整理好数值往数组中push
3.排序操作:
3.1
1:综合,2:价格 asc:升序,desc:降序
示例:"1:desc"
order:属性的属性值最多有多少种写法(4种)
3.2思考
类名分布在:通过order属性值当中包含1或2
3.3谁应该有箭头
谁有类名谁就有箭头
1.4箭头怎么制作
iconfont 阿里巴巴适量图标库
https://at.alicdn.com/t/c/font_3672422_m687jp1krc7.css
【1】根据类名来判断是否显示样式,使用计算排序与隐藏 返回this.searchParams.order.indexOf(' ')!=-1
【2】v-show根据计算属性来显示箭头
【3】class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}"
【4】添加点击事件changeOrder('')参数是1或2表示是综合或价格
获取初始this.searchParams.order.split(':')[0] / [1] 得到数据
准备一个新的order属性值用来赋值,flag当前值与原本选项进行判断
newOrder=`${orginFlag}:${orginSort=='desc'?'asc':'desc'}`
newOrder=`${flag}:${'desc'}`
然后数据再次发送请求
4.分页器静态组件
为什么很多电商平台需要用到分页?
当项目的数据时候需要采用分页功能
ElementUI是有分页组件,使用较简单,但是原理更重要
分页器展示需要哪些数据?
当前页数,每一页展示数据数量,一共有多少数据,连续页码个数5、7对称
当前页数\一页的多少商品数据\总共有多少商品数据\连续页码个数5
pageNo\pageSize\total\contiues
对于分页器而言,需要算出连续的起始页面和结束页面数字
以及从子传父的(自定义事件分页器回调中获取)当前页数,用来整理并发送请求获取数据
5.分页器动态展示
分为上中下三种方式
分页器大致结构
第一页 1
省略 ...
某一页 n
省略 ...
最后一页 m
【1】首先使用假数据测试,父传子props,接收
【2】在子组件中使用计算属性去计算一共多少页
(使用总数据/单页数据)向上取整=总共页数
【3】去计算起始的数字与结束的数字,因为我们设置了连续数字至少为5
3.1先把所需的数据如 连续多少页数continues
当前页数pageNo 总页数totalPage
使用解构传入
再定义初始start 与结束end
※注意如果总页数小于连续页数
开始页数=1 结束页数=总页数
else(总页面大于连续页数)
开始页数=当前页数-parseInt(连续页数/2)
parseInt()-->向下取整parseInt(5/2)=2
结束页数=当前页数+parseInt(连续页数/2)
※开始页数<1的情况
开始页数=1 结束页数=持续页数
※结束页数>总页数的情况
开始页数=总页数-持续页数+1
结束页数=总页数
所以就得到起始页数 结束页数
【4】分页器 中
v-for去循环startNumAndEndNum.end+v-if判断要大于等于startNumAndEndNum.start才会显示
【5】分页器 上+下
包含数字1 省略框 省略框 总页数
※如何控制它们的显示与否呢?
使用v-if
数字1显示的条件是 起始页数>1
省略框 表明大于2 因为 1与2之间不需要省略所以起始页数start大于2才显示
同理
后面的省略框要在 end ... 总页数 之间
所以end要小于totalPage-1
而总页数显示就是end小于总页数时候
而上一页与下一页?
上一页 :disabled='' 条件是当前页为1
下一页 :disabled='' 条件是当前页是总页数
【6】通 真数据发送请求 并且获取数据使用自定义事件 v-on this.$emit('事件名',数据)
在父组件中的事件
去整理参数(当前页数)再次发送请求
在子组件中
使用点击监听事件 $emit('当前页',运算) 参数
上一页 获取当前页-1
下一页 获取当前页+1
1 1
for生成页数 page
最后一页 总页数
【7】如何添加类名
:class={active:条件}
6.开发某一个产品的详情页面
1.静态组件2.发请求3.vuex4.动态展示组件
6.1路由设置中,发现当点击进行跳转的时候滚动条不会默认在顶端显示,所以可以在路由中修改滚动事件:
scrollBehavior(to,from,savePosition)
{
return{y:0}//置为顶端
}
6.2写api请求接口
6.3vuex获取产品详情信息
vuex中还需要再新增一个模块detail
然后再总仓库也要去引用合并一下
配色
#CC3333
#FFCCCC
#99CC00
6.4数据解释--售卖属性
[
{
attr:'颜色',
attrValue:['粉色','天蓝色','红色']
},{
attr:'版本',
attrValue:['16','29']
}
]
请求数据
获取产品的详细信息与产品添加入购物车(获取更新某个产品的个数)
注意:如果使用swiper轮播图,可以去官网查询相关的api
像 slidesPerView设置的是一页多少张图片
在Grid(网格/多行多列布局)
slidesPerView:5一页可以设置5张图片
slidesPerGroup每组可以移动的图片数量,点击按钮切换的图片
商品缩略图的点击效果通过:class=”{active:currentIndex==index}”@click=”changeCurrentIndex(index)”在方法中让currentIndex=index并通知兄弟组件Zoom大图挂载全局事件总线回调this.$bus.$on(“getIndex”,(index)=>{
//修改当前响应数据 每次点击缩略图大图也跟着变化
this.currentIndex=index;
})
6.5商品属性的切换功能
首先,v-for去把数据进行遍历出来
然后添加事件监听changeActive(spuSaleAttrValue,spuSaleAttr.spuSaleAttrValueList)传入的参数当前激活的对象以及该商品某个属性的属性类型名(是一个数组)
排他方法:在数组中把它遍历全部售卖数字值,(forEach),为数组中所有的对象更改isCheked属性为未选中,最后给激活(点击高亮)的对象的属性显示效果
而添加样式就是判断isChecked==1
6.6放大镜效果
默认给放大镜中添加一个空的标签去增加事件监听mousmove="handler"
【1】在事件中,获取遮罩层和放大镜层
this.$refs.mask this.$refs.big
【2】设置遮罩层的位置
left=event.offsetX-mask.offsetWidth/2;
top=event.offsrtY-mask.offsetHeight/2;
【3】进行条件设置【判断】,且它的条件是遮罩层是底图的50%,所以只要left大于mask.offsetWidth就说明右边已经顶边了
left<=0 left>=mask.offsetWidth
top<=0 top>=mask.offsetHeight
【4】设置遮罩层与放大镜的位置
//图是往上往左走所以为负值
mask.style.top mask.style.left
big.style.top=-2*top+'px' big.style.left=-2*left+'px'
6.7修改商品的数量 常用组件
首先构建结构,输入框、减号,加号
在这个输入框需要完成:数量显示、数量变化时确保变化正确(格式、大小)
v-model=”skuNum”初始化数量为1,@change绑定值变化的时候回调changeSkuNum
加号让skuNum++,减号完成skuNum>1?skuNum--:skuNum
在修改表单元素产品的个数中,个能会有用户输出的问题包括输入字母,字符串,要进行过滤,所以让输入的文本*1,可以先判断受否为数字型
let value=event.target.value*1;
情况1:如果时非数字型或该数字小于1
if(isNaN(value)||value<1)
{
this.skuNum=1;//让他变为1
}else{
情况2:是数字型但是是浮点类型
this.skuNum=parentInt(value)
}
7.加入购物车
需要给服务器发请求,表示购买的物品信息
服务器如果存储成功,进行路由跳转传递参数
如果失败,给用户提示并跳转到首页
发送给服务器的接口有两个作用:
把产品加入到购物车中,对已有的物品进行数量改动
路由跳转时需要把产品信息携带给下一级路由组件
【1】使用路由跳转+传参 但是不美观
【2】本地存储
一些简单的数据可以作为query与params进行传参,但是对于较为复杂的产品信息数据,需要通过会话进行存储,它的生命周期时会话结束就会小时,但是不能去储存一个对象,一般本地存储与会话存储是存储字符串,所以要去存储一个对象需要把它转化格式为JSON
try{
发请求
sessionStorage.setItem(‘SKUINFO’,JSON.stringify(this.skuInfo))//这个存储商品属性信息(包括图片、数量、)
然后进行相应的路由跳转,把商品数量并在路由中
this.$router.push(‘Addcartsuccess’,query:{skuNum:this.skuNum}})
}catch(error)
{alert(error.message);}
8.如何把商品详细信息传递到购物车页面
本地存储:H5 浏览器存储功能 本地存储与会话存储
本地存储:持久化 ---5M
会话存储:非持久化---会话结束数据就消失
sessionStorage
9.购物车
搭建购物车结构
向服务器发起ajax请求,获取购物车数据
UUID临时游客身份
动态展示购物车
10.uuid游客身份验证
如果要加入购物车进行结算,后端需要获取产品ID,skuNum,游客身份(用户身份)
uuid生成一个随机字符串
发请求的时候,获取不到你购物车里面数据,因为没有进行身份验证
请求头上携带用户身份,请求拦截器中去进行添加
在axios请求中引入仓库,查询浏览器是否有uuid,如果有就添加到请求头(与后端商议命名)后把数据添加上发送请求
安装完毕后 npm intall uuid
首先,引入import {v4 as uuidv4} from ‘uuid’
对外暴露,要生成一个随机字符串,且每次指向不能发送变化,且游客的身份是持久存储
export const getUUID=()=>{
let uuid_token=localStorage.getItem(‘UUIDTOKEN’);
if(!uuid_token)
{
//不存在就生成一个uuid
uuid_token=uuidv4();//生成一个游客临时身份
localStorage.setItem(‘UUIDTOKEN’,uuid_token)//然后进行存储,存储一次
}}
return uuid_token;//没有就返回undefined
在vuex中进行调用
import {getUUID} from xxx
uuid_token:getUUID()
11.动态展示购物车模式
数据格式是数组,要经过getters简化整理数据
展示相应的商品数据后,计算一下产品的总价
已经获取到总共产品种类的数量,所以使用forEach
总价+=单品的数量*单品的价格
问题:购物车商品数量该怎么显示与修改?
给修改商品事件添加方法 每次修改数量都需要向后台发送请求
增加+ 发送请求1
减少- 请求-1
输入时 将现在数量-原来 进行发送
为变化 请求0
监听数量改变的参数有:事件类型(add,minus,change) 数量(disNum) 商品编号(cart)
创建事件handler(事件类型名,向后台发送的数字[1,-1,现在-之前],商品编号[cart])
在s输入框中让:value=”cart.skuNum”//从后台获取数据
且它的事件是hander(‘change’,$event.target.value*1,cart)
采用节流,这样可以让多请求缩减为少,且要事件是否成功,故
handler:throttle(async function(type,disNum,cart){})
判断事件类型switch...case
当增加时disNum=1 减少时,原本数量是否>1,disNum=-1,否则保持原样disNum=0 当修改时,判断输入是否为数字类型或大于0,不是保持原样0;是取整
try...catch+async...await 发送修改请求和再次发送获取数据请求更新
问题:怎么删除购物车商品?
写接口 vuex发送请求(ajax) 在购物车中写事件发送请求(删除购物车商品/参数:商品列表) 发请求async+await/try+catch 发请求删除商品+再一次获取最新数据
问题:点击商品数量切换过快会出现负数错误,怎么解决?
节流(修改产品个数使用到节流)使用节流函数throttle(回调)
事件名:throttle(function(参数){
},间隔时间)
复习:
需要发请求
1.修改数量2.删除购物车商品3.勾选商品双向绑定
1.1加入购物车,需要判断用户信息
UUID:生成临时字符串,点击加入购物车时候,通过请求头给服务器带临时身份给服务器,(可以作为本地存储永久保存,自行删除)
存储某一个用户购物车数据
路由传参+会话存储
会话存储:去存储产品信息以及展示数据(临时性)
1.2购物车展示信息
修改产品的数据 删除某一个产品的接口 某个产品勾选状态切换
/*****************************************************/
12.删除选中的全部产品信息
注意:没有一次删除很多产品的接口,但是有通过ID可以删除产品的接口【一次删除一个】
//调用删除单独产品接口返回的是promise,同时删除好几个就返回
Promise.all([p1,p2,p3])
p1|p2|p3:每一个都是promise对象,如果有一个promise失败就都失败,如果都成功,才返回成功。
实际代码:
创建一个数组来存放promise对象,每次发送请求都会返回一个promise
let PromiseAll=[];
getters.cartlist.cartInfoList.forEach(item=>{
let promise=item.isChecked==1?dispatch(‘xxxx’):’’
将返回的promise往数组中推
PromiseAll.push(promise);
})
//去发消息表示状态,有一个失败就失败
return Promise.all(PromiseAll)
- 处理选中同步的问题
当全部商品选择则全选,有一个未选中就未全选;
当全选时全部商品勾选,未选中则全部商品未选中
在单品选框中事件监听选中情况,
@change=”updateChecked(商品信息cart,$event)”
当前选中状态isChecked=event.target.isChecked?”1”:”0”
发请求 商品id 选中状态
再次发请求
结算中选中状态
:checked=”商品数量>0&&isAllChecked”
isAllChecked(){
return this.cartInfoList.every((item)=>item.isChecked==1)
}
同样监听事件@change=updateAllCartChecked
同单选修改
Ⅶ
登录与注册静态组件(处理共用图片资源问题)
登录与注册功能(git):
assets文件夹--放置全部组件共用静态资源
在样式当中也可以使用@符号【src别名】,需要在前面添加~
注册的业务
注册业务|登录业务中表单验证先不处理【最后一天统一处理】
1.1获取验证码 ---/api/user/passport/sendCode/{phone} get
1.2注册用户
2.登录业务
2.1注册---通过数据库存储用户信息(名字、密码)
2.2登录---登录成功的时候,后台为了区分用户身份---服务器下发token【令牌:唯一标识符】
登录接口:不太完美,一般登录服务器成功会下发token,前台持久化存储token【带有token找服务器要用户信息进行展示】
用户登录只保存用户token
token令牌理解
注意:vuex仓库储存数据---不是持久化,所以需要存储在本地浏览器中持久储存
/api/user/passport/auth/getUserInfo
复习1:
1.完成了登录与注册的静态组件【assets文件夹:组件共用的静态资源 CSS当中书写@符号】
2.表单验证暂时不处理
3.vuex存储数据非持久化
复习2:
2.1当用户注册完成,用户登录【用户名/密码】向服务器发请求(组件派发action:userLogin)
登录成功获取token,仓库是非持久化,路由跳转到home首页
2.2因此在首页当中就需要去派发action,去getUserInfo获取用户信息,不带参数,以及动态展示header组件内容,参数是token在请求头发送(就是当vuex中获取到token令牌[从本地仓库来],就会把它加入请求头中发送,获取用户的个人信息)
2.3但是一刷新,vuex数据清空,获取不到用户信息
因为token存放在vuex仓库中,而vuex是非持久化储存
所以要把token存储在浏览器缓存中
2.4持久化存储
封装一个模块去储存 读取 删除token在浏览器中显示
因为vuex每次刷新都会销毁自身数据,所以在用户登录成功获取服务器返回token时就储存了token:setToken(result.data.token)
而在储存token仓库中getToken()
模块代码逻辑:
需要对外暴露,且登录存储token,vuex的state中初始化获取token,当退出登陆时删除token
//存储设置
export const setToken=(token)=>{
localStorage.setItem(‘TOKEN’,token)
}
export const getToken=()=>{
localStorage.getItem(‘TOKEN’)
}
export const removeToken=()=>{
localStorage.removeItem(‘TOKEN’)
}
每次登录在vuex设置与存储token,(刷新)或初始化从本地缓存中取之前保存的token,这样就能让数据永久存储,不会因为每次刷新就丢失数据
添加退出登录功能
给服务器发请求说明退出登录,以及清除项目当中的数据userInfo,token,removeToken(),最后返回首页
2.5存在问题1.多个组件展示用户信息需要在每个组件的mounted中触发
this.$store.dispatch('getUserInfo')
存在问题2.用户已经登录不能再次登录,不应该再展示登录页
3.导航守卫
表示路由正在发生变化,进行路由跳转,切换
守卫:
全局守卫:所有都进行排查 区域
路由独享守卫:负责相应区域
组件内守卫:组件内外
3.1用户已经登录,用户不应该还能返回login页面
其他url--->login 拦截
全局守卫,只要发生路由变化就进行相应的拦截监听
全局守卫还分为:全局前置、全局解析、全局后置 时间概念
3.2从首页到搜索页如果不带查询参数也不能直接跳转
home--->search 不带参数 拦截
router.beforeEach(async(to,from,next)=>{
//to:获取跳转到的路由信息
//from: 从哪个路由来的
//next:next()放行 next(path)放行
//获取token,因为如果有token则表示用户已经登录
let token=store.state.user.token
//并且在登陆后,想要所有页面都可以显示用户信息,这个需要在所有路由页面上挂载获取用户信息 太不方便
//注意:如果直接拿取userInfo用户信息去判断,因为它的类型是对象,所以如何都是true,所以拿取里面的属性name进行判断,它的类型是字符串可以使用
let name=store.state.user.userInfo.name
//表示用户登陆了
if(token)
{
状态1:已经登录了,但是还是跳转到登录页面|注册页面(不能实现,要拦截)
if(to.path=='/login'||to.path=="/register")
{
//把你放行到首页
next('/home')
}else{
//其他情况,状态2:在其他页面上没有用户的信息显示,没有挂载肯定没有,怎么办?
//先判断此时状态管理仓库中有没有信息
if(name)
{
//有的话就全部放行
next()
}else{
//有成功和失败的状态,为什么会失败
//因为当token过期就销毁了,此时肯定也获取不到用户信息,跟退出用户没有差别,清除残留的token
//跳转到登录页面
try{
//此时因为刷新没有了用户信息,就去发请求获取
await store.dispatch('getUserInfo')
//获取到信息之后放行
next()
}catch(error)
{
await store.dispatch('userLogout')
next('/login')
}
}
}
}else{
//那就用户没有登录
//1.不可调整的情况有
//交易相关 trade\addcartsuccess
//支付相关 \pay\paysuccess
//个人中心center
//2.未登录当去以上路由则跳转到登录页面
//3.去的不是上面这些路由(home\search\shopCart)--放行
//先保存想去的路径url
Let toPath=to.path;
if(toPath.indexOf('/trade')!=-1||toPath.indexOf('/addcartsuccess')!=-1
||toPath.indexOf('/pay')!=-1||toPath.indexOf('/paysuccess')!=-1
||toPath.indexOf('/center')!=-1)
{
next(‘/login?redirect=’+toPath)
}else{
next()
}
}
})
4.获取交易界面的数据
用户需要登录后才能获取到用户地址信息,不登陆没办法获取到
在路由独享守卫
4.1提交订单
静态组件
点击提交订单的按钮,需要向服务器发起一次请求【把支付的信息传递给服务器】
如何全部引入api发请求?
在入口文件中统一引入import * as API from '@/api'
并在事件总线中使用原型派发,Vue.prototype.$API=API
挂载时调用this.$API.请求名(参数)
修改选中地址
1.修改样式2.修改js逻辑,点击触发回调事件(该地址信息,地址信息集合)
样式:selected与默认地址标志显示
动态绑定selected:address.isDefault==1,显示样式并使用v-show表示是默认地址
点击的参数是索引和地址数组
修改默认地址
排他法:使用 数组对象.forEach((item)=>{item.isDefault==0});//全部清除样式
再把当前点击的address的isDefault=1,这样一点击就会获取当前点击的对象,修改属性
确定最终选定地址,find查找数组中符合条件的元素并返回得到最终结果
return this.addressInfo.find((item)=>item.isDefault==1)||{}
提交订单(不使用vuex,使用原生发送请求的方法)
- 收集买家数据(用户留言,订单号)
- 整理数据
- 发请求
- 判断成功与失败
获取交易编码
let {tradeNo}=this.orderInfo;
整理数据data
包括:收货人名字、地址、电话,交易方式、留言以及商品清单信息
向后台发请求
let result=await this.$API.reqSubmitOrder(tradeNo,data)
如果请求成功,保存返回结果订单号,拼接订单号进行路由跳转+路由传参
this.orderId=result.data;
this.$router.push(`/pay?orderId=${this.orderId}`);
注意:向后台获取订单数据,但是又不能在生命周期函数中加async
4.2获取支付信息
elementUI使用+按需引入
已经学过的组件库:
React(Vue):antd[PC] antd-mobile[移动端]
Vue:Element[PC] vant[移动端]
先安装,再选择全局还是局部按需引入
1.element-ui 注册全局组件
import {MessageBox} from 'element-ui';
Vue.component(MessageBox.name,MessageBox);
2.另一种方式 挂载在原型上
Vue.prototype.$msgbox=MessageBox;
Vue.prototype.$alert=MessafeBox.alert;
3.完整引入
import ElementUI from ‘element-ui’
Vue.use(ElementUI)
3.注意:配置文件修改项目需要重启
babel.config.js中
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
生成二维码qrcode
引入
import QRCode from "qrcode";
生成二维码-把从后端获取的字符串作为参数引入
let url=await QRCode.toDataURL(this.payInfo.codeUrl);
就会获取到url得到一张二维码图片
问题:如何判断支付状态?
设置长连接定时器,保证状态正常,且保存支付状态码
data(){
return{
timer:null,
code:””
}}
在QRCode中配置,当要点击关闭弹出窗配置时
beforeClose:(type,instance,done)=>{
//type 区分取消与确认按钮,instance 当前组件实例,done关闭弹出框的方法
if(type==”cancel”)
{
//清除定时器
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){
thia.timer=setInterval(async ()=>{
let result=await this.$API.reqPayStatus(this.orderId)//发请求获取用户支付状态
如果result.code==200,需要
- 清除定时器2.隐藏msgbox弹出窗3.进行页面跳转(点击支付成功也可以跳转)
clearInterval(this.timer)
this.timer=null;
//保存支付状态以便在关闭弹出窗之前进行判断
this.code=result.code;
this.$msgbox.close();
this.$router.push(“/paysuccess”)
},3000)}
复习:
(elementUI,qrcode),个人中心(二级路由)
children:[
{
path:'myorder',
component:MyOrder
},{
path:'grouporder',
component:GroupOrder
},{
//重定向,当在/center路径下会跳转到myorder组件中
path:'/center',
redirect:'/center/myorder'
}
]
5.个人中心
5.1是否封装过组件、分页器、日历
个人中心当中分页器
5.2全局守卫
未登录进行访问,交易相关(trade)、支付相关(pay、paysuccess)、用户中心(center)相关跳转到 登录页面
只有登录了界面才能进行结算trade界面
5.3路由独享守卫
只有从购物车界面才能跳转到交易页面(创建订单)
只有从交易界面(创建订单)页面才能跳转到支付页面
只有从支付页面才能跳转到支付成功页面
只有从购物车界面才能到交易界面的具体代码:
{
//订单核对信息
path:,component:,
beforeEnter:(to,from,next)=>{
if(from.path==’/shopcart’){
next();}else{
next(false);//从哪来往哪回,在进行路由跳转的时候中断当前导航,如果浏览器的URL改变了,那么URL会被重置到from路由对应地址
alert(‘请从购物车页面进入’)
} }}
5.4组件内的守卫
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
6.图片懒加载
https://www.npmjs.com/package/vue-lazyload
先安装
cnpm i vue-lazyload -s
在入口文件中引入
import VueLazyload from ‘vue-lazyload’
并进入懒加载图片,因为它不会对外暴露所以需要引入
import lazyGif from ‘@/assets/lazy-loader.gif’
注册插件
Vue.use(VueLazyload,{
loading:lazyGif
})
在动态绑定图片时,改成v-lazy=””
7.自定义插件(plugins文件夹中)
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'
中文显示
进行一些配置,避免ElementUI与vee-validate发生冲突
const config={
errorBagName:'errorBags',
fieldsBagName:'fieldBags',
};
Vue.use(VeeValidate,config);
第二步:列出所需表单验证的规则
VeeValidate.Validator.localize('zh_CN',{
messages:{
...zh_CN.messages,
is:(field)=>`${field}必须与密码相同` //修改内置规则的message,让确认和密码相同
},
attributes:{//给校验的field属性名映射中文名称
phone:'手机号',
code:'验证码',
password:'密码',
password1:'确认密码',
isCheck:'协议'
}
})
//自定义校验规则
VeeValidate.Validator.extend('agree',{
validate:value=>{
return value
},
getMessage:field=>field+'必须同意'
})
第三步:基本使用
<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>
const success=await this.$validator.validateAll();
//全部表单验证,表示全部验证通过才能注册
//用户注册,需要向后台发请求
if(success){
try
//如果成功,跳转到登录页面
const {phone,code,password,passwordAgain}=this;
//如果都存在且密码相同发送请求
phone&&code&&password==passwordAgain&&(await this.$store.dispatch(“请求接口名”,{参数}));this.$router.push(‘/login’) catch
}
9.路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。
如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
eg:路由中,
component:()=>import(‘@/pages/Login’)
10.打包上线
10.1打包npm run build
处理map文件
项目打包,项目进行压缩加密
运行时候输出错误信息无法判断哪里报错
有了map可以像未加密的代码一样,准确地输出哪一行出错
但是如果不需要可以去掉
vue.config.js配置
productionSourceMap:false
11.购买服务器
1.阿里云2.腾讯云
复习
1.组件通信6种方式
第一种:props
适用于的场景:父子组件通信
注意事项:
如果父组件给子组件传递数据,不同情况:
[1]函数,本质其实是子组件给父组件传递数据
[2]非函数,如果父组件给子组件传递的数据,本质就是父组件给子组件传递数据
书写方式:3种
['todos'],{type:Array},{type:Array,default:[]}
小提示:路由props
书写方式:3种
布尔值,对象,函数形式
props:true
props:{a:1,b:2}
props:($route)=>{
return {keyword:$route.params.keyword,k:$route.query.k}
}
第二种:自定义事件
适用于的场景:子给父传递数据
$emit $on
第三种:全局事件总线
适用于的场景:万能
Vue.prototype.$bus=this;
第四种:pubsub-js,在React框架中使用比较多
发布订阅
适用于场景:万能
第五种:Vuex
适用于场景:万能
第六种:插槽
传递于场景:父子组件通信--(一般结构)
三种插槽
默认插槽,具名插槽,作用域插槽
2.组件通信进阶版
event深入
v-model深入
属性修饰符sync
$attrs和$listeners
$children和$parent
作用域插槽scope-slot
以及EventTest组件
2.1事件注意事项:
事件:系统事件click,dlbclick,鼠标系列等等
自定义事件
事件源 事件类型 事件回调
在EventTest组件中
EG1.
//这是原生DOM绑定系统事件
<button@click="handler">原生DOM绑定原生事件</button>
<Event1@click.native="handler1"></Event1>
<!--.native指的是原生,可以把自定义事件变成原生的事件-->
<!--下面的给原生DOM绑定自定义事件-->
<!--没有任何意义,没办法触发$emit函数-->
<button@xxx="handler2">原生DOM绑定自定义事件</button>
<!--组件标签-->
<Event2@click="handler3" @xxx="handler3"></Event2>
<script>
importEvent1 from'./Event1.vue'
importEvent2 from'./Event2.vue'
export default{
name:'EventTest',
components:{
Event1,
Event2
},
methods:{
//事件回调
handler(event)
{
},
handler1(){
},
handler2(){
},
handler3(params){
console.log("evnet3的自定义事件",params);
}
}
}
</script>
在Event2.vue2中
<template>
<div>
<button@click="$emit('click','自定义事件click')">分发自定义事件click</button><br>
<button@click="$emit('xxx','自定义事件xxx')">分发自定义事件</button>
</div>
</templte>
2.1.1.原生DOM---button可以绑定系统事件---click单机事件
2.1.2.组件标签---event1可以绑定系统事件,但是不起作用,因为此时属于自定义事件----需要.native(可以把自定义事件变成原生的事件)
原理是:当前原生DOMclick事件,其实是给子组件的根节点绑定了单机事件---利用了事件委派
2.2v-model[组件通信方式的一种]
v-model它是Vue框架中指令,它主要结合表单元素一起使用(文本框、复选、单选等)
作用:收集表单数据
v-model实现原理:value与input事件实现,注意可以通过v-model实现父子组件通信
在ModelTest组件中
EG2.
<div>
<h2>深入v-model</h2>
<inputtype="text"v-model="msg">
<span>{{msg}}</span>
<br>
<hr/>
<h2>v-model实现原理(vue2)</h2>
<!--原生DOM当中有oninput事件,经常结合表单元素使用,当表单元素文本内容发生变化的时候会触发一次回调-->
<inputtype="text":value="msg" @input="msg=$event.target.value"/>
<span>{{msg}}</span>
<!--深入学习v-model:实现父子组件数据同步(实现父子组件通信)-->
<CustomInput:value="msg" @input="msg=$event"/>
<!--:value是什么?
props:父子组件通信
@input是什么?
不是原生DOM的input事件,属于自定义事件
-->
</div>
<!--上面简化成就是-->
<CustomInputv-model="msg"/>
<script>
importCustomInput from'./CustomInput.vue'
exportdefault{
name:'ModelTest',
data(){
return{
msg:"呱呱"
}
},
components:{
CustomInput
}
}
</script>
在CustomInput组件中
<template>
<div>
<h2>input包装组件</h2>
<!--绑定原生属性-->
<inputtype="test":value="value" @input="$emit('input',$event.target.value)"/>
<!--:value动态属性,
@input给原生DOM绑定原生DOM事件-->
</div>
</template>
<script>
exportdefault{
name:"CustomInput",
//接收
props:['value']
}
</script>
2.3属性修饰符sync 同步
在SyncTest.vue组件中
也是组件通信的一种,可以实现父子组件通信
:money-sync,代表父组件给字符串传递props[money],给当前子组件绑定了一个自定义事件(update:money)
EG3.
<template>
<div>
今天赚了{{money}}元
<h2>不使用sync</h2>
<!--update自定义事件,获取money子组件事件-->
<!--:money 父组件给子组件传递props
@update:money 给子组件绑定的自定义事件,名字叫做@update:money
目前的操作v-model 实现父子组件通信-->
<!--props与自定义事件实现父子组件通信-->
<Child:money="money" @update:money="money=$event"/>
<h2>使用sync</h2>
<!--:money.sync:父组件给字符串传递props money
给当前子组件绑定了一个自定义事件,而且事件的名称是update:money-->
<!--语法糖-->
</Child2:money.sync="money"/>
</div>
</template>
<script>
importChild from'./Child.vue'
importChild2 from'./Child2.vue'
exportdefault{
name:'SyncTest',
data(){
return{
money:10000
}
},
components:{
Child,
Child2
}
}
</script>
在Child子组件中
<template>
<div>
<h3>我<button@click="$emit('update:money',money-100)">花掉100元</button></h3>
<span>还剩下{{money}}元</span>
</div>
</template>
<script>
exportdefault{
name:'Child',
props:['money']
}
</script>
2.4$attrs与$listeners(也是组件通信的一种)
EG4.AttrsListenersTest.vue
准备封装一个按钮能够自定义显示内容
使用vue-helper
两者是组件实例的属性,可以获取到父组件给子组件传递的props属性和自定义事件
<template>
<div>
<h2>自定义带Hover提示的按钮</h2>
<!--当用户在使用封装的按钮时,需要向HintButton组件传递相应的参数
(el-button进行二次封装)-->
<!--底下的事件代表的是自定义事件-->
<HintButtontype="success"icon="el-icon-delete"
size="mini"title="提示按钮"@click="handler"></HintButton>
</div>
</template>
<script>
importHintButton from'./HintButton'
exportdefault{
name:'AttrsListenersTest',
components:{
HintButton,
methods:{
//点击事件的回调
handler(){
console.log('123');//能够成功打印
}
}
}
</script>
在自定义插件HintButton中
<template>
<div>
<!--可以使用a标签实现提示功能-->
<a:title="title"></a>
注意!!! <!--下面这些就不能使用冒号(:)-->!!!
<!--v-on不能用@符号简写-->
<el-buttonv-bind="$attrs" v-on="$listeners"></el-button>
</div>
</template>
<script>
exportdefault{
//对于子组件而言,获取数据可以通过父组件传递过来接收,
//接收的属性,在$attrs属性中是获取不到的
//props:['title'],
mounted(){
//$attrs属于组件的一个属性,可以获取到父组件传递过来的props数据
//console.log(this.$attrs);
//也是组件自身的一个属性,获取父组件给子组件传递的事件
console.log(this.$listeners);
}
}
</script>
2.5$children与$parent
ref可以获取到某个组件,子组件
$children组件实例的属性,可以获取到当前组件的全部子组件【数组】
$parent组件实例的属性,可以获取到当前子组件的父组件,进而可以操作父组件的数据和方法
EG5.
<template>
<div>
<h2></h2>
<button>我有{{money}}</button>
<button@click='jieQianFromXM(100)'>小米100</button>
<button@click='jieQianFromXH(200)'>小红200</button>
<button@click='jieQianFromAll(250)'>所有250</button>
<!--ref:获取节点(组件标签)-->
<!--在Vue组件当中可以通过ref获取子组件,可以操作子组件的数据和方法-->
<Sonref='xm'/>
<Daughterref='xh'/>
</div>
</template>
<script>
importSon from'./Son'
importDaughter from'./Daughter'
exportdefault{
data(){
return{
money:1000
}
},
components:{
Son,Daughter
},
methods:{
jieQianFromXM(money){
//父组件累加
//向小米借,金额增加
this.money+=money;
//小米减少
//ref可以获取真实的DOM节点,
//也可以获取子组件的标签(可以操作子组件的数据和方法)
this.$refs.xm.money-=money;
},
jieQianFromXM(money){
this.money+=money;
//小红减少
this.$refs.xh.money-=money;
},
jieQianFromAll(money){
this.money+=2*money;
//组件实例自身拥有一个属性$children<可以获取到当前组件,全部所有的子组件
this.$children.forEach(item=>item.money-=money)
}
//如果子组件过多,第零项可能不是小米
//所以尽可能不使用
//this.$children[0],money-=200;
//this.$children[1],money-=200;
}
}
</script>
在Son.vue中
<div>
<h2>小米有{{money}}</h2>
<button@click='getMoney(50)'>给了50</button>
</div>
<script>
exportdefault{
data(){
return{
money:3000
}
},
methods:{
//儿子小米给我
getMoney(money){
//小米减少
this.money-=money;
//我增加
//通过this.$parent属性获取某一个组件的父组件,可以操作父组件的数据与方法
this.$parent.money+=money;
}
}
}
</script>
2.6混入mixin
如果项目当中出现很多结构类似的功能,组件复用
JS业务逻辑相似,mixin,[可以把多个组件JS部分进行复用]
上面的逻辑JS相似
myMixin>myMinxin.js
export default{
//对外暴露的对象,可以放置组件重复的JS逻辑
methods:{
giveMoney(money)
{
this.money-=money;
this.$parent.money+=money;
}
}
}
在上面的Son和Daughter组件中
<script>
importmyMixin from'@/pages/mixin/mixin.js'
exportdefault{
mixins:[myMixin],
}
</script>
2.7插槽
实现父子组件通信(结构)
默认插槽 具名插槽 作用域插槽
作用域插槽:子组件的数据来源于父组件,子组件无法决定自身的结构和外观
在父组件中
<div>
//子组件:数据来源于父组件
<List:todos='todos'/>
//子组件决定不了结构与外观
<templateslot-scope='todo'>
//{{todo}}
<span:style="{color:todo.todo.isComplete?'green':'red'}">{{todo.todo.text}}</span>
</template>
</div>
在子组件中
<ul>
<liv-for="(item,index) intodos" :key='index'>
<!--作用域插槽-->
<slot:todo='item'></slot>
</li>
</ul>
<script>
exportdefault{
props:{
todos:Array
}
}
</script>
在子组件1中
<slot:todo='item' :$index='index'></slot>
在父组件中
<templateslot-scope="{todo,$index}">
<span:style="{color:todo.isComplete?'yellow':'purple'}">{{$index}}--{{todo.text}}</span>
</template>
分页器封装原理?
首先,要知道当前第几页:pageNo
分页器一共展示多少数据:total
每一页需要展示的数据个数:pageSize
分页器一共多少页:totalPage
连续页码数5|7
父组件传递给子组件,向上取整得到总共多少页Math.ceil
再计算连续的页码数
比如,当前是第5页
3 4 5 6 7
分页器情况需要考虑
当前项目:连续的页码数---5(分页器至少5页)
不正常情况:当前页数未到5页
start=1 end=totalPage
正常情况:总页数大于连续页码数
start=pageNo-parseInt(continues/2)
end=pageNo-parseInt(continues/2)
在此下,出现不正常情况,得到的start出现0或负数
start=1 end=continues
得到的end大于总页数
end=totalPage start=totalPage-contiues+1
1.路由传参的面试题
(1)路由传参(对象写法)可以path与params结合使用吗
(path--query)|(name--params)
回答:路由传参的时候可以是name,path形式,但是需要注意,path写法不能与params结合使用
(2)如何指定params参数可传可不传
回答:在配置路由的时候已经使用占位了(params),但是路由跳转的时候就不传参,路径(url)会出现问题
http://localhost:8080/#/?k=AFRH
其中的search没有了
可以在占位后面加上问号(?)
(3)params可传也不传,但是传过来是空值,如何解决
回答:使用underfined解决params参数可传可不传的问题(包括空字符串)
比如:在设置params:{keyword:''||undefined}
(4)路由组件能不能传递props参数